diff --git a/EasySourceGenerators.GeneratorTests/BodyGenerationDataExtractorTests.cs b/EasySourceGenerators.GeneratorTests/BodyGenerationDataExtractorTests.cs new file mode 100644 index 0000000..da242a2 --- /dev/null +++ b/EasySourceGenerators.GeneratorTests/BodyGenerationDataExtractorTests.cs @@ -0,0 +1,128 @@ +using EasySourceGenerators.Generators.DataBuilding; +using EasySourceGenerators.Generators.IncrementalGenerators; + +namespace EasySourceGenerators.GeneratorTests; + +[TestFixture] +public class BodyGenerationDataExtractorTests +{ + [Test] + public void Extract_WithCompileTimeConstants_BodyReturningConstant_InvokesWithConstants() + { + DataMethodBodyGenerator generator = new DataMethodBodyGenerator( + new BodyGenerationData( + ReturnType: typeof(string), + ParametersTypes: [], + CompileTimeConstants: 42, + ReturnConstantValueFactory: (Func)(constants => $"value_{constants}"))); + + FluentBodyResult result = BodyGenerationDataExtractor.Extract(generator, false); + + Assert.That(result.ReturnValue, Is.EqualTo("value_42")); + Assert.That(result.IsVoid, Is.False); + } + + [Test] + public void Extract_WithCompileTimeConstants_RuntimeBodyNoArgs_InvokesWithConstants() + { + DataMethodBodyGenerator generator = new DataMethodBodyGenerator( + new BodyGenerationData( + ReturnType: typeof(string), + ParametersTypes: [], + CompileTimeConstants: 10, + RuntimeDelegateBody: (Func)(constants => $"body_{constants}"))); + + FluentBodyResult result = BodyGenerationDataExtractor.Extract(generator, false); + + Assert.That(result.ReturnValue, Is.EqualTo("body_10")); + Assert.That(result.IsVoid, Is.False); + } + + [Test] + public void Extract_WithCompileTimeConstants_RuntimeBodyWithAdditionalParams_ReturnsNullValue() + { + DataMethodBodyGenerator generator = new DataMethodBodyGenerator( + new BodyGenerationData( + ReturnType: typeof(string), + ParametersTypes: [typeof(int)], + CompileTimeConstants: 10, + RuntimeDelegateBody: (Func)((constants, param) => $"{constants}_{param}"))); + + FluentBodyResult result = BodyGenerationDataExtractor.Extract(generator, false); + + Assert.That(result.ReturnValue, Is.Null); + Assert.That(result.IsVoid, Is.False); + } + + [Test] + public void Extract_WithoutConstants_BodyReturningConstant_InvokesWithoutArgs() + { + DataMethodBodyGenerator generator = new DataMethodBodyGenerator( + new BodyGenerationData( + ReturnType: typeof(int), + ParametersTypes: [], + ReturnConstantValueFactory: (Func)(() => 99))); + + FluentBodyResult result = BodyGenerationDataExtractor.Extract(generator, false); + + Assert.That(result.ReturnValue, Is.EqualTo("99")); + } + + [Test] + public void Extract_WithoutConstants_RuntimeBodyNoParams_InvokesDirectly() + { + DataMethodBodyGenerator generator = new DataMethodBodyGenerator( + new BodyGenerationData( + ReturnType: typeof(string), + ParametersTypes: [], + RuntimeDelegateBody: (Func)(() => "hello"))); + + FluentBodyResult result = BodyGenerationDataExtractor.Extract(generator, false); + + Assert.That(result.ReturnValue, Is.EqualTo("hello")); + } + + [Test] + public void Extract_VoidReturnType_WithConstants_RuntimeBody_InvokesWithConstants() + { + string captured = ""; + DataMethodBodyGenerator generator = new DataMethodBodyGenerator( + new BodyGenerationData( + ReturnType: typeof(void), + ParametersTypes: [], + CompileTimeConstants: "test", + RuntimeDelegateBody: (Action)(constants => { captured = constants; }))); + + FluentBodyResult result = BodyGenerationDataExtractor.Extract(generator, true); + + Assert.That(result.IsVoid, Is.True); + Assert.That(captured, Is.EqualTo("test")); + } + + [Test] + public void Extract_NullBodyGenerationData_ReturnsNullReturnValue() + { + DataMethodBodyGenerator generator = new DataMethodBodyGenerator( + new BodyGenerationData(ReturnType: typeof(string))); + + FluentBodyResult result = BodyGenerationDataExtractor.Extract(generator, false); + + Assert.That(result.ReturnValue, Is.Null); + } + + [Test] + public void Extract_WithCompileTimeConstants_ConstantFactoryTakesPriority() + { + DataMethodBodyGenerator generator = new DataMethodBodyGenerator( + new BodyGenerationData( + ReturnType: typeof(string), + ParametersTypes: [], + CompileTimeConstants: 5, + ReturnConstantValueFactory: (Func)(constants => $"factory_{constants}"), + RuntimeDelegateBody: (Func)(constants => $"body_{constants}"))); + + FluentBodyResult result = BodyGenerationDataExtractor.Extract(generator, false); + + Assert.That(result.ReturnValue, Is.EqualTo("factory_5")); + } +} diff --git a/EasySourceGenerators.GeneratorTests/MethodBodyBuilderTests.cs b/EasySourceGenerators.GeneratorTests/MethodBodyBuilderTests.cs index a1ad3ed..04486cd 100644 --- a/EasySourceGenerators.GeneratorTests/MethodBodyBuilderTests.cs +++ b/EasySourceGenerators.GeneratorTests/MethodBodyBuilderTests.cs @@ -189,4 +189,198 @@ public void FullFluentChain_UseProvidedBody_ProducesCorrectData() object? bodyValue = generator.Data.RuntimeDelegateBody!.DynamicInvoke(); Assert.That(bodyValue, Is.EqualTo(42)); } + + [Test] + public void WithCompileTimeConstants_WithParam_ReturnsStage5WithConstants() + { + DataMethodBodyBuilderStage4 stage4 = new DataMethodBodyBuilderStage4( + new BodyGenerationData(ReturnType: typeof(string), ParametersTypes: [typeof(int)])); + + IMethodBodyBuilderStage5WithConstants result = stage4.WithCompileTimeConstants(() => 42); + + Assert.That(result, Is.TypeOf>()); + DataMethodBodyBuilderStage5WithConstants stage5 = (DataMethodBodyBuilderStage5WithConstants)result; + Assert.That(stage5.Data.CompileTimeConstants, Is.EqualTo(42)); + } + + [Test] + public void WithCompileTimeConstants_NoArg_ReturnsStage5NoArgWithConstants() + { + DataMethodBodyBuilderStage4NoArg stage4 = new DataMethodBodyBuilderStage4NoArg( + new BodyGenerationData(ReturnType: typeof(string), ParametersTypes: [])); + + IMethodBodyBuilderStage5NoArgWithConstants result = stage4.WithCompileTimeConstants(() => 99); + + Assert.That(result, Is.TypeOf>()); + DataMethodBodyBuilderStage5NoArgWithConstants stage5 = (DataMethodBodyBuilderStage5NoArgWithConstants)result; + Assert.That(stage5.Data.CompileTimeConstants, Is.EqualTo(99)); + } + + [Test] + public void WithCompileTimeConstants_ReturnVoid_ReturnsStage5ReturnVoidWithConstants() + { + DataMethodBodyBuilderStage4ReturnVoid stage4 = new DataMethodBodyBuilderStage4ReturnVoid( + new BodyGenerationData(ReturnType: typeof(void), ParametersTypes: [typeof(int)])); + + IMethodBodyBuilderStage5ReturnVoidWithConstants result = stage4.WithCompileTimeConstants(() => "test"); + + Assert.That(result, Is.TypeOf>()); + DataMethodBodyBuilderStage5ReturnVoidWithConstants stage5 = (DataMethodBodyBuilderStage5ReturnVoidWithConstants)result; + Assert.That(stage5.Data.CompileTimeConstants, Is.EqualTo("test")); + } + + [Test] + public void WithCompileTimeConstants_ReturnVoidNoArg_ReturnsStage5ReturnVoidNoArgWithConstants() + { + DataMethodBodyBuilderStage4ReturnVoidNoArg stage4 = new DataMethodBodyBuilderStage4ReturnVoidNoArg( + new BodyGenerationData(ReturnType: typeof(void), ParametersTypes: [])); + + IMethodBodyBuilderStage5ReturnVoidNoArgWithConstants result = stage4.WithCompileTimeConstants(() => 7); + + Assert.That(result, Is.TypeOf>()); + DataMethodBodyBuilderStage5ReturnVoidNoArgWithConstants stage5 = (DataMethodBodyBuilderStage5ReturnVoidNoArgWithConstants)result; + Assert.That(stage5.Data.CompileTimeConstants, Is.EqualTo(7)); + } + + [Test] + public void Stage5WithConstants_UseProvidedBody_SetsRuntimeDelegateBody() + { + DataMethodBodyBuilderStage5WithConstants stage5 = new DataMethodBodyBuilderStage5WithConstants( + new BodyGenerationData(ReturnType: typeof(string), ParametersTypes: [typeof(int)], CompileTimeConstants: 42)); + + IMethodBodyGenerator result = stage5.UseProvidedBody((constants, param) => $"{constants}_{param}"); + + Assert.That(result, Is.TypeOf()); + DataMethodBodyGenerator generator = (DataMethodBodyGenerator)result; + Assert.That(generator.Data.RuntimeDelegateBody, Is.Not.Null); + Assert.That(generator.Data.CompileTimeConstants, Is.EqualTo(42)); + } + + [Test] + public void Stage5WithConstants_BodyReturningConstant_SetsReturnConstantValueFactory() + { + DataMethodBodyBuilderStage5WithConstants stage5 = new DataMethodBodyBuilderStage5WithConstants( + new BodyGenerationData(ReturnType: typeof(string), ParametersTypes: [typeof(int)], CompileTimeConstants: 42)); + + IMethodBodyGenerator result = stage5.BodyReturningConstant(constants => $"value_{constants}"); + + Assert.That(result, Is.TypeOf()); + DataMethodBodyGenerator generator = (DataMethodBodyGenerator)result; + Assert.That(generator.Data.ReturnConstantValueFactory, Is.Not.Null); + Assert.That(generator.Data.CompileTimeConstants, Is.EqualTo(42)); + } + + [Test] + public void Stage5NoArgWithConstants_UseProvidedBody_SetsRuntimeDelegateBody() + { + DataMethodBodyBuilderStage5NoArgWithConstants stage5 = new DataMethodBodyBuilderStage5NoArgWithConstants( + new BodyGenerationData(ReturnType: typeof(string), ParametersTypes: [], CompileTimeConstants: 10)); + + IMethodBodyGenerator result = stage5.UseProvidedBody(constants => $"value_{constants}"); + + Assert.That(result, Is.TypeOf()); + DataMethodBodyGenerator generator = (DataMethodBodyGenerator)result; + Assert.That(generator.Data.RuntimeDelegateBody, Is.Not.Null); + Assert.That(generator.Data.CompileTimeConstants, Is.EqualTo(10)); + } + + [Test] + public void Stage5NoArgWithConstants_BodyReturningConstant_SetsReturnConstantValueFactory() + { + DataMethodBodyBuilderStage5NoArgWithConstants stage5 = new DataMethodBodyBuilderStage5NoArgWithConstants( + new BodyGenerationData(ReturnType: typeof(string), ParametersTypes: [], CompileTimeConstants: 10)); + + IMethodBodyGenerator result = stage5.BodyReturningConstant(constants => $"const_{constants}"); + + Assert.That(result, Is.TypeOf()); + DataMethodBodyGenerator generator = (DataMethodBodyGenerator)result; + Assert.That(generator.Data.ReturnConstantValueFactory, Is.Not.Null); + Assert.That(generator.Data.CompileTimeConstants, Is.EqualTo(10)); + } + + [Test] + public void Stage5ReturnVoidWithConstants_UseProvidedBody_SetsRuntimeDelegateBody() + { + DataMethodBodyBuilderStage5ReturnVoidWithConstants stage5 = new DataMethodBodyBuilderStage5ReturnVoidWithConstants( + new BodyGenerationData(ReturnType: typeof(void), ParametersTypes: [typeof(int)], CompileTimeConstants: "ctx")); + + IMethodBodyGenerator result = stage5.UseProvidedBody((constants, param) => { }); + + Assert.That(result, Is.TypeOf()); + DataMethodBodyGenerator generator = (DataMethodBodyGenerator)result; + Assert.That(generator.Data.RuntimeDelegateBody, Is.Not.Null); + Assert.That(generator.Data.CompileTimeConstants, Is.EqualTo("ctx")); + } + + [Test] + public void Stage5ReturnVoidNoArgWithConstants_UseProvidedBody_SetsRuntimeDelegateBody() + { + DataMethodBodyBuilderStage5ReturnVoidNoArgWithConstants stage5 = new DataMethodBodyBuilderStage5ReturnVoidNoArgWithConstants( + new BodyGenerationData(ReturnType: typeof(void), ParametersTypes: [], CompileTimeConstants: "ctx")); + + IMethodBodyGenerator result = stage5.UseProvidedBody(constants => { }); + + Assert.That(result, Is.TypeOf()); + DataMethodBodyGenerator generator = (DataMethodBodyGenerator)result; + Assert.That(generator.Data.RuntimeDelegateBody, Is.Not.Null); + Assert.That(generator.Data.CompileTimeConstants, Is.EqualTo("ctx")); + } + + [Test] + public void FullFluentChain_WithConstants_NoArg_BodyReturningConstant_ProducesCorrectData() + { + DataGeneratorsFactory factory = new DataGeneratorsFactory(); + + IMethodBodyGenerator result = factory.StartFluentApiBuilderForBody() + .ForMethod() + .WithReturnType() + .WithNoParameters() + .WithCompileTimeConstants(() => 42) + .BodyReturningConstant(constants => $"value_{constants}"); + + Assert.That(result, Is.TypeOf()); + DataMethodBodyGenerator generator = (DataMethodBodyGenerator)result; + Assert.That(generator.Data.ReturnType, Is.EqualTo(typeof(string))); + Assert.That(generator.Data.ParametersTypes, Is.Empty); + Assert.That(generator.Data.CompileTimeConstants, Is.EqualTo(42)); + Assert.That(generator.Data.ReturnConstantValueFactory, Is.Not.Null); + object? constantValue = generator.Data.ReturnConstantValueFactory!.DynamicInvoke(42); + Assert.That(constantValue, Is.EqualTo("value_42")); + } + + [Test] + public void FullFluentChain_WithConstants_WithParam_UseProvidedBody_ProducesCorrectData() + { + DataGeneratorsFactory factory = new DataGeneratorsFactory(); + + IMethodBodyGenerator result = factory.StartFluentApiBuilderForBody() + .ForMethod() + .WithReturnType() + .WithParameter() + .WithCompileTimeConstants(() => new { Offset = 100 }) + .UseProvidedBody((constants, param) => constants.Offset + param); + + Assert.That(result, Is.TypeOf()); + DataMethodBodyGenerator generator = (DataMethodBodyGenerator)result; + Assert.That(generator.Data.ReturnType, Is.EqualTo(typeof(int))); + Assert.That(generator.Data.ParametersTypes, Is.EqualTo(new[] { typeof(int) })); + Assert.That(generator.Data.CompileTimeConstants, Is.Not.Null); + Assert.That(generator.Data.RuntimeDelegateBody, Is.Not.Null); + } + + [Test] + public void WithCompileTimeConstants_FactoryIsInvokedImmediately() + { + int invocationCount = 0; + DataMethodBodyBuilderStage4NoArg stage4 = new DataMethodBodyBuilderStage4NoArg( + new BodyGenerationData(ReturnType: typeof(string), ParametersTypes: [])); + + stage4.WithCompileTimeConstants(() => + { + invocationCount++; + return 42; + }); + + Assert.That(invocationCount, Is.EqualTo(1)); + } } diff --git a/EasySourceGenerators.Generators/DataBuilding/DataMethodBodyBuilders.cs b/EasySourceGenerators.Generators/DataBuilding/DataMethodBodyBuilders.cs index cef2554..02e3695 100644 --- a/EasySourceGenerators.Generators/DataBuilding/DataMethodBodyBuilders.cs +++ b/EasySourceGenerators.Generators/DataBuilding/DataMethodBodyBuilders.cs @@ -38,6 +38,9 @@ public IMethodBodyBuilderStage4 WithParameter() => public record DataMethodBodyBuilderStage4(BodyGenerationData Data) : IMethodBodyBuilderStage4 { + public IMethodBodyBuilderStage5WithConstants WithCompileTimeConstants(Func compileTimeConstantsFactory) => + new DataMethodBodyBuilderStage5WithConstants(Data with { CompileTimeConstants = compileTimeConstantsFactory() }); + public IMethodBodyGenerator UseProvidedBody(Func body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body }); public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) => @@ -46,6 +49,9 @@ public IMethodBodyGenerator BodyReturningConstant(Func constantValu public record DataMethodBodyBuilderStage4NoArg(BodyGenerationData Data) : IMethodBodyBuilderStage4NoArg { + public IMethodBodyBuilderStage5NoArgWithConstants WithCompileTimeConstants(Func compileTimeConstantsFactory) => + new DataMethodBodyBuilderStage5NoArgWithConstants(Data with { CompileTimeConstants = compileTimeConstantsFactory() }); + public IMethodBodyGenerator UseProvidedBody(Func body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body }); public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) => @@ -54,10 +60,42 @@ public IMethodBodyGenerator BodyReturningConstant(Func constantValu public record DataMethodBodyBuilderStage4ReturnVoid(BodyGenerationData BodyGenerationData) : IMethodBodyBuilderStage4ReturnVoid { + public IMethodBodyBuilderStage5ReturnVoidWithConstants WithCompileTimeConstants(Func compileTimeConstantsFactory) => + new DataMethodBodyBuilderStage5ReturnVoidWithConstants(BodyGenerationData with { CompileTimeConstants = compileTimeConstantsFactory() }); + public IMethodBodyGenerator UseProvidedBody(Action body) => new DataMethodBodyGenerator(BodyGenerationData with { RuntimeDelegateBody = body }); } public record DataMethodBodyBuilderStage4ReturnVoidNoArg(BodyGenerationData BodyGenerationData) : IMethodBodyBuilderStage4ReturnVoidNoArg { + public IMethodBodyBuilderStage5ReturnVoidNoArgWithConstants WithCompileTimeConstants(Func compileTimeConstantsFactory) => + new DataMethodBodyBuilderStage5ReturnVoidNoArgWithConstants(BodyGenerationData with { CompileTimeConstants = compileTimeConstantsFactory() }); + public IMethodBodyGenerator UseProvidedBody(Action body) => new DataMethodBodyGenerator(BodyGenerationData with { RuntimeDelegateBody = body }); +} + +public record DataMethodBodyBuilderStage5WithConstants(BodyGenerationData Data) : IMethodBodyBuilderStage5WithConstants +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body }); + + public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) => + new DataMethodBodyGenerator(Data with { ReturnConstantValueFactory = constantValueFactory }); +} + +public record DataMethodBodyBuilderStage5NoArgWithConstants(BodyGenerationData Data) : IMethodBodyBuilderStage5NoArgWithConstants +{ + public IMethodBodyGenerator UseProvidedBody(Func body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body }); + + public IMethodBodyGenerator BodyReturningConstant(Func constantValueFactory) => + new DataMethodBodyGenerator(Data with { ReturnConstantValueFactory = constantValueFactory }); +} + +public record DataMethodBodyBuilderStage5ReturnVoidWithConstants(BodyGenerationData Data) : IMethodBodyBuilderStage5ReturnVoidWithConstants +{ + public IMethodBodyGenerator UseProvidedBody(Action body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body }); +} + +public record DataMethodBodyBuilderStage5ReturnVoidNoArgWithConstants(BodyGenerationData Data) : IMethodBodyBuilderStage5ReturnVoidNoArgWithConstants +{ + public IMethodBodyGenerator UseProvidedBody(Action body) => new DataMethodBodyGenerator(Data with { RuntimeDelegateBody = body }); } \ No newline at end of file diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/BodyGenerationDataExtractor.cs b/EasySourceGenerators.Generators/IncrementalGenerators/BodyGenerationDataExtractor.cs index 9130a7a..d45328b 100644 --- a/EasySourceGenerators.Generators/IncrementalGenerators/BodyGenerationDataExtractor.cs +++ b/EasySourceGenerators.Generators/IncrementalGenerators/BodyGenerationDataExtractor.cs @@ -40,18 +40,32 @@ internal static FluentBodyResult Extract(object methodResult, bool isVoidReturnT Type? dataReturnType = returnTypeProperty?.GetValue(bodyGenerationData) as Type; bool isVoid = dataReturnType == typeof(void); - return TryExtractFromConstantFactory(dataType, bodyGenerationData, isVoid) - ?? TryExtractFromRuntimeBody(dataType, bodyGenerationData, isVoid) + object? compileTimeConstants = GetCompileTimeConstants(dataType, bodyGenerationData); + + return TryExtractFromConstantFactory(dataType, bodyGenerationData, isVoid, compileTimeConstants) + ?? TryExtractFromRuntimeBody(dataType, bodyGenerationData, isVoid, compileTimeConstants) ?? new FluentBodyResult(null, isVoid); } + /// + /// Retrieves the CompileTimeConstants value from the body generation data, if present. + /// + private static object? GetCompileTimeConstants(Type dataType, object bodyGenerationData) + { + PropertyInfo? constantsProperty = dataType.GetProperty("CompileTimeConstants"); + return constantsProperty?.GetValue(bodyGenerationData); + } + /// /// Attempts to extract a return value by invoking the ReturnConstantValueFactory delegate. + /// If is provided and the factory accepts a parameter, + /// the constants are passed as the first argument. /// private static FluentBodyResult? TryExtractFromConstantFactory( Type dataType, object bodyGenerationData, - bool isVoid) + bool isVoid, + object? compileTimeConstants) { PropertyInfo? constantFactoryProperty = dataType.GetProperty("ReturnConstantValueFactory"); Delegate? constantFactory = constantFactoryProperty?.GetValue(bodyGenerationData) as Delegate; @@ -60,19 +74,37 @@ internal static FluentBodyResult Extract(object methodResult, bool isVoidReturnT return null; } - object? constantValue = constantFactory.DynamicInvoke(); + ParameterInfo[] factoryParams = constantFactory.Method.GetParameters(); + object? constantValue; + + if (factoryParams.Length == 1 && compileTimeConstants != null) + { + constantValue = constantFactory.DynamicInvoke(compileTimeConstants); + } + else if (factoryParams.Length == 0) + { + constantValue = constantFactory.DynamicInvoke(); + } + else + { + return null; + } + return new FluentBodyResult(constantValue?.ToString(), isVoid); } /// /// Attempts to extract a return value by invoking the RuntimeDelegateBody delegate. - /// Only invokes delegates with zero parameters; parameterized delegates cannot be executed - /// at compile time without concrete values. + /// If the delegate has no parameters, it is invoked directly. + /// If is provided and the delegate has exactly one parameter + /// (the constants), it is invoked with the constants. Delegates with additional parameters + /// (method parameters) cannot be executed at compile time without concrete values. /// private static FluentBodyResult? TryExtractFromRuntimeBody( Type dataType, object bodyGenerationData, - bool isVoid) + bool isVoid, + object? compileTimeConstants) { PropertyInfo? runtimeBodyProperty = dataType.GetProperty("RuntimeDelegateBody"); Delegate? runtimeBody = runtimeBodyProperty?.GetValue(bodyGenerationData) as Delegate; @@ -88,7 +120,13 @@ internal static FluentBodyResult Extract(object methodResult, bool isVoidReturnT return new FluentBodyResult(bodyResult?.ToString(), isVoid); } - // For delegates with parameters, we can't invoke at compile time without values + if (bodyParams.Length == 1 && compileTimeConstants != null) + { + object? bodyResult = runtimeBody.DynamicInvoke(compileTimeConstants); + return new FluentBodyResult(bodyResult?.ToString(), isVoid); + } + + // For delegates with additional parameters, we can't invoke at compile time without values return new FluentBodyResult(null, isVoid); } }