diff --git a/EasySourceGenerators.Examples/PiExample.cs b/EasySourceGenerators.Examples/PiExample.cs index 1da780f..dbc172d 100644 --- a/EasySourceGenerators.Examples/PiExample.cs +++ b/EasySourceGenerators.Examples/PiExample.cs @@ -3,21 +3,23 @@ namespace EasySourceGenerators.Examples; -public static partial class PiExample -{ - public static partial int GetPiDecimal(int decimalNumber); - - [GeneratesMethod(nameof(GetPiDecimal))] - [SwitchCase(arg1: 0)] - [SwitchCase(arg1: 1)] - [SwitchCase(arg1: 2)] - static int GetPiDecimal_Generator_Specialized(int decimalNumber) => - SlowMath.CalculatePiDecimal(decimalNumber); - - [GeneratesMethod(nameof(GetPiDecimal))] - [SwitchDefault] - static Func GetPiDecimal_Generator_Fallback() => decimalNumber => SlowMath.CalculatePiDecimal(decimalNumber); -} +// NOTE: [SwitchCase] attribute-based generation is commented out pending replacement +// with a data-driven approach. See PiExampleFluent.cs for the fluent API equivalent. +// public static partial class PiExample +// { +// public static partial int GetPiDecimal(int decimalNumber); +// +// [GeneratesMethod(nameof(GetPiDecimal))] +// [SwitchCase(arg1: 0)] +// [SwitchCase(arg1: 1)] +// [SwitchCase(arg1: 2)] +// static int GetPiDecimal_Generator_Specialized(int decimalNumber) => +// SlowMath.CalculatePiDecimal(decimalNumber); +// +// [GeneratesMethod(nameof(GetPiDecimal))] +// [SwitchDefault] +// static Func GetPiDecimal_Generator_Fallback() => decimalNumber => SlowMath.CalculatePiDecimal(decimalNumber); +// } /* This will generate the following method: diff --git a/EasySourceGenerators.GeneratorTests/DataGeneratorsFactoryTests.cs b/EasySourceGenerators.GeneratorTests/DataGeneratorsFactoryTests.cs new file mode 100644 index 0000000..20f9719 --- /dev/null +++ b/EasySourceGenerators.GeneratorTests/DataGeneratorsFactoryTests.cs @@ -0,0 +1,117 @@ +using EasySourceGenerators.Generators; + +namespace EasySourceGenerators.GeneratorTests; + +[TestFixture] +public class DataGeneratorsFactoryTests +{ + [Test] + public void CreateSimpleReturnBody_WithValue_CreatesCorrectData() + { + DataSimpleReturnBody result = DataGeneratorsFactory.CreateSimpleReturnBody("hello"); + + Assert.That(result.ReturnValue, Is.EqualTo("hello")); + } + + [Test] + public void CreateSimpleReturnBody_WithNull_CreatesDataWithNullValue() + { + DataSimpleReturnBody result = DataGeneratorsFactory.CreateSimpleReturnBody(null); + + Assert.That(result.ReturnValue, Is.Null); + } + + [Test] + public void CreateSwitchBodyFromFluentData_WithCasesAndDefault_CreatesCorrectData() + { + SwitchBodyData fluentData = new SwitchBodyData( + CasePairs: new List<(object key, string value)> { (1, "2"), (2, "4"), (3, "6") }, + HasDefaultCase: true); + + DataSwitchBody result = DataGeneratorsFactory.CreateSwitchBodyFromFluentData(fluentData, "0"); + + Assert.That(result.Cases, Has.Count.EqualTo(3)); + Assert.That(result.Cases[0].Key, Is.EqualTo(1)); + Assert.That(result.Cases[0].FormattedValue, Is.EqualTo("2")); + Assert.That(result.Cases[1].Key, Is.EqualTo(2)); + Assert.That(result.Cases[1].FormattedValue, Is.EqualTo("4")); + Assert.That(result.Cases[2].Key, Is.EqualTo(3)); + Assert.That(result.Cases[2].FormattedValue, Is.EqualTo("6")); + Assert.That(result.DefaultCase, Is.Not.Null); + Assert.That(result.DefaultCase!.Expression, Is.EqualTo("0")); + } + + [Test] + public void CreateSwitchBodyFromFluentData_WithoutDefault_CreatesDataWithNullDefaultCase() + { + SwitchBodyData fluentData = new SwitchBodyData( + CasePairs: new List<(object key, string value)> { (1, "one") }, + HasDefaultCase: false); + + DataSwitchBody result = DataGeneratorsFactory.CreateSwitchBodyFromFluentData(fluentData, null); + + Assert.That(result.Cases, Has.Count.EqualTo(1)); + Assert.That(result.DefaultCase, Is.Null); + } + + [Test] + public void CreateSwitchBodyFromFluentData_WithEmptyCases_CreatesEmptyData() + { + SwitchBodyData fluentData = new SwitchBodyData( + CasePairs: new List<(object key, string value)>(), + HasDefaultCase: false); + + DataSwitchBody result = DataGeneratorsFactory.CreateSwitchBodyFromFluentData(fluentData, null); + + Assert.That(result.Cases, Is.Empty); + Assert.That(result.DefaultCase, Is.Null); + } + + [Test] + public void CreateSwitchBodyFromFluentData_WithDefaultOnly_CreatesDataWithDefaultAndNoCases() + { + SwitchBodyData fluentData = new SwitchBodyData( + CasePairs: new List<(object key, string value)>(), + HasDefaultCase: true); + + DataSwitchBody result = DataGeneratorsFactory.CreateSwitchBodyFromFluentData(fluentData, "888"); + + Assert.That(result.Cases, Is.Empty); + Assert.That(result.DefaultCase, Is.Not.Null); + Assert.That(result.DefaultCase!.Expression, Is.EqualTo("888")); + } + + [Test] + public void CreateSwitchBodyFromFluentData_WithThrowExpression_PreservesExpression() + { + SwitchBodyData fluentData = new SwitchBodyData( + CasePairs: new List<(object key, string value)> { (1, "\"Dog\"") }, + HasDefaultCase: true); + + DataSwitchBody result = DataGeneratorsFactory.CreateSwitchBodyFromFluentData( + fluentData, + "throw new ArgumentException(\"Unknown\")"); + + Assert.That(result.DefaultCase, Is.Not.Null); + Assert.That(result.DefaultCase!.Expression, Is.EqualTo("throw new ArgumentException(\"Unknown\")")); + } + + [Test] + public void CreateSwitchBodyFromFluentData_PreservesKeyTypes() + { + SwitchBodyData fluentData = new SwitchBodyData( + CasePairs: new List<(object key, string value)> + { + (true, "\"Yes\""), + ("hello", "\"World\""), + (42, "\"Answer\"") + }, + HasDefaultCase: false); + + DataSwitchBody result = DataGeneratorsFactory.CreateSwitchBodyFromFluentData(fluentData, null); + + Assert.That(result.Cases[0].Key, Is.TypeOf()); + Assert.That(result.Cases[1].Key, Is.TypeOf()); + Assert.That(result.Cases[2].Key, Is.TypeOf()); + } +} diff --git a/EasySourceGenerators.GeneratorTests/DataMethodBodyBuildersTests.cs b/EasySourceGenerators.GeneratorTests/DataMethodBodyBuildersTests.cs new file mode 100644 index 0000000..6ab8c96 --- /dev/null +++ b/EasySourceGenerators.GeneratorTests/DataMethodBodyBuildersTests.cs @@ -0,0 +1,348 @@ +using System.Collections.Immutable; +using System.Reflection; +using EasySourceGenerators.Abstractions; +using EasySourceGenerators.Generators; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace EasySourceGenerators.GeneratorTests; + +[TestFixture] +public class DataMethodBodyBuildersTests +{ + [Test] + public void BuildMethodSource_SimpleReturn_GeneratesCorrectSource() + { + CSharpCompilation compilation = CreateCompilation(""" + namespace TestNamespace; + + public partial class MyClass + { + public partial string GetValue(); + } + """); + + IMethodSymbol partialMethod = GetMethodSymbol(compilation, "TestNamespace.MyClass", "GetValue"); + INamedTypeSymbol containingType = partialMethod.ContainingType; + + DataSimpleReturnBody data = new DataSimpleReturnBody("hello"); + string source = DataMethodBodyBuilders.BuildMethodSource(data, partialMethod, containingType); + + Assert.That(source, Does.Contain("return \"hello\";")); + Assert.That(source, Does.Contain("partial string GetValue()")); + Assert.That(source, Does.Contain("namespace TestNamespace;")); + } + + [Test] + public void BuildMethodSource_SimpleReturnVoid_GeneratesEmptyBody() + { + CSharpCompilation compilation = CreateCompilation(""" + namespace TestNamespace; + + public partial class MyClass + { + public partial void DoSomething(); + } + """); + + IMethodSymbol partialMethod = GetMethodSymbol(compilation, "TestNamespace.MyClass", "DoSomething"); + INamedTypeSymbol containingType = partialMethod.ContainingType; + + DataSimpleReturnBody data = new DataSimpleReturnBody(null); + string source = DataMethodBodyBuilders.BuildMethodSource(data, partialMethod, containingType); + + Assert.That(source, Does.Not.Contain("return")); + Assert.That(source, Does.Contain("partial void DoSomething()")); + } + + [Test] + public void BuildMethodSource_SimpleReturnBool_FormatsBoolLiteral() + { + CSharpCompilation compilation = CreateCompilation(""" + namespace TestNamespace; + + public static partial class MyClass + { + public static partial bool IsEnabled(); + } + """); + + IMethodSymbol partialMethod = GetMethodSymbol(compilation, "TestNamespace.MyClass", "IsEnabled"); + INamedTypeSymbol containingType = partialMethod.ContainingType; + + DataSimpleReturnBody data = new DataSimpleReturnBody("True"); + string source = DataMethodBodyBuilders.BuildMethodSource(data, partialMethod, containingType); + + Assert.That(source, Does.Contain("return true;")); + } + + [Test] + public void BuildMethodSource_SwitchBody_GeneratesCorrectSwitch() + { + CSharpCompilation compilation = CreateCompilation(""" + namespace TestNamespace; + + public static partial class MyClass + { + public static partial int GetValue(int key); + } + """); + + IMethodSymbol partialMethod = GetMethodSymbol(compilation, "TestNamespace.MyClass", "GetValue"); + INamedTypeSymbol containingType = partialMethod.ContainingType; + + DataSwitchBody data = new DataSwitchBody( + Cases: new List + { + new DataSwitchCase(1, "10"), + new DataSwitchCase(2, "20") + }, + DefaultCase: new DataSwitchDefaultCase("0")); + + string source = DataMethodBodyBuilders.BuildMethodSource(data, partialMethod, containingType); + + Assert.That(source, Does.Contain("switch (key)")); + Assert.That(source, Does.Contain("case 1: return 10;")); + Assert.That(source, Does.Contain("case 2: return 20;")); + Assert.That(source, Does.Contain("default: return 0;")); + } + + [Test] + public void BuildMethodSource_SwitchBody_WithThrowDefault_GeneratesThrowStatement() + { + CSharpCompilation compilation = CreateCompilation(""" + namespace TestNamespace; + + public static partial class MyClass + { + public static partial int GetValue(int key); + } + """); + + IMethodSymbol partialMethod = GetMethodSymbol(compilation, "TestNamespace.MyClass", "GetValue"); + INamedTypeSymbol containingType = partialMethod.ContainingType; + + DataSwitchBody data = new DataSwitchBody( + Cases: new List(), + DefaultCase: new DataSwitchDefaultCase("throw new ArgumentException(\"bad\")")); + + string source = DataMethodBodyBuilders.BuildMethodSource(data, partialMethod, containingType); + + Assert.That(source, Does.Contain("default: throw new ArgumentException(\"bad\");")); + Assert.That(source, Does.Not.Contain("default: return throw")); + } + + [Test] + public void BuildMethodSource_SwitchBody_WithNoParameters_UsesDefaultExpressionOnly() + { + CSharpCompilation compilation = CreateCompilation(""" + namespace TestNamespace; + + public static partial class MyClass + { + public static partial string GetValue(); + } + """); + + IMethodSymbol partialMethod = GetMethodSymbol(compilation, "TestNamespace.MyClass", "GetValue"); + INamedTypeSymbol containingType = partialMethod.ContainingType; + + DataSwitchBody data = new DataSwitchBody( + Cases: new List { new DataSwitchCase(1, "\"one\"") }, + DefaultCase: new DataSwitchDefaultCase("\"fallback\"")); + + string source = DataMethodBodyBuilders.BuildMethodSource(data, partialMethod, containingType); + + Assert.That(source, Does.Contain("return \"fallback\";")); + Assert.That(source, Does.Not.Contain("switch")); + } + + [Test] + public void BuildMethodSource_SwitchBody_WithBoolKeys_FormatsBoolLiterals() + { + CSharpCompilation compilation = CreateCompilation(""" + namespace TestNamespace; + + public static partial class MyClass + { + public static partial string GetLabel(bool flag); + } + """); + + IMethodSymbol partialMethod = GetMethodSymbol(compilation, "TestNamespace.MyClass", "GetLabel"); + INamedTypeSymbol containingType = partialMethod.ContainingType; + + DataSwitchBody data = new DataSwitchBody( + Cases: new List + { + new DataSwitchCase(true, "\"Yes\""), + new DataSwitchCase(false, "\"No\"") + }, + DefaultCase: new DataSwitchDefaultCase("\"Unknown\"")); + + string source = DataMethodBodyBuilders.BuildMethodSource(data, partialMethod, containingType); + + Assert.That(source, Does.Contain("case true: return \"Yes\";")); + Assert.That(source, Does.Contain("case false: return \"No\";")); + Assert.That(source, Does.Contain("default: return \"Unknown\";")); + } + + [Test] + public void BuildMethodSource_SwitchBody_WithoutDefault_OmitsDefaultClause() + { + CSharpCompilation compilation = CreateCompilation(""" + namespace TestNamespace; + + public static partial class MyClass + { + public static partial int GetValue(int key); + } + """); + + IMethodSymbol partialMethod = GetMethodSymbol(compilation, "TestNamespace.MyClass", "GetValue"); + INamedTypeSymbol containingType = partialMethod.ContainingType; + + DataSwitchBody data = new DataSwitchBody( + Cases: new List + { + new DataSwitchCase(1, "10") + }, + DefaultCase: null); + + string source = DataMethodBodyBuilders.BuildMethodSource(data, partialMethod, containingType); + + Assert.That(source, Does.Contain("case 1: return 10;")); + Assert.That(source, Does.Not.Contain("default:")); + } + + [Test] + public void BuildMethodSource_IncludesAutoGeneratedHeader() + { + CSharpCompilation compilation = CreateCompilation(""" + namespace TestNamespace; + + public partial class MyClass + { + public partial string GetValue(); + } + """); + + IMethodSymbol partialMethod = GetMethodSymbol(compilation, "TestNamespace.MyClass", "GetValue"); + INamedTypeSymbol containingType = partialMethod.ContainingType; + + DataSimpleReturnBody data = new DataSimpleReturnBody("hello"); + string source = DataMethodBodyBuilders.BuildMethodSource(data, partialMethod, containingType); + + Assert.That(source, Does.StartWith("// ")); + Assert.That(source, Does.Contain("#pragma warning disable")); + } + + [Test] + public void FormatValueAsCSharpLiteral_String_QuotesCorrectly() + { + CSharpCompilation compilation = CreateCompilation(""" + namespace TestNamespace; + + public partial class MyClass + { + public partial string GetValue(); + } + """); + + IMethodSymbol partialMethod = GetMethodSymbol(compilation, "TestNamespace.MyClass", "GetValue"); + string result = DataMethodBodyBuilders.FormatValueAsCSharpLiteral("hello", partialMethod.ReturnType); + + Assert.That(result, Is.EqualTo("\"hello\"")); + } + + [Test] + public void FormatValueAsCSharpLiteral_Null_ReturnsDefault() + { + CSharpCompilation compilation = CreateCompilation(""" + namespace TestNamespace; + + public partial class MyClass + { + public partial string GetValue(); + } + """); + + IMethodSymbol partialMethod = GetMethodSymbol(compilation, "TestNamespace.MyClass", "GetValue"); + string result = DataMethodBodyBuilders.FormatValueAsCSharpLiteral(null, partialMethod.ReturnType); + + Assert.That(result, Is.EqualTo("default")); + } + + [Test] + public void FormatKeyAsCSharpLiteral_Bool_ReturnsLowerCase() + { + string trueResult = DataMethodBodyBuilders.FormatKeyAsCSharpLiteral(true, null); + string falseResult = DataMethodBodyBuilders.FormatKeyAsCSharpLiteral(false, null); + + Assert.That(trueResult, Is.EqualTo("true")); + Assert.That(falseResult, Is.EqualTo("false")); + } + + [Test] + public void FormatKeyAsCSharpLiteral_String_QuotesCorrectly() + { + string result = DataMethodBodyBuilders.FormatKeyAsCSharpLiteral("test", null); + + Assert.That(result, Is.EqualTo("\"test\"")); + } + + [Test] + public void FormatKeyAsCSharpLiteral_Int_ReturnsToString() + { + string result = DataMethodBodyBuilders.FormatKeyAsCSharpLiteral(42, null); + + Assert.That(result, Is.EqualTo("42")); + } + + private static CSharpCompilation CreateCompilation(string source) + { + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source); + CSharpCompilation compilation = CSharpCompilation.Create( + assemblyName: "TestAssembly", + syntaxTrees: new[] { syntaxTree }, + references: GetMetadataReferences(), + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + return compilation; + } + + private static ImmutableArray GetMetadataReferences() + { + string dotnetDirectory = Path.GetDirectoryName(typeof(object).Assembly.Location)!; + List references = new List + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(Path.Combine(dotnetDirectory, "System.Runtime.dll")), + MetadataReference.CreateFromFile(Path.Combine(dotnetDirectory, "netstandard.dll")), + MetadataReference.CreateFromFile(typeof(Generate).Assembly.Location) + }; + + foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (!string.IsNullOrEmpty(assembly.Location) && references.All(reference => reference.Display != assembly.Location)) + { + try + { + references.Add(MetadataReference.CreateFromFile(assembly.Location)); + } + catch (FileNotFoundException) { } + catch (BadImageFormatException) { } + } + } + + return references.ToImmutableArray(); + } + + private static IMethodSymbol GetMethodSymbol(Compilation compilation, string typeName, string methodName) + { + INamedTypeSymbol? typeSymbol = compilation.GetTypeByMetadataName(typeName); + Assert.That(typeSymbol, Is.Not.Null, $"Type '{typeName}' not found."); + IMethodSymbol? methodSymbol = typeSymbol!.GetMembers(methodName).OfType().FirstOrDefault(); + Assert.That(methodSymbol, Is.Not.Null, $"Method '{methodName}' not found in '{typeName}'."); + return methodSymbol!; + } +} diff --git a/EasySourceGenerators.GeneratorTests/GeneratorDiagnosticsTests.cs b/EasySourceGenerators.GeneratorTests/GeneratorDiagnosticsTests.cs index 5e357bf..7e9df39 100644 --- a/EasySourceGenerators.GeneratorTests/GeneratorDiagnosticsTests.cs +++ b/EasySourceGenerators.GeneratorTests/GeneratorDiagnosticsTests.cs @@ -89,83 +89,85 @@ public partial class MyClass // ----------------------------------------------------------------------- // MSGH005 – Generator method has too many parameters (switch pattern) + // NOTE: [SwitchCase] attribute-based tests commented out pending replacement + // with a data-driven approach. // ----------------------------------------------------------------------- - [Test] - public void GeneratesMethod_SwitchCaseWithMoreThanOneParameter_EmitsMSGH005() - { - string source = """ - using EasySourceGenerators.Abstractions; - - namespace TestNamespace; - - public static partial class MyClass - { - public static partial int GetValue(int key); - - [GeneratesMethod(nameof(GetValue))] - [SwitchCase(arg1: 1)] - private static int GetValue_Generator(int key, string extraParam) => 42; - } - """; - - ImmutableArray diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source); - - Diagnostic? msgh005 = diagnostics.FirstOrDefault(d => d.Id == "MSGH005"); - Assert.That(msgh005, Is.Not.Null, "Expected MSGH005 diagnostic for generator method with too many parameters"); - Assert.That(msgh005!.GetMessage(), Does.Contain("GetValue_Generator"), - "Error message should mention the generator method name"); - Assert.That(msgh005.GetMessage(), Does.Contain("2"), - "Error message should mention the number of parameters"); - } - - [Test] - public void GeneratesMethod_SwitchCaseWithOneParameter_DoesNotEmitMSGH005() - { - string source = """ - using EasySourceGenerators.Abstractions; - - namespace TestNamespace; - - public static partial class MyClass - { - public static partial int GetValue(int key); - - [GeneratesMethod(nameof(GetValue))] - [SwitchCase(arg1: 1)] - private static int GetValue_Generator(int key) => 42; - } - """; - - ImmutableArray diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source); - - Assert.That(diagnostics.Any(d => d.Id == "MSGH005"), Is.False, - "Should not emit MSGH005 for a generator method with exactly one parameter"); - } - - [Test] - public void GeneratesMethod_SwitchCaseWithZeroParameters_DoesNotEmitMSGH005() - { - string source = """ - using EasySourceGenerators.Abstractions; - - namespace TestNamespace; - - public static partial class MyClass - { - public static partial int GetValue(int key); - - [GeneratesMethod(nameof(GetValue))] - [SwitchCase(arg1: 1)] - private static int GetValue_Generator() => 42; - } - """; - - ImmutableArray diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source); - - Assert.That(diagnostics.Any(d => d.Id == "MSGH005"), Is.False, - "Should not emit MSGH005 for a generator method with zero parameters"); - } + // [Test] + // public void GeneratesMethod_SwitchCaseWithMoreThanOneParameter_EmitsMSGH005() + // { + // string source = """ + // using EasySourceGenerators.Abstractions; + // + // namespace TestNamespace; + // + // public static partial class MyClass + // { + // public static partial int GetValue(int key); + // + // [GeneratesMethod(nameof(GetValue))] + // [SwitchCase(arg1: 1)] + // private static int GetValue_Generator(int key, string extraParam) => 42; + // } + // """; + // + // ImmutableArray diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source); + // + // Diagnostic? msgh005 = diagnostics.FirstOrDefault(d => d.Id == "MSGH005"); + // Assert.That(msgh005, Is.Not.Null, "Expected MSGH005 diagnostic for generator method with too many parameters"); + // Assert.That(msgh005!.GetMessage(), Does.Contain("GetValue_Generator"), + // "Error message should mention the generator method name"); + // Assert.That(msgh005.GetMessage(), Does.Contain("2"), + // "Error message should mention the number of parameters"); + // } + + // [Test] + // public void GeneratesMethod_SwitchCaseWithOneParameter_DoesNotEmitMSGH005() + // { + // string source = """ + // using EasySourceGenerators.Abstractions; + // + // namespace TestNamespace; + // + // public static partial class MyClass + // { + // public static partial int GetValue(int key); + // + // [GeneratesMethod(nameof(GetValue))] + // [SwitchCase(arg1: 1)] + // private static int GetValue_Generator(int key) => 42; + // } + // """; + // + // ImmutableArray diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source); + // + // Assert.That(diagnostics.Any(d => d.Id == "MSGH005"), Is.False, + // "Should not emit MSGH005 for a generator method with exactly one parameter"); + // } + + // [Test] + // public void GeneratesMethod_SwitchCaseWithZeroParameters_DoesNotEmitMSGH005() + // { + // string source = """ + // using EasySourceGenerators.Abstractions; + // + // namespace TestNamespace; + // + // public static partial class MyClass + // { + // public static partial int GetValue(int key); + // + // [GeneratesMethod(nameof(GetValue))] + // [SwitchCase(arg1: 1)] + // private static int GetValue_Generator() => 42; + // } + // """; + // + // ImmutableArray diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source); + // + // Assert.That(diagnostics.Any(d => d.Id == "MSGH005"), Is.False, + // "Should not emit MSGH005 for a generator method with zero parameters"); + // } // ----------------------------------------------------------------------- // MSGH004 – Generator method execution failed (unfinished fluent API) @@ -201,104 +203,108 @@ private static IMethodImplementationGenerator GetValue_Generator() => // ----------------------------------------------------------------------- // Partial method called from fluent API / generator execution + // NOTE: [SwitchCase] attribute-based tests commented out pending replacement + // with a data-driven approach. // ----------------------------------------------------------------------- - [Test] - public void GeneratesMethod_PartialMethodCalledInsideGenerator_EmitsMSGH004WithHelpfulMessage() - { - // The generator method calls a partial method that hasn't been implemented yet. - // The dummy implementation should throw PartialMethodCalledDuringGenerationException, - // which should be surfaced as MSGH004 with an informative message. - string source = """ - using EasySourceGenerators.Abstractions; - - namespace TestNamespace; - - public static partial class MyClass - { - public static partial int GetValue(int key); - - public static partial string GetLabel(int key); - - [GeneratesMethod(nameof(GetValue))] - [SwitchCase(arg1: 1)] - private static int GetValue_Generator(int key) - { - // Calling the other partial method from inside a generator — this should throw. - string label = GetLabel(key); - return label.Length; - } - } - """; - - ImmutableArray diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source); - - Diagnostic? msgh004 = diagnostics.FirstOrDefault(d => d.Id == "MSGH004"); - Assert.That(msgh004, Is.Not.Null, - "Expected MSGH004 when a partial method is called inside a generator method"); - Assert.That(msgh004!.GetMessage(), Does.Contain("GetValue_Generator"), - "Error message should mention the generator method name"); - Assert.That(msgh004.GetMessage(), Does.Contain("PartialMethodCalledDuringGenerationException") - .Or.Contain("GetLabel") - .Or.Contain("partial method"), - "Error message should hint that a partial method was called during generation"); - } + // [Test] + // public void GeneratesMethod_PartialMethodCalledInsideGenerator_EmitsMSGH004WithHelpfulMessage() + // { + // // The generator method calls a partial method that hasn't been implemented yet. + // // The dummy implementation should throw PartialMethodCalledDuringGenerationException, + // // which should be surfaced as MSGH004 with an informative message. + // string source = """ + // using EasySourceGenerators.Abstractions; + // + // namespace TestNamespace; + // + // public static partial class MyClass + // { + // public static partial int GetValue(int key); + // + // public static partial string GetLabel(int key); + // + // [GeneratesMethod(nameof(GetValue))] + // [SwitchCase(arg1: 1)] + // private static int GetValue_Generator(int key) + // { + // // Calling the other partial method from inside a generator — this should throw. + // string label = GetLabel(key); + // return label.Length; + // } + // } + // """; + // + // ImmutableArray diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source); + // + // Diagnostic? msgh004 = diagnostics.FirstOrDefault(d => d.Id == "MSGH004"); + // Assert.That(msgh004, Is.Not.Null, + // "Expected MSGH004 when a partial method is called inside a generator method"); + // Assert.That(msgh004!.GetMessage(), Does.Contain("GetValue_Generator"), + // "Error message should mention the generator method name"); + // Assert.That(msgh004.GetMessage(), Does.Contain("PartialMethodCalledDuringGenerationException") + // .Or.Contain("GetLabel") + // .Or.Contain("partial method"), + // "Error message should hint that a partial method was called during generation"); + // } // ----------------------------------------------------------------------- // MSGH006 – SwitchCase argument type mismatch + // NOTE: [SwitchCase] attribute-based tests commented out pending replacement + // with a data-driven approach. // ----------------------------------------------------------------------- - [Test] - public void GeneratesMethod_SwitchCaseWithWrongArgumentType_EmitsMSGH006() - { - string source = """ - using EasySourceGenerators.Abstractions; - - namespace TestNamespace; - - public static partial class MyClass - { - public static partial int GetValue(int key); - - [GeneratesMethod(nameof(GetValue))] - [SwitchCase(arg1: "aaa")] - private static int GetValue_Generator(int key) => 42; - } - """; - - ImmutableArray diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source); - - Diagnostic? msgh006 = diagnostics.FirstOrDefault(d => d.Id == "MSGH006"); - Assert.That(msgh006, Is.Not.Null, "Expected MSGH006 diagnostic for SwitchCase argument type mismatch"); - Assert.That(msgh006!.GetMessage(), Does.Contain("string"), - "Error message should mention the provided type"); - Assert.That(msgh006.GetMessage(), Does.Contain("int"), - "Error message should mention the expected parameter type"); - } - - [Test] - public void GeneratesMethod_SwitchCaseWithCorrectArgumentType_DoesNotEmitMSGH006() - { - string source = """ - using EasySourceGenerators.Abstractions; - - namespace TestNamespace; - - public static partial class MyClass - { - public static partial int GetValue(int key); - - [GeneratesMethod(nameof(GetValue))] - [SwitchCase(arg1: 1)] - private static int GetValue_Generator(int key) => 42; - } - """; - - ImmutableArray diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source); - - Assert.That(diagnostics.Any(d => d.Id == "MSGH006"), Is.False, - "Should not emit MSGH006 when SwitchCase argument type matches the parameter type"); - } + // [Test] + // public void GeneratesMethod_SwitchCaseWithWrongArgumentType_EmitsMSGH006() + // { + // string source = """ + // using EasySourceGenerators.Abstractions; + // + // namespace TestNamespace; + // + // public static partial class MyClass + // { + // public static partial int GetValue(int key); + // + // [GeneratesMethod(nameof(GetValue))] + // [SwitchCase(arg1: "aaa")] + // private static int GetValue_Generator(int key) => 42; + // } + // """; + // + // ImmutableArray diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source); + // + // Diagnostic? msgh006 = diagnostics.FirstOrDefault(d => d.Id == "MSGH006"); + // Assert.That(msgh006, Is.Not.Null, "Expected MSGH006 diagnostic for SwitchCase argument type mismatch"); + // Assert.That(msgh006!.GetMessage(), Does.Contain("string"), + // "Error message should mention the provided type"); + // Assert.That(msgh006.GetMessage(), Does.Contain("int"), + // "Error message should mention the expected parameter type"); + // } + + // [Test] + // public void GeneratesMethod_SwitchCaseWithCorrectArgumentType_DoesNotEmitMSGH006() + // { + // string source = """ + // using EasySourceGenerators.Abstractions; + // + // namespace TestNamespace; + // + // public static partial class MyClass + // { + // public static partial int GetValue(int key); + // + // [GeneratesMethod(nameof(GetValue))] + // [SwitchCase(arg1: 1)] + // private static int GetValue_Generator(int key) => 42; + // } + // """; + // + // ImmutableArray diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source); + // + // Assert.That(diagnostics.Any(d => d.Id == "MSGH006"), Is.False, + // "Should not emit MSGH006 when SwitchCase argument type matches the parameter type"); + // } // ----------------------------------------------------------------------- // Type mismatch between generator return type and partial method return type @@ -358,30 +364,32 @@ public partial class MyClass "Valid generator configuration should produce no error diagnostics"); } - [Test] - public void GeneratesMethod_SwitchCasePattern_ValidConfiguration_ProducesNoDiagnosticErrors() - { - string source = """ - using EasySourceGenerators.Abstractions; - - namespace TestNamespace; - - public static partial class MyClass - { - public static partial int GetPiDigit(int index); - - [GeneratesMethod(nameof(GetPiDigit))] - [SwitchCase(arg1: 0)] - [SwitchCase(arg1: 1)] - private static int GetPiDigit_Generator(int index) => index == 0 ? 3 : 1; - } - """; - - ImmutableArray diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source); - - Assert.That(diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error), Is.Empty, - "Valid switch-case generator configuration should produce no error diagnostics"); - } + // NOTE: [SwitchCase] attribute-based tests commented out pending replacement + // with a data-driven approach. + // [Test] + // public void GeneratesMethod_SwitchCasePattern_ValidConfiguration_ProducesNoDiagnosticErrors() + // { + // string source = """ + // using EasySourceGenerators.Abstractions; + // + // namespace TestNamespace; + // + // public static partial class MyClass + // { + // public static partial int GetPiDigit(int index); + // + // [GeneratesMethod(nameof(GetPiDigit))] + // [SwitchCase(arg1: 0)] + // [SwitchCase(arg1: 1)] + // private static int GetPiDigit_Generator(int index) => index == 0 ? 3 : 1; + // } + // """; + // + // ImmutableArray diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source); + // + // Assert.That(diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error), Is.Empty, + // "Valid switch-case generator configuration should produce no error diagnostics"); + // } // ----------------------------------------------------------------------- // CompilationReference scenario – simulates Rider's code inspector @@ -452,35 +460,37 @@ public partial class MyClass // ----------------------------------------------------------------------- // Bool switch parameter – valid configuration + // NOTE: [SwitchCase] attribute-based tests commented out pending replacement + // with a data-driven approach. // ----------------------------------------------------------------------- - [Test] - public void GeneratesMethod_SwitchCaseWithBoolParameter_ValidConfiguration_ProducesNoDiagnosticErrors() - { - string source = """ - using EasySourceGenerators.Abstractions; - using System; - - namespace TestNamespace; - - public static partial class MyClass - { - public static partial string GetLabel(bool flag); - - [GeneratesMethod(nameof(GetLabel))] - [SwitchCase(arg1: true)] - [SwitchCase(arg1: false)] - private static string GetLabel_Generator(bool flag) => flag ? "Yes" : "No"; - - [GeneratesMethod(nameof(GetLabel))] - [SwitchDefault] - private static Func GetLabel_Default() => _ => "Unknown"; - } - """; - - ImmutableArray diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source); - - Assert.That(diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error), Is.Empty, - "Valid bool switch case generator should produce no error diagnostics"); - } + // [Test] + // public void GeneratesMethod_SwitchCaseWithBoolParameter_ValidConfiguration_ProducesNoDiagnosticErrors() + // { + // string source = """ + // using EasySourceGenerators.Abstractions; + // using System; + // + // namespace TestNamespace; + // + // public static partial class MyClass + // { + // public static partial string GetLabel(bool flag); + // + // [GeneratesMethod(nameof(GetLabel))] + // [SwitchCase(arg1: true)] + // [SwitchCase(arg1: false)] + // private static string GetLabel_Generator(bool flag) => flag ? "Yes" : "No"; + // + // [GeneratesMethod(nameof(GetLabel))] + // [SwitchDefault] + // private static Func GetLabel_Default() => _ => "Unknown"; + // } + // """; + // + // ImmutableArray diagnostics = GeneratorTestHelper.GetGeneratorOnlyDiagnostics(source); + // + // Assert.That(diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error), Is.Empty, + // "Valid bool switch case generator should produce no error diagnostics"); + // } } diff --git a/EasySourceGenerators.Generators/DataGeneratorsFactory.cs b/EasySourceGenerators.Generators/DataGeneratorsFactory.cs new file mode 100644 index 0000000..13c1462 --- /dev/null +++ b/EasySourceGenerators.Generators/DataGeneratorsFactory.cs @@ -0,0 +1,44 @@ +using Microsoft.CodeAnalysis; + +namespace EasySourceGenerators.Generators; + +/// +/// Factory for building instances from various sources. +/// This provides the bridge between different generation patterns (fluent API, attributes, simple) +/// and the unified data model consumed by . +/// +internal static class DataGeneratorsFactory +{ + /// + /// Creates a from a simple generator method execution result. + /// + internal static DataSimpleReturnBody CreateSimpleReturnBody(string? returnValue) + { + return new DataSimpleReturnBody(returnValue); + } + + /// + /// Creates a from a record + /// (produced by ). + /// + internal static DataSwitchBody CreateSwitchBodyFromFluentData( + SwitchBodyData switchBodyData, + string? defaultExpression) + { + List cases = new(); + foreach ((object key, string value) in switchBodyData.CasePairs) + { + cases.Add(new DataSwitchCase(key, value)); + } + + DataSwitchDefaultCase? defaultCase = defaultExpression != null + ? new DataSwitchDefaultCase(defaultExpression) + : null; + + return new DataSwitchBody(cases, defaultCase); + } + + // NOTE: Explicit [SwitchCase] attribute-based data building will be added in a future PR. + // The SwitchCase attribute pattern will be replaced with a different approach. + // See DataMethodBodyBuilders.cs for details on the planned data flow. +} diff --git a/EasySourceGenerators.Generators/DataMethodBody.cs b/EasySourceGenerators.Generators/DataMethodBody.cs new file mode 100644 index 0000000..8922d30 --- /dev/null +++ b/EasySourceGenerators.Generators/DataMethodBody.cs @@ -0,0 +1,29 @@ +namespace EasySourceGenerators.Generators; + +/// +/// Base type for method body data. Generators should build source code based on this data alone, +/// regardless of how it was constructed (fluent API, attributes, etc.). +/// +internal abstract record DataMethodBody; + +/// +/// A method body that returns a single constant value. +/// +internal sealed record DataSimpleReturnBody(string? ReturnValue) : DataMethodBody; + +/// +/// A method body that uses a switch statement. +/// +internal sealed record DataSwitchBody( + IReadOnlyList Cases, + DataSwitchDefaultCase? DefaultCase) : DataMethodBody; + +/// +/// A single case in a switch body, with a key and a formatted C# literal value. +/// +internal sealed record DataSwitchCase(object Key, string FormattedValue); + +/// +/// The default case in a switch body, with an expression to use (e.g., return value or throw). +/// +internal sealed record DataSwitchDefaultCase(string Expression); diff --git a/EasySourceGenerators.Generators/DataMethodBodyBuilders.cs b/EasySourceGenerators.Generators/DataMethodBodyBuilders.cs new file mode 100644 index 0000000..bf5ff42 --- /dev/null +++ b/EasySourceGenerators.Generators/DataMethodBodyBuilders.cs @@ -0,0 +1,166 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using System.Text; + +namespace EasySourceGenerators.Generators; + +/// +/// Builds C# source code for partial method bodies based on the provided . +/// This class consumes only the data model, regardless of how the data was produced +/// (fluent API, attributes, simple execution, etc.). +/// +internal static class DataMethodBodyBuilders +{ + internal static string BuildMethodSource( + DataMethodBody body, + IMethodSymbol partialMethod, + INamedTypeSymbol containingType) + { + return body switch + { + DataSimpleReturnBody simple => BuildSimpleReturnSource(simple, partialMethod, containingType), + DataSwitchBody switchBody => BuildSwitchSource(switchBody, partialMethod, containingType), + _ => throw new NotSupportedException($"Unsupported method body data type: {body.GetType().Name}") + }; + } + + private static string BuildSimpleReturnSource( + DataSimpleReturnBody body, + IMethodSymbol partialMethod, + INamedTypeSymbol containingType) + { + StringBuilder builder = new(); + AppendNamespaceAndTypeHeader(builder, containingType, partialMethod); + + if (!partialMethod.ReturnsVoid) + { + string literal = FormatValueAsCSharpLiteral(body.ReturnValue, partialMethod.ReturnType); + builder.AppendLine($" return {literal};"); + } + + builder.AppendLine(" }"); + builder.AppendLine("}"); + return builder.ToString(); + } + + private static string BuildSwitchSource( + DataSwitchBody body, + IMethodSymbol partialMethod, + INamedTypeSymbol containingType) + { + StringBuilder builder = new(); + AppendNamespaceAndTypeHeader(builder, containingType, partialMethod); + + if (partialMethod.Parameters.Length == 0) + { + string fallbackExpression = body.DefaultCase?.Expression ?? "default"; + builder.AppendLine($" return {fallbackExpression};"); + builder.AppendLine(" }"); + builder.AppendLine("}"); + return builder.ToString(); + } + + string switchParameterName = partialMethod.Parameters[0].Name; + builder.AppendLine($" switch ({switchParameterName})"); + builder.AppendLine(" {"); + + ITypeSymbol? parameterType = partialMethod.Parameters.Length > 0 ? partialMethod.Parameters[0].Type : null; + foreach (DataSwitchCase switchCase in body.Cases) + { + string formattedKey = FormatKeyAsCSharpLiteral(switchCase.Key, parameterType); + builder.AppendLine($" case {formattedKey}: return {switchCase.FormattedValue};"); + } + + if (body.DefaultCase != null) + { + string defaultStatement = body.DefaultCase.Expression.TrimStart().StartsWith("throw ", StringComparison.Ordinal) + ? $" default: {body.DefaultCase.Expression};" + : $" default: return {body.DefaultCase.Expression};"; + builder.AppendLine(defaultStatement); + } + + builder.AppendLine(" }"); + builder.AppendLine(" }"); + builder.AppendLine("}"); + return builder.ToString(); + } + + private static void AppendNamespaceAndTypeHeader(StringBuilder builder, INamedTypeSymbol containingType, IMethodSymbol partialMethod) + { + builder.AppendLine("// "); + builder.AppendLine($"// Generated by {typeof(GeneratesMethodGenerator).FullName} for method '{partialMethod.Name}'."); + 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 accessibility = partialMethod.DeclaredAccessibility switch + { + Accessibility.Public => "public", + Accessibility.Protected => "protected", + Accessibility.Internal => "internal", + Accessibility.ProtectedOrInternal => "protected internal", + Accessibility.ProtectedAndInternal => "private protected", + _ => "private" + }; + + string returnTypeName = partialMethod.ReturnType.ToDisplayString(); + string methodName = partialMethod.Name; + string parameters = string.Join(", ", partialMethod.Parameters.Select(parameter => $"{parameter.Type.ToDisplayString()} {parameter.Name}")); + string methodModifiers = partialMethod.IsStatic ? "static partial" : "partial"; + + builder.AppendLine($" {accessibility} {methodModifiers} {returnTypeName} {methodName}({parameters})"); + builder.AppendLine(" {"); + } + + internal static string FormatValueAsCSharpLiteral(string? value, ITypeSymbol returnType) + { + if (value == null) + { + return "default"; + } + + return returnType.SpecialType switch + { + SpecialType.System_String => SyntaxFactory.Literal(value).Text, + SpecialType.System_Char when value.Length == 1 => SyntaxFactory.Literal(value[0]).Text, + SpecialType.System_Boolean => value.ToLowerInvariant(), + _ when returnType.TypeKind == TypeKind.Enum => $"{returnType.ToDisplayString()}.{value}", + _ => value + }; + } + + internal static string FormatKeyAsCSharpLiteral(object key, ITypeSymbol? parameterType) + { + if (parameterType?.TypeKind == TypeKind.Enum) + { + return $"{parameterType.ToDisplayString()}.{key}"; + } + + return key switch + { + bool b => b ? "true" : "false", + // SyntaxFactory.Literal handles escaping and quoting (e.g. "hello" → "\"hello\"") + string s => SyntaxFactory.Literal(s).Text, + _ => key.ToString()! + }; + } +} diff --git a/EasySourceGenerators.Generators/GeneratesMethodGenerationPipeline.cs b/EasySourceGenerators.Generators/GeneratesMethodGenerationPipeline.cs index 22c8858..20303b3 100644 --- a/EasySourceGenerators.Generators/GeneratesMethodGenerationPipeline.cs +++ b/EasySourceGenerators.Generators/GeneratesMethodGenerationPipeline.cs @@ -47,29 +47,29 @@ private static string GenerateSourceForGroup( IReadOnlyList allPartials, Compilation compilation) { - 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; - if (hasSwitchCase || hasSwitchDefault) - { - return GeneratesMethodPatternSourceBuilder.GenerateFromSwitchAttributes( - context, - methods, - firstMethod.PartialMethod, - firstMethod.ContainingType, - allPartials, - compilation); - } + // NOTE: Explicit [SwitchCase]/[SwitchDefault] attribute-based generation is commented out. + // This pattern will be replaced with a data-driven approach in a future PR. + // The data abstraction layer (DataMethodBody, DataMethodBodyBuilders, DataGeneratorsFactory) + // will be extended to support the new attribute-based pattern. + // bool hasSwitchCase = methods.Any(method => HasAttribute(method.Symbol, SwitchCaseAttributeFullName)); + // bool hasSwitchDefault = methods.Any(method => HasAttribute(method.Symbol, SwitchDefaultAttributeFullName)); + // + // if (hasSwitchCase || hasSwitchDefault) + // { + // return GeneratesMethodPatternSourceBuilder.GenerateFromSwitchAttributes( + // context, + // methods, + // firstMethod.PartialMethod, + // firstMethod.ContainingType, + // allPartials, + // compilation); + // } if (isFluentPattern) { - return GeneratesMethodPatternSourceBuilder.GenerateFromFluent( - context, - methods[0], - firstMethod.PartialMethod, - firstMethod.ContainingType, - compilation); + return GenerateFromFluentPattern(context, methods[0], firstMethod, compilation); } List methodsWithParameters = methods @@ -90,6 +90,36 @@ private static string GenerateSourceForGroup( return GenerateFromSimplePattern(context, firstMethod, compilation); } + private static string GenerateFromFluentPattern( + SourceProductionContext context, + GeneratesMethodGenerationTarget methodInfo, + GeneratesMethodGenerationTarget firstMethod, + Compilation compilation) + { + (SwitchBodyData? record, string? error) = GeneratesMethodExecutionRuntime.ExecuteFluentGeneratorMethod( + methodInfo.Symbol, + firstMethod.PartialMethod, + compilation); + + if (error != null) + { + context.ReportDiagnostic(Diagnostic.Create( + GeneratesMethodGeneratorDiagnostics.GeneratorMethodExecutionError, + methodInfo.Syntax.GetLocation(), + methodInfo.Symbol.Name, + error)); + return string.Empty; + } + + SwitchBodyData switchBodyData = record!; + string? defaultExpression = switchBodyData.HasDefaultCase + ? GeneratesMethodPatternSourceBuilder.ExtractDefaultExpressionFromFluentMethod(methodInfo.Syntax) + : null; + + DataSwitchBody data = DataGeneratorsFactory.CreateSwitchBodyFromFluentData(switchBodyData, defaultExpression); + return DataMethodBodyBuilders.BuildMethodSource(data, firstMethod.PartialMethod, firstMethod.ContainingType); + } + private static string GenerateFromSimplePattern( SourceProductionContext context, GeneratesMethodGenerationTarget firstMethod, @@ -110,10 +140,8 @@ private static string GenerateFromSimplePattern( return string.Empty; } - return GeneratesMethodPatternSourceBuilder.GenerateSimplePartialMethod( - firstMethod.ContainingType, - firstMethod.PartialMethod, - returnValue); + DataSimpleReturnBody data = DataGeneratorsFactory.CreateSimpleReturnBody(returnValue); + return DataMethodBodyBuilders.BuildMethodSource(data, firstMethod.PartialMethod, firstMethod.ContainingType); } private static bool HasAttribute(IMethodSymbol methodSymbol, string fullAttributeTypeName) diff --git a/EasySourceGenerators.Generators/GeneratesMethodPatternSourceBuilder.cs b/EasySourceGenerators.Generators/GeneratesMethodPatternSourceBuilder.cs index 5aab0ba..efe8d3a 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? ExtractDefaultExpressionFromFluentMethod(MethodDeclarationSyntax method) { IEnumerable invocations = method.DescendantNodes().OfType(); foreach (InvocationExpressionSyntax invocation in invocations) diff --git a/EasySourceGenerators.Tests/BoolSwitchKeyTests.cs b/EasySourceGenerators.Tests/BoolSwitchKeyTests.cs index 70e947b..5f376d6 100644 --- a/EasySourceGenerators.Tests/BoolSwitchKeyTests.cs +++ b/EasySourceGenerators.Tests/BoolSwitchKeyTests.cs @@ -3,62 +3,64 @@ namespace EasySourceGenerators.Tests; -[TestFixture] -public class BoolSwitchKeyTests -{ - [TestCase(true, "Yes")] - [TestCase(false, "No")] - public void BoolSwitchKey_ProducesExpectedRuntimeOutput(bool flag, string expected) - { - string result = TestBoolSwitchClass.GetBoolLabel(flag); - Assert.That(result, Is.EqualTo(expected)); - } - - [Test] - public void BoolSwitchKey_ProducesExpectedGeneratedCode() - { - string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCode("TestBoolSwitchClass_GetBoolLabel.g.cs"); - string expectedCode = """ - namespace EasySourceGenerators.Tests; - - static partial class TestBoolSwitchClass - { - public static partial string GetBoolLabel(bool flag) - { - switch (flag) - { - case true: return "Yes"; - case false: return "No"; - default: return "Unknown"; - } - } - } - """.ReplaceLineEndings("\n").TrimEnd(); - - Assert.That(generatedCode, Is.EqualTo(expectedCode)); - } - - [Test] - public void BoolSwitchKey_GeneratedCode_HasAutoGeneratedHeaderAndWarningSuppression() - { - string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCodeRaw("TestBoolSwitchClass_GetBoolLabel.g.cs"); - - Assert.That(generatedCode, Does.StartWith("// ")); - Assert.That(generatedCode, Does.Contain("Generated by EasySourceGenerators.Generators.GeneratesMethodGenerator for method 'GetBoolLabel'")); - Assert.That(generatedCode, Does.Contain("#pragma warning disable")); - } -} - -public static partial class TestBoolSwitchClass -{ - public static partial string GetBoolLabel(bool flag); - - [GeneratesMethod(nameof(GetBoolLabel))] - [SwitchCase(arg1: true)] - [SwitchCase(arg1: false)] - static string GetBoolLabel_Generator(bool flag) => flag ? "Yes" : "No"; - - [GeneratesMethod(nameof(GetBoolLabel))] - [SwitchDefault] - static Func GetBoolLabel_Default() => _ => "Unknown"; -} +// NOTE: [SwitchCase] attribute-based generation is commented out pending replacement +// with a data-driven approach. These tests will be re-enabled with the new pattern. +// [TestFixture] +// public class BoolSwitchKeyTests +// { +// [TestCase(true, "Yes")] +// [TestCase(false, "No")] +// public void BoolSwitchKey_ProducesExpectedRuntimeOutput(bool flag, string expected) +// { +// string result = TestBoolSwitchClass.GetBoolLabel(flag); +// Assert.That(result, Is.EqualTo(expected)); +// } +// +// [Test] +// public void BoolSwitchKey_ProducesExpectedGeneratedCode() +// { +// string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCode("TestBoolSwitchClass_GetBoolLabel.g.cs"); +// string expectedCode = """ +// namespace EasySourceGenerators.Tests; +// +// static partial class TestBoolSwitchClass +// { +// public static partial string GetBoolLabel(bool flag) +// { +// switch (flag) +// { +// case true: return "Yes"; +// case false: return "No"; +// default: return "Unknown"; +// } +// } +// } +// """.ReplaceLineEndings("\n").TrimEnd(); +// +// Assert.That(generatedCode, Is.EqualTo(expectedCode)); +// } +// +// [Test] +// public void BoolSwitchKey_GeneratedCode_HasAutoGeneratedHeaderAndWarningSuppression() +// { +// string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCodeRaw("TestBoolSwitchClass_GetBoolLabel.g.cs"); +// +// Assert.That(generatedCode, Does.StartWith("// ")); +// Assert.That(generatedCode, Does.Contain("Generated by EasySourceGenerators.Generators.GeneratesMethodGenerator for method 'GetBoolLabel'")); +// Assert.That(generatedCode, Does.Contain("#pragma warning disable")); +// } +// } +// +// public static partial class TestBoolSwitchClass +// { +// public static partial string GetBoolLabel(bool flag); +// +// [GeneratesMethod(nameof(GetBoolLabel))] +// [SwitchCase(arg1: true)] +// [SwitchCase(arg1: false)] +// static string GetBoolLabel_Generator(bool flag) => flag ? "Yes" : "No"; +// +// [GeneratesMethod(nameof(GetBoolLabel))] +// [SwitchDefault] +// static Func GetBoolLabel_Default() => _ => "Unknown"; +// } diff --git a/EasySourceGenerators.Tests/DefaultCaseConstValue.cs b/EasySourceGenerators.Tests/DefaultCaseConstValue.cs index 53b03d7..8bc31fe 100644 --- a/EasySourceGenerators.Tests/DefaultCaseConstValue.cs +++ b/EasySourceGenerators.Tests/DefaultCaseConstValue.cs @@ -3,47 +3,49 @@ namespace EasySourceGenerators.Tests; -[TestFixture] -public class DefaultCaseConstValue -{ - [TestCase(0, 777)] - [TestCase(777, 777)] - [TestCase(123456789, 777)] - public void PiExampleLikeGenerator_ProducesExpectedRuntimeOutput(int decimalNumber, int expectedDigit) - { - int result = DefaultCaseConstValueClass.Foo(decimalNumber); - - Assert.That(result, Is.EqualTo(expectedDigit)); - } - - [Test] - public void PiExampleLikeGenerator_ProducesExpectedGeneratedCode() - { - string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCode("DefaultCaseConstValueClass_Foo.g.cs"); - string expectedCode = """ - namespace EasySourceGenerators.Tests; - - static partial class DefaultCaseConstValueClass - { - public static partial int Foo(int decimalNumber) - { - switch (decimalNumber) - { - default: return 777; - } - } - } - """.ReplaceLineEndings("\n").TrimEnd(); - - Assert.That(generatedCode, Is.EqualTo(expectedCode)); - } -} - -public static partial class DefaultCaseConstValueClass -{ - public static partial int Foo(int decimalNumber); - - [GeneratesMethod(nameof(Foo))] - [SwitchDefault] - static Func Foo_Generator_Default() => decimalNumber => 777; -} +// NOTE: [SwitchDefault] attribute-based generation is commented out pending replacement +// with a data-driven approach. These tests will be re-enabled with the new pattern. +// [TestFixture] +// public class DefaultCaseConstValue +// { +// [TestCase(0, 777)] +// [TestCase(777, 777)] +// [TestCase(123456789, 777)] +// public void PiExampleLikeGenerator_ProducesExpectedRuntimeOutput(int decimalNumber, int expectedDigit) +// { +// int result = DefaultCaseConstValueClass.Foo(decimalNumber); +// +// Assert.That(result, Is.EqualTo(expectedDigit)); +// } +// +// [Test] +// public void PiExampleLikeGenerator_ProducesExpectedGeneratedCode() +// { +// string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCode("DefaultCaseConstValueClass_Foo.g.cs"); +// string expectedCode = """ +// namespace EasySourceGenerators.Tests; +// +// static partial class DefaultCaseConstValueClass +// { +// public static partial int Foo(int decimalNumber) +// { +// switch (decimalNumber) +// { +// default: return 777; +// } +// } +// } +// """.ReplaceLineEndings("\n").TrimEnd(); +// +// Assert.That(generatedCode, Is.EqualTo(expectedCode)); +// } +// } +// +// public static partial class DefaultCaseConstValueClass +// { +// public static partial int Foo(int decimalNumber); +// +// [GeneratesMethod(nameof(Foo))] +// [SwitchDefault] +// static Func Foo_Generator_Default() => decimalNumber => 777; +// } diff --git a/EasySourceGenerators.Tests/DefaultCaseThrowExpressionTests.cs b/EasySourceGenerators.Tests/DefaultCaseThrowExpressionTests.cs index e4c6736..8cc18db 100644 --- a/EasySourceGenerators.Tests/DefaultCaseThrowExpressionTests.cs +++ b/EasySourceGenerators.Tests/DefaultCaseThrowExpressionTests.cs @@ -4,44 +4,46 @@ namespace EasySourceGenerators.Tests; -[TestFixture] -public class DefaultCaseThrowExpressionTests -{ - [Test] - public void SwitchDefaultThrowExpression_ThrowsExpectedExceptionAtRuntime() - { - InvalidOperationException exception = Assert.Throws(() => DefaultCaseThrowExpressionClass.Foo(123))!; - Assert.That(exception.Message, Is.EqualTo("Unsupported input")); - } - - [Test] - public void SwitchDefaultThrowExpression_ProducesThrowDefaultClauseInGeneratedCode() - { - string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCode("DefaultCaseThrowExpressionClass_Foo.g.cs"); - string expectedCode = """ - namespace EasySourceGenerators.Tests; - - static partial class DefaultCaseThrowExpressionClass - { - public static partial int Foo(int input) - { - switch (input) - { - default: throw new InvalidOperationException("Unsupported input"); - } - } - } - """.ReplaceLineEndings("\n").TrimEnd(); - - Assert.That(generatedCode, Is.EqualTo(expectedCode)); - } -} - -public static partial class DefaultCaseThrowExpressionClass -{ - public static partial int Foo(int input); - - [GeneratesMethod(nameof(Foo))] - [SwitchDefault] - private static Func Foo_Generator_Default() => _ => throw new InvalidOperationException("Unsupported input"); -} +// NOTE: [SwitchDefault] attribute-based generation is commented out pending replacement +// with a data-driven approach. These tests will be re-enabled with the new pattern. +// [TestFixture] +// public class DefaultCaseThrowExpressionTests +// { +// [Test] +// public void SwitchDefaultThrowExpression_ThrowsExpectedExceptionAtRuntime() +// { +// InvalidOperationException exception = Assert.Throws(() => DefaultCaseThrowExpressionClass.Foo(123))!; +// Assert.That(exception.Message, Is.EqualTo("Unsupported input")); +// } +// +// [Test] +// public void SwitchDefaultThrowExpression_ProducesThrowDefaultClauseInGeneratedCode() +// { +// string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCode("DefaultCaseThrowExpressionClass_Foo.g.cs"); +// string expectedCode = """ +// namespace EasySourceGenerators.Tests; +// +// static partial class DefaultCaseThrowExpressionClass +// { +// public static partial int Foo(int input) +// { +// switch (input) +// { +// default: throw new InvalidOperationException("Unsupported input"); +// } +// } +// } +// """.ReplaceLineEndings("\n").TrimEnd(); +// +// Assert.That(generatedCode, Is.EqualTo(expectedCode)); +// } +// } +// +// public static partial class DefaultCaseThrowExpressionClass +// { +// public static partial int Foo(int input); +// +// [GeneratesMethod(nameof(Foo))] +// [SwitchDefault] +// private static Func Foo_Generator_Default() => _ => throw new InvalidOperationException("Unsupported input"); +// } diff --git a/EasySourceGenerators.Tests/PiExampleTests.cs b/EasySourceGenerators.Tests/PiExampleTests.cs index 7391b6a..626070d 100644 --- a/EasySourceGenerators.Tests/PiExampleTests.cs +++ b/EasySourceGenerators.Tests/PiExampleTests.cs @@ -3,66 +3,68 @@ namespace EasySourceGenerators.Tests; -[TestFixture] -public class PiExampleTests -{ - [Test] - public void SwitchCaseAttribute_StoresArg1Value() - { - SwitchCase switchCase = new(arg1: 7); - - Assert.That(switchCase.Arg1, Is.EqualTo(7)); - } - - [TestCase(0, 3)] - [TestCase(1, 1)] - [TestCase(2, 4)] - [TestCase(5, 9)] - public void PiExampleLikeGenerator_ProducesExpectedRuntimeOutput(int decimalNumber, int expectedDigit) - { - int result = TestPiClass.GetPiDecimal(decimalNumber); - - Assert.That(result, Is.EqualTo(expectedDigit)); - } - - [Test] - public void PiExampleLikeGenerator_ProducesExpectedGeneratedCode() - { - string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCode("TestPiClass_GetPiDecimal.g.cs"); - string expectedCode = """ - namespace EasySourceGenerators.Tests; - - static partial class TestPiClass - { - public static partial int GetPiDecimal(int decimalNumber) - { - switch (decimalNumber) - { - case 0: return 3; - case 1: return 1; - case 2: return 4; - default: return TestSlowMath.CalculatePiDecimal(decimalNumber); - } - } - } - """.ReplaceLineEndings("\n").TrimEnd(); - - Assert.That(generatedCode, Is.EqualTo(expectedCode)); - } -} - -public static partial class TestPiClass -{ - public static partial int GetPiDecimal(int decimalNumber); - - [GeneratesMethod(nameof(GetPiDecimal))] - [SwitchCase(arg1: 0)] - [SwitchCase(arg1: 1)] - [SwitchCase(arg1: 2)] - static int GetPiDecimal_Generator_Specialized(int decimalNumber) => - TestSlowMath.CalculatePiDecimal(decimalNumber); - - [GeneratesMethod(nameof(GetPiDecimal))] - [SwitchDefault] - static Func GetPiDecimal_Generator_Fallback() => decimalNumber => TestSlowMath.CalculatePiDecimal(decimalNumber); -} +// NOTE: [SwitchCase] attribute-based generation is commented out pending replacement +// with a data-driven approach. These tests will be re-enabled with the new pattern. +// [TestFixture] +// public class PiExampleTests +// { +// [Test] +// public void SwitchCaseAttribute_StoresArg1Value() +// { +// SwitchCase switchCase = new(arg1: 7); +// +// Assert.That(switchCase.Arg1, Is.EqualTo(7)); +// } +// +// [TestCase(0, 3)] +// [TestCase(1, 1)] +// [TestCase(2, 4)] +// [TestCase(5, 9)] +// public void PiExampleLikeGenerator_ProducesExpectedRuntimeOutput(int decimalNumber, int expectedDigit) +// { +// int result = TestPiClass.GetPiDecimal(decimalNumber); +// +// Assert.That(result, Is.EqualTo(expectedDigit)); +// } +// +// [Test] +// public void PiExampleLikeGenerator_ProducesExpectedGeneratedCode() +// { +// string generatedCode = GeneratedCodeTestHelper.ReadGeneratedCode("TestPiClass_GetPiDecimal.g.cs"); +// string expectedCode = """ +// namespace EasySourceGenerators.Tests; +// +// static partial class TestPiClass +// { +// public static partial int GetPiDecimal(int decimalNumber) +// { +// switch (decimalNumber) +// { +// case 0: return 3; +// case 1: return 1; +// case 2: return 4; +// default: return TestSlowMath.CalculatePiDecimal(decimalNumber); +// } +// } +// } +// """.ReplaceLineEndings("\n").TrimEnd(); +// +// Assert.That(generatedCode, Is.EqualTo(expectedCode)); +// } +// } +// +// public static partial class TestPiClass +// { +// public static partial int GetPiDecimal(int decimalNumber); +// +// [GeneratesMethod(nameof(GetPiDecimal))] +// [SwitchCase(arg1: 0)] +// [SwitchCase(arg1: 1)] +// [SwitchCase(arg1: 2)] +// static int GetPiDecimal_Generator_Specialized(int decimalNumber) => +// TestSlowMath.CalculatePiDecimal(decimalNumber); +// +// [GeneratesMethod(nameof(GetPiDecimal))] +// [SwitchDefault] +// static Func GetPiDecimal_Generator_Fallback() => decimalNumber => TestSlowMath.CalculatePiDecimal(decimalNumber); +// }