diff --git a/EasySourceGenerators.Abstractions/FluentApiStarters/Generate.cs b/EasySourceGenerators.Abstractions/FluentApiStarters/Generate.cs index 1b42ecd..0d3265a 100644 --- a/EasySourceGenerators.Abstractions/FluentApiStarters/Generate.cs +++ b/EasySourceGenerators.Abstractions/FluentApiStarters/Generate.cs @@ -1,4 +1,5 @@ -using JetBrains.Annotations; +using EasySourceGenerators.Abstractions.Method; +using JetBrains.Annotations; namespace EasySourceGenerators.Abstractions; diff --git a/EasySourceGenerators.Abstractions/FluentApiStarters/IGeneratorsFactory.cs b/EasySourceGenerators.Abstractions/FluentApiStarters/IGeneratorsFactory.cs index b0c4777..2b882b1 100644 --- a/EasySourceGenerators.Abstractions/FluentApiStarters/IGeneratorsFactory.cs +++ b/EasySourceGenerators.Abstractions/FluentApiStarters/IGeneratorsFactory.cs @@ -1,6 +1,9 @@ -namespace EasySourceGenerators.Abstractions; +using EasySourceGenerators.Abstractions.Method; + +namespace EasySourceGenerators.Abstractions; public interface IGeneratorsFactory { IMethodBodyBuilderStage1 StartFluentApiBuilderForBody(); + IMethodBuilderStage1 StartFluentApiBuilderForMethod(); } \ No newline at end of file diff --git a/EasySourceGenerators.Abstractions/Method/IMethodBuilderStage1.cs b/EasySourceGenerators.Abstractions/Method/IMethodBuilderStage1.cs index 3f86848..9987397 100644 --- a/EasySourceGenerators.Abstractions/Method/IMethodBuilderStage1.cs +++ b/EasySourceGenerators.Abstractions/Method/IMethodBuilderStage1.cs @@ -30,13 +30,13 @@ public interface IMethodBuilderStage3ReturningVoid public interface IMethodBuilderStage3 { - IMethodBuilderStage4NoParam WithNoParameters(); + IMethodBuilderStage4NoParam WithNoParameters(); - IMethodBuilderStage4 WithParameter(); - IMethodBuilderStage4 WithParameter(Type param1); - IMethodBuilderStage4 WithParameter(string param1); - IMethodBuilderStage4 WithParameter(Func param1Factory); - IMethodBuilderStage4 WithParameter(Func param1Factory); + IMethodBuilderStage4 WithParameter(); + IMethodBuilderStage4WithSomeParam WithParameter(Type param1); + IMethodBuilderStage4WithSomeParam WithParameter(string param1); + IMethodBuilderStage4WithSomeParam WithParameter(Func param1Factory); + IMethodBuilderStage4WithSomeParam WithParameter(Func param1Factory); } public interface IMethodBuilderStage3WithSomeReturnType @@ -52,5 +52,52 @@ public interface IMethodBuilderStage3WithSomeReturnType public interface IMethodBuilderStage4ReturnVoidNoParam { - + IMethodBodyGenerator UseProvidedBody(Action body); +} + +public interface IMethodBuilderStage4ReturnVoid +{ + IMethodBodyGenerator UseProvidedBody(Action body); +} + +public interface IMethodBuilderStage4ReturnVoid +{ + IMethodBodyGenerator UseProvidedBody(Action body); +} + +public interface IMethodBuilderStage4NoParam +{ + IMethodBodyGenerator UseProvidedBody(Func body); + IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory); +} + +public interface IMethodBuilderStage4 +{ + IMethodBodyGenerator UseProvidedBody(Func body); + IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory); + IMethodBodyGeneratorSwitchBody BodyWithSwitchStatement(); +} + +public interface IMethodBuilderStage4WithSomeParam +{ + IMethodBodyGenerator UseProvidedBody(Func body); + IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory); +} + +public interface IMethodBuilderStage4WithSomeReturnTypeNoParam +{ + IMethodBodyGenerator UseProvidedBody(Func body); + IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory); +} + +public interface IMethodBuilderStage4WithSomeReturnType +{ + IMethodBodyGenerator UseProvidedBody(Func body); + IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory); +} + +public interface IMethodBuilderStage4WithSomeReturnTypeWithSomeParam +{ + IMethodBodyGenerator UseProvidedBody(Func body); + IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory); } \ No newline at end of file diff --git a/EasySourceGenerators.Abstractions/MethodBody/IMethodBodyBuilder.cs b/EasySourceGenerators.Abstractions/MethodBody/IMethodBodyBuilder.cs index c01799b..a2962f9 100644 --- a/EasySourceGenerators.Abstractions/MethodBody/IMethodBodyBuilder.cs +++ b/EasySourceGenerators.Abstractions/MethodBody/IMethodBodyBuilder.cs @@ -2,7 +2,7 @@ // ReSharper disable TypeParameterCanBeVariant - not available for every overload, so not used for consistency -public interface IMethodBodyBuilderStage1 +public interface IMethodBodyBuilderStage1 : IMethodBodyBuilder { IMethodBodyBuilderStage2 ForMethod(); } @@ -41,7 +41,7 @@ public interface IMethodBodyBuilderStage4ReturnVoid IMethodBodyGenerator UseProvidedBody(Action body); } -public interface IMethodBodyBuilderStage4 +public interface IMethodBodyBuilderStage4 { IMethodBodyGenerator UseProvidedBody(Func body); IMethodBodyGenerator BodyRetuningConstant(Func constantValueFactory); diff --git a/EasySourceGenerators.Abstractions/MethodBody/IMethodBodyGenerator.cs b/EasySourceGenerators.Abstractions/MethodBody/IMethodBodyGenerator.cs index 0b97ea0..151d6ee 100644 --- a/EasySourceGenerators.Abstractions/MethodBody/IMethodBodyGenerator.cs +++ b/EasySourceGenerators.Abstractions/MethodBody/IMethodBodyGenerator.cs @@ -1,3 +1,34 @@ namespace EasySourceGenerators.Abstractions; -public interface IMethodBodyGenerator; \ No newline at end of file +public interface IMethodBodyGenerator; + +public interface IMethodBodyGeneratorWithNoParameter : IMethodBodyGenerator; + +public interface IMethodBodyGenerator : IMethodBodyGenerator +{ + IMethodBodyGeneratorWithNoParameter BodyReturningConstantValue(Func body); +} + +public interface IMethodBodyGenerator : IMethodBodyGenerator +{ + IMethodBodyGeneratorSwitchBody GenerateSwitchBody(); +} + +public interface IMethodBodyGeneratorStage0 +{ + IMethodBodyGeneratorWithNoParameter CreateImplementation(); + IMethodBodyGenerator CreateImplementation(); + IMethodBodyGenerator CreateImplementation(); + IMethodBodyBuilder ForMethod(); +} + +public interface IMethodBodyBuilder +{ + IMethodBodyBuilder WithParameter(); + IMethodBodyGenerator WithReturnType(); +} + +public interface IMethodBodyBuilder +{ + IMethodBodyGenerator WithReturnType(); +} \ No newline at end of file diff --git a/EasySourceGenerators.Abstractions/MethodBody/IMethodBodyGeneratorSwitchBody.cs b/EasySourceGenerators.Abstractions/MethodBody/IMethodBodyGeneratorSwitchBody.cs index d669ab1..a4de829 100644 --- a/EasySourceGenerators.Abstractions/MethodBody/IMethodBodyGeneratorSwitchBody.cs +++ b/EasySourceGenerators.Abstractions/MethodBody/IMethodBodyGeneratorSwitchBody.cs @@ -1,33 +1,30 @@ namespace EasySourceGenerators.Abstractions; -public interface IMethodBodyGeneratorSwitchBody +// ReSharper disable TypeParameterCanBeVariant - variance removed for simplicity + +public interface IMethodBodyGeneratorSwitchBody { - IMethodBodyGeneratorSwitchBodyCase ForCases(params TParam1[] cases); + IMethodBodyGeneratorSwitchBodyCase ForCases(params object[] cases); IMethodBodyGeneratorSwitchBodyDefaultCase ForDefaultCase(); } -public interface IMethodBodyGeneratorSwitchBodyCase +public interface IMethodBodyGeneratorSwitchBodyCase { /// /// Specific case(s) will use the body provided. /// /// During code generation this body will be emitted. - IMethodBodyGeneratorSwitchBodyCaseStage2 UseProvidedBody(Func body); + IMethodBodyGeneratorSwitchBody UseProvidedBody(Func body); /// /// Specify case(s) will return a constant value. /// /// During code generation, this delegate will be run to calculate the constant value. /// The delegate will not be used in the generated code. Only the value it produces after it's executed during code generation. - IMethodBodyGeneratorSwitchBodyCaseStage2 ReturnConstantValue(Func constantValueFactory); -} - -public interface IMethodBodyGeneratorSwitchBodyCaseStage2 -{ - IMethodBodyGeneratorSwitchBodyDefaultCase ForDefaultCase(); + IMethodBodyGeneratorSwitchBody ReturnConstantValue(Func constantValueFactory); } -public interface IMethodBodyGeneratorSwitchBodyDefaultCase : IMethodBodyGenerator +public interface IMethodBodyGeneratorSwitchBodyDefaultCase : IMethodBodyGenerator { IMethodBodyGenerator UseProvidedBody(Func body); IMethodBodyGenerator ReturnConstantValue(Func constantValueFactory); diff --git a/EasySourceGenerators.Abstractions/Mocks.cs b/EasySourceGenerators.Abstractions/Mocks.cs index 764ed8c..87606b1 100644 --- a/EasySourceGenerators.Abstractions/Mocks.cs +++ b/EasySourceGenerators.Abstractions/Mocks.cs @@ -1,7 +1,13 @@ -namespace EasySourceGenerators.Abstractions; +using EasySourceGenerators.Abstractions.Method; + +namespace EasySourceGenerators.Abstractions; public class MockGeneratorsFactory : IGeneratorsFactory { + public IMethodBodyBuilderStage1 StartFluentApiBuilderForBody() => new MockMethodBodyBuilderStage1(); + + public IMethodBuilderStage1 StartFluentApiBuilderForMethod() => new MockMethodBuilderStage1(); + public IMethodBodyBuilder ForMethod() => new MockMethodBodyBuilder(); public IMethodBodyGenerator CreateImplementation() => new MockMethodImplementationGenerator(); @@ -10,11 +16,64 @@ public IMethodBodyGenerator CreateImplementation(); } +public class MockMethodBodyBuilderStage1 : IMethodBodyBuilderStage1 +{ + public IMethodBodyBuilderStage2 ForMethod() => new MockMethodBodyBuilderStage2(); + public IMethodBodyBuilder WithParameter() => new MockMethodBodyBuilder(); + public IMethodBodyGenerator WithReturnType() => new MockMethodImplementationGenerator(); +} + +public class MockMethodBodyBuilderStage2 : IMethodBodyBuilderStage2 +{ + public IMethodBodyBuilderStage3ReturnVoid WithVoidReturnType() => new MockMethodBodyBuilderStage3ReturnVoid(); + public IMethodBodyBuilderStage3 WithReturnType() => new MockMethodBodyBuilderStage3(); +} + +public class MockMethodBodyBuilderStage3ReturnVoid : IMethodBodyBuilderStage3ReturnVoid +{ + public IMethodBodyBuilderStage4ReturnVoidNoArg WithNoParameters() => new MockMethodBodyBuilderStage4ReturnVoidNoArg(); + public IMethodBodyBuilderStage4ReturnVoid WithOneParameter() => new MockMethodBodyBuilderStage4ReturnVoid(); +} + +public class MockMethodBodyBuilderStage3 : IMethodBodyBuilderStage3 +{ + public IMethodBodyBuilderStage4NoArg WithNoParameters() => new MockMethodBodyBuilderStage4NoArg(); + public IMethodBodyBuilderStage4 WithOneParameter() => new MockMethodBodyBuilderStage4(); +} + +public class MockMethodBodyBuilderStage4ReturnVoidNoArg : IMethodBodyBuilderStage4ReturnVoidNoArg +{ + public IMethodBodyGenerator UseProvidedBody(Action body) => new MockMethodBodyGenerator(); +} + +public class MockMethodBodyBuilderStage4ReturnVoid : IMethodBodyBuilderStage4ReturnVoid +{ + public IMethodBodyGenerator UseProvidedBody(Action body) => new MockMethodBodyGenerator(); +} + +public class MockMethodBodyBuilderStage4NoArg : IMethodBodyBuilderStage4NoArg +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new MockMethodBodyGenerator(); + public IMethodBodyGenerator BodyRetuningConstant(Func constantValueFactory) => new MockMethodBodyGenerator(); +} + +public class MockMethodBodyBuilderStage4 : IMethodBodyBuilderStage4 +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new MockMethodBodyGenerator(); + public IMethodBodyGenerator BodyRetuningConstant(Func constantValueFactory) => new MockMethodBodyGenerator(); + public IMethodBodyGeneratorSwitchBody BodyWithSwitchStatement() => + new MockMethodImplementationGeneratorSwitchBody(); +} + +public class MockMethodBodyGenerator : IMethodBodyGenerator; + public class MockMethodImplementationGenerator : IMethodBodyGenerator { - public IMethodBodyGenerator BodyReturningConstantValue(Func body) => this; + public IMethodBodyGeneratorWithNoParameter BodyReturningConstantValue(Func body) => new MockMethodBodyGeneratorWithNoParameter(); } +public class MockMethodBodyGeneratorWithNoParameter : IMethodBodyGeneratorWithNoParameter; + public class MockMethodImplementationGenerator : IMethodBodyGenerator { public IMethodBodyGeneratorSwitchBody GenerateSwitchBody() => @@ -33,11 +92,11 @@ public IMethodBodyGeneratorSwitchBodyDefaultCase ForDefaultC public class MockMethodImplementationGeneratorSwitchBodyDefaultCase : IMethodBodyGeneratorSwitchBodyDefaultCase { - public IMethodBodyGenerator ReturnConstantValue(Func func) - => new MockMethodImplementationGenerator(); + public IMethodBodyGenerator ReturnConstantValue(Func func) + => new MockMethodBodyGenerator(); - public IMethodBodyGenerator UseProvidedBody(Func> func) - => new MockMethodImplementationGenerator(); + public IMethodBodyGenerator UseProvidedBody(Func func) + => new MockMethodBodyGenerator(); } public class MockMethodImplementationGeneratorSwitchBodyCase : IMethodBodyGeneratorSwitchBodyCase @@ -45,7 +104,7 @@ public class MockMethodImplementationGeneratorSwitchBodyCase public IMethodBodyGeneratorSwitchBody ReturnConstantValue(Func constantValueFactory) => new MockMethodImplementationGeneratorSwitchBody(); - public IMethodBodyGeneratorSwitchBody UseBody(Func> body) + public IMethodBodyGeneratorSwitchBody UseProvidedBody(Func body) => new MockMethodImplementationGeneratorSwitchBody(); } @@ -53,16 +112,111 @@ public class MockMethodBodyBuilder : IMethodBodyBuilder { public IMethodBodyBuilder WithParameter() => new MockMethodBodyBuilder(); - public IMethodBodyGenerator WithReturnType() => new MockImplementationGenerator(); -} - -public class MockImplementationGenerator : IMethodBodyGenerator -{ - public IMethodBodyGenerator BodyReturningConstantValue(Func body) => this; + public IMethodBodyGenerator WithReturnType() => new MockMethodImplementationGenerator(); } public class MockMethodBodyBuilder : IMethodBodyBuilder { public IMethodBodyGenerator WithReturnType() => new MockMethodImplementationGenerator(); +} + +// Mock implementations for Method builder (entire method generation) +public class MockMethodBuilderStage1 : IMethodBuilderStage1 +{ + public IMethodBuilderStage2 WithName(string name) => new MockMethodBuilderStage2(); + public IMethodBuilderStage2 WithName(Func nameFactory) => new MockMethodBuilderStage2(); +} + +public class MockMethodBuilderStage2 : IMethodBuilderStage2 +{ + public IMethodBuilderStage3ReturningVoid WithVoidReturn() => new MockMethodBuilderStage3ReturningVoid(); + public IMethodBuilderStage3 WithReturnType() => new MockMethodBuilderStage3(); + public IMethodBuilderStage3WithSomeReturnType WithReturnType(Type returnType) => new MockMethodBuilderStage3WithSomeReturnType(); + public IMethodBuilderStage3WithSomeReturnType WithReturnType(string returnType) => new MockMethodBuilderStage3WithSomeReturnType(); + public IMethodBuilderStage3WithSomeReturnType WithReturnType(Func returnTypeFactory) => new MockMethodBuilderStage3WithSomeReturnType(); + public IMethodBuilderStage3WithSomeReturnType WithReturnType(Func returnTypeFactory) => new MockMethodBuilderStage3WithSomeReturnType(); +} + +public class MockMethodBuilderStage3ReturningVoid : IMethodBuilderStage3ReturningVoid +{ + public IMethodBuilderStage4ReturnVoidNoParam WithNoParameters() => new MockMethodBuilderStage4ReturnVoidNoParam(); + public IMethodBuilderStage4ReturnVoid WithParameter() => new MockMethodBuilderStage4ReturnVoid(); + public IMethodBuilderStage4ReturnVoid WithParameter(Type param1) => new MockMethodBuilderStage4ReturnVoidNonGeneric(); + public IMethodBuilderStage4ReturnVoid WithParameter(string param1) => new MockMethodBuilderStage4ReturnVoidNonGeneric(); + public IMethodBuilderStage4ReturnVoid WithParameter(Func param1Factory) => new MockMethodBuilderStage4ReturnVoidNonGeneric(); + public IMethodBuilderStage4ReturnVoid WithParameter(Func param1Factory) => new MockMethodBuilderStage4ReturnVoidNonGeneric(); +} + +public class MockMethodBuilderStage3 : IMethodBuilderStage3 +{ + public IMethodBuilderStage4NoParam WithNoParameters() => new MockMethodBuilderStage4NoParam(); + public IMethodBuilderStage4 WithParameter() => new MockMethodBuilderStage4(); + public IMethodBuilderStage4WithSomeParam WithParameter(Type param1) => new MockMethodBuilderStage4WithSomeParam(); + public IMethodBuilderStage4WithSomeParam WithParameter(string param1) => new MockMethodBuilderStage4WithSomeParam(); + public IMethodBuilderStage4WithSomeParam WithParameter(Func param1Factory) => new MockMethodBuilderStage4WithSomeParam(); + public IMethodBuilderStage4WithSomeParam WithParameter(Func param1Factory) => new MockMethodBuilderStage4WithSomeParam(); +} + +public class MockMethodBuilderStage3WithSomeReturnType : IMethodBuilderStage3WithSomeReturnType +{ + public IMethodBuilderStage4WithSomeReturnTypeNoParam WithNoParameters() => new MockMethodBuilderStage4WithSomeReturnTypeNoParam(); + public IMethodBuilderStage4WithSomeReturnType WithParameter() => new MockMethodBuilderStage4WithSomeReturnType(); + public IMethodBuilderStage4WithSomeReturnTypeWithSomeParam WithParameter(Type param1) => new MockMethodBuilderStage4WithSomeReturnTypeWithSomeParam(); + public IMethodBuilderStage4WithSomeReturnTypeWithSomeParam WithParameter(string param1) => new MockMethodBuilderStage4WithSomeReturnTypeWithSomeParam(); + public IMethodBuilderStage4WithSomeReturnTypeWithSomeParam WithParameter(Func param1Factory) => new MockMethodBuilderStage4WithSomeReturnTypeWithSomeParam(); + public IMethodBuilderStage4WithSomeReturnTypeWithSomeParam WithParameter(Func param1Factory) => new MockMethodBuilderStage4WithSomeReturnTypeWithSomeParam(); +} + +public class MockMethodBuilderStage4ReturnVoidNoParam : IMethodBuilderStage4ReturnVoidNoParam +{ + public IMethodBodyGenerator UseProvidedBody(Action body) => new MockMethodBodyGenerator(); +} + +public class MockMethodBuilderStage4ReturnVoid : IMethodBuilderStage4ReturnVoid +{ + public IMethodBodyGenerator UseProvidedBody(Action body) => new MockMethodBodyGenerator(); +} + +public class MockMethodBuilderStage4ReturnVoidNonGeneric : IMethodBuilderStage4ReturnVoid +{ + public IMethodBodyGenerator UseProvidedBody(Action body) => new MockMethodBodyGenerator(); +} + +public class MockMethodBuilderStage4NoParam : IMethodBuilderStage4NoParam +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new MockMethodBodyGenerator(); + public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) => new MockMethodBodyGenerator(); +} + +public class MockMethodBuilderStage4 : IMethodBuilderStage4 +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new MockMethodBodyGenerator(); + public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) => new MockMethodBodyGenerator(); + public IMethodBodyGeneratorSwitchBody BodyWithSwitchStatement() => + new MockMethodImplementationGeneratorSwitchBody(); +} + +public class MockMethodBuilderStage4WithSomeParam : IMethodBuilderStage4WithSomeParam +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new MockMethodBodyGenerator(); + public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) => new MockMethodBodyGenerator(); +} + +public class MockMethodBuilderStage4WithSomeReturnTypeNoParam : IMethodBuilderStage4WithSomeReturnTypeNoParam +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new MockMethodBodyGenerator(); + public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) => new MockMethodBodyGenerator(); +} + +public class MockMethodBuilderStage4WithSomeReturnType : IMethodBuilderStage4WithSomeReturnType +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new MockMethodBodyGenerator(); + public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) => new MockMethodBodyGenerator(); +} + +public class MockMethodBuilderStage4WithSomeReturnTypeWithSomeParam : IMethodBuilderStage4WithSomeReturnTypeWithSomeParam +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new MockMethodBodyGenerator(); + public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) => new MockMethodBodyGenerator(); } \ No newline at end of file diff --git a/EasySourceGenerators.Examples/ColorsClassFluentCreated.cs b/EasySourceGenerators.Examples/ColorsClassFluentCreated.cs new file mode 100644 index 0000000..b3f05ab --- /dev/null +++ b/EasySourceGenerators.Examples/ColorsClassFluentCreated.cs @@ -0,0 +1,23 @@ +using EasySourceGenerators.Abstractions; + +namespace EasySourceGenerators.Examples; + +public partial class ColorsClassFluentCreated +{ + [MethodBodyGenerator("GetAllColorsString")] + static IMethodBodyGenerator GetAllColorsString_Generator() => + Generate.Method() + .WithName("GetAllColorsString") + .WithReturnType() + .WithNoParameters() + .BodyReturningConstant(() => string.Join(", ", Enum.GetNames())); +} + +/* + This will generate the following method: + + public string GetAllColorsString() + { + return "Red, Green, Blue"; + } +*/ diff --git a/EasySourceGenerators.Examples/MapperFluentCreated.cs b/EasySourceGenerators.Examples/MapperFluentCreated.cs new file mode 100644 index 0000000..f4bad83 --- /dev/null +++ b/EasySourceGenerators.Examples/MapperFluentCreated.cs @@ -0,0 +1,36 @@ +using EasySourceGenerators.Abstractions; + +namespace EasySourceGenerators.Examples; + +public partial class MapperFluentCreated +{ + [MethodBodyGenerator("MapToMammal")] + static IMethodBodyGenerator MapToMammal_Generator() => + Generate.Method() + .WithName("MapToMammal") + .WithReturnType() + .WithParameter() + .BodyWithSwitchStatement() + .ForCases(GetFourLeggedAnimalsThatHasMatchInMammalAnimal()).ReturnConstantValue(fourLegged => Enum.Parse(fourLegged.ToString(), true)) + .ForDefaultCase().UseProvidedBody(fourLegged => throw new ArgumentException($"Cannot map {fourLegged} to a Mammal")); + + static FourLegged[] GetFourLeggedAnimalsThatHasMatchInMammalAnimal() => + Enum + .GetValues() + .Where(fourLeggedAnimal => Enum.TryParse(typeof(Mammal), fourLeggedAnimal.ToString(), true, out _)) + .ToArray(); +} + +/* + This will generate the following method: + + public Mammal MapToMammal(FourLegged fourLegged) + { + switch (fourLegged) + { + case FourLegged.Dog: return Mammal.Dog; + case FourLegged.Cat: return Mammal.Cat; + default: throw new ArgumentException($"Cannot map {fourLegged} to a Mammal"); + } + } +*/ diff --git a/EasySourceGenerators.GeneratorTests/GeneratesMethodExecutionRuntimeTests.cs b/EasySourceGenerators.GeneratorTests/GeneratesMethodExecutionRuntimeTests.cs index 4ec682f..16cfede 100644 --- a/EasySourceGenerators.GeneratorTests/GeneratesMethodExecutionRuntimeTests.cs +++ b/EasySourceGenerators.GeneratorTests/GeneratesMethodExecutionRuntimeTests.cs @@ -222,8 +222,8 @@ public static IMethodBodyGenerator Generate() private static IMethodBodyGenerator GenerateMethod() { - return global::EasySourceGenerators.Abstractions.Generate.Method() - .WithOneParameter() + return global::EasySourceGenerators.Abstractions.Generate.MethodBody() + .WithParameter() .WithReturnType() .GenerateSwitchBody() .ForCases(1, new[] { 2, 3 }, "4") diff --git a/EasySourceGenerators.GeneratorTests/MethodBodyBuilderTests.cs b/EasySourceGenerators.GeneratorTests/MethodBodyBuilderTests.cs index 311c7d9..faf1d7f 100644 --- a/EasySourceGenerators.GeneratorTests/MethodBodyBuilderTests.cs +++ b/EasySourceGenerators.GeneratorTests/MethodBodyBuilderTests.cs @@ -49,6 +49,8 @@ private sealed class TrackingGeneratorsFactory : IMethodBodyGeneratorStage0 public int TypedCreateImplementationCalls { get; private set; } public int ArgCreateImplementationCalls { get; private set; } + public IMethodBodyGeneratorWithNoParameter CreateImplementation() => new TrackingNoParamGenerator(); + public IMethodBodyBuilder ForMethod() => new MethodBodyBuilder(this); public IMethodBodyGenerator CreateImplementation() @@ -64,9 +66,12 @@ public IMethodBodyGenerator CreateImplementation : IMethodBodyGenerator { - public IMethodBodyGeneratorWithNoParameter BodyReturningConstantValue(Func body) => this; + public IMethodBodyGeneratorWithNoParameter BodyReturningConstantValue(Func body) => + new TrackingNoParamGenerator(); } private sealed class TrackingArgImplementationGenerator : IMethodBodyGenerator diff --git a/EasySourceGenerators.GeneratorTests/MocksTests.cs b/EasySourceGenerators.GeneratorTests/MocksTests.cs index 4e78fef..03270ae 100644 --- a/EasySourceGenerators.GeneratorTests/MocksTests.cs +++ b/EasySourceGenerators.GeneratorTests/MocksTests.cs @@ -36,13 +36,13 @@ public void MockGeneratorsFactory_CreateImplementationWithArg_ReturnsArgMock() } [Test] - public void MockMethodImplementationGeneratorTyped_UseBody_ReturnsSameInstance() + public void MockMethodImplementationGeneratorTyped_UseBody_ReturnsWithNoParameter() { MockMethodImplementationGenerator generator = new MockMethodImplementationGenerator(); IMethodBodyGeneratorWithNoParameter result = generator.BodyReturningConstantValue(() => "x"); - Assert.That(result, Is.SameAs(generator)); + Assert.That(result, Is.TypeOf()); } [Test] @@ -76,23 +76,23 @@ public void MockSwitchBody_ForDefaultCase_ReturnsDefaultCaseMock() } [Test] - public void MockDefaultCase_ReturnConstantValue_ReturnsMethodImplementationGenerator() + public void MockDefaultCase_ReturnConstantValue_ReturnsMethodBodyGenerator() { MockMethodImplementationGeneratorSwitchBodyDefaultCase defaultCase = new MockMethodImplementationGeneratorSwitchBodyDefaultCase(); - IMethodBodyGenerator result = defaultCase.ReturnConstantValue(value => value.ToString()); + IMethodBodyGenerator result = defaultCase.ReturnConstantValue(value => value.ToString()); - Assert.That(result, Is.TypeOf>()); + Assert.That(result, Is.TypeOf()); } [Test] - public void MockDefaultCase_UseBody_ReturnsMethodImplementationGenerator() + public void MockDefaultCase_UseProvidedBody_ReturnsMethodBodyGenerator() { MockMethodImplementationGeneratorSwitchBodyDefaultCase defaultCase = new MockMethodImplementationGeneratorSwitchBodyDefaultCase(); - IMethodBodyGenerator result = defaultCase.UseProvidedBody(_ => () => "v"); + IMethodBodyGenerator result = defaultCase.UseProvidedBody(_ => "v"); - Assert.That(result, Is.TypeOf>()); + Assert.That(result, Is.TypeOf()); } [Test] @@ -106,11 +106,11 @@ public void MockCase_ReturnConstantValue_ReturnsSwitchBody() } [Test] - public void MockCase_UseBody_ReturnsSwitchBody() + public void MockCase_UseProvidedBody_ReturnsSwitchBody() { MockMethodImplementationGeneratorSwitchBodyCase caseBuilder = new MockMethodImplementationGeneratorSwitchBodyCase(); - IMethodBodyGeneratorSwitchBody result = caseBuilder.UseBody(_ => _ => { }); + IMethodBodyGeneratorSwitchBody result = caseBuilder.UseProvidedBody(_ => "test"); Assert.That(result, Is.TypeOf>()); } @@ -132,17 +132,17 @@ public void MockMethodBuilder_WithReturnType_ReturnsMockImplementationGenerator( IMethodBodyGenerator result = bodyBuilder.WithReturnType(); - Assert.That(result, Is.TypeOf>()); + Assert.That(result, Is.TypeOf>()); } [Test] - public void MockImplementationGenerator_UseBody_ReturnsSameInstance() + public void MockImplementationGenerator_UseBody_ReturnsWithNoParameter() { - MockImplementationGenerator generator = new MockImplementationGenerator(); + MockMethodImplementationGenerator generator = new MockMethodImplementationGenerator(); IMethodBodyGeneratorWithNoParameter result = generator.BodyReturningConstantValue(() => "x"); - Assert.That(result, Is.SameAs(generator)); + Assert.That(result, Is.TypeOf()); } [Test] diff --git a/EasySourceGenerators.GeneratorTests/RecordingGeneratorsFactoryTests.cs b/EasySourceGenerators.GeneratorTests/RecordingGeneratorsFactoryTests.cs index 035c3f4..e735f5a 100644 --- a/EasySourceGenerators.GeneratorTests/RecordingGeneratorsFactoryTests.cs +++ b/EasySourceGenerators.GeneratorTests/RecordingGeneratorsFactoryTests.cs @@ -50,13 +50,13 @@ public void CreateImplementationWithArg_SetsLastRecordAndReturnsArgGenerator() } [Test] - public void TypedUseBody_ReturnsSameInstance() + public void TypedUseBody_ReturnsRecordingImplementationGenerator() { RecordingMethodImplementationGeneratorTyped generator = new RecordingMethodImplementationGeneratorTyped(); IMethodBodyGeneratorWithNoParameter result = generator.BodyReturningConstantValue(() => "a"); - Assert.That(result, Is.SameAs(generator)); + Assert.That(result, Is.TypeOf()); } [Test] @@ -103,7 +103,7 @@ public void UseBody_RecordsCaseKeysWithNullValues() RecordingMethodImplementationGeneratorSwitchBody switchBody = new RecordingMethodImplementationGeneratorSwitchBody(record); IMethodBodyGeneratorSwitchBodyCase caseBuilder = switchBody.ForCases(5, 6); - IMethodBodyGeneratorSwitchBody result = caseBuilder.UseProvidedBody(_ => _ => { }); + IMethodBodyGeneratorSwitchBody result = caseBuilder.UseProvidedBody(_ => 0); Assert.That(result, Is.TypeOf>()); Assert.That(record.CaseKeys, Is.EqualTo(new object[] { 5, 6 })); @@ -129,7 +129,7 @@ public void UseBody_WithNullCase_ThrowsInvalidOperationException() RecordingMethodImplementationGeneratorSwitchBody switchBody = new RecordingMethodImplementationGeneratorSwitchBody(record); IMethodBodyGeneratorSwitchBodyCase caseBuilder = switchBody.ForCases((object?)null!); - TestDelegate action = () => caseBuilder.UseProvidedBody(_ => _ => { }); + TestDelegate action = () => caseBuilder.UseProvidedBody(_ => 0); Assert.That(action, Throws.TypeOf()); } @@ -141,7 +141,7 @@ public void ForDefaultCase_ReturnConstantValue_SetsDefaultFlag() RecordingMethodImplementationGeneratorSwitchBody switchBody = new RecordingMethodImplementationGeneratorSwitchBody(record); IMethodBodyGeneratorSwitchBodyDefaultCase defaultCase = switchBody.ForDefaultCase(); - IMethodBodyGenerator result = defaultCase.ReturnConstantValue(value => value + 1); + IMethodBodyGenerator result = defaultCase.ReturnConstantValue(value => value + 1); Assert.That(record.HasDefaultCase, Is.True); Assert.That(result, Is.TypeOf>()); @@ -154,7 +154,7 @@ public void ForDefaultCase_UseBody_SetsDefaultFlag() RecordingMethodImplementationGeneratorSwitchBody switchBody = new RecordingMethodImplementationGeneratorSwitchBody(record); IMethodBodyGeneratorSwitchBodyDefaultCase defaultCase = switchBody.ForDefaultCase(); - IMethodBodyGenerator result = defaultCase.UseProvidedBody(_ => () => 1); + IMethodBodyGenerator result = defaultCase.UseProvidedBody(_ => 1); Assert.That(record.HasDefaultCase, Is.True); Assert.That(result, Is.TypeOf>()); diff --git a/EasySourceGenerators.Generators/Consts.cs b/EasySourceGenerators.Generators/Consts.cs index 0b8ebfc..ca9b06e 100644 --- a/EasySourceGenerators.Generators/Consts.cs +++ b/EasySourceGenerators.Generators/Consts.cs @@ -19,4 +19,5 @@ public static class Consts public const string RecordingGeneratorsFactoryTypeFullName = $"{GeneratorsNamespace}.{nameof(RecordingGeneratorsFactory)}"; public const string CurrentGeneratorPropertyName = nameof(Generate.CurrentGenerator); public const string LastRecordPropertyName = nameof(RecordingGeneratorsFactory.LastRecord); + public const string LastMethodRecordPropertyName = nameof(RecordingGeneratorsFactory.LastMethodRecord); } diff --git a/EasySourceGenerators.Generators/EasySourceGenerators.Generators.csproj b/EasySourceGenerators.Generators/EasySourceGenerators.Generators.csproj index 3560dc3..9919b08 100644 --- a/EasySourceGenerators.Generators/EasySourceGenerators.Generators.csproj +++ b/EasySourceGenerators.Generators/EasySourceGenerators.Generators.csproj @@ -60,7 +60,7 @@ - + diff --git a/EasySourceGenerators.Generators/GeneratesMethodExecutionRuntime.cs b/EasySourceGenerators.Generators/GeneratesMethodExecutionRuntime.cs index 6c047c3..8f93263 100644 --- a/EasySourceGenerators.Generators/GeneratesMethodExecutionRuntime.cs +++ b/EasySourceGenerators.Generators/GeneratesMethodExecutionRuntime.cs @@ -14,6 +14,13 @@ internal sealed record SwitchBodyData( IReadOnlyList<(object key, string value)> CasePairs, bool HasDefaultCase); +internal sealed record MethodData( + string MethodName, + string ReturnTypeName, + IReadOnlyList ParameterTypeNames, + string? ConstantValue, + SwitchBodyData? SwitchBody); + internal static class GeneratesMethodExecutionRuntime { internal static (string? value, string? error) ExecuteSimpleGeneratorMethod( @@ -66,7 +73,9 @@ internal static (SwitchBodyData? record, string? error) ExecuteFluentGeneratorMe { Assembly loaded = context.LoadFromStream(new MemoryStream(bytes)); if (string.Equals(assemblyName.Name, Consts.AbstractionsAssemblyName, StringComparison.OrdinalIgnoreCase)) + { capturedAbstractionsAssembly = loaded; + } return loaded; } @@ -179,6 +188,150 @@ reference.Display is not null } } + internal static (MethodData? methodData, string? error) ExecuteEntireMethodGeneratorMethod( + IMethodSymbol generatorMethod, + Compilation compilation) + { + IReadOnlyList allPartials = GetAllUnimplementedPartialMethods(compilation); + CSharpCompilation executableCompilation = BuildExecutionCompilation(allPartials, compilation); + + using MemoryStream stream = new(); + EmitResult emitResult = executableCompilation.Emit(stream); + if (!emitResult.Success) + { + string errors = string.Join("; ", emitResult.Diagnostics + .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .Select(diagnostic => diagnostic.GetMessage())); + return (null, $"Compilation failed: {errors}"); + } + + stream.Position = 0; + AssemblyLoadContext? loadContext = null; + try + { + Dictionary compilationReferenceBytes = EmitCompilationReferences(compilation); + + loadContext = new AssemblyLoadContext("__GeneratorExec", isCollectible: true); + Assembly? capturedAbstractionsAssembly = null; + loadContext.Resolving += (context, assemblyName) => + { + PortableExecutableReference? match = compilation.References + .OfType() + .FirstOrDefault(reference => reference.FilePath is not null && string.Equals( + Path.GetFileNameWithoutExtension(reference.FilePath), + assemblyName.Name, + StringComparison.OrdinalIgnoreCase)); + if (match?.FilePath != null) + return context.LoadFromAssemblyPath(ResolveImplementationAssemblyPath(match.FilePath)); + + if (assemblyName.Name != null && compilationReferenceBytes.TryGetValue(assemblyName.Name, out byte[]? bytes)) + { + Assembly loaded = context.LoadFromStream(new MemoryStream(bytes)); + if (string.Equals(assemblyName.Name, Consts.AbstractionsAssemblyName, StringComparison.OrdinalIgnoreCase)) + { + capturedAbstractionsAssembly = loaded; + } + return loaded; + } + + return null; + }; + + Assembly assembly = loadContext.LoadFromStream(stream); + + MetadataReference[] abstractionsMatchingReferences = compilation.References.Where(reference => reference.Display is not null && ( + reference.Display.Equals(Consts.AbstractionsAssemblyName, StringComparison.OrdinalIgnoreCase) + || (reference is PortableExecutableReference peRef && peRef.FilePath is not null && Path.GetFileNameWithoutExtension(peRef.FilePath) + .Equals(Consts.AbstractionsAssemblyName, StringComparison.OrdinalIgnoreCase)))) + .ToArray(); + + if (abstractionsMatchingReferences.Length == 0) + { + return (null, $"Could not find any reference matching '{Consts.AbstractionsAssemblyName}' in compilation references."); + } + + PortableExecutableReference[] peMatchingReferences = abstractionsMatchingReferences.OfType().ToArray(); + CompilationReference[] csharpCompilationReference = abstractionsMatchingReferences.OfType().ToArray(); + + Assembly abstractionsAssembly; + + if (peMatchingReferences.Length > 0) + { + PortableExecutableReference abstractionsReference = peMatchingReferences.First(); + + if (string.IsNullOrEmpty(abstractionsReference.FilePath)) + { + return (null, $"The reference matching '{Consts.AbstractionsAssemblyName}' does not have a valid file path."); + } + + string abstractionsAssemblyPath = ResolveImplementationAssemblyPath(abstractionsReference.FilePath); + abstractionsAssembly = loadContext.LoadFromAssemblyPath(abstractionsAssemblyPath); + } + else if (csharpCompilationReference.Length > 0) + { + if (capturedAbstractionsAssembly != null) + { + abstractionsAssembly = capturedAbstractionsAssembly; + } + else if (compilationReferenceBytes.TryGetValue(Consts.AbstractionsAssemblyName, out byte[]? abstractionBytes)) + { + abstractionsAssembly = loadContext.LoadFromStream(new MemoryStream(abstractionBytes)); + } + else + { + return (null, $"Found reference matching '{Consts.AbstractionsAssemblyName}' as a CompilationReference, but failed to emit it."); + } + } + else + { + return (null, $"Found references matching '{Consts.AbstractionsAssemblyName}' but none were usable."); + } + + Type? generatorStaticType = abstractionsAssembly.GetType(Consts.GenerateTypeFullName); + Type? recordingFactoryType = assembly.GetType(Consts.RecordingGeneratorsFactoryTypeFullName); + if (generatorStaticType == null || recordingFactoryType == null) + { + return (null, $"Could not find {Consts.GenerateTypeFullName} or {Consts.RecordingGeneratorsFactoryTypeFullName} types in compiled assembly"); + } + + object? recordingFactory = Activator.CreateInstance(recordingFactoryType); + PropertyInfo? currentGeneratorProperty = generatorStaticType.GetProperty(Consts.CurrentGeneratorPropertyName, BindingFlags.NonPublic | BindingFlags.Static); + currentGeneratorProperty?.SetValue(null, recordingFactory); + + string typeName = generatorMethod.ContainingType.ToDisplayString(); + Type? loadedType = assembly.GetType(typeName); + if (loadedType == null) + { + return (null, $"Could not find type '{typeName}' in compiled assembly"); + } + + MethodInfo? generatorMethodInfo = loadedType.GetMethod(generatorMethod.Name, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); + if (generatorMethodInfo == null) + { + return (null, $"Could not find method '{generatorMethod.Name}' in type '{typeName}'"); + } + + generatorMethodInfo.Invoke(null, null); + + PropertyInfo? lastMethodRecordProperty = recordingFactoryType.GetProperty(Consts.LastMethodRecordPropertyName); + object? lastMethodRecord = lastMethodRecordProperty?.GetValue(recordingFactory); + if (lastMethodRecord == null) + { + return (null, "RecordingGeneratorsFactory did not produce a method record"); + } + + return (ExtractMethodData(lastMethodRecord, recordingFactory, recordingFactoryType), null); + } + catch (Exception ex) + { + return (null, $"Error executing generator method '{generatorMethod.Name}': {ex.GetBaseException()}"); + } + finally + { + loadContext?.Unload(); + } + } + internal static (string? value, string? error) ExecuteGeneratorMethodWithArgs( IMethodSymbol generatorMethod, IReadOnlyList allPartialMethods, @@ -304,6 +457,58 @@ private static SwitchBodyData ExtractSwitchBodyData(object lastRecord, ITypeSymb return new SwitchBodyData(pairs, hasDefaultCase); } + private static MethodData ExtractMethodData(object lastMethodRecord, object? recordingFactory, Type recordingFactoryType) + { + Type methodRecordType = lastMethodRecord.GetType(); + string methodName = methodRecordType.GetProperty(nameof(MethodRecord.MethodName))?.GetValue(lastMethodRecord)?.ToString() ?? string.Empty; + string returnTypeName = methodRecordType.GetProperty(nameof(MethodRecord.ReturnTypeName))?.GetValue(lastMethodRecord)?.ToString() ?? "void"; + object? constantValue = methodRecordType.GetProperty(nameof(MethodRecord.ConstantValue))?.GetValue(lastMethodRecord); + + IList? parameterTypeNamesList = methodRecordType.GetProperty(nameof(MethodRecord.ParameterTypeNames))?.GetValue(lastMethodRecord) as IList; + List parameterTypeNames = new(); + if (parameterTypeNamesList != null) + { + foreach (object? paramTypeName in parameterTypeNamesList) + { + parameterTypeNames.Add(paramTypeName?.ToString() ?? string.Empty); + } + } + + // Check if there's a switch body record + object? switchBodyObj = methodRecordType.GetProperty(nameof(MethodRecord.SwitchBody))?.GetValue(lastMethodRecord); + SwitchBodyData? switchBodyData = null; + if (switchBodyObj != null) + { + // Also check the LastRecord from the factory for case pairs + PropertyInfo? lastRecordProperty = recordingFactoryType.GetProperty(Consts.LastRecordPropertyName); + object? lastRecord = lastRecordProperty?.GetValue(recordingFactory); + if (lastRecord != null) + { + Type switchRecordType = lastRecord.GetType(); + IList caseKeys = (switchRecordType.GetProperty(nameof(SwitchBodyRecord.CaseKeys))?.GetValue(lastRecord) as IList) ?? new List(); + IList caseValues = (switchRecordType.GetProperty(nameof(SwitchBodyRecord.CaseValues))?.GetValue(lastRecord) as IList) ?? new List(); + bool hasDefaultCase = (bool)(switchRecordType.GetProperty(nameof(SwitchBodyRecord.HasDefaultCase))?.GetValue(lastRecord) ?? false); + + List<(object key, string value)> pairs = new(); + for (int index = 0; index < caseKeys.Count; index++) + { + object key = caseKeys[index]!; + string? value = index < caseValues.Count ? caseValues[index]?.ToString() : null; + pairs.Add((key, value ?? "default")); + } + + switchBodyData = new SwitchBodyData(pairs, hasDefaultCase); + } + } + + return new MethodData( + methodName, + returnTypeName, + parameterTypeNames, + constantValue?.ToString(), + switchBodyData); + } + private static Dictionary EmitCompilationReferences(Compilation compilation) { Dictionary result = new(StringComparer.OrdinalIgnoreCase); @@ -338,7 +543,8 @@ private static CSharpCompilation BuildExecutionCompilation( IReadOnlyList allPartialMethods, Compilation compilation) { - string dummySource = BuildDummyImplementation(allPartialMethods); + List entireMethodDummies = GetEntireMethodDummies(compilation); + string dummySource = BuildDummyImplementation(allPartialMethods, entireMethodDummies); string methodBuilderSource = ReadEmbeddedResource($"{Consts.GeneratorsAssemblyName}.MethodBodyBuilder.cs"); string recordingFactorySource = ReadEmbeddedResource($"{Consts.GeneratorsAssemblyName}.RecordingGeneratorsFactory.cs"); CSharpParseOptions parseOptions = compilation.SyntaxTrees.FirstOrDefault()?.Options as CSharpParseOptions @@ -360,7 +566,111 @@ private static string ReadEmbeddedResource(string resourceName) return reader.ReadToEnd(); } - private static string BuildDummyImplementation(IEnumerable partialMethods) + private sealed record EntireMethodDummy( + string? Namespace, + string TypeName, + bool IsStatic, + TypeKind TypeKind, + string MethodName, + string ReturnType, + List ParameterTypes); + + private static List GetEntireMethodDummies(Compilation compilation) + { + List dummies = new(); + + foreach (SyntaxTree syntaxTree in compilation.SyntaxTrees) + { + SemanticModel semanticModel = compilation.GetSemanticModel(syntaxTree); + IEnumerable methods = syntaxTree.GetRoot().DescendantNodes() + .OfType(); + + foreach (MethodDeclarationSyntax method in methods) + { + if (semanticModel.GetDeclaredSymbol(method) is not IMethodSymbol methodSymbol) + continue; + + // Check if it has [MethodBodyGenerator] attribute and returns IMethodBodyGenerator + if (methodSymbol.ReturnType.ToDisplayString() != Consts.IMethodImplementationGeneratorFullName) + continue; + + AttributeData? attribute = methodSymbol.GetAttributes() + .FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == Consts.GeneratesMethodAttributeFullName); + if (attribute == null) + continue; + + string? targetMethodName = attribute.ConstructorArguments.FirstOrDefault().Value?.ToString(); + if (targetMethodName == null) + continue; + + // Check if the target method already exists as a partial method + INamedTypeSymbol containingType = methodSymbol.ContainingType; + bool hasPartialMethod = containingType.GetMembers(targetMethodName) + .OfType() + .Any(m => m.IsPartialDefinition); + if (hasPartialMethod) + continue; + + // This is an entire method generation target - extract signature from fluent API + EntireMethodDummy? dummy = ExtractMethodSignatureFromFluent(method, methodSymbol, targetMethodName); + if (dummy != null) + dummies.Add(dummy); + } + } + + return dummies; + } + + private static EntireMethodDummy? ExtractMethodSignatureFromFluent( + MethodDeclarationSyntax method, + IMethodSymbol methodSymbol, + string targetMethodName) + { + INamedTypeSymbol containingType = methodSymbol.ContainingType; + + // Try to extract return type and parameters from the fluent API calls + string returnType = "object"; + List parameterTypes = new(); + + IEnumerable invocations = method.DescendantNodes().OfType(); + foreach (InvocationExpressionSyntax invocation in invocations) + { + if (invocation.Expression is not MemberAccessExpressionSyntax memberAccess) + continue; + + string callName = memberAccess.Name.Identifier.Text; + + if (callName == "WithReturnType" && memberAccess.Name is GenericNameSyntax returnTypeGeneric + && returnTypeGeneric.TypeArgumentList.Arguments.Count == 1) + { + returnType = returnTypeGeneric.TypeArgumentList.Arguments[0].ToString(); + } + + if (callName == "WithParameter" && memberAccess.Name is GenericNameSyntax paramGeneric + && paramGeneric.TypeArgumentList.Arguments.Count == 1) + { + parameterTypes.Add(paramGeneric.TypeArgumentList.Arguments[0].ToString()); + } + + if (callName == "WithVoidReturn") + { + returnType = "void"; + } + } + + return new EntireMethodDummy( + containingType.ContainingNamespace?.IsGlobalNamespace == false + ? containingType.ContainingNamespace.ToDisplayString() + : null, + containingType.Name, + containingType.IsStatic, + containingType.TypeKind, + targetMethodName, + returnType, + parameterTypes); + } + + private static string BuildDummyImplementation(IEnumerable partialMethods, List entireMethodDummies) { StringBuilder builder = new(); @@ -421,6 +731,45 @@ private static string BuildDummyImplementation(IEnumerable partia } } + // Add dummy methods for entire method generation targets + IEnumerable> entireMethodGroups = + entireMethodDummies.GroupBy(d => (d.Namespace, d.TypeName, d.IsStatic, d.TypeKind)); + + foreach (IGrouping<(string? Namespace, string TypeName, bool IsStatic, TypeKind TypeKind), EntireMethodDummy> group in entireMethodGroups) + { + string? ns = group.Key.Namespace; + if (ns != null) + { + builder.AppendLine($"namespace {ns} {{"); + } + + string typeKeyword = group.Key.TypeKind switch + { + TypeKind.Struct => "struct", + _ => "class" + }; + + string typeModifiers = group.Key.IsStatic ? "static partial" : "partial"; + builder.AppendLine($"{typeModifiers} {typeKeyword} {group.Key.TypeName} {{"); + + foreach (EntireMethodDummy dummy in group) + { + string staticModifier = group.Key.IsStatic ? "static " : ""; + string parameters = string.Join(", ", dummy.ParameterTypes.Select((type, index) => $"{type} arg{index}")); + string body = dummy.ReturnType == "void" + ? "{ }" + : $"{{ return default({dummy.ReturnType})!; }}"; + builder.AppendLine($"public {staticModifier}{dummy.ReturnType} {dummy.MethodName}({parameters}) {body}"); + } + + builder.AppendLine("}"); + + if (ns != null) + { + builder.AppendLine("}"); + } + } + return builder.ToString(); } } diff --git a/EasySourceGenerators.Generators/GeneratesMethodGenerationPipeline.cs b/EasySourceGenerators.Generators/GeneratesMethodGenerationPipeline.cs index 22c8858..5726ed9 100644 --- a/EasySourceGenerators.Generators/GeneratesMethodGenerationPipeline.cs +++ b/EasySourceGenerators.Generators/GeneratesMethodGenerationPipeline.cs @@ -50,24 +50,30 @@ private static string GenerateSourceForGroup( bool hasSwitchCase = methods.Any(method => HasAttribute(method.Symbol, SwitchCaseAttributeFullName)); bool hasSwitchDefault = methods.Any(method => HasAttribute(method.Symbol, SwitchDefaultAttributeFullName)); bool isFluentPattern = methods.Count == 1 && methods[0].Symbol.ReturnType.ToDisplayString() == IMethodImplementationGeneratorFullName; + bool isEntireMethodGeneration = firstMethod.PartialMethod is null; if (hasSwitchCase || hasSwitchDefault) { return GeneratesMethodPatternSourceBuilder.GenerateFromSwitchAttributes( context, methods, - firstMethod.PartialMethod, + firstMethod.PartialMethod!, firstMethod.ContainingType, allPartials, compilation); } + if (isFluentPattern && isEntireMethodGeneration) + { + return GenerateFromEntireMethodPattern(context, methods[0], compilation); + } + if (isFluentPattern) { return GeneratesMethodPatternSourceBuilder.GenerateFromFluent( context, methods[0], - firstMethod.PartialMethod, + firstMethod.PartialMethod!, firstMethod.ContainingType, compilation); } @@ -97,7 +103,7 @@ private static string GenerateFromSimplePattern( { (string? returnValue, string? error) = GeneratesMethodExecutionRuntime.ExecuteSimpleGeneratorMethod( firstMethod.Symbol, - firstMethod.PartialMethod, + firstMethod.PartialMethod!, compilation); if (error != null) @@ -112,10 +118,52 @@ private static string GenerateFromSimplePattern( return GeneratesMethodPatternSourceBuilder.GenerateSimplePartialMethod( firstMethod.ContainingType, - firstMethod.PartialMethod, + firstMethod.PartialMethod!, returnValue); } + private static string GenerateFromEntireMethodPattern( + SourceProductionContext context, + GeneratesMethodGenerationTarget methodInfo, + Compilation compilation) + { + (MethodData? methodData, string? error) = GeneratesMethodExecutionRuntime.ExecuteEntireMethodGeneratorMethod( + methodInfo.Symbol, + compilation); + + if (error != null) + { + context.ReportDiagnostic(Diagnostic.Create( + GeneratesMethodGeneratorDiagnostics.GeneratorMethodExecutionError, + methodInfo.Syntax.GetLocation(), + methodInfo.Symbol.Name, + error)); + return string.Empty; + } + + MethodData data = methodData!; + + if (data.SwitchBody != null) + { + (string? defaultExpression, List lambdaParamNames) = GeneratesMethodPatternSourceBuilder.ExtractDefaultExpressionAndParamNames(methodInfo.Syntax); + + if (!data.SwitchBody.HasDefaultCase) + { + defaultExpression = null; + } + + return GeneratesMethodPatternSourceBuilder.GenerateEntireMethodWithSwitch( + methodInfo.ContainingType, + data, + defaultExpression, + lambdaParamNames); + } + + return GeneratesMethodPatternSourceBuilder.GenerateEntireMethodSimple( + methodInfo.ContainingType, + data); + } + private static bool HasAttribute(IMethodSymbol methodSymbol, string fullAttributeTypeName) { return methodSymbol.GetAttributes() diff --git a/EasySourceGenerators.Generators/GeneratesMethodGenerationTargetCollector.cs b/EasySourceGenerators.Generators/GeneratesMethodGenerationTargetCollector.cs index 8c4d7ea..4fc0131 100644 --- a/EasySourceGenerators.Generators/GeneratesMethodGenerationTargetCollector.cs +++ b/EasySourceGenerators.Generators/GeneratesMethodGenerationTargetCollector.cs @@ -10,7 +10,7 @@ internal sealed record GeneratesMethodGenerationTarget( MethodDeclarationSyntax Syntax, IMethodSymbol Symbol, string TargetMethodName, - IMethodSymbol PartialMethod, + IMethodSymbol? PartialMethod, INamedTypeSymbol ContainingType); internal static class GeneratesMethodGenerationTargetCollector @@ -83,7 +83,9 @@ internal static List Collect( .OfType() .FirstOrDefault(method => method.IsPartialDefinition); - if (partialMethodSymbol is null) + bool isFluentPattern = methodSymbol.ReturnType.ToDisplayString() == IMethodImplementationGeneratorFullName; + + if (partialMethodSymbol is null && !isFluentPattern) { Location? attributeLocation = attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation(); diff --git a/EasySourceGenerators.Generators/GeneratesMethodPatternSourceBuilder.cs b/EasySourceGenerators.Generators/GeneratesMethodPatternSourceBuilder.cs index ab044d9..d7288f9 100644 --- a/EasySourceGenerators.Generators/GeneratesMethodPatternSourceBuilder.cs +++ b/EasySourceGenerators.Generators/GeneratesMethodPatternSourceBuilder.cs @@ -175,7 +175,7 @@ internal static string GenerateSimplePartialMethod( return ExtractInnermostLambdaBody(bodyExpression); } - private static string? ExtractDefaultExpressionFromFluentMethod(MethodDeclarationSyntax method) + internal static (string? expression, List lambdaParamNames) ExtractDefaultExpressionAndParamNames(MethodDeclarationSyntax method) { IEnumerable invocations = method.DescendantNodes().OfType(); foreach (InvocationExpressionSyntax invocation in invocations) @@ -186,16 +186,57 @@ internal static string GenerateSimplePartialMethod( } string methodName = memberAccessExpression.Name.Identifier.Text; - if (methodName is not ("ReturnConstantValue" or "BodyReturningConstantValue")) + if (methodName is not ("ReturnConstantValue" or "BodyReturningConstantValue" or "UseProvidedBody" or "BodyRetuningConstant")) { continue; } - ExpressionSyntax? argumentExpression = invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression; - return ExtractInnermostLambdaBody(argumentExpression); + // Check if this method is called on the result of ForDefaultCase() + if (memberAccessExpression.Expression is InvocationExpressionSyntax receiverInvocation + && receiverInvocation.Expression is MemberAccessExpressionSyntax receiverMember + && receiverMember.Name.Identifier.Text == "ForDefaultCase") + { + ExpressionSyntax? argumentExpression = invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression; + List paramNames = ExtractLambdaParameterNames(argumentExpression); + string? body = ExtractInnermostLambdaBody(argumentExpression); + return (body, paramNames); + } } - return null; + return (null, new List()); + } + + internal static string? ExtractDefaultExpressionFromFluentMethod(MethodDeclarationSyntax method) + { + (string? expression, _) = ExtractDefaultExpressionAndParamNames(method); + return expression; + } + + private static List ExtractLambdaParameterNames(ExpressionSyntax? expression) + { + List names = new(); + while (expression != null) + { + if (expression is SimpleLambdaExpressionSyntax simpleLambda) + { + names.Add(simpleLambda.Parameter.Identifier.Text); + expression = simpleLambda.Body as ExpressionSyntax; + } + else if (expression is ParenthesizedLambdaExpressionSyntax parenthesizedLambda) + { + foreach (ParameterSyntax parameter in parenthesizedLambda.ParameterList.Parameters) + { + names.Add(parameter.Identifier.Text); + } + expression = parenthesizedLambda.Body as ExpressionSyntax; + } + else + { + break; + } + } + + return names; } private static string? ExtractInnermostLambdaBody(ExpressionSyntax? expression) @@ -259,6 +300,194 @@ private static string GenerateSwitchMethodSource( return builder.ToString(); } + internal static string GenerateEntireMethodSimple( + INamedTypeSymbol containingType, + MethodData methodData) + { + StringBuilder builder = new(); + AppendNamespaceAndTypeHeaderForEntireMethod(builder, containingType, methodData); + + if (methodData.ReturnTypeName != "void" && methodData.ConstantValue != null) + { + string literal = FormatValueAsCSharpLiteralByTypeName(methodData.ConstantValue, methodData.ReturnTypeName); + builder.AppendLine($" return {literal};"); + } + + builder.AppendLine(" }"); + builder.AppendLine("}"); + return builder.ToString(); + } + + internal static string GenerateEntireMethodWithSwitch( + INamedTypeSymbol containingType, + MethodData methodData, + string? defaultExpression, + List lambdaParamNames) + { + StringBuilder builder = new(); + List paramNames = lambdaParamNames.Count >= methodData.ParameterTypeNames.Count + ? lambdaParamNames + : new List(); + AppendNamespaceAndTypeHeaderForEntireMethod(builder, containingType, methodData, paramNames); + + if (methodData.ParameterTypeNames.Count == 0) + { + string fallbackExpression = defaultExpression ?? "default"; + builder.AppendLine($" return {fallbackExpression};"); + builder.AppendLine(" }"); + builder.AppendLine("}"); + return builder.ToString(); + } + + string switchParameterName = paramNames.Count > 0 ? paramNames[0] : GetParameterName(methodData.ParameterTypeNames[0]); + builder.AppendLine($" switch ({switchParameterName})"); + builder.AppendLine(" {"); + + SwitchBodyData switchBody = methodData.SwitchBody!; + string? parameterTypeName = methodData.ParameterTypeNames.Count > 0 ? methodData.ParameterTypeNames[0] : null; + foreach ((object key, string value) in switchBody.CasePairs) + { + string formattedKey = FormatKeyAsCSharpLiteralByTypeName(key, parameterTypeName); + string formattedValue = FormatValueByReturnTypeName(value, methodData.ReturnTypeName); + builder.AppendLine($" case {formattedKey}: return {formattedValue};"); + } + + if (defaultExpression != null) + { + string defaultStatement = defaultExpression.TrimStart().StartsWith("throw ", StringComparison.Ordinal) + ? $" default: {defaultExpression};" + : $" default: return {defaultExpression};"; + builder.AppendLine(defaultStatement); + } + + builder.AppendLine(" }"); + builder.AppendLine(" }"); + builder.AppendLine("}"); + return builder.ToString(); + } + + private static void AppendNamespaceAndTypeHeaderForEntireMethod(StringBuilder builder, INamedTypeSymbol containingType, MethodData methodData, List? parameterNames = null) + { + builder.AppendLine("// "); + builder.AppendLine($"// Generated by {typeof(GeneratesMethodGenerator).FullName} for method '{methodData.MethodName}'."); + builder.AppendLine("#pragma warning disable"); + builder.AppendLine(); + + string? namespaceName = containingType.ContainingNamespace?.IsGlobalNamespace == false + ? containingType.ContainingNamespace.ToDisplayString() + : null; + if (namespaceName != null) + { + builder.AppendLine($"namespace {namespaceName};"); + builder.AppendLine(); + } + + string typeKeyword = containingType.TypeKind switch + { + TypeKind.Struct => "struct", + TypeKind.Interface => "interface", + _ => "class" + }; + + string typeModifiers = containingType.IsStatic ? "static partial" : "partial"; + builder.AppendLine($"{typeModifiers} {typeKeyword} {containingType.Name}"); + builder.AppendLine("{"); + + string returnTypeName = methodData.ReturnTypeName; + string methodName = methodData.MethodName; + string parameters = BuildParameterList(methodData.ParameterTypeNames, parameterNames); + string staticModifier = containingType.IsStatic ? "static " : ""; + + builder.AppendLine($" public {staticModifier}{returnTypeName} {methodName}({parameters})"); + builder.AppendLine(" {"); + } + + private static string BuildParameterList(IReadOnlyList parameterTypeNames, List? parameterNames = null) + { + List parameters = new(); + for (int index = 0; index < parameterTypeNames.Count; index++) + { + string paramName = (parameterNames != null && index < parameterNames.Count) + ? parameterNames[index] + : GetParameterName(parameterTypeNames[index]); + parameters.Add($"{parameterTypeNames[index]} {paramName}"); + } + return string.Join(", ", parameters); + } + + private static string GetParameterName(string typeName) + { + string simpleName = typeName.Contains('.') ? typeName.Substring(typeName.LastIndexOf('.') + 1) : typeName; + return char.ToLowerInvariant(simpleName[0]) + simpleName.Substring(1); + } + + private static string FormatValueAsCSharpLiteralByTypeName(string? value, string returnTypeName) + { + if (value == null) + { + return "default"; + } + + if (returnTypeName == "System.String" || returnTypeName == "string") + { + return SyntaxFactory.Literal(value).Text; + } + + if (returnTypeName == "System.Boolean" || returnTypeName == "bool") + { + return value.ToLowerInvariant(); + } + + return value; + } + + private static string FormatKeyAsCSharpLiteralByTypeName(object key, string? parameterTypeName) + { + if (key is bool b) + { + return b ? "true" : "false"; + } + + if (key is string s) + { + return SyntaxFactory.Literal(s).Text; + } + + // Check if it looks like an enum value (the key is the enum name) + if (parameterTypeName != null && key.GetType().IsEnum) + { + return $"{parameterTypeName}.{key}"; + } + + return key.ToString()!; + } + + private static string FormatValueByReturnTypeName(string value, string returnTypeName) + { + // Check if the return type looks like an enum (contains a dot and doesn't start with System.) + // For enum values, the value will be the enum name, so prefix with the type + if (!string.IsNullOrEmpty(returnTypeName) + && returnTypeName != "System.String" && returnTypeName != "string" + && returnTypeName != "System.Int32" && returnTypeName != "int" + && returnTypeName != "System.Boolean" && returnTypeName != "bool" + && !returnTypeName.StartsWith("System.", StringComparison.Ordinal)) + { + return $"{returnTypeName}.{value}"; + } + + if (returnTypeName == "System.String" || returnTypeName == "string") + { + return SyntaxFactory.Literal(value).Text; + } + + if (returnTypeName == "System.Boolean" || returnTypeName == "bool") + { + return value.ToLowerInvariant(); + } + + return value; + } + private static void AppendNamespaceAndTypeHeader(StringBuilder builder, INamedTypeSymbol containingType, IMethodSymbol partialMethod) { builder.AppendLine("// "); diff --git a/EasySourceGenerators.Generators/MethodBodyBuilder.cs b/EasySourceGenerators.Generators/MethodBodyBuilder.cs index ba2900d..5120745 100644 --- a/EasySourceGenerators.Generators/MethodBodyBuilder.cs +++ b/EasySourceGenerators.Generators/MethodBodyBuilder.cs @@ -1,14 +1,73 @@ +using System; using EasySourceGenerators.Abstractions; namespace EasySourceGenerators.Generators; -public class MethodBodyBuilder(IMethodBodyGeneratorStage0 generatorsFactory) : IMethodBodyBuilder +public class MethodBodyBuilder(IMethodBodyGeneratorStage0 generatorsFactory) : IMethodBodyBuilderStage1 { + public IMethodBodyBuilderStage2 ForMethod() => new MethodBodyBuilderStage2(generatorsFactory); + public IMethodBodyBuilder WithParameter() => new MethodBodyBodyBuilder(generatorsFactory); public IMethodBodyGenerator WithReturnType() => generatorsFactory.CreateImplementation(); } +public class MethodBodyBuilderStage2(IMethodBodyGeneratorStage0 generatorsFactory) : IMethodBodyBuilderStage2 +{ + public IMethodBodyBuilderStage3ReturnVoid WithVoidReturnType() => new MethodBodyBuilderStage3ReturnVoid(generatorsFactory); + + public IMethodBodyBuilderStage3 WithReturnType() => new MethodBodyBuilderStage3(generatorsFactory); +} + +public class MethodBodyBuilderStage3ReturnVoid(IMethodBodyGeneratorStage0 generatorsFactory) : IMethodBodyBuilderStage3ReturnVoid +{ + public IMethodBodyBuilderStage4ReturnVoidNoArg WithNoParameters() => new MethodBodyBuilderStage4ReturnVoidNoArg(generatorsFactory); + + public IMethodBodyBuilderStage4ReturnVoid WithOneParameter() => new MethodBodyBuilderStage4ReturnVoid(generatorsFactory); +} + +public class MethodBodyBuilderStage3(IMethodBodyGeneratorStage0 generatorsFactory) : IMethodBodyBuilderStage3 +{ + public IMethodBodyBuilderStage4NoArg WithNoParameters() => new MethodBodyBuilderStage4NoArg(generatorsFactory); + + public IMethodBodyBuilderStage4 WithOneParameter() => + new MethodBodyBuilderStage4(generatorsFactory); +} + +public class MethodBodyBuilderStage4ReturnVoidNoArg(IMethodBodyGeneratorStage0 generatorsFactory) : IMethodBodyBuilderStage4ReturnVoidNoArg +{ + public IMethodBodyGenerator UseProvidedBody(Action body) + { + body(); + return generatorsFactory.CreateImplementation(); + } +} + +public class MethodBodyBuilderStage4ReturnVoid(IMethodBodyGeneratorStage0 generatorsFactory) : IMethodBodyBuilderStage4ReturnVoid +{ + public IMethodBodyGenerator UseProvidedBody(Action body) => generatorsFactory.CreateImplementation(); +} + +public class MethodBodyBuilderStage4NoArg(IMethodBodyGeneratorStage0 generatorsFactory) : IMethodBodyBuilderStage4NoArg +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => generatorsFactory.CreateImplementation(); + + public IMethodBodyGenerator BodyRetuningConstant(Func constantValueFactory) => + generatorsFactory.CreateImplementation(); +} + +public class MethodBodyBuilderStage4(IMethodBodyGeneratorStage0 generatorsFactory) : IMethodBodyBuilderStage4 +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => + generatorsFactory.CreateImplementation(); + + public IMethodBodyGenerator BodyRetuningConstant(Func constantValueFactory) => + generatorsFactory.CreateImplementation(); + + public IMethodBodyGeneratorSwitchBody BodyWithSwitchStatement() => + generatorsFactory.CreateImplementation().GenerateSwitchBody(); +} + public class MethodBodyBodyBuilder(IMethodBodyGeneratorStage0 generatorsFactory) : IMethodBodyBuilder { public IMethodBodyGenerator WithReturnType() => generatorsFactory.CreateImplementation(); diff --git a/EasySourceGenerators.Generators/RecordingGeneratorsFactory.cs b/EasySourceGenerators.Generators/RecordingGeneratorsFactory.cs index 56f2592..d70283b 100644 --- a/EasySourceGenerators.Generators/RecordingGeneratorsFactory.cs +++ b/EasySourceGenerators.Generators/RecordingGeneratorsFactory.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using EasySourceGenerators.Abstractions; +using EasySourceGenerators.Abstractions.Method; namespace EasySourceGenerators.Generators; @@ -12,9 +13,30 @@ public class SwitchBodyRecord public bool HasDefaultCase { get; set; } } -public class RecordingGeneratorsFactory : IMethodBodyGeneratorStage0 +public class MethodRecord +{ + public string? MethodName { get; set; } + public string? ReturnTypeName { get; set; } + public List ParameterTypeNames { get; } = new(); + public object? ConstantValue { get; set; } + public SwitchBodyRecord? SwitchBody { get; set; } + public bool IsEntireMethodGeneration { get; set; } +} + +public class RecordingGeneratorsFactory : IMethodBodyGeneratorStage0, IGeneratorsFactory { public SwitchBodyRecord? LastRecord { get; private set; } + public MethodRecord? LastMethodRecord { get; private set; } + + public IMethodBodyBuilderStage1 StartFluentApiBuilderForBody() => new MethodBodyBuilder(this); + + public IMethodBuilderStage1 StartFluentApiBuilderForMethod() + { + MethodRecord record = new MethodRecord(); + record.IsEntireMethodGeneration = true; + LastMethodRecord = record; + return new RecordingMethodBuilderStage1(record, this); + } public IMethodBodyGeneratorWithNoParameter CreateImplementation() { @@ -38,13 +60,22 @@ public IMethodBodyGenerator CreateImplementation(record); } + + internal void SetSwitchBodyFromMethod(SwitchBodyRecord record) + { + LastRecord = record; + } } public class RecordingMethodImplementationGenerator : IMethodBodyGeneratorWithNoParameter; public class RecordingMethodImplementationGeneratorTyped : IMethodBodyGenerator { - public IMethodBodyGeneratorWithNoParameter BodyReturningConstantValue(Func body) => this; + public IMethodBodyGeneratorWithNoParameter BodyReturningConstantValue(Func body) + { + body(); + return new RecordingMethodImplementationGenerator(); + } } public class RecordingMethodImplementationGenerator(SwitchBodyRecord record) : IMethodBodyGenerator @@ -106,7 +137,7 @@ public IMethodBodyGeneratorSwitchBody ReturnConstantValue(Fu return new RecordingMethodImplementationGeneratorSwitchBody(record); } - public IMethodBodyGeneratorSwitchBody UseBody(Func> body) + public IMethodBodyGeneratorSwitchBody UseProvidedBody(Func body) { foreach (TArg1? caseValue in cases) { @@ -120,15 +151,284 @@ public IMethodBodyGeneratorSwitchBody UseBody(Func(SwitchBodyRecord record) : IMethodBodyGeneratorSwitchBodyDefaultCase { - public IMethodBodyGenerator ReturnConstantValue(Func func) + public IMethodBodyGenerator ReturnConstantValue(Func func) { record.HasDefaultCase = true; return new RecordingMethodImplementationGenerator(record); } - public IMethodBodyGenerator UseProvidedBody(Func> func) + public IMethodBodyGenerator UseProvidedBody(Func func) { record.HasDefaultCase = true; return new RecordingMethodImplementationGenerator(record); } } + +// Recording implementations for Method builder (entire method generation) +public class RecordingMethodBuilderStage1(MethodRecord methodRecord, RecordingGeneratorsFactory factory) : IMethodBuilderStage1 +{ + public IMethodBuilderStage2 WithName(string name) + { + methodRecord.MethodName = name; + return new RecordingMethodBuilderStage2(methodRecord, factory); + } + + public IMethodBuilderStage2 WithName(Func nameFactory) + { + methodRecord.MethodName = nameFactory(); + return new RecordingMethodBuilderStage2(methodRecord, factory); + } +} + +public class RecordingMethodBuilderStage2(MethodRecord methodRecord, RecordingGeneratorsFactory factory) : IMethodBuilderStage2 +{ + public IMethodBuilderStage3ReturningVoid WithVoidReturn() + { + methodRecord.ReturnTypeName = "void"; + return new RecordingMethodBuilderStage3ReturningVoid(methodRecord, factory); + } + + public IMethodBuilderStage3 WithReturnType() + { + methodRecord.ReturnTypeName = typeof(TReturnType).FullName ?? typeof(TReturnType).Name; + return new RecordingMethodBuilderStage3(methodRecord, factory); + } + + public IMethodBuilderStage3WithSomeReturnType WithReturnType(Type returnType) + { + methodRecord.ReturnTypeName = returnType.FullName ?? returnType.Name; + return new RecordingMethodBuilderStage3WithSomeReturnType(methodRecord, factory); + } + + public IMethodBuilderStage3WithSomeReturnType WithReturnType(string returnType) + { + methodRecord.ReturnTypeName = returnType; + return new RecordingMethodBuilderStage3WithSomeReturnType(methodRecord, factory); + } + + public IMethodBuilderStage3WithSomeReturnType WithReturnType(Func returnTypeFactory) + { + Type returnType = returnTypeFactory(); + methodRecord.ReturnTypeName = returnType.FullName ?? returnType.Name; + return new RecordingMethodBuilderStage3WithSomeReturnType(methodRecord, factory); + } + + public IMethodBuilderStage3WithSomeReturnType WithReturnType(Func returnTypeFactory) + { + methodRecord.ReturnTypeName = returnTypeFactory(); + return new RecordingMethodBuilderStage3WithSomeReturnType(methodRecord, factory); + } +} + +public class RecordingMethodBuilderStage3ReturningVoid(MethodRecord methodRecord, RecordingGeneratorsFactory factory) : IMethodBuilderStage3ReturningVoid +{ + public IMethodBuilderStage4ReturnVoidNoParam WithNoParameters() => + new RecordingMethodBuilderStage4ReturnVoidNoParam(methodRecord); + + public IMethodBuilderStage4ReturnVoid WithParameter() + { + methodRecord.ParameterTypeNames.Add(typeof(TParam1).FullName ?? typeof(TParam1).Name); + return new RecordingMethodBuilderStage4ReturnVoid(methodRecord); + } + + public IMethodBuilderStage4ReturnVoid WithParameter(Type param1) + { + methodRecord.ParameterTypeNames.Add(param1.FullName ?? param1.Name); + return new RecordingMethodBuilderStage4ReturnVoidNonGeneric(methodRecord); + } + + public IMethodBuilderStage4ReturnVoid WithParameter(string param1) + { + methodRecord.ParameterTypeNames.Add(param1); + return new RecordingMethodBuilderStage4ReturnVoidNonGeneric(methodRecord); + } + + public IMethodBuilderStage4ReturnVoid WithParameter(Func param1Factory) + { + Type param1 = param1Factory(); + methodRecord.ParameterTypeNames.Add(param1.FullName ?? param1.Name); + return new RecordingMethodBuilderStage4ReturnVoidNonGeneric(methodRecord); + } + + public IMethodBuilderStage4ReturnVoid WithParameter(Func param1Factory) + { + methodRecord.ParameterTypeNames.Add(param1Factory()); + return new RecordingMethodBuilderStage4ReturnVoidNonGeneric(methodRecord); + } +} + +public class RecordingMethodBuilderStage3(MethodRecord methodRecord, RecordingGeneratorsFactory factory) : IMethodBuilderStage3 +{ + public IMethodBuilderStage4NoParam WithNoParameters() => + new RecordingMethodBuilderStage4NoParam(methodRecord, factory); + + public IMethodBuilderStage4 WithParameter() + { + methodRecord.ParameterTypeNames.Add(typeof(TParam1).FullName ?? typeof(TParam1).Name); + return new RecordingMethodBuilderStage4(methodRecord, factory); + } + + public IMethodBuilderStage4WithSomeParam WithParameter(Type param1) + { + methodRecord.ParameterTypeNames.Add(param1.FullName ?? param1.Name); + return new RecordingMethodBuilderStage4WithSomeParam(methodRecord, factory); + } + + public IMethodBuilderStage4WithSomeParam WithParameter(string param1) + { + methodRecord.ParameterTypeNames.Add(param1); + return new RecordingMethodBuilderStage4WithSomeParam(methodRecord, factory); + } + + public IMethodBuilderStage4WithSomeParam WithParameter(Func param1Factory) + { + Type param1 = param1Factory(); + methodRecord.ParameterTypeNames.Add(param1.FullName ?? param1.Name); + return new RecordingMethodBuilderStage4WithSomeParam(methodRecord, factory); + } + + public IMethodBuilderStage4WithSomeParam WithParameter(Func param1Factory) + { + methodRecord.ParameterTypeNames.Add(param1Factory()); + return new RecordingMethodBuilderStage4WithSomeParam(methodRecord, factory); + } +} + +public class RecordingMethodBuilderStage3WithSomeReturnType(MethodRecord methodRecord, RecordingGeneratorsFactory factory) : IMethodBuilderStage3WithSomeReturnType +{ + public IMethodBuilderStage4WithSomeReturnTypeNoParam WithNoParameters() => + new RecordingMethodBuilderStage4WithSomeReturnTypeNoParam(methodRecord, factory); + + public IMethodBuilderStage4WithSomeReturnType WithParameter() + { + methodRecord.ParameterTypeNames.Add(typeof(TParam1).FullName ?? typeof(TParam1).Name); + return new RecordingMethodBuilderStage4WithSomeReturnType(methodRecord, factory); + } + + public IMethodBuilderStage4WithSomeReturnTypeWithSomeParam WithParameter(Type param1) + { + methodRecord.ParameterTypeNames.Add(param1.FullName ?? param1.Name); + return new RecordingMethodBuilderStage4WithSomeReturnTypeWithSomeParam(methodRecord, factory); + } + + public IMethodBuilderStage4WithSomeReturnTypeWithSomeParam WithParameter(string param1) + { + methodRecord.ParameterTypeNames.Add(param1); + return new RecordingMethodBuilderStage4WithSomeReturnTypeWithSomeParam(methodRecord, factory); + } + + public IMethodBuilderStage4WithSomeReturnTypeWithSomeParam WithParameter(Func param1Factory) + { + Type param1 = param1Factory(); + methodRecord.ParameterTypeNames.Add(param1.FullName ?? param1.Name); + return new RecordingMethodBuilderStage4WithSomeReturnTypeWithSomeParam(methodRecord, factory); + } + + public IMethodBuilderStage4WithSomeReturnTypeWithSomeParam WithParameter(Func param1Factory) + { + methodRecord.ParameterTypeNames.Add(param1Factory()); + return new RecordingMethodBuilderStage4WithSomeReturnTypeWithSomeParam(methodRecord, factory); + } +} + +// Stage4 recording implementations - Terminal methods +public class RecordingMethodBuilderStage4ReturnVoidNoParam(MethodRecord methodRecord) : IMethodBuilderStage4ReturnVoidNoParam +{ + public IMethodBodyGenerator UseProvidedBody(Action body) => new RecordingMethodImplementationGenerator(); +} + +public class RecordingMethodBuilderStage4ReturnVoid(MethodRecord methodRecord) : IMethodBuilderStage4ReturnVoid +{ + public IMethodBodyGenerator UseProvidedBody(Action body) => new RecordingMethodImplementationGenerator(); +} + +public class RecordingMethodBuilderStage4ReturnVoidNonGeneric(MethodRecord methodRecord) : IMethodBuilderStage4ReturnVoid +{ + public IMethodBodyGenerator UseProvidedBody(Action body) => new RecordingMethodImplementationGenerator(); +} + +public class RecordingMethodBuilderStage4NoParam(MethodRecord methodRecord, RecordingGeneratorsFactory factory) + : IMethodBuilderStage4NoParam +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new RecordingMethodImplementationGenerator(); + + public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) + { + object? value = constantValueFactory(); + methodRecord.ConstantValue = value; + return new RecordingMethodImplementationGenerator(); + } +} + +public class RecordingMethodBuilderStage4(MethodRecord methodRecord, RecordingGeneratorsFactory factory) + : IMethodBuilderStage4 +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new RecordingMethodImplementationGenerator(); + + public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) + { + object? value = constantValueFactory(); + methodRecord.ConstantValue = value; + return new RecordingMethodImplementationGenerator(); + } + + public IMethodBodyGeneratorSwitchBody BodyWithSwitchStatement() + { + SwitchBodyRecord switchRecord = new SwitchBodyRecord(); + methodRecord.SwitchBody = switchRecord; + factory.SetSwitchBodyFromMethod(switchRecord); + return new RecordingMethodImplementationGeneratorSwitchBody(switchRecord); + } +} + +public class RecordingMethodBuilderStage4WithSomeParam(MethodRecord methodRecord, RecordingGeneratorsFactory factory) + : IMethodBuilderStage4WithSomeParam +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new RecordingMethodImplementationGenerator(); + + public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) + { + object? value = constantValueFactory(); + methodRecord.ConstantValue = value; + return new RecordingMethodImplementationGenerator(); + } +} + +public class RecordingMethodBuilderStage4WithSomeReturnTypeNoParam(MethodRecord methodRecord, RecordingGeneratorsFactory factory) + : IMethodBuilderStage4WithSomeReturnTypeNoParam +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new RecordingMethodImplementationGenerator(); + + public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) + { + object? value = constantValueFactory(); + methodRecord.ConstantValue = value; + return new RecordingMethodImplementationGenerator(); + } +} + +public class RecordingMethodBuilderStage4WithSomeReturnType(MethodRecord methodRecord, RecordingGeneratorsFactory factory) + : IMethodBuilderStage4WithSomeReturnType +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new RecordingMethodImplementationGenerator(); + + public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) + { + object? value = constantValueFactory(); + methodRecord.ConstantValue = value; + return new RecordingMethodImplementationGenerator(); + } +} + +public class RecordingMethodBuilderStage4WithSomeReturnTypeWithSomeParam(MethodRecord methodRecord, RecordingGeneratorsFactory factory) + : IMethodBuilderStage4WithSomeReturnTypeWithSomeParam +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new RecordingMethodImplementationGenerator(); + + public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) + { + object? value = constantValueFactory(); + methodRecord.ConstantValue = value; + return new RecordingMethodImplementationGenerator(); + } +} diff --git a/EasySourceGenerators.Tests/ColorsClassFluentCreatedTests.cs b/EasySourceGenerators.Tests/ColorsClassFluentCreatedTests.cs new file mode 100644 index 0000000..1c82973 --- /dev/null +++ b/EasySourceGenerators.Tests/ColorsClassFluentCreatedTests.cs @@ -0,0 +1,47 @@ +using EasySourceGenerators.Abstractions; + +namespace EasySourceGenerators.Tests; + +[TestFixture] +public class ColorsClassFluentCreatedTests +{ + [Test] + public void ColorsClassFluentCreated_ProducesExpectedRuntimeOutput() + { + TestColorsClassFluentCreated testColorsClass = new TestColorsClassFluentCreated(); + + string allColors = testColorsClass.GetAllColorsString(); + + Assert.That(allColors, Is.EqualTo("Red, Green, Blue")); + } + + [Test] + public void ColorsClassFluentCreated_ProducesExpectedGeneratedCode() + { + string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCode("TestColorsClassFluentCreated_GetAllColorsString.g.cs"); + string expectedCode = """ + namespace EasySourceGenerators.Tests; + + partial class TestColorsClassFluentCreated + { + public System.String GetAllColorsString() + { + return "Red, Green, Blue"; + } + } + """.ReplaceLineEndings("\n").TrimEnd(); + + Assert.That(generatedCode, Is.EqualTo(expectedCode)); + } +} + +public partial class TestColorsClassFluentCreated +{ + [MethodBodyGenerator("GetAllColorsString")] + static IMethodBodyGenerator GetAllColorsString_Generator() => + Generate.Method() + .WithName("GetAllColorsString") + .WithReturnType() + .WithNoParameters() + .BodyReturningConstant(() => string.Join(", ", Enum.GetNames())); +} diff --git a/EasySourceGenerators.Tests/MapperFluentCreatedTests.cs b/EasySourceGenerators.Tests/MapperFluentCreatedTests.cs new file mode 100644 index 0000000..d8527a6 --- /dev/null +++ b/EasySourceGenerators.Tests/MapperFluentCreatedTests.cs @@ -0,0 +1,68 @@ +using EasySourceGenerators.Abstractions; + +namespace EasySourceGenerators.Tests; + +[TestFixture] +public class MapperFluentCreatedTests +{ + [TestCase(TestFourLeggedAnimal.Dog, TestMammalAnimal.Dog)] + [TestCase(TestFourLeggedAnimal.Cat, TestMammalAnimal.Cat)] + public void MapperFluentCreated_ProducesExpectedRuntimeOutput(TestFourLeggedAnimal source, TestMammalAnimal expected) + { + TestMapperFluentCreated mapper = new TestMapperFluentCreated(); + + TestMammalAnimal result = mapper.MapToMammal(source); + + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void MapperFluentCreated_ThrowsForUnmappableValue() + { + TestMapperFluentCreated mapper = new TestMapperFluentCreated(); + + Assert.Throws(() => mapper.MapToMammal(TestFourLeggedAnimal.Lizard)); + } + + [Test] + public void MapperFluentCreated_ProducesExpectedGeneratedCode() + { + string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCode("TestMapperFluentCreated_MapToMammal.g.cs"); + string expectedCode = """ + namespace EasySourceGenerators.Tests; + + partial class TestMapperFluentCreated + { + public EasySourceGenerators.Tests.TestMammalAnimal MapToMammal(EasySourceGenerators.Tests.TestFourLeggedAnimal fourLeggedAnimal) + { + switch (fourLeggedAnimal) + { + case EasySourceGenerators.Tests.TestFourLeggedAnimal.Dog: return EasySourceGenerators.Tests.TestMammalAnimal.Dog; + case EasySourceGenerators.Tests.TestFourLeggedAnimal.Cat: return EasySourceGenerators.Tests.TestMammalAnimal.Cat; + default: throw new ArgumentException($"Cannot map {fourLeggedAnimal} to a Mammal"); + } + } + } + """.ReplaceLineEndings("\n").TrimEnd(); + + Assert.That(generatedCode, Is.EqualTo(expectedCode)); + } +} + +public partial class TestMapperFluentCreated +{ + [MethodBodyGenerator("MapToMammal")] + static IMethodBodyGenerator MapToMammal_Generator() => + Generate.Method() + .WithName("MapToMammal") + .WithReturnType() + .WithParameter() + .BodyWithSwitchStatement() + .ForCases(GetFourLeggedAnimalsThatAreAlsoMammal()).ReturnConstantValue(a => Enum.Parse(a.ToString(), true)) + .ForDefaultCase().UseProvidedBody(fourLeggedAnimal => throw new ArgumentException($"Cannot map {fourLeggedAnimal} to a Mammal")); + + static TestFourLeggedAnimal[] GetFourLeggedAnimalsThatAreAlsoMammal() => + Enum.GetValues() + .Where(a => Enum.TryParse(typeof(TestMammalAnimal), a.ToString(), true, out _)) + .ToArray(); +} diff --git a/EasySourceGenerators.Tests/PiExampleFluentTests.cs b/EasySourceGenerators.Tests/PiExampleFluentTests.cs index d26b339..dcb5c9d 100644 --- a/EasySourceGenerators.Tests/PiExampleFluentTests.cs +++ b/EasySourceGenerators.Tests/PiExampleFluentTests.cs @@ -137,7 +137,7 @@ static IMethodBodyGenerator MapToMammal_Generator() => .MethodBody().WithParameter().WithReturnType() .GenerateSwitchBody() .ForCases(GetFourLeggedAnimalsThatAreAlsoMammal()).ReturnConstantValue(a => Enum.Parse(a.ToString(), true)) - .ForDefaultCase().UseBody(fourLeggedAnimal => () => throw new ArgumentException($"Cannot map {fourLeggedAnimal} to a mammal")); + .ForDefaultCase().UseProvidedBody(fourLeggedAnimal => throw new ArgumentException($"Cannot map {fourLeggedAnimal} to a mammal")); static TestFourLeggedAnimal[] GetFourLeggedAnimalsThatAreAlsoMammal() => Enum.GetValues() @@ -155,7 +155,7 @@ static IMethodBodyGenerator GetPiDecimal_Generator() => .MethodBody().WithParameter().WithReturnType() .GenerateSwitchBody() .ForCases(0, 1, 2, new[]{300, 301, 302, 303}).ReturnConstantValue(decimalNumber => TestSlowMath.CalculatePiDecimal(decimalNumber)) - .ForDefaultCase().UseBody(decimalNumber => () => TestSlowMath.CalculatePiDecimal(decimalNumber)); + .ForDefaultCase().UseProvidedBody(decimalNumber => TestSlowMath.CalculatePiDecimal(decimalNumber)); } public static partial class TestMapperFluent @@ -169,5 +169,5 @@ static IMethodBodyGenerator MapToMammal_Generator() => .GenerateSwitchBody() .ForCases(1).ReturnConstantValue(_ => "Dog") .ForCases(2).ReturnConstantValue(_ => "Cat") - .ForDefaultCase().UseBody(_ => () => "Unknown"); + .ForDefaultCase().UseProvidedBody(_ => "Unknown"); }