Switch generators to use data abstraction layer#87
Conversation
…ataGeneratorsFactory - Create DataMethodBody hierarchy (DataSimpleReturnBody, DataSwitchBody) - Create DataMethodBodyBuilders for generating C# source from data model - Create DataGeneratorsFactory for building data from various sources - Refactor fluent and simple patterns in pipeline to use data layer - Add 22 unit tests for data building layer and code generation Co-authored-by: dex3r <3155725+dex3r@users.noreply.github.com> Agent-Logs-Url: https://github.com/dex3r/EasySourceGenerators/sessions/e9007179-53d4-426e-8329-db95a8b21854
…eration Commented out the attribute-based SwitchCase/SwitchDefault pattern in the pipeline and all related tests. This pattern will be replaced with a data-driven approach in a future PR. The data abstraction layer (DataMethodBody, DataMethodBodyBuilders, DataGeneratorsFactory) is ready to be extended for the new pattern. Co-authored-by: dex3r <3155725+dex3r@users.noreply.github.com> Agent-Logs-Url: https://github.com/dex3r/EasySourceGenerators/sessions/e9007179-53d4-426e-8329-db95a8b21854
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
Refactors the method-body generation pipeline to build an intermediate data model (DataMethodBody) first and then emit C# from that model, while temporarily disabling the legacy [SwitchCase]/[SwitchDefault] attribute-based switch generation path.
Changes:
- Introduces
DataMethodBody+DataGeneratorsFactory+DataMethodBodyBuildersto decouple data extraction from source emission. - Updates
GeneratesMethodGenerationPipelineto route fluent + simple patterns through the new data layer. - Comments out attribute-based switch generation and dependent tests/examples until a replacement pattern is implemented.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| EasySourceGenerators.Tests/PiExampleTests.cs | Comments out [SwitchCase] attribute-based tests/examples pending new pattern. |
| EasySourceGenerators.Tests/DefaultCaseThrowExpressionTests.cs | Comments out [SwitchDefault] attribute-based tests pending new pattern. |
| EasySourceGenerators.Tests/DefaultCaseConstValue.cs | Comments out [SwitchDefault] attribute-based tests pending new pattern. |
| EasySourceGenerators.Tests/BoolSwitchKeyTests.cs | Comments out attribute-based bool switch tests pending new pattern. |
| EasySourceGenerators.Generators/GeneratesMethodPatternSourceBuilder.cs | Exposes fluent default-expression extraction helper for pipeline reuse. |
| EasySourceGenerators.Generators/GeneratesMethodGenerationPipeline.cs | Routes fluent/simple patterns through data layer; comments out attribute-switch path. |
| EasySourceGenerators.Generators/DataMethodBodyBuilders.cs | New: emits method bodies from DataMethodBody representations. |
| EasySourceGenerators.Generators/DataMethodBody.cs | New: defines the intermediate data model for method bodies. |
| EasySourceGenerators.Generators/DataGeneratorsFactory.cs | New: constructs DataMethodBody from fluent/simple inputs. |
| EasySourceGenerators.GeneratorTests/GeneratorDiagnosticsTests.cs | Comments out switch-attribute diagnostics tests while the pattern is disabled. |
| EasySourceGenerators.GeneratorTests/DataMethodBodyBuildersTests.cs | New: unit tests for source emission from DataMethodBody. |
| EasySourceGenerators.GeneratorTests/DataGeneratorsFactoryTests.cs | New: unit tests for data construction from fluent/simple inputs. |
| EasySourceGenerators.Examples/PiExample.cs | Comments out attribute-based example in favor of fluent equivalent. |
Comments suppressed due to low confidence (1)
EasySourceGenerators.Generators/GeneratesMethodGenerationPipeline.cs:86
- With the [SwitchCase]/[SwitchDefault] path commented out, generator methods using those attributes (and therefore having a runtime parameter) will now fall into the “simple pattern” validation and emit MSGH007 (“Method generators cannot have any parameters…”). That diagnostic is misleading for the (temporarily disabled) switch-attribute pattern; consider short-circuiting when SwitchCase/SwitchDefault attributes are present and emitting a dedicated “attribute-based switch generation is currently disabled” diagnostic (or suppressing MSGH007 for that case).
List<GeneratesMethodGenerationTarget> methodsWithParameters = methods
.Where(method => method.Symbol.Parameters.Length > 0)
.ToList();
if (methodsWithParameters.Count > 0)
{
foreach (GeneratesMethodGenerationTarget methodWithParameters in methodsWithParameters)
{
context.ReportDiagnostic(Diagnostic.Create(
GeneratesMethodGeneratorDiagnostics.CannotUseRuntimeParameterForCompileTimeGeneratorError,
GetMethodSignatureLocation(methodWithParameters.Syntax)));
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| builder.AppendLine("// <auto-generated/>"); | ||
| 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()! | ||
| }; |
There was a problem hiding this comment.
DataMethodBodyBuilders duplicates the string/namespace/type header and literal formatting logic that already exists in GeneratesMethodPatternSourceBuilder. Keeping two implementations in sync is error-prone; consider extracting the shared helpers into a single utility (or delegating from one builder to the other) so formatting and escaping rules don’t drift.
| builder.AppendLine("// <auto-generated/>"); | |
| 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()! | |
| }; | |
| // Delegate to the shared implementation in GeneratesMethodPatternSourceBuilder | |
| GeneratesMethodPatternSourceBuilder.AppendNamespaceAndTypeHeader(builder, containingType, partialMethod); | |
| } | |
| internal static string FormatValueAsCSharpLiteral(string? value, ITypeSymbol returnType) | |
| { | |
| // Delegate to the shared implementation in GeneratesMethodPatternSourceBuilder | |
| return GeneratesMethodPatternSourceBuilder.FormatValueAsCSharpLiteral(value, returnType); | |
| } | |
| internal static string FormatKeyAsCSharpLiteral(object key, ITypeSymbol? parameterType) | |
| { | |
| // Delegate to the shared implementation in GeneratesMethodPatternSourceBuilder | |
| return GeneratesMethodPatternSourceBuilder.FormatKeyAsCSharpLiteral(key, parameterType); |
| 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) { } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
The test-local GetMetadataReferences() enumerates and adds every loaded AppDomain assembly as a Roslyn reference. This can make tests environment-dependent (loaded assemblies vary by runner/IDE) and may hide missing explicit references. Consider reusing the existing GeneratorTestHelper metadata-reference builder (or mirroring the explicit core-reference list used by GeneratesMethodExecutionRuntimeTests) to keep compilation inputs deterministic.
| 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) { } | |
| } | |
| } |
Generators now produce intermediate data objects (
DataMethodBody) before generating C# source, decoupling data extraction from code emission. Explicit[SwitchCase]attribute-based generation is commented out pending replacement with the new data-driven approach.Data model
DataMethodBody(abstract),DataSimpleReturnBody,DataSwitchBody,DataSwitchCase,DataSwitchDefaultCase— unified representation of what a method body should containCode generation from data
DataMethodBodyBuilders— takes aDataMethodBodyand produces C# source. Single entry point regardless of how the data was built:Data construction
DataGeneratorsFactory— bridges fluent API recordings (SwitchBodyRecord) and simple execution results intoDataMethodBodyinstancesPipeline refactoring
GeneratesMethodGenerationPipelineroutes both fluent and simple patterns throughDataGeneratorsFactory→DataMethodBodyBuildersCommented out (to be replaced)
[SwitchCase]/[SwitchDefault]attribute-based generation path and all dependent tests/examples — the data layer will be extended to support a new attribute pattern in a follow-upTests
DataGeneratorsFactory(data construction correctness)DataMethodBodyBuilders(source generation from data: simple return, switch bodies, bool keys, throw defaults, void, formatting)Original prompt
⚡ Quickly spin up Copilot coding agent tasks from anywhere on your macOS or Windows machine with Raycast.