From b0bcd79740027ae2f969209c3ebbcdba8c7ddcf0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 25 Mar 2026 12:39:33 +0000
Subject: [PATCH 1/6] Initial plan
From 8abff00c450c7e3da17d49c00f09379d3b40e281 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 25 Mar 2026 12:55:34 +0000
Subject: [PATCH 2/6] Extract source emitting code into SourceEmitting
namespace and refactor ExecutionRuntime
- Create CSharpLiteralFormatter for value/key literal formatting
- Create CSharpAccessibilityKeyword for accessibility mapping
- Create CSharpTypeKeyword for type kind mapping
- Create PartialMethodEmitData record and PartialMethodSourceEmitter
- Create DummyImplementationEmitter with data records
- Create RoslynSymbolDataMapper to bridge Roslyn types to emitter records
- Create GeneratorAssemblyExecutor for shared compile/load logic
- Create BodyGenerationDataExtractor for reflection-based data extraction
- Refactor GeneratesMethodExecutionRuntime to use new classes
- Refactor GeneratesMethodPatternSourceBuilder to delegate to SourceEmitting
- Add documentation comments to all internal/public methods
Co-authored-by: dex3r <3155725+dex3r@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dex3r/EasySourceGenerators/sessions/cb205769-20ee-4e1e-9b44-3ef890163968
---
.../BodyGenerationDataExtractor.cs | 94 ++++
.../GeneratesMethodExecutionRuntime.cs | 452 +++++-------------
.../GeneratesMethodGenerationPipeline.cs | 20 +
...eneratesMethodGenerationTargetCollector.cs | 13 +
.../GeneratesMethodGenerator.cs | 8 +
.../GeneratesMethodPatternSourceBuilder.cs | 368 ++------------
.../GeneratorAssemblyExecutor.cs | 229 +++++++++
.../CSharpAccessibilityKeyword.cs | 44 ++
.../SourceEmitting/CSharpLiteralFormatter.cs | 54 +++
.../SourceEmitting/CSharpTypeKeyword.cs | 23 +
.../DummyImplementationEmitter.cs | 68 +++
.../SourceEmitting/PartialMethodEmitData.cs | 18 +
.../PartialMethodSourceEmitter.cs | 60 +++
.../SourceEmitting/RoslynSymbolDataMapper.cs | 103 ++++
14 files changed, 896 insertions(+), 658 deletions(-)
create mode 100644 EasySourceGenerators.Generators/IncrementalGenerators/BodyGenerationDataExtractor.cs
create mode 100644 EasySourceGenerators.Generators/IncrementalGenerators/GeneratorAssemblyExecutor.cs
create mode 100644 EasySourceGenerators.Generators/SourceEmitting/CSharpAccessibilityKeyword.cs
create mode 100644 EasySourceGenerators.Generators/SourceEmitting/CSharpLiteralFormatter.cs
create mode 100644 EasySourceGenerators.Generators/SourceEmitting/CSharpTypeKeyword.cs
create mode 100644 EasySourceGenerators.Generators/SourceEmitting/DummyImplementationEmitter.cs
create mode 100644 EasySourceGenerators.Generators/SourceEmitting/PartialMethodEmitData.cs
create mode 100644 EasySourceGenerators.Generators/SourceEmitting/PartialMethodSourceEmitter.cs
create mode 100644 EasySourceGenerators.Generators/SourceEmitting/RoslynSymbolDataMapper.cs
diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/BodyGenerationDataExtractor.cs b/EasySourceGenerators.Generators/IncrementalGenerators/BodyGenerationDataExtractor.cs
new file mode 100644
index 0000000..9130a7a
--- /dev/null
+++ b/EasySourceGenerators.Generators/IncrementalGenerators/BodyGenerationDataExtractor.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Reflection;
+
+namespace EasySourceGenerators.Generators.IncrementalGenerators;
+
+///
+/// Extracts data from a method result object via reflection.
+/// The result object is expected to be a DataMethodBodyGenerator containing a
+/// BodyGenerationData property, from which return values and delegate bodies are extracted.
+///
+internal static class BodyGenerationDataExtractor
+{
+ ///
+ /// Extracts the return value from a fluent body generator method result using reflection.
+ /// Checks for ReturnConstantValueFactory first, then RuntimeDelegateBody.
+ /// Returns a with the extracted value, or null return value
+ /// if neither factory nor body are present.
+ ///
+ internal static FluentBodyResult Extract(object methodResult, bool isVoidReturnType)
+ {
+ Type resultType = methodResult.GetType();
+
+ // The result should be a DataMethodBodyGenerator containing a BodyGenerationData Data property
+ PropertyInfo? dataProperty = resultType.GetProperty(Consts.BodyGenerationDataPropertyName);
+ if (dataProperty == null)
+ {
+ // The method returned something that isn't a DataMethodBodyGenerator.
+ // This may happen when the fluent chain is incomplete (e.g., user returned an intermediate builder).
+ return new FluentBodyResult(null, isVoidReturnType);
+ }
+
+ object? bodyGenerationData = dataProperty.GetValue(methodResult);
+ if (bodyGenerationData == null)
+ {
+ return new FluentBodyResult(null, isVoidReturnType);
+ }
+
+ Type dataType = bodyGenerationData.GetType();
+ PropertyInfo? returnTypeProperty = dataType.GetProperty("ReturnType");
+ Type? dataReturnType = returnTypeProperty?.GetValue(bodyGenerationData) as Type;
+ bool isVoid = dataReturnType == typeof(void);
+
+ return TryExtractFromConstantFactory(dataType, bodyGenerationData, isVoid)
+ ?? TryExtractFromRuntimeBody(dataType, bodyGenerationData, isVoid)
+ ?? new FluentBodyResult(null, isVoid);
+ }
+
+ ///
+ /// Attempts to extract a return value by invoking the ReturnConstantValueFactory delegate.
+ ///
+ private static FluentBodyResult? TryExtractFromConstantFactory(
+ Type dataType,
+ object bodyGenerationData,
+ bool isVoid)
+ {
+ PropertyInfo? constantFactoryProperty = dataType.GetProperty("ReturnConstantValueFactory");
+ Delegate? constantFactory = constantFactoryProperty?.GetValue(bodyGenerationData) as Delegate;
+ if (constantFactory == null)
+ {
+ return null;
+ }
+
+ object? constantValue = constantFactory.DynamicInvoke();
+ 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.
+ ///
+ private static FluentBodyResult? TryExtractFromRuntimeBody(
+ Type dataType,
+ object bodyGenerationData,
+ bool isVoid)
+ {
+ PropertyInfo? runtimeBodyProperty = dataType.GetProperty("RuntimeDelegateBody");
+ Delegate? runtimeBody = runtimeBodyProperty?.GetValue(bodyGenerationData) as Delegate;
+ if (runtimeBody == null)
+ {
+ return null;
+ }
+
+ ParameterInfo[] bodyParams = runtimeBody.Method.GetParameters();
+ if (bodyParams.Length == 0)
+ {
+ object? bodyResult = runtimeBody.DynamicInvoke();
+ return new FluentBodyResult(bodyResult?.ToString(), isVoid);
+ }
+
+ // For delegates with parameters, we can't invoke at compile time without values
+ return new FluentBodyResult(null, isVoid);
+ }
+}
diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodExecutionRuntime.cs b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodExecutionRuntime.cs
index 0f998f0..4c8d5cb 100644
--- a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodExecutionRuntime.cs
+++ b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodExecutionRuntime.cs
@@ -3,13 +3,9 @@
using System.IO;
using System.Linq;
using System.Reflection;
-using System.Runtime.Loader;
-using System.Text;
-using EasySourceGenerators.Abstractions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
-using Microsoft.CodeAnalysis.Emit;
namespace EasySourceGenerators.Generators.IncrementalGenerators;
@@ -24,8 +20,16 @@ internal sealed record FluentBodyResult(
string? ReturnValue,
bool IsVoid);
+///
+/// Orchestrates the execution of generator methods at compile time.
+/// Delegates compilation and assembly loading to ,
+/// and data extraction to .
+///
internal static class GeneratesMethodExecutionRuntime
{
+ ///
+ /// Executes a simple (non-fluent) generator method with no arguments and returns its string result.
+ ///
internal static (string? value, string? error) ExecuteSimpleGeneratorMethod(
IMethodSymbol generatorMethod,
IMethodSymbol partialMethod,
@@ -35,228 +39,116 @@ internal static (string? value, string? error) ExecuteSimpleGeneratorMethod(
return ExecuteGeneratorMethodWithArgs(generatorMethod, allPartials, compilation, null);
}
- // SwitchBodyData-based fluent execution has been replaced by the data abstraction layer.
- // Use ExecuteFluentBodyGeneratorMethod instead.
-
+ ///
+ /// Executes a fluent body generator method and extracts the
+ /// from the returned DataMethodBodyGenerator.
+ ///
internal static (FluentBodyResult? result, string? error) ExecuteFluentBodyGeneratorMethod(
IMethodSymbol generatorMethod,
IMethodSymbol partialMethod,
Compilation compilation)
{
IReadOnlyList allPartials = GetAllUnimplementedPartialMethods(compilation);
- CSharpCompilation executableCompilation = BuildExecutionCompilation(allPartials, compilation);
- using MemoryStream stream = new();
- EmitResult emitResult = executableCompilation.Emit(stream);
- if (!emitResult.Success)
+ (LoadedAssemblyContext? loadedContext, string? loadError) =
+ GeneratorAssemblyExecutor.CompileAndLoadAssembly(allPartials, compilation);
+ if (loadError != null)
{
- string errors = string.Join("; ", emitResult.Diagnostics
- .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
- .Select(diagnostic => diagnostic.GetMessage()));
- return (null, $"Compilation failed: {errors}");
+ return (null, loadError);
}
- stream.Position = 0;
- AssemblyLoadContext? loadContext = null;
+ using LoadedAssemblyContext context = loadedContext!;
try
{
- Dictionary compilationReferenceBytes = EmitCompilationReferences(compilation);
-
- loadContext = new AssemblyLoadContext("__GeneratorExec", isCollectible: true);
- Assembly? capturedAbstractionsAssembly = null;
- loadContext.Resolving += (context, assemblyName) =>
+ (Assembly? abstractionsAssembly, string? abstractionsError) =
+ ResolveAbstractionsAssembly(context, compilation);
+ if (abstractionsError != null)
{
- PortableExecutableReference? match = compilation.References
- .OfType()
- .FirstOrDefault(reference => reference.FilePath is not null && string.Equals(
- Path.GetFileNameWithoutExtension(reference.FilePath),
- assemblyName.Name,
- StringComparison.OrdinalIgnoreCase));
- if (match?.FilePath != null)
- return context.LoadFromAssemblyPath(ResolveImplementationAssemblyPath(match.FilePath));
-
- if (assemblyName.Name != null && compilationReferenceBytes.TryGetValue(assemblyName.Name, out byte[]? bytes))
- {
- Assembly loaded = context.LoadFromStream(new MemoryStream(bytes));
- if (string.Equals(assemblyName.Name, Consts.AbstractionsAssemblyName, StringComparison.OrdinalIgnoreCase))
- capturedAbstractionsAssembly = loaded;
- return loaded;
- }
-
- return null;
- };
-
- Assembly assembly = loadContext.LoadFromStream(stream);
-
- MetadataReference[] abstractionsMatchingReferences = compilation.References.Where(reference => reference.Display is not null && (
- reference.Display.Equals(Consts.AbstractionsAssemblyName, StringComparison.OrdinalIgnoreCase)
- || (reference is PortableExecutableReference peRef && peRef.FilePath is not null && Path.GetFileNameWithoutExtension(peRef.FilePath)
- .Equals(Consts.AbstractionsAssemblyName, StringComparison.OrdinalIgnoreCase))))
- .ToArray();
-
- if (abstractionsMatchingReferences.Length == 0)
- {
- MetadataReference[] closestMatches = compilation.References.Where(reference =>
- reference.Display is not null
- && reference.Display.Contains(Consts.SolutionNamespace, StringComparison.OrdinalIgnoreCase))
- .ToArray();
-
- string closestMatchesString = string.Join(", ", closestMatches.Select(reference => reference.Display));
-
- return (null, $"Could not find any reference matching '{Consts.AbstractionsAssemblyName}' in compilation references.\n" +
- $" Found total references: {compilation.References.Count()}. \nMatching references: {closestMatches.Length}: \n{closestMatchesString}");
+ return (null, abstractionsError);
}
- PortableExecutableReference[] peMatchingReferences = abstractionsMatchingReferences.OfType().ToArray();
- CompilationReference[] csharpCompilationReference = abstractionsMatchingReferences.OfType().ToArray();
-
- Assembly abstractionsAssembly;
-
- if (peMatchingReferences.Length > 0)
- {
- PortableExecutableReference abstractionsReference = peMatchingReferences.First();
-
- if (string.IsNullOrEmpty(abstractionsReference.FilePath))
- {
- return (null, $"The reference matching '{Consts.AbstractionsAssemblyName}' does not have a valid file path.");
- }
-
- string abstractionsAssemblyPath = ResolveImplementationAssemblyPath(abstractionsReference.FilePath);
- abstractionsAssembly = loadContext.LoadFromAssemblyPath(abstractionsAssemblyPath);
- }
- else if (csharpCompilationReference.Length > 0)
- {
- if (capturedAbstractionsAssembly != null)
- {
- abstractionsAssembly = capturedAbstractionsAssembly;
- }
- else if (compilationReferenceBytes.TryGetValue(Consts.AbstractionsAssemblyName, out byte[]? abstractionBytes))
- {
- abstractionsAssembly = loadContext.LoadFromStream(new MemoryStream(abstractionBytes));
- }
- else
- {
- return (null, $"Found reference matching '{Consts.AbstractionsAssemblyName}' as a CompilationReference, but failed to emit it to a loadable assembly.");
- }
- }
- else
- {
- string matchesString = string.Join(", ", abstractionsMatchingReferences.Select(reference => $"{reference.Display} (type: {reference.GetType().Name})"));
- return (null, $"Found references matching '{Consts.AbstractionsAssemblyName}' but none were PortableExecutableReference or CompilationReference with valid file paths. \nMatching references: {matchesString}");
- }
-
- Type? generatorStaticType = abstractionsAssembly.GetType(Consts.GenerateTypeFullName);
- Type? dataGeneratorsFactoryType = assembly.GetType(Consts.DataGeneratorsFactoryTypeFullName);
- if (generatorStaticType == null || dataGeneratorsFactoryType == null)
+ string? setupError = SetupDataGeneratorsFactory(context.Assembly, abstractionsAssembly!, context);
+ if (setupError != null)
{
- return (null, $"Could not find {Consts.GenerateTypeFullName} or {Consts.DataGeneratorsFactoryTypeFullName} types in compiled assembly");
+ return (null, setupError);
}
- object? dataGeneratorsFactory = Activator.CreateInstance(dataGeneratorsFactoryType);
- PropertyInfo? currentGeneratorProperty = generatorStaticType.GetProperty(Consts.CurrentGeneratorPropertyName, BindingFlags.NonPublic | BindingFlags.Static);
- currentGeneratorProperty?.SetValue(null, dataGeneratorsFactory);
-
string typeName = generatorMethod.ContainingType.ToDisplayString();
- Type? loadedType = assembly.GetType(typeName);
- if (loadedType == null)
+ (Type? loadedType, string? typeError) = GeneratorAssemblyExecutor.FindType(context.Assembly, typeName);
+ if (typeError != null)
{
- return (null, $"Could not find type '{typeName}' in compiled assembly");
+ return (null, typeError);
}
- MethodInfo? generatorMethodInfo = loadedType.GetMethod(generatorMethod.Name, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
- if (generatorMethodInfo == null)
+ (MethodInfo? methodInfo, string? methodError) =
+ GeneratorAssemblyExecutor.FindStaticMethod(loadedType!, generatorMethod.Name, typeName);
+ if (methodError != null)
{
- return (null, $"Could not find method '{generatorMethod.Name}' in type '{typeName}'");
+ return (null, methodError);
}
- object? methodResult = generatorMethodInfo.Invoke(null, null);
+ object? methodResult = methodInfo!.Invoke(null, null);
if (methodResult == null)
{
return (null, "Fluent body generator method returned null");
}
- return (ExtractBodyGenerationData(methodResult, partialMethod.ReturnType), null);
+ bool isVoidReturnType = partialMethod.ReturnType.SpecialType == SpecialType.System_Void;
+ FluentBodyResult bodyResult = BodyGenerationDataExtractor.Extract(methodResult, isVoidReturnType);
+ return (bodyResult, null);
}
catch (Exception ex)
{
return (null, $"Error executing generator method '{generatorMethod.Name}': {ex.GetBaseException()}");
}
- finally
- {
- loadContext?.Unload();
- }
}
+ ///
+ /// Executes a generator method with optional arguments and returns its string result.
+ ///
internal static (string? value, string? error) ExecuteGeneratorMethodWithArgs(
IMethodSymbol generatorMethod,
IReadOnlyList allPartialMethods,
Compilation compilation,
object?[]? args)
{
- CSharpCompilation executableCompilation = BuildExecutionCompilation(allPartialMethods, compilation);
-
- using MemoryStream stream = new();
- EmitResult emitResult = executableCompilation.Emit(stream);
- if (!emitResult.Success)
+ (LoadedAssemblyContext? loadedContext, string? loadError) =
+ GeneratorAssemblyExecutor.CompileAndLoadAssembly(allPartialMethods, compilation);
+ if (loadError != null)
{
- string errors = string.Join("; ", emitResult.Diagnostics
- .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
- .Select(diagnostic => diagnostic.GetMessage()));
- return (null, $"Compilation failed: {errors}");
+ return (null, loadError);
}
- stream.Position = 0;
- AssemblyLoadContext? loadContext = null;
+ using LoadedAssemblyContext context = loadedContext!;
try
{
- Dictionary compilationReferenceBytes = EmitCompilationReferences(compilation);
-
- loadContext = new AssemblyLoadContext("__GeneratorExec", isCollectible: true);
- loadContext.Resolving += (context, assemblyName) =>
- {
- PortableExecutableReference? match = compilation.References
- .OfType()
- .FirstOrDefault(reference => reference.FilePath is not null && string.Equals(
- Path.GetFileNameWithoutExtension(reference.FilePath),
- assemblyName.Name,
- StringComparison.OrdinalIgnoreCase));
- if (match?.FilePath != null)
- return context.LoadFromAssemblyPath(ResolveImplementationAssemblyPath(match.FilePath));
-
- if (assemblyName.Name != null && compilationReferenceBytes.TryGetValue(assemblyName.Name, out byte[]? bytes))
- return context.LoadFromStream(new MemoryStream(bytes));
-
- return null;
- };
-
- Assembly assembly = loadContext.LoadFromStream(stream);
string typeName = generatorMethod.ContainingType.ToDisplayString();
- Type? loadedType = assembly.GetType(typeName);
- if (loadedType == null)
+ (Type? loadedType, string? typeError) = GeneratorAssemblyExecutor.FindType(context.Assembly, typeName);
+ if (typeError != null)
{
- return (null, $"Could not find type '{typeName}' in compiled assembly");
+ return (null, typeError);
}
- MethodInfo? generatorMethodInfo = loadedType.GetMethod(generatorMethod.Name, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
- if (generatorMethodInfo == null)
+ (MethodInfo? methodInfo, string? methodError) =
+ GeneratorAssemblyExecutor.FindStaticMethod(loadedType!, generatorMethod.Name, typeName);
+ if (methodError != null)
{
- return (null, $"Could not find method '{generatorMethod.Name}' in type '{typeName}'");
+ return (null, methodError);
}
- object?[]? convertedArgs = ConvertArguments(args, generatorMethodInfo);
- object? result = generatorMethodInfo.Invoke(null, convertedArgs);
+ object?[]? convertedArgs = GeneratorAssemblyExecutor.ConvertArguments(args, methodInfo!);
+ object? result = methodInfo!.Invoke(null, convertedArgs);
return (result?.ToString(), null);
}
catch (Exception ex)
{
return (null, $"Error executing generator method '{generatorMethod.Name}': {ex.GetBaseException()}");
}
- finally
- {
- loadContext?.Unload();
- }
}
+ ///
+ /// Finds all unimplemented partial method definitions across all syntax trees in the compilation.
+ ///
internal static IReadOnlyList GetAllUnimplementedPartialMethods(Compilation compilation)
{
List methods = new();
@@ -279,188 +171,106 @@ internal static IReadOnlyList GetAllUnimplementedPartialMethods(C
return methods;
}
- private static object?[]? ConvertArguments(object?[]? args, MethodInfo methodInfo)
- {
- if (args == null || methodInfo.GetParameters().Length == 0)
- {
- return null;
- }
-
- Type parameterType = methodInfo.GetParameters()[0].ParameterType;
- return new[] { Convert.ChangeType(args[0], parameterType) };
- }
-
- private static FluentBodyResult ExtractBodyGenerationData(object methodResult, ITypeSymbol returnType)
+ ///
+ /// Resolves the abstractions assembly from the compilation references.
+ /// Handles both (file-based) and
+ /// (in-memory, e.g., from Rider's code inspector).
+ ///
+ private static (Assembly? assembly, string? error) ResolveAbstractionsAssembly(
+ LoadedAssemblyContext context,
+ Compilation compilation)
{
- Type resultType = methodResult.GetType();
+ MetadataReference[] abstractionsMatchingReferences = compilation.References.Where(reference =>
+ reference.Display is not null && (
+ reference.Display.Equals(Consts.AbstractionsAssemblyName, StringComparison.OrdinalIgnoreCase)
+ || (reference is PortableExecutableReference peRef && peRef.FilePath is not null &&
+ Path.GetFileNameWithoutExtension(peRef.FilePath)
+ .Equals(Consts.AbstractionsAssemblyName, StringComparison.OrdinalIgnoreCase))))
+ .ToArray();
- // The result should be a DataMethodBodyGenerator containing a BodyGenerationData Data property
- PropertyInfo? dataProperty = resultType.GetProperty(Consts.BodyGenerationDataPropertyName);
- if (dataProperty == null)
+ if (abstractionsMatchingReferences.Length == 0)
{
- // The method returned something that isn't a DataMethodBodyGenerator.
- // This may happen when the fluent chain is incomplete (e.g., user returned an intermediate builder).
- return new FluentBodyResult(null, returnType.SpecialType == SpecialType.System_Void);
- }
+ MetadataReference[] closestMatches = compilation.References.Where(reference =>
+ reference.Display is not null
+ && reference.Display.Contains(Consts.SolutionNamespace, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
- object? bodyGenerationData = dataProperty.GetValue(methodResult);
- if (bodyGenerationData == null)
- {
- return new FluentBodyResult(null, returnType.SpecialType == SpecialType.System_Void);
+ string closestMatchesString = string.Join(", ", closestMatches.Select(reference => reference.Display));
+
+ return (null,
+ $"Could not find any reference matching '{Consts.AbstractionsAssemblyName}' in compilation references.\n" +
+ $" Found total references: {compilation.References.Count()}. \nMatching references: {closestMatches.Length}: \n{closestMatchesString}");
}
- Type dataType = bodyGenerationData.GetType();
- PropertyInfo? returnTypeProperty = dataType.GetProperty("ReturnType");
- Type? dataReturnType = returnTypeProperty?.GetValue(bodyGenerationData) as Type;
- bool isVoid = dataReturnType == typeof(void);
+ PortableExecutableReference[] peMatchingReferences =
+ abstractionsMatchingReferences.OfType().ToArray();
+ CompilationReference[] csharpCompilationReference =
+ abstractionsMatchingReferences.OfType().ToArray();
- // Try ReturnConstantValueFactory first
- PropertyInfo? constantFactoryProperty = dataType.GetProperty("ReturnConstantValueFactory");
- Delegate? constantFactory = constantFactoryProperty?.GetValue(bodyGenerationData) as Delegate;
- if (constantFactory != null)
+ if (peMatchingReferences.Length > 0)
{
- object? constantValue = constantFactory.DynamicInvoke();
- return new FluentBodyResult(constantValue?.ToString(), isVoid);
- }
+ PortableExecutableReference abstractionsReference = peMatchingReferences.First();
- // Try RuntimeDelegateBody
- PropertyInfo? runtimeBodyProperty = dataType.GetProperty("RuntimeDelegateBody");
- Delegate? runtimeBody = runtimeBodyProperty?.GetValue(bodyGenerationData) as Delegate;
- if (runtimeBody != null)
- {
- ParameterInfo[] bodyParams = runtimeBody.Method.GetParameters();
- if (bodyParams.Length == 0)
+ if (string.IsNullOrEmpty(abstractionsReference.FilePath))
{
- object? bodyResult = runtimeBody.DynamicInvoke();
- return new FluentBodyResult(bodyResult?.ToString(), isVoid);
+ return (null,
+ $"The reference matching '{Consts.AbstractionsAssemblyName}' does not have a valid file path.");
}
- // For delegates with parameters, we can't invoke at compile time without values
- return new FluentBodyResult(null, isVoid);
+ string abstractionsAssemblyPath =
+ GeneratorAssemblyExecutor.ResolveImplementationAssemblyPath(abstractionsReference.FilePath);
+ Assembly abstractionsAssembly = context.LoadContext.LoadFromAssemblyPath(abstractionsAssemblyPath);
+ return (abstractionsAssembly, null);
}
- return new FluentBodyResult(null, isVoid);
- }
-
- private static Dictionary EmitCompilationReferences(Compilation compilation)
- {
- Dictionary result = new(StringComparer.OrdinalIgnoreCase);
- foreach (CompilationReference compilationRef in compilation.References.OfType())
+ if (csharpCompilationReference.Length > 0)
{
- string assemblyName = compilationRef.Compilation.AssemblyName ?? string.Empty;
- if (string.IsNullOrEmpty(assemblyName))
- continue;
- using MemoryStream refStream = new();
- if (compilationRef.Compilation.Emit(refStream).Success)
- result[assemblyName] = refStream.ToArray();
- }
+ if (context.CapturedAbstractionsAssembly != null)
+ {
+ return (context.CapturedAbstractionsAssembly, null);
+ }
- return result;
- }
+ if (context.CompilationReferenceBytes.TryGetValue(Consts.AbstractionsAssemblyName,
+ out byte[]? abstractionBytes))
+ {
+ Assembly abstractionsAssembly =
+ context.LoadContext.LoadFromStream(new MemoryStream(abstractionBytes));
+ return (abstractionsAssembly, null);
+ }
- private static string ResolveImplementationAssemblyPath(string path)
- {
- string? directory = Path.GetDirectoryName(path);
- string? parentDirectory = directory != null ? Path.GetDirectoryName(directory) : null;
- if (directory != null &&
- parentDirectory != null &&
- string.Equals(Path.GetFileName(directory), "ref", StringComparison.OrdinalIgnoreCase))
- {
- return Path.Combine(parentDirectory, Path.GetFileName(path));
+ return (null,
+ $"Found reference matching '{Consts.AbstractionsAssemblyName}' as a CompilationReference, but failed to emit it to a loadable assembly.");
}
- return path;
- }
-
- private static CSharpCompilation BuildExecutionCompilation(
- IReadOnlyList allPartialMethods,
- Compilation compilation)
- {
- string dummySource = BuildDummyImplementation(allPartialMethods);
- string dataGeneratorsFactorySource = ReadEmbeddedResource($"{Consts.GeneratorsAssemblyName}.DataGeneratorsFactory.cs");
- string dataMethodBodyBuildersSource = ReadEmbeddedResource($"{Consts.GeneratorsAssemblyName}.DataMethodBodyBuilders.cs");
- string dataRecordsSource = ReadEmbeddedResource($"{Consts.GeneratorsAssemblyName}.DataRecords.cs");
- CSharpParseOptions parseOptions = compilation.SyntaxTrees.FirstOrDefault()?.Options as CSharpParseOptions
- ?? CSharpParseOptions.Default;
-
- return (CSharpCompilation)compilation
- .AddSyntaxTrees(
- CSharpSyntaxTree.ParseText(dummySource, parseOptions),
- CSharpSyntaxTree.ParseText(dataGeneratorsFactorySource, parseOptions),
- CSharpSyntaxTree.ParseText(dataMethodBodyBuildersSource, parseOptions),
- CSharpSyntaxTree.ParseText(dataRecordsSource, parseOptions));
- }
-
- private static string ReadEmbeddedResource(string resourceName)
- {
- using Stream? stream = typeof(GeneratesMethodExecutionRuntime).Assembly.GetManifestResourceStream(resourceName);
- if (stream == null)
- throw new InvalidOperationException($"Embedded resource '{resourceName}' not found in {Consts.GeneratorsAssemblyName} assembly");
- using StreamReader reader = new StreamReader(stream);
- return reader.ReadToEnd();
+ string matchesString = string.Join(", ",
+ abstractionsMatchingReferences.Select(reference =>
+ $"{reference.Display} (type: {reference.GetType().Name})"));
+ return (null,
+ $"Found references matching '{Consts.AbstractionsAssemblyName}' but none were PortableExecutableReference or CompilationReference with valid file paths. \nMatching references: {matchesString}");
}
- private static string BuildDummyImplementation(IEnumerable partialMethods)
+ ///
+ /// Sets up the DataGeneratorsFactory and assigns it to Generate.CurrentGenerator
+ /// in the loaded abstractions assembly, enabling fluent API usage during generator execution.
+ ///
+ private static string? SetupDataGeneratorsFactory(
+ Assembly executionAssembly,
+ Assembly abstractionsAssembly,
+ LoadedAssemblyContext context)
{
- StringBuilder builder = new();
-
- IEnumerable> groupedMethods = partialMethods.GroupBy(
- method => (Namespace: method.ContainingType.ContainingNamespace?.IsGlobalNamespace == false
- ? method.ContainingType.ContainingNamespace.ToDisplayString()
- : null,
- TypeName: method.ContainingType.Name,
- IsStatic: method.ContainingType.IsStatic,
- TypeKind: method.ContainingType.TypeKind));
-
- foreach (IGrouping<(string? Namespace, string TypeName, bool IsStatic, TypeKind TypeKind), IMethodSymbol> typeGroup in groupedMethods)
+ Type? generatorStaticType = abstractionsAssembly.GetType(Consts.GenerateTypeFullName);
+ Type? dataGeneratorsFactoryType = executionAssembly.GetType(Consts.DataGeneratorsFactoryTypeFullName);
+ if (generatorStaticType == null || dataGeneratorsFactoryType == null)
{
- string? namespaceName = typeGroup.Key.Namespace;
- if (namespaceName != null)
- {
- builder.AppendLine($"namespace {namespaceName} {{");
- }
-
- string typeKeyword = typeGroup.Key.TypeKind switch
- {
- TypeKind.Struct => "struct",
- _ => "class"
- };
-
- string typeModifiers = typeGroup.Key.IsStatic ? "static partial" : "partial";
- builder.AppendLine($"{typeModifiers} {typeKeyword} {typeGroup.Key.TypeName} {{");
-
- foreach (IMethodSymbol partialMethod in typeGroup)
- {
- string accessibility = partialMethod.DeclaredAccessibility switch
- {
- Accessibility.Public => "public",
- Accessibility.Protected => "protected",
- Accessibility.Internal => "internal",
- Accessibility.ProtectedOrInternal => "protected internal",
- Accessibility.ProtectedAndInternal => "private protected",
- _ => ""
- };
-
- string staticModifier = partialMethod.IsStatic ? "static " : "";
- string returnType = partialMethod.ReturnType.ToDisplayString();
- string parameters = string.Join(", ", partialMethod.Parameters.Select(parameter => $"{parameter.Type.ToDisplayString()} {parameter.Name}"));
-
- builder.AppendLine($"{accessibility} {staticModifier}partial {returnType} {partialMethod.Name}({parameters}) {{");
- string exceptionName = $"{Consts.AbstractionsAssemblyName}.{nameof(PartialMethodCalledDuringGenerationException)}";
- string throwStatement = $"throw new global::{exceptionName}(\"{partialMethod.Name}\", \"{partialMethod.ContainingType.Name}\");";
- builder.AppendLine(throwStatement);
-
- builder.AppendLine("}");
- }
-
- builder.AppendLine("}");
-
- if (namespaceName != null)
- {
- builder.AppendLine("}");
- }
+ return
+ $"Could not find {Consts.GenerateTypeFullName} or {Consts.DataGeneratorsFactoryTypeFullName} types in compiled assembly";
}
- return builder.ToString();
+ object? dataGeneratorsFactory = Activator.CreateInstance(dataGeneratorsFactoryType);
+ PropertyInfo? currentGeneratorProperty = generatorStaticType.GetProperty(
+ Consts.CurrentGeneratorPropertyName, BindingFlags.NonPublic | BindingFlags.Static);
+ currentGeneratorProperty?.SetValue(null, dataGeneratorsFactory);
+
+ return null;
}
}
diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerationPipeline.cs b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerationPipeline.cs
index c795277..802c97f 100644
--- a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerationPipeline.cs
+++ b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerationPipeline.cs
@@ -8,8 +8,16 @@
namespace EasySourceGenerators.Generators.IncrementalGenerators;
+///
+/// Orchestrates the full source generation pipeline: collecting generation targets,
+/// grouping them by target method, and generating C# source for each group.
+///
internal static class GeneratesMethodGenerationPipeline
{
+ ///
+ /// Executes the generation pipeline for all methods marked with the generator attribute.
+ /// Collects valid targets, groups by containing type and target method, then generates source.
+ ///
internal static void Execute(
SourceProductionContext context,
ImmutableArray generatorMethods,
@@ -42,6 +50,10 @@ internal static void Execute(
}
}
+ ///
+ /// Determines the generation pattern (fluent or simple) and generates source for a group
+ /// of generator methods targeting the same partial method.
+ ///
private static string GenerateSourceForGroup(
SourceProductionContext context,
List methods,
@@ -83,6 +95,10 @@ private static string GenerateSourceForGroup(
return GenerateFromSimplePattern(context, firstMethod, compilation);
}
+ ///
+ /// Generates source code from a fluent body pattern, executing the generator method
+ /// and extracting the return value from the fluent API result.
+ ///
private static string GenerateFromFluentBodyPattern(
SourceProductionContext context,
GeneratesMethodGenerationTarget methodInfo,
@@ -111,6 +127,10 @@ private static string GenerateFromFluentBodyPattern(
result!.ReturnValue);
}
+ ///
+ /// Generates source code from a simple pattern, executing the generator method
+ /// and using its return value as the partial method's return expression.
+ ///
private static string GenerateFromSimplePattern(
SourceProductionContext context,
GeneratesMethodGenerationTarget firstMethod,
diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerationTargetCollector.cs b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerationTargetCollector.cs
index dd74631..7d69104 100644
--- a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerationTargetCollector.cs
+++ b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerationTargetCollector.cs
@@ -9,6 +9,10 @@
namespace EasySourceGenerators.Generators.IncrementalGenerators;
+///
+/// Represents a validated generation target: a generator method with its resolved
+/// target partial method and containing type information.
+///
internal sealed record GeneratesMethodGenerationTarget(
MethodDeclarationSyntax Syntax,
IMethodSymbol Symbol,
@@ -16,8 +20,17 @@ internal sealed record GeneratesMethodGenerationTarget(
IMethodSymbol PartialMethod,
INamedTypeSymbol ContainingType);
+///
+/// Collects and validates generator methods marked with [MethodBodyGenerator],
+/// resolving each to its target partial method and reporting diagnostics for invalid configurations.
+///
internal static class GeneratesMethodGenerationTargetCollector
{
+ ///
+ /// Scans all generator methods, validates their configuration, and returns a list of
+ /// valid generation targets. Reports diagnostics for non-static generators (MSGH002)
+ /// and missing partial methods (MSGH001).
+ ///
internal static List Collect(
SourceProductionContext context,
ImmutableArray generatorMethods,
diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerator.cs b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerator.cs
index 3a4e9e2..4730ee6 100644
--- a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerator.cs
+++ b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodGenerator.cs
@@ -7,9 +7,17 @@
namespace EasySourceGenerators.Generators.IncrementalGenerators;
+///
+/// Roslyn incremental source generator entry point. Detects methods marked with
+/// [MethodBodyGenerator] and delegates to
+/// for source code generation.
+///
[Generator]
public sealed class GeneratesMethodGenerator : IIncrementalGenerator
{
+ ///
+ /// Registers the syntax provider and source output for the incremental generator pipeline.
+ ///
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValueProvider> methodsWithAttribute = context.SyntaxProvider
diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodPatternSourceBuilder.cs b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodPatternSourceBuilder.cs
index 310660b..215668b 100644
--- a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodPatternSourceBuilder.cs
+++ b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodPatternSourceBuilder.cs
@@ -1,361 +1,55 @@
using System.Linq;
-using System.Text;
+using EasySourceGenerators.Generators.SourceEmitting;
using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
namespace EasySourceGenerators.Generators.IncrementalGenerators;
+///
+/// Builds C# source code for partial method implementations by delegating to
+/// the classes for string emission and literal formatting.
+///
internal static class GeneratesMethodPatternSourceBuilder
{
- // SwitchCase attribute-based and fluent switch generation are commented out pending replacement
- // with a data-driven approach. See DataMethodBodyBuilders.cs for details.
- /*
- internal static string GenerateFromSwitchAttributes(
- SourceProductionContext context,
- List methods,
- IMethodSymbol partialMethod,
- INamedTypeSymbol containingType,
- IReadOnlyList allPartials,
- Compilation compilation)
- {
- List switchCaseMethods = methods
- .Where(method => method.Symbol.GetAttributes().Any(attribute => attribute.AttributeClass?.ToDisplayString() == SwitchCaseAttributeFullName))
- .ToList();
- GeneratesMethodGenerationTarget? switchDefaultMethod = methods
- .FirstOrDefault(method => method.Symbol.GetAttributes().Any(attribute => attribute.AttributeClass?.ToDisplayString() == SwitchDefaultAttributeFullName));
-
- List<(object key, string value)> switchCases = new();
- foreach (GeneratesMethodGenerationTarget switchMethod in switchCaseMethods)
- {
- if (switchMethod.Symbol.Parameters.Length > 1)
- {
- context.ReportDiagnostic(Diagnostic.Create(
- GeneratesMethodGeneratorDiagnostics.GeneratorMethodTooManyParametersError,
- switchMethod.Syntax.GetLocation(),
- switchMethod.Symbol.Name,
- switchMethod.Symbol.Parameters.Length));
- continue;
- }
-
- IEnumerable switchCaseAttributes = switchMethod.Symbol.GetAttributes()
- .Where(attribute => attribute.AttributeClass?.ToDisplayString() == SwitchCaseAttributeFullName);
-
- foreach (AttributeData switchCaseAttribute in switchCaseAttributes)
- {
- if (switchCaseAttribute.ConstructorArguments.Length == 0)
- {
- continue;
- }
-
- IMethodSymbol? attributeConstructor = switchCaseAttribute.AttributeConstructor;
- if (attributeConstructor is null)
- {
- continue;
- }
-
- int switchCaseArgIndex = attributeConstructor.Parameters
- .Select((parameter, index) => (parameter, index))
- .Where(tuple => string.Equals(tuple.parameter.Name, nameof(SwitchCase.Arg1), StringComparison.OrdinalIgnoreCase))
- .Select(tuple => tuple.index)
- .DefaultIfEmpty(-1)
- .First();
- if (switchCaseArgIndex < 0 || switchCaseArgIndex >= switchCaseAttribute.ConstructorArguments.Length)
- {
- continue;
- }
-
- object? caseArgument = switchCaseAttribute.ConstructorArguments[switchCaseArgIndex].Value;
- if (caseArgument is null)
- {
- continue;
- }
-
- if (partialMethod.Parameters.Length > 0)
- {
- ITypeSymbol? switchArgType = switchCaseAttribute.ConstructorArguments[switchCaseArgIndex].Type;
- ITypeSymbol partialMethodParamType = partialMethod.Parameters[0].Type;
-
- if (switchArgType != null && !SymbolEqualityComparer.Default.Equals(switchArgType, partialMethodParamType))
- {
- Location attributeLocation = switchCaseAttribute.ApplicationSyntaxReference?.GetSyntax()?.GetLocation()
- ?? switchMethod.Syntax.GetLocation();
- context.ReportDiagnostic(Diagnostic.Create(
- GeneratesMethodGeneratorDiagnostics.SwitchCaseArgumentTypeMismatchError,
- attributeLocation,
- switchArgType.ToDisplayString(),
- partialMethodParamType.ToDisplayString()));
- continue;
- }
- }
-
- (string? result, string? error) = GeneratesMethodExecutionRuntime.ExecuteGeneratorMethodWithArgs(
- switchMethod.Symbol,
- allPartials,
- compilation,
- new[] { caseArgument });
-
- if (error != null)
- {
- context.ReportDiagnostic(Diagnostic.Create(
- GeneratesMethodGeneratorDiagnostics.GeneratorMethodExecutionError,
- switchMethod.Syntax.GetLocation(),
- switchMethod.Symbol.Name,
- error));
- continue;
- }
-
- switchCases.Add((caseArgument, FormatValueAsCSharpLiteral(result, partialMethod.ReturnType)));
- }
- }
-
- string? defaultExpression = switchDefaultMethod is not null
- ? ExtractDefaultExpressionFromSwitchDefaultMethod(switchDefaultMethod.Syntax)
- : null;
-
- return GenerateSwitchMethodSource(containingType, partialMethod, switchCases, defaultExpression);
- }
-
- internal static string GenerateFromFluent(
- SourceProductionContext context,
- GeneratesMethodGenerationTarget methodInfo,
- IMethodSymbol partialMethod,
- INamedTypeSymbol containingType,
- Compilation compilation)
- {
- (SwitchBodyData? record, string? error) = GeneratesMethodExecutionRuntime.ExecuteFluentGeneratorMethod(
- methodInfo.Symbol,
- 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
- ? ExtractDefaultExpressionFromFluentMethod(methodInfo.Syntax)
- : null;
-
- return GenerateSwitchMethodSource(containingType, partialMethod, switchBodyData.CasePairs, defaultExpression);
- }
- */
-
+ ///
+ /// Generates a complete C# source file containing a simple partial method implementation
+ /// that returns the given value, formatted as a C# literal.
+ ///
internal static string GenerateSimplePartialMethod(
INamedTypeSymbol containingType,
IMethodSymbol partialMethod,
string? returnValue)
{
- StringBuilder builder = new();
- AppendNamespaceAndTypeHeader(builder, containingType, partialMethod);
-
- if (!partialMethod.ReturnsVoid)
- {
- string literal = FormatValueAsCSharpLiteral(returnValue, partialMethod.ReturnType);
- builder.AppendLine($" return {literal};");
- }
-
- builder.AppendLine(" }");
- builder.AppendLine("}");
- return builder.ToString();
- }
-
- // Switch-related helper methods are commented out pending replacement with data-driven approach
- /*
- private static string? ExtractDefaultExpressionFromSwitchDefaultMethod(MethodDeclarationSyntax method)
- {
- ExpressionSyntax? bodyExpression = method.ExpressionBody?.Expression;
- if (bodyExpression == null && method.Body != null)
- {
- ReturnStatementSyntax? returnStatement = method.Body.Statements.OfType().FirstOrDefault();
- bodyExpression = returnStatement?.Expression;
- }
+ PartialMethodEmitData emitData = RoslynSymbolDataMapper.ToPartialMethodEmitData(containingType, partialMethod);
- return ExtractInnermostLambdaBody(bodyExpression);
- }
-
- private static string? ExtractDefaultExpressionFromFluentMethod(MethodDeclarationSyntax method)
- {
- IEnumerable invocations = method.DescendantNodes().OfType();
- foreach (InvocationExpressionSyntax invocation in invocations)
- {
- if (invocation.Expression is not MemberAccessExpressionSyntax memberAccessExpression)
- {
- continue;
- }
-
- string methodName = memberAccessExpression.Name.Identifier.Text;
- if (methodName is not ("ReturnConstantValue" or "BodyReturningConstantValue"))
- {
- continue;
- }
-
- ExpressionSyntax? argumentExpression = invocation.ArgumentList.Arguments.FirstOrDefault()?.Expression;
- return ExtractInnermostLambdaBody(argumentExpression);
- }
-
- return null;
- }
-
- private static string? ExtractInnermostLambdaBody(ExpressionSyntax? expression)
- {
- while (true)
- {
- switch (expression)
- {
- case SimpleLambdaExpressionSyntax simpleLambdaExpression:
- expression = simpleLambdaExpression.Body as ExpressionSyntax;
- break;
- case ParenthesizedLambdaExpressionSyntax parenthesizedLambdaExpression:
- expression = parenthesizedLambdaExpression.Body as ExpressionSyntax;
- break;
- default:
- return expression?.ToString();
- }
- }
- }
-
- private static string GenerateSwitchMethodSource(
- INamedTypeSymbol containingType,
- IMethodSymbol partialMethod,
- IReadOnlyList<(object key, string value)> cases,
- string? defaultExpression)
- {
- StringBuilder builder = new();
- AppendNamespaceAndTypeHeader(builder, containingType, partialMethod);
-
- if (partialMethod.Parameters.Length == 0)
- {
- string fallbackExpression = defaultExpression ?? "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 ((object key, string value) in cases)
- {
- string formattedKey = FormatKeyAsCSharpLiteral(key, parameterType);
- builder.AppendLine($" case {formattedKey}: return {value};");
- }
-
- if (defaultExpression != null)
- {
- string defaultStatement = defaultExpression.TrimStart().StartsWith("throw ", StringComparison.Ordinal)
- ? $" default: {defaultExpression};"
- : $" default: return {defaultExpression};";
- builder.AppendLine(defaultStatement);
- }
-
- builder.AppendLine(" }");
- builder.AppendLine(" }");
- builder.AppendLine("}");
- return builder.ToString();
- }
-
- private static 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()!
- };
- }
- */
-
- 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()
+ string? returnValueLiteral = !partialMethod.ReturnsVoid
+ ? FormatValueAsCSharpLiteral(returnValue, partialMethod.ReturnType)
: 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(" {");
+ return PartialMethodSourceEmitter.Emit(emitData, returnValueLiteral);
}
+ ///
+ /// Formats a string value as a C# literal expression based on the target return type.
+ /// Delegates to .
+ ///
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
- };
+ return CSharpLiteralFormatter.FormatValueAsLiteral(
+ value,
+ returnType.SpecialType,
+ returnType.TypeKind,
+ returnType.ToDisplayString());
}
- private static string FormatKeyAsCSharpLiteral(object key, ITypeSymbol? parameterType)
+ ///
+ /// Formats a key object as a C# literal expression for use in switch case labels.
+ /// Delegates to .
+ ///
+ 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()!
- };
+ return CSharpLiteralFormatter.FormatKeyAsLiteral(
+ key,
+ parameterType?.TypeKind,
+ parameterType?.ToDisplayString());
}
}
diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratorAssemblyExecutor.cs b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratorAssemblyExecutor.cs
new file mode 100644
index 0000000..86839f7
--- /dev/null
+++ b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratorAssemblyExecutor.cs
@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Loader;
+using EasySourceGenerators.Generators.SourceEmitting;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Emit;
+
+namespace EasySourceGenerators.Generators.IncrementalGenerators;
+
+///
+/// Holds the result of compiling and loading a generator assembly in an isolated context.
+/// Implements to ensure the is unloaded.
+///
+internal sealed class LoadedAssemblyContext : IDisposable
+{
+ internal Assembly Assembly { get; }
+ internal AssemblyLoadContext LoadContext { get; }
+ internal Dictionary CompilationReferenceBytes { get; }
+
+ ///
+ /// The abstractions assembly loaded during assembly resolution, if it was resolved
+ /// from a . May be null if the abstractions
+ /// assembly was loaded from a file path instead.
+ ///
+ internal Assembly? CapturedAbstractionsAssembly { get; }
+
+ internal LoadedAssemblyContext(
+ Assembly assembly,
+ AssemblyLoadContext loadContext,
+ Dictionary compilationReferenceBytes,
+ Assembly? capturedAbstractionsAssembly)
+ {
+ Assembly = assembly;
+ LoadContext = loadContext;
+ CompilationReferenceBytes = compilationReferenceBytes;
+ CapturedAbstractionsAssembly = capturedAbstractionsAssembly;
+ }
+
+ ///
+ /// Unloads the isolated and all assemblies loaded within it.
+ ///
+ public void Dispose()
+ {
+ LoadContext.Unload();
+ }
+}
+
+///
+/// Handles the compilation, assembly loading, and method invocation steps
+/// required to execute generator methods at compile time in an isolated context.
+///
+internal static class GeneratorAssemblyExecutor
+{
+ ///
+ /// Compiles the generator source code and loads the resulting assembly in an isolated
+ /// . Returns a
+ /// that must be disposed to unload the context.
+ ///
+ internal static (LoadedAssemblyContext? context, string? error) CompileAndLoadAssembly(
+ IReadOnlyList allPartialMethods,
+ Compilation compilation)
+ {
+ CSharpCompilation executableCompilation = BuildExecutionCompilation(allPartialMethods, compilation);
+
+ using MemoryStream stream = new();
+ EmitResult emitResult = executableCompilation.Emit(stream);
+ if (!emitResult.Success)
+ {
+ string errors = string.Join("; ", emitResult.Diagnostics
+ .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .Select(diagnostic => diagnostic.GetMessage()));
+ return (null, $"Compilation failed: {errors}");
+ }
+
+ stream.Position = 0;
+ Dictionary compilationReferenceBytes = EmitCompilationReferences(compilation);
+
+ AssemblyLoadContext loadContext = new("__GeneratorExec", isCollectible: true);
+ Assembly? capturedAbstractionsAssembly = null;
+ loadContext.Resolving += (context, assemblyName) =>
+ {
+ PortableExecutableReference? match = compilation.References
+ .OfType()
+ .FirstOrDefault(reference => reference.FilePath is not null && string.Equals(
+ Path.GetFileNameWithoutExtension(reference.FilePath),
+ assemblyName.Name,
+ StringComparison.OrdinalIgnoreCase));
+ if (match?.FilePath != null)
+ return context.LoadFromAssemblyPath(ResolveImplementationAssemblyPath(match.FilePath));
+
+ if (assemblyName.Name != null && compilationReferenceBytes.TryGetValue(assemblyName.Name, out byte[]? bytes))
+ {
+ Assembly loaded = context.LoadFromStream(new MemoryStream(bytes));
+ if (string.Equals(assemblyName.Name, Consts.AbstractionsAssemblyName, StringComparison.OrdinalIgnoreCase))
+ capturedAbstractionsAssembly = loaded;
+ return loaded;
+ }
+
+ return null;
+ };
+
+ Assembly assembly = loadContext.LoadFromStream(stream);
+
+ return (new LoadedAssemblyContext(assembly, loadContext, compilationReferenceBytes, capturedAbstractionsAssembly), null);
+ }
+
+ ///
+ /// Finds a type in the loaded assembly by its full name.
+ ///
+ internal static (Type? type, string? error) FindType(Assembly assembly, string typeName)
+ {
+ Type? loadedType = assembly.GetType(typeName);
+ if (loadedType == null)
+ {
+ return (null, $"Could not find type '{typeName}' in compiled assembly");
+ }
+
+ return (loadedType, null);
+ }
+
+ ///
+ /// Finds a static method in the given type by name.
+ ///
+ internal static (MethodInfo? method, string? error) FindStaticMethod(Type type, string methodName, string typeName)
+ {
+ MethodInfo? methodInfo = type.GetMethod(methodName, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
+ if (methodInfo == null)
+ {
+ return (null, $"Could not find method '{methodName}' in type '{typeName}'");
+ }
+
+ return (methodInfo, null);
+ }
+
+ ///
+ /// Converts argument values to match the target method's parameter types.
+ /// Returns null when is null or the method has no parameters.
+ ///
+ internal static object?[]? ConvertArguments(object?[]? args, MethodInfo methodInfo)
+ {
+ if (args == null || methodInfo.GetParameters().Length == 0)
+ {
+ return null;
+ }
+
+ Type parameterType = methodInfo.GetParameters()[0].ParameterType;
+ return new[] { Convert.ChangeType(args[0], parameterType) };
+ }
+
+ ///
+ /// Builds a suitable for executing generator methods,
+ /// by adding dummy partial method implementations and embedded data building sources
+ /// to the original compilation.
+ ///
+ internal static CSharpCompilation BuildExecutionCompilation(
+ IReadOnlyList allPartialMethods,
+ Compilation compilation)
+ {
+ IReadOnlyList dummyTypeGroups = RoslynSymbolDataMapper.ToDummyTypeGroups(allPartialMethods);
+ string dummySource = DummyImplementationEmitter.Emit(dummyTypeGroups);
+ string dataGeneratorsFactorySource = ReadEmbeddedResource($"{Consts.GeneratorsAssemblyName}.DataGeneratorsFactory.cs");
+ string dataMethodBodyBuildersSource = ReadEmbeddedResource($"{Consts.GeneratorsAssemblyName}.DataMethodBodyBuilders.cs");
+ string dataRecordsSource = ReadEmbeddedResource($"{Consts.GeneratorsAssemblyName}.DataRecords.cs");
+ CSharpParseOptions parseOptions = compilation.SyntaxTrees.FirstOrDefault()?.Options as CSharpParseOptions
+ ?? CSharpParseOptions.Default;
+
+ return (CSharpCompilation)compilation
+ .AddSyntaxTrees(
+ CSharpSyntaxTree.ParseText(dummySource, parseOptions),
+ CSharpSyntaxTree.ParseText(dataGeneratorsFactorySource, parseOptions),
+ CSharpSyntaxTree.ParseText(dataMethodBodyBuildersSource, parseOptions),
+ CSharpSyntaxTree.ParseText(dataRecordsSource, parseOptions));
+ }
+
+ ///
+ /// Reads an embedded resource from the current assembly by its logical name.
+ ///
+ internal static string ReadEmbeddedResource(string resourceName)
+ {
+ using Stream? stream = typeof(GeneratorAssemblyExecutor).Assembly.GetManifestResourceStream(resourceName);
+ if (stream == null)
+ throw new InvalidOperationException($"Embedded resource '{resourceName}' not found in {Consts.GeneratorsAssemblyName} assembly");
+ using StreamReader reader = new StreamReader(stream);
+ return reader.ReadToEnd();
+ }
+
+ ///
+ /// Emits all s in the compilation to in-memory byte arrays,
+ /// keyed by assembly name. These are used to resolve assemblies during isolated execution.
+ ///
+ internal static Dictionary EmitCompilationReferences(Compilation compilation)
+ {
+ Dictionary result = new(StringComparer.OrdinalIgnoreCase);
+ foreach (CompilationReference compilationRef in compilation.References.OfType())
+ {
+ string assemblyName = compilationRef.Compilation.AssemblyName ?? string.Empty;
+ if (string.IsNullOrEmpty(assemblyName))
+ continue;
+ using MemoryStream refStream = new();
+ if (compilationRef.Compilation.Emit(refStream).Success)
+ result[assemblyName] = refStream.ToArray();
+ }
+
+ return result;
+ }
+
+ ///
+ /// Resolves a reference assembly path to its implementation assembly path.
+ /// When the path points to a ref/ directory, returns the corresponding
+ /// implementation assembly one level up.
+ ///
+ internal static string ResolveImplementationAssemblyPath(string path)
+ {
+ string? directory = Path.GetDirectoryName(path);
+ string? parentDirectory = directory != null ? Path.GetDirectoryName(directory) : null;
+ if (directory != null &&
+ parentDirectory != null &&
+ string.Equals(Path.GetFileName(directory), "ref", StringComparison.OrdinalIgnoreCase))
+ {
+ return Path.Combine(parentDirectory, Path.GetFileName(path));
+ }
+
+ return path;
+ }
+}
diff --git a/EasySourceGenerators.Generators/SourceEmitting/CSharpAccessibilityKeyword.cs b/EasySourceGenerators.Generators/SourceEmitting/CSharpAccessibilityKeyword.cs
new file mode 100644
index 0000000..9ea6518
--- /dev/null
+++ b/EasySourceGenerators.Generators/SourceEmitting/CSharpAccessibilityKeyword.cs
@@ -0,0 +1,44 @@
+using Microsoft.CodeAnalysis;
+
+namespace EasySourceGenerators.Generators.SourceEmitting;
+
+///
+/// Maps Roslyn values to their C# keyword representations.
+///
+internal static class CSharpAccessibilityKeyword
+{
+ ///
+ /// Returns the C# keyword for the given accessibility level.
+ /// Returns "private" for and unrecognized values.
+ ///
+ internal static string From(Accessibility accessibility)
+ {
+ return accessibility switch
+ {
+ Accessibility.Public => "public",
+ Accessibility.Protected => "protected",
+ Accessibility.Internal => "internal",
+ Accessibility.ProtectedOrInternal => "protected internal",
+ Accessibility.ProtectedAndInternal => "private protected",
+ _ => "private"
+ };
+ }
+
+ ///
+ /// Returns the C# keyword for the given accessibility level, or an empty string
+ /// for and unrecognized values.
+ /// Used in contexts where the private keyword is implicit (e.g., dummy implementations).
+ ///
+ internal static string FromOrEmpty(Accessibility accessibility)
+ {
+ return accessibility switch
+ {
+ Accessibility.Public => "public",
+ Accessibility.Protected => "protected",
+ Accessibility.Internal => "internal",
+ Accessibility.ProtectedOrInternal => "protected internal",
+ Accessibility.ProtectedAndInternal => "private protected",
+ _ => ""
+ };
+ }
+}
diff --git a/EasySourceGenerators.Generators/SourceEmitting/CSharpLiteralFormatter.cs b/EasySourceGenerators.Generators/SourceEmitting/CSharpLiteralFormatter.cs
new file mode 100644
index 0000000..2e60af0
--- /dev/null
+++ b/EasySourceGenerators.Generators/SourceEmitting/CSharpLiteralFormatter.cs
@@ -0,0 +1,54 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace EasySourceGenerators.Generators.SourceEmitting;
+
+///
+/// Formats values as C# literal expressions suitable for source-generated code.
+///
+internal static class CSharpLiteralFormatter
+{
+ ///
+ /// Formats a string value as a C# literal expression based on the target return type.
+ /// Returns "default" when is null.
+ ///
+ internal static string FormatValueAsLiteral(
+ string? value,
+ SpecialType specialType,
+ TypeKind typeKind,
+ string typeDisplayString)
+ {
+ if (value == null)
+ {
+ return "default";
+ }
+
+ return 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 typeKind == TypeKind.Enum => $"{typeDisplayString}.{value}",
+ _ => value
+ };
+ }
+
+ ///
+ /// Formats a key object as a C# literal expression for use in switch case labels.
+ ///
+ internal static string FormatKeyAsLiteral(object key, TypeKind? typeKind, string? typeDisplayString)
+ {
+ if (typeKind == TypeKind.Enum)
+ {
+ return $"{typeDisplayString}.{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/SourceEmitting/CSharpTypeKeyword.cs b/EasySourceGenerators.Generators/SourceEmitting/CSharpTypeKeyword.cs
new file mode 100644
index 0000000..d8f5232
--- /dev/null
+++ b/EasySourceGenerators.Generators/SourceEmitting/CSharpTypeKeyword.cs
@@ -0,0 +1,23 @@
+using Microsoft.CodeAnalysis;
+
+namespace EasySourceGenerators.Generators.SourceEmitting;
+
+///
+/// Maps Roslyn values to their C# type-declaration keyword.
+///
+internal static class CSharpTypeKeyword
+{
+ ///
+ /// Returns the C# keyword ("class", "struct", or "interface")
+ /// for the given type kind. Returns "class" for unrecognized values.
+ ///
+ internal static string From(TypeKind typeKind)
+ {
+ return typeKind switch
+ {
+ TypeKind.Struct => "struct",
+ TypeKind.Interface => "interface",
+ _ => "class"
+ };
+ }
+}
diff --git a/EasySourceGenerators.Generators/SourceEmitting/DummyImplementationEmitter.cs b/EasySourceGenerators.Generators/SourceEmitting/DummyImplementationEmitter.cs
new file mode 100644
index 0000000..494b595
--- /dev/null
+++ b/EasySourceGenerators.Generators/SourceEmitting/DummyImplementationEmitter.cs
@@ -0,0 +1,68 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace EasySourceGenerators.Generators.SourceEmitting;
+
+///
+/// Contains the data for a group of dummy partial method implementations within a single type.
+///
+internal sealed record DummyTypeGroupData(
+ string? NamespaceName,
+ string TypeName,
+ string TypeKeyword,
+ string TypeModifiers,
+ IReadOnlyList Methods);
+
+///
+/// Contains the data for a single dummy partial method implementation.
+///
+internal sealed record DummyMethodData(
+ string AccessibilityKeyword,
+ string StaticModifier,
+ string ReturnTypeName,
+ string MethodName,
+ string ParameterList,
+ string BodyStatement);
+
+///
+/// Emits dummy partial method implementations used during generator execution compilations.
+/// These implementations throw exceptions when called, preventing accidental invocations
+/// of unimplemented partial methods during compile-time code generation.
+///
+internal static class DummyImplementationEmitter
+{
+ ///
+ /// Generates C# source containing dummy implementations for all provided type groups.
+ /// Each dummy method body consists of a single statement (typically a throw expression).
+ ///
+ internal static string Emit(IEnumerable typeGroups)
+ {
+ StringBuilder builder = new();
+
+ foreach (DummyTypeGroupData typeGroup in typeGroups)
+ {
+ if (typeGroup.NamespaceName != null)
+ {
+ builder.AppendLine($"namespace {typeGroup.NamespaceName} {{");
+ }
+
+ builder.AppendLine($"{typeGroup.TypeModifiers} {typeGroup.TypeKeyword} {typeGroup.TypeName} {{");
+
+ foreach (DummyMethodData method in typeGroup.Methods)
+ {
+ builder.AppendLine($"{method.AccessibilityKeyword} {method.StaticModifier}partial {method.ReturnTypeName} {method.MethodName}({method.ParameterList}) {{");
+ builder.AppendLine(method.BodyStatement);
+ builder.AppendLine("}");
+ }
+
+ builder.AppendLine("}");
+
+ if (typeGroup.NamespaceName != null)
+ {
+ builder.AppendLine("}");
+ }
+ }
+
+ return builder.ToString();
+ }
+}
diff --git a/EasySourceGenerators.Generators/SourceEmitting/PartialMethodEmitData.cs b/EasySourceGenerators.Generators/SourceEmitting/PartialMethodEmitData.cs
new file mode 100644
index 0000000..9b56dc4
--- /dev/null
+++ b/EasySourceGenerators.Generators/SourceEmitting/PartialMethodEmitData.cs
@@ -0,0 +1,18 @@
+namespace EasySourceGenerators.Generators.SourceEmitting;
+
+///
+/// Contains all data needed to emit a partial method implementation source file,
+/// decoupled from Roslyn symbol types for easier unit testing.
+///
+internal sealed record PartialMethodEmitData(
+ string GeneratorFullName,
+ string? NamespaceName,
+ string TypeName,
+ string TypeKeyword,
+ string TypeModifiers,
+ string AccessibilityKeyword,
+ string MethodModifiers,
+ string ReturnTypeName,
+ string MethodName,
+ string ParameterList,
+ bool ReturnsVoid);
diff --git a/EasySourceGenerators.Generators/SourceEmitting/PartialMethodSourceEmitter.cs b/EasySourceGenerators.Generators/SourceEmitting/PartialMethodSourceEmitter.cs
new file mode 100644
index 0000000..ce557ae
--- /dev/null
+++ b/EasySourceGenerators.Generators/SourceEmitting/PartialMethodSourceEmitter.cs
@@ -0,0 +1,60 @@
+using System.Text;
+
+namespace EasySourceGenerators.Generators.SourceEmitting;
+
+///
+/// Emits C# source code for partial method implementations.
+/// Works with plain data records, independent of Roslyn symbol types.
+///
+internal static class PartialMethodSourceEmitter
+{
+ ///
+ /// Generates a complete C# source file containing a partial method implementation
+ /// that returns the given literal value.
+ ///
+ internal static string Emit(PartialMethodEmitData data, string? returnValueLiteral)
+ {
+ StringBuilder builder = new();
+ AppendFileHeader(builder, data);
+
+ if (!data.ReturnsVoid)
+ {
+ builder.AppendLine($" return {returnValueLiteral ?? "default"};");
+ }
+
+ AppendClosingBraces(builder);
+ return builder.ToString();
+ }
+
+ ///
+ /// Appends the auto-generated file header, namespace declaration, type declaration,
+ /// and method signature opening to the .
+ ///
+ internal static void AppendFileHeader(StringBuilder builder, PartialMethodEmitData data)
+ {
+ builder.AppendLine("// ");
+ builder.AppendLine($"// Generated by {data.GeneratorFullName} for method '{data.MethodName}'.");
+ builder.AppendLine("#pragma warning disable");
+ builder.AppendLine();
+
+ if (data.NamespaceName != null)
+ {
+ builder.AppendLine($"namespace {data.NamespaceName};");
+ builder.AppendLine();
+ }
+
+ builder.AppendLine($"{data.TypeModifiers} {data.TypeKeyword} {data.TypeName}");
+ builder.AppendLine("{");
+ builder.AppendLine($" {data.AccessibilityKeyword} {data.MethodModifiers} {data.ReturnTypeName} {data.MethodName}({data.ParameterList})");
+ builder.AppendLine(" {");
+ }
+
+ ///
+ /// Appends closing braces for the method and type declarations.
+ ///
+ internal static void AppendClosingBraces(StringBuilder builder)
+ {
+ builder.AppendLine(" }");
+ builder.AppendLine("}");
+ }
+}
diff --git a/EasySourceGenerators.Generators/SourceEmitting/RoslynSymbolDataMapper.cs b/EasySourceGenerators.Generators/SourceEmitting/RoslynSymbolDataMapper.cs
new file mode 100644
index 0000000..fbddd75
--- /dev/null
+++ b/EasySourceGenerators.Generators/SourceEmitting/RoslynSymbolDataMapper.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using EasySourceGenerators.Abstractions;
+using EasySourceGenerators.Generators.IncrementalGenerators;
+using Microsoft.CodeAnalysis;
+
+namespace EasySourceGenerators.Generators.SourceEmitting;
+
+///
+/// Converts Roslyn symbol types to plain data records used by source emitters.
+/// This is the thin bridge between Roslyn's type system and the Roslyn-free emitter classes.
+///
+internal static class RoslynSymbolDataMapper
+{
+ ///
+ /// Converts Roslyn and
+ /// into a record for source emission.
+ ///
+ internal static PartialMethodEmitData ToPartialMethodEmitData(
+ INamedTypeSymbol containingType,
+ IMethodSymbol partialMethod)
+ {
+ string? namespaceName = containingType.ContainingNamespace?.IsGlobalNamespace == false
+ ? containingType.ContainingNamespace.ToDisplayString()
+ : null;
+
+ string typeKeyword = CSharpTypeKeyword.From(containingType.TypeKind);
+ string typeModifiers = containingType.IsStatic ? "static partial" : "partial";
+ string accessibility = CSharpAccessibilityKeyword.From(partialMethod.DeclaredAccessibility);
+ string methodModifiers = partialMethod.IsStatic ? "static partial" : "partial";
+ string returnTypeName = partialMethod.ReturnType.ToDisplayString();
+ string parameterList = string.Join(", ", partialMethod.Parameters.Select(
+ parameter => $"{parameter.Type.ToDisplayString()} {parameter.Name}"));
+
+ return new PartialMethodEmitData(
+ GeneratorFullName: typeof(GeneratesMethodGenerator).FullName!,
+ NamespaceName: namespaceName,
+ TypeName: containingType.Name,
+ TypeKeyword: typeKeyword,
+ TypeModifiers: typeModifiers,
+ AccessibilityKeyword: accessibility,
+ MethodModifiers: methodModifiers,
+ ReturnTypeName: returnTypeName,
+ MethodName: partialMethod.Name,
+ ParameterList: parameterList,
+ ReturnsVoid: partialMethod.ReturnsVoid);
+ }
+
+ ///
+ /// Converts a collection of partial method symbols into
+ /// records grouped by containing type, suitable for .
+ ///
+ internal static IReadOnlyList ToDummyTypeGroups(IEnumerable partialMethods)
+ {
+ List result = new();
+
+ IEnumerable> groupedMethods =
+ partialMethods.GroupBy(method => (
+ Namespace: method.ContainingType.ContainingNamespace?.IsGlobalNamespace == false
+ ? method.ContainingType.ContainingNamespace.ToDisplayString()
+ : null,
+ TypeName: method.ContainingType.Name,
+ IsStatic: method.ContainingType.IsStatic,
+ TypeKind: method.ContainingType.TypeKind));
+
+ foreach (IGrouping<(string? Namespace, string TypeName, bool IsStatic, TypeKind TypeKind), IMethodSymbol> typeGroup in groupedMethods)
+ {
+ string typeKeyword = CSharpTypeKeyword.From(typeGroup.Key.TypeKind);
+ string typeModifiers = typeGroup.Key.IsStatic ? "static partial" : "partial";
+
+ List methods = new();
+ foreach (IMethodSymbol partialMethod in typeGroup)
+ {
+ string accessibility = CSharpAccessibilityKeyword.FromOrEmpty(partialMethod.DeclaredAccessibility);
+ string staticModifier = partialMethod.IsStatic ? "static " : "";
+ string returnTypeName = partialMethod.ReturnType.ToDisplayString();
+ string parameterList = string.Join(", ", partialMethod.Parameters.Select(
+ parameter => $"{parameter.Type.ToDisplayString()} {parameter.Name}"));
+
+ string exceptionFullName = $"{Consts.AbstractionsAssemblyName}.{nameof(PartialMethodCalledDuringGenerationException)}";
+ string bodyStatement = $"throw new global::{exceptionFullName}(\"{partialMethod.Name}\", \"{partialMethod.ContainingType.Name}\");";
+
+ methods.Add(new DummyMethodData(
+ AccessibilityKeyword: accessibility,
+ StaticModifier: staticModifier,
+ ReturnTypeName: returnTypeName,
+ MethodName: partialMethod.Name,
+ ParameterList: parameterList,
+ BodyStatement: bodyStatement));
+ }
+
+ result.Add(new DummyTypeGroupData(
+ NamespaceName: typeGroup.Key.Namespace,
+ TypeName: typeGroup.Key.TypeName,
+ TypeKeyword: typeKeyword,
+ TypeModifiers: typeModifiers,
+ Methods: methods));
+ }
+
+ return result;
+ }
+}
From 0e8ada5299a57d4d2ab8fe7e77a928476055dd54 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 25 Mar 2026 12:57:29 +0000
Subject: [PATCH 3/6] Add unit tests for all extracted SourceEmitting classes
- CSharpLiteralFormatterTests: 16 tests for value/key literal formatting
- CSharpAccessibilityKeywordTests: 10 tests for From and FromOrEmpty
- CSharpTypeKeywordTests: 5 tests for type kind mapping
- PartialMethodSourceEmitterTests: 12 tests for partial method source emission
- DummyImplementationEmitterTests: 11 tests for dummy implementation generation
Co-authored-by: dex3r <3155725+dex3r@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dex3r/EasySourceGenerators/sessions/cb205769-20ee-4e1e-9b44-3ef890163968
---
.../CSharpAccessibilityKeywordTests.cs | 112 ++++++++
.../CSharpLiteralFormatterTests.cs | 137 +++++++++
.../CSharpTypeKeywordTests.cs | 48 ++++
.../DummyImplementationEmitterTests.cs | 269 ++++++++++++++++++
.../PartialMethodSourceEmitterTests.cs | 164 +++++++++++
5 files changed, 730 insertions(+)
create mode 100644 EasySourceGenerators.GeneratorTests/CSharpAccessibilityKeywordTests.cs
create mode 100644 EasySourceGenerators.GeneratorTests/CSharpLiteralFormatterTests.cs
create mode 100644 EasySourceGenerators.GeneratorTests/CSharpTypeKeywordTests.cs
create mode 100644 EasySourceGenerators.GeneratorTests/DummyImplementationEmitterTests.cs
create mode 100644 EasySourceGenerators.GeneratorTests/PartialMethodSourceEmitterTests.cs
diff --git a/EasySourceGenerators.GeneratorTests/CSharpAccessibilityKeywordTests.cs b/EasySourceGenerators.GeneratorTests/CSharpAccessibilityKeywordTests.cs
new file mode 100644
index 0000000..e98033b
--- /dev/null
+++ b/EasySourceGenerators.GeneratorTests/CSharpAccessibilityKeywordTests.cs
@@ -0,0 +1,112 @@
+using EasySourceGenerators.Generators.SourceEmitting;
+using Microsoft.CodeAnalysis;
+
+namespace EasySourceGenerators.GeneratorTests;
+
+[TestFixture]
+public class CSharpAccessibilityKeywordTests
+{
+ // -----------------------------------------------------------------------
+ // From (returns "private" as default)
+ // -----------------------------------------------------------------------
+
+ [Test]
+ public void From_Public_ReturnsPublic()
+ {
+ string result = CSharpAccessibilityKeyword.From(Accessibility.Public);
+
+ Assert.That(result, Is.EqualTo("public"));
+ }
+
+ [Test]
+ public void From_Protected_ReturnsProtected()
+ {
+ string result = CSharpAccessibilityKeyword.From(Accessibility.Protected);
+
+ Assert.That(result, Is.EqualTo("protected"));
+ }
+
+ [Test]
+ public void From_Internal_ReturnsInternal()
+ {
+ string result = CSharpAccessibilityKeyword.From(Accessibility.Internal);
+
+ Assert.That(result, Is.EqualTo("internal"));
+ }
+
+ [Test]
+ public void From_ProtectedOrInternal_ReturnsProtectedInternal()
+ {
+ string result = CSharpAccessibilityKeyword.From(Accessibility.ProtectedOrInternal);
+
+ Assert.That(result, Is.EqualTo("protected internal"));
+ }
+
+ [Test]
+ public void From_ProtectedAndInternal_ReturnsPrivateProtected()
+ {
+ string result = CSharpAccessibilityKeyword.From(Accessibility.ProtectedAndInternal);
+
+ Assert.That(result, Is.EqualTo("private protected"));
+ }
+
+ [Test]
+ public void From_Private_ReturnsPrivate()
+ {
+ string result = CSharpAccessibilityKeyword.From(Accessibility.Private);
+
+ Assert.That(result, Is.EqualTo("private"));
+ }
+
+ [Test]
+ public void From_NotApplicable_ReturnsPrivate()
+ {
+ string result = CSharpAccessibilityKeyword.From(Accessibility.NotApplicable);
+
+ Assert.That(result, Is.EqualTo("private"));
+ }
+
+ // -----------------------------------------------------------------------
+ // FromOrEmpty (returns "" as default)
+ // -----------------------------------------------------------------------
+
+ [Test]
+ public void FromOrEmpty_Public_ReturnsPublic()
+ {
+ string result = CSharpAccessibilityKeyword.FromOrEmpty(Accessibility.Public);
+
+ Assert.That(result, Is.EqualTo("public"));
+ }
+
+ [Test]
+ public void FromOrEmpty_Protected_ReturnsProtected()
+ {
+ string result = CSharpAccessibilityKeyword.FromOrEmpty(Accessibility.Protected);
+
+ Assert.That(result, Is.EqualTo("protected"));
+ }
+
+ [Test]
+ public void FromOrEmpty_Internal_ReturnsInternal()
+ {
+ string result = CSharpAccessibilityKeyword.FromOrEmpty(Accessibility.Internal);
+
+ Assert.That(result, Is.EqualTo("internal"));
+ }
+
+ [Test]
+ public void FromOrEmpty_Private_ReturnsEmptyString()
+ {
+ string result = CSharpAccessibilityKeyword.FromOrEmpty(Accessibility.Private);
+
+ Assert.That(result, Is.EqualTo(""));
+ }
+
+ [Test]
+ public void FromOrEmpty_NotApplicable_ReturnsEmptyString()
+ {
+ string result = CSharpAccessibilityKeyword.FromOrEmpty(Accessibility.NotApplicable);
+
+ Assert.That(result, Is.EqualTo(""));
+ }
+}
diff --git a/EasySourceGenerators.GeneratorTests/CSharpLiteralFormatterTests.cs b/EasySourceGenerators.GeneratorTests/CSharpLiteralFormatterTests.cs
new file mode 100644
index 0000000..da5eb36
--- /dev/null
+++ b/EasySourceGenerators.GeneratorTests/CSharpLiteralFormatterTests.cs
@@ -0,0 +1,137 @@
+using EasySourceGenerators.Generators.SourceEmitting;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace EasySourceGenerators.GeneratorTests;
+
+[TestFixture]
+public class CSharpLiteralFormatterTests
+{
+ // -----------------------------------------------------------------------
+ // FormatValueAsLiteral
+ // -----------------------------------------------------------------------
+
+ [Test]
+ public void FormatValueAsLiteral_NullValue_ReturnsDefault()
+ {
+ string result = CSharpLiteralFormatter.FormatValueAsLiteral(null, SpecialType.System_String, TypeKind.Class, "string");
+
+ Assert.That(result, Is.EqualTo("default"));
+ }
+
+ [Test]
+ public void FormatValueAsLiteral_StringType_ReturnsQuotedLiteral()
+ {
+ string result = CSharpLiteralFormatter.FormatValueAsLiteral("hello", SpecialType.System_String, TypeKind.Class, "string");
+
+ Assert.That(result, Is.EqualTo("\"hello\""));
+ }
+
+ [Test]
+ public void FormatValueAsLiteral_StringWithSpecialCharacters_ReturnsEscapedLiteral()
+ {
+ string result = CSharpLiteralFormatter.FormatValueAsLiteral("line1\nline2", SpecialType.System_String, TypeKind.Class, "string");
+
+ Assert.That(result, Does.Contain("\\n"));
+ }
+
+ [Test]
+ public void FormatValueAsLiteral_CharType_ReturnsSingleQuotedLiteral()
+ {
+ string result = CSharpLiteralFormatter.FormatValueAsLiteral("A", SpecialType.System_Char, TypeKind.Struct, "char");
+
+ Assert.That(result, Is.EqualTo("'A'"));
+ }
+
+ [Test]
+ public void FormatValueAsLiteral_BooleanTrue_ReturnsLowercase()
+ {
+ string result = CSharpLiteralFormatter.FormatValueAsLiteral("True", SpecialType.System_Boolean, TypeKind.Struct, "bool");
+
+ Assert.That(result, Is.EqualTo("true"));
+ }
+
+ [Test]
+ public void FormatValueAsLiteral_BooleanFalse_ReturnsLowercase()
+ {
+ string result = CSharpLiteralFormatter.FormatValueAsLiteral("False", SpecialType.System_Boolean, TypeKind.Struct, "bool");
+
+ Assert.That(result, Is.EqualTo("false"));
+ }
+
+ [Test]
+ public void FormatValueAsLiteral_EnumType_ReturnsPrefixedValue()
+ {
+ string result = CSharpLiteralFormatter.FormatValueAsLiteral("Red", SpecialType.None, TypeKind.Enum, "MyNamespace.Colors");
+
+ Assert.That(result, Is.EqualTo("MyNamespace.Colors.Red"));
+ }
+
+ [Test]
+ public void FormatValueAsLiteral_IntegerType_ReturnsValueAsIs()
+ {
+ string result = CSharpLiteralFormatter.FormatValueAsLiteral("42", SpecialType.System_Int32, TypeKind.Struct, "int");
+
+ Assert.That(result, Is.EqualTo("42"));
+ }
+
+ [Test]
+ public void FormatValueAsLiteral_DecimalType_ReturnsValueAsIs()
+ {
+ string result = CSharpLiteralFormatter.FormatValueAsLiteral("3.14", SpecialType.System_Double, TypeKind.Struct, "double");
+
+ Assert.That(result, Is.EqualTo("3.14"));
+ }
+
+ // -----------------------------------------------------------------------
+ // FormatKeyAsLiteral
+ // -----------------------------------------------------------------------
+
+ [Test]
+ public void FormatKeyAsLiteral_EnumType_ReturnsPrefixedValue()
+ {
+ string result = CSharpLiteralFormatter.FormatKeyAsLiteral("Green", TypeKind.Enum, "MyNamespace.Colors");
+
+ Assert.That(result, Is.EqualTo("MyNamespace.Colors.Green"));
+ }
+
+ [Test]
+ public void FormatKeyAsLiteral_BoolTrue_ReturnsTrueLiteral()
+ {
+ string result = CSharpLiteralFormatter.FormatKeyAsLiteral(true, TypeKind.Struct, "bool");
+
+ Assert.That(result, Is.EqualTo("true"));
+ }
+
+ [Test]
+ public void FormatKeyAsLiteral_BoolFalse_ReturnsFalseLiteral()
+ {
+ string result = CSharpLiteralFormatter.FormatKeyAsLiteral(false, TypeKind.Struct, "bool");
+
+ Assert.That(result, Is.EqualTo("false"));
+ }
+
+ [Test]
+ public void FormatKeyAsLiteral_String_ReturnsQuotedLiteral()
+ {
+ string result = CSharpLiteralFormatter.FormatKeyAsLiteral("hello", TypeKind.Class, "string");
+
+ Assert.That(result, Is.EqualTo("\"hello\""));
+ }
+
+ [Test]
+ public void FormatKeyAsLiteral_Integer_ReturnsToStringResult()
+ {
+ string result = CSharpLiteralFormatter.FormatKeyAsLiteral(42, TypeKind.Struct, "int");
+
+ Assert.That(result, Is.EqualTo("42"));
+ }
+
+ [Test]
+ public void FormatKeyAsLiteral_NullTypeKind_UsesDefaultBehavior()
+ {
+ string result = CSharpLiteralFormatter.FormatKeyAsLiteral(99, null, null);
+
+ Assert.That(result, Is.EqualTo("99"));
+ }
+}
diff --git a/EasySourceGenerators.GeneratorTests/CSharpTypeKeywordTests.cs b/EasySourceGenerators.GeneratorTests/CSharpTypeKeywordTests.cs
new file mode 100644
index 0000000..2f86359
--- /dev/null
+++ b/EasySourceGenerators.GeneratorTests/CSharpTypeKeywordTests.cs
@@ -0,0 +1,48 @@
+using EasySourceGenerators.Generators.SourceEmitting;
+using Microsoft.CodeAnalysis;
+
+namespace EasySourceGenerators.GeneratorTests;
+
+[TestFixture]
+public class CSharpTypeKeywordTests
+{
+ [Test]
+ public void From_Class_ReturnsClass()
+ {
+ string result = CSharpTypeKeyword.From(TypeKind.Class);
+
+ Assert.That(result, Is.EqualTo("class"));
+ }
+
+ [Test]
+ public void From_Struct_ReturnsStruct()
+ {
+ string result = CSharpTypeKeyword.From(TypeKind.Struct);
+
+ Assert.That(result, Is.EqualTo("struct"));
+ }
+
+ [Test]
+ public void From_Interface_ReturnsInterface()
+ {
+ string result = CSharpTypeKeyword.From(TypeKind.Interface);
+
+ Assert.That(result, Is.EqualTo("interface"));
+ }
+
+ [Test]
+ public void From_Enum_ReturnsClass()
+ {
+ string result = CSharpTypeKeyword.From(TypeKind.Enum);
+
+ Assert.That(result, Is.EqualTo("class"));
+ }
+
+ [Test]
+ public void From_Delegate_ReturnsClass()
+ {
+ string result = CSharpTypeKeyword.From(TypeKind.Delegate);
+
+ Assert.That(result, Is.EqualTo("class"));
+ }
+}
diff --git a/EasySourceGenerators.GeneratorTests/DummyImplementationEmitterTests.cs b/EasySourceGenerators.GeneratorTests/DummyImplementationEmitterTests.cs
new file mode 100644
index 0000000..2996408
--- /dev/null
+++ b/EasySourceGenerators.GeneratorTests/DummyImplementationEmitterTests.cs
@@ -0,0 +1,269 @@
+using EasySourceGenerators.Generators.SourceEmitting;
+
+namespace EasySourceGenerators.GeneratorTests;
+
+[TestFixture]
+public class DummyImplementationEmitterTests
+{
+ [Test]
+ public void Emit_SingleMethodWithNamespace_ContainsNamespaceDeclaration()
+ {
+ List groups = new()
+ {
+ new DummyTypeGroupData(
+ NamespaceName: "TestNamespace",
+ TypeName: "MyClass",
+ TypeKeyword: "class",
+ TypeModifiers: "partial",
+ Methods: new List
+ {
+ new DummyMethodData(
+ AccessibilityKeyword: "public",
+ StaticModifier: "",
+ ReturnTypeName: "string",
+ MethodName: "GetValue",
+ ParameterList: "",
+ BodyStatement: "throw new System.Exception();")
+ })
+ };
+
+ string result = DummyImplementationEmitter.Emit(groups);
+
+ Assert.That(result, Does.Contain("namespace TestNamespace {"));
+ }
+
+ [Test]
+ public void Emit_SingleMethodWithNamespace_ContainsClosingNamespaceBrace()
+ {
+ List groups = new()
+ {
+ new DummyTypeGroupData(
+ NamespaceName: "TestNamespace",
+ TypeName: "MyClass",
+ TypeKeyword: "class",
+ TypeModifiers: "partial",
+ Methods: new List
+ {
+ new DummyMethodData(
+ AccessibilityKeyword: "public",
+ StaticModifier: "",
+ ReturnTypeName: "string",
+ MethodName: "GetValue",
+ ParameterList: "",
+ BodyStatement: "throw new System.Exception();")
+ })
+ };
+
+ string result = DummyImplementationEmitter.Emit(groups);
+
+ // Count closing braces: should have namespace }, type }, and method }
+ int closingBraceCount = result.Split('\n').Count(line => line.Trim() == "}");
+ Assert.That(closingBraceCount, Is.GreaterThanOrEqualTo(2));
+ }
+
+ [Test]
+ public void Emit_WithoutNamespace_DoesNotContainNamespaceDeclaration()
+ {
+ List groups = new()
+ {
+ new DummyTypeGroupData(
+ NamespaceName: null,
+ TypeName: "MyClass",
+ TypeKeyword: "class",
+ TypeModifiers: "partial",
+ Methods: new List
+ {
+ new DummyMethodData(
+ AccessibilityKeyword: "public",
+ StaticModifier: "",
+ ReturnTypeName: "void",
+ MethodName: "DoWork",
+ ParameterList: "",
+ BodyStatement: "throw new System.Exception();")
+ })
+ };
+
+ string result = DummyImplementationEmitter.Emit(groups);
+
+ Assert.That(result, Does.Not.Contain("namespace"));
+ }
+
+ [Test]
+ public void Emit_ContainsTypeDeclaration()
+ {
+ List groups = new()
+ {
+ new DummyTypeGroupData(
+ NamespaceName: null,
+ TypeName: "Helper",
+ TypeKeyword: "class",
+ TypeModifiers: "static partial",
+ Methods: new List
+ {
+ new DummyMethodData(
+ AccessibilityKeyword: "public",
+ StaticModifier: "static ",
+ ReturnTypeName: "int",
+ MethodName: "Calculate",
+ ParameterList: "",
+ BodyStatement: "throw new System.Exception();")
+ })
+ };
+
+ string result = DummyImplementationEmitter.Emit(groups);
+
+ Assert.That(result, Does.Contain("static partial class Helper {"));
+ }
+
+ [Test]
+ public void Emit_ContainsMethodWithPartialKeyword()
+ {
+ List groups = new()
+ {
+ new DummyTypeGroupData(
+ NamespaceName: null,
+ TypeName: "MyClass",
+ TypeKeyword: "class",
+ TypeModifiers: "partial",
+ Methods: new List
+ {
+ new DummyMethodData(
+ AccessibilityKeyword: "public",
+ StaticModifier: "",
+ ReturnTypeName: "string",
+ MethodName: "GetValue",
+ ParameterList: "",
+ BodyStatement: "return \"dummy\";")
+ })
+ };
+
+ string result = DummyImplementationEmitter.Emit(groups);
+
+ Assert.That(result, Does.Contain("public partial string GetValue() {"));
+ }
+
+ [Test]
+ public void Emit_StaticMethod_ContainsStaticModifier()
+ {
+ List groups = new()
+ {
+ new DummyTypeGroupData(
+ NamespaceName: null,
+ TypeName: "MyClass",
+ TypeKeyword: "class",
+ TypeModifiers: "partial",
+ Methods: new List
+ {
+ new DummyMethodData(
+ AccessibilityKeyword: "public",
+ StaticModifier: "static ",
+ ReturnTypeName: "int",
+ MethodName: "Compute",
+ ParameterList: "int x",
+ BodyStatement: "return 0;")
+ })
+ };
+
+ string result = DummyImplementationEmitter.Emit(groups);
+
+ Assert.That(result, Does.Contain("public static partial int Compute(int x) {"));
+ }
+
+ [Test]
+ public void Emit_ContainsBodyStatement()
+ {
+ string expectedBody = "throw new global::MyException(\"test\");";
+ List groups = new()
+ {
+ new DummyTypeGroupData(
+ NamespaceName: null,
+ TypeName: "MyClass",
+ TypeKeyword: "class",
+ TypeModifiers: "partial",
+ Methods: new List
+ {
+ new DummyMethodData(
+ AccessibilityKeyword: "public",
+ StaticModifier: "",
+ ReturnTypeName: "void",
+ MethodName: "DoWork",
+ ParameterList: "",
+ BodyStatement: expectedBody)
+ })
+ };
+
+ string result = DummyImplementationEmitter.Emit(groups);
+
+ Assert.That(result, Does.Contain(expectedBody));
+ }
+
+ [Test]
+ public void Emit_MultipleMethodsInSameType_ContainsBothMethods()
+ {
+ List groups = new()
+ {
+ new DummyTypeGroupData(
+ NamespaceName: "NS",
+ TypeName: "MyClass",
+ TypeKeyword: "class",
+ TypeModifiers: "partial",
+ Methods: new List
+ {
+ new DummyMethodData(
+ AccessibilityKeyword: "public",
+ StaticModifier: "",
+ ReturnTypeName: "string",
+ MethodName: "First",
+ ParameterList: "",
+ BodyStatement: "throw new System.Exception();"),
+ new DummyMethodData(
+ AccessibilityKeyword: "internal",
+ StaticModifier: "",
+ ReturnTypeName: "int",
+ MethodName: "Second",
+ ParameterList: "int x",
+ BodyStatement: "throw new System.Exception();")
+ })
+ };
+
+ string result = DummyImplementationEmitter.Emit(groups);
+
+ Assert.That(result, Does.Contain("partial string First()"));
+ Assert.That(result, Does.Contain("partial int Second(int x)"));
+ }
+
+ [Test]
+ public void Emit_EmptyGroups_ReturnsEmptyString()
+ {
+ string result = DummyImplementationEmitter.Emit(new List());
+
+ Assert.That(result, Is.EqualTo(""));
+ }
+
+ [Test]
+ public void Emit_StructType_ContainsStructKeyword()
+ {
+ List groups = new()
+ {
+ new DummyTypeGroupData(
+ NamespaceName: null,
+ TypeName: "MyStruct",
+ TypeKeyword: "struct",
+ TypeModifiers: "partial",
+ Methods: new List
+ {
+ new DummyMethodData(
+ AccessibilityKeyword: "public",
+ StaticModifier: "",
+ ReturnTypeName: "int",
+ MethodName: "GetValue",
+ ParameterList: "",
+ BodyStatement: "return 0;")
+ })
+ };
+
+ string result = DummyImplementationEmitter.Emit(groups);
+
+ Assert.That(result, Does.Contain("partial struct MyStruct {"));
+ }
+}
diff --git a/EasySourceGenerators.GeneratorTests/PartialMethodSourceEmitterTests.cs b/EasySourceGenerators.GeneratorTests/PartialMethodSourceEmitterTests.cs
new file mode 100644
index 0000000..185f838
--- /dev/null
+++ b/EasySourceGenerators.GeneratorTests/PartialMethodSourceEmitterTests.cs
@@ -0,0 +1,164 @@
+using EasySourceGenerators.Generators.SourceEmitting;
+
+namespace EasySourceGenerators.GeneratorTests;
+
+[TestFixture]
+public class PartialMethodSourceEmitterTests
+{
+ private static PartialMethodEmitData CreateBasicEmitData(
+ string? namespaceName = "TestNamespace",
+ string typeName = "MyClass",
+ string typeKeyword = "class",
+ string typeModifiers = "partial",
+ string accessibilityKeyword = "public",
+ string methodModifiers = "partial",
+ string returnTypeName = "string",
+ string methodName = "GetValue",
+ string parameterList = "",
+ bool returnsVoid = false)
+ {
+ return new PartialMethodEmitData(
+ GeneratorFullName: "TestGenerator",
+ NamespaceName: namespaceName,
+ TypeName: typeName,
+ TypeKeyword: typeKeyword,
+ TypeModifiers: typeModifiers,
+ AccessibilityKeyword: accessibilityKeyword,
+ MethodModifiers: methodModifiers,
+ ReturnTypeName: returnTypeName,
+ MethodName: methodName,
+ ParameterList: parameterList,
+ ReturnsVoid: returnsVoid);
+ }
+
+ [Test]
+ public void Emit_WithReturnValue_ContainsReturnStatement()
+ {
+ PartialMethodEmitData data = CreateBasicEmitData();
+
+ string result = PartialMethodSourceEmitter.Emit(data, "\"hello\"");
+
+ Assert.That(result, Does.Contain("return \"hello\";"));
+ }
+
+ [Test]
+ public void Emit_WithNullReturnValue_ReturnsDefault()
+ {
+ PartialMethodEmitData data = CreateBasicEmitData();
+
+ string result = PartialMethodSourceEmitter.Emit(data, null);
+
+ Assert.That(result, Does.Contain("return default;"));
+ }
+
+ [Test]
+ public void Emit_VoidReturn_DoesNotContainReturnStatement()
+ {
+ PartialMethodEmitData data = CreateBasicEmitData(returnTypeName: "void", returnsVoid: true);
+
+ string result = PartialMethodSourceEmitter.Emit(data, null);
+
+ Assert.That(result, Does.Not.Contain("return"));
+ }
+
+ [Test]
+ public void Emit_ContainsAutoGeneratedHeader()
+ {
+ PartialMethodEmitData data = CreateBasicEmitData();
+
+ string result = PartialMethodSourceEmitter.Emit(data, "42");
+
+ Assert.That(result, Does.Contain("// "));
+ }
+
+ [Test]
+ public void Emit_ContainsGeneratorNameInComment()
+ {
+ PartialMethodEmitData data = CreateBasicEmitData(methodName: "MyMethod");
+
+ string result = PartialMethodSourceEmitter.Emit(data, "42");
+
+ Assert.That(result, Does.Contain("TestGenerator"));
+ Assert.That(result, Does.Contain("MyMethod"));
+ }
+
+ [Test]
+ public void Emit_ContainsPragmaWarningDisable()
+ {
+ PartialMethodEmitData data = CreateBasicEmitData();
+
+ string result = PartialMethodSourceEmitter.Emit(data, "42");
+
+ Assert.That(result, Does.Contain("#pragma warning disable"));
+ }
+
+ [Test]
+ public void Emit_WithNamespace_ContainsNamespaceDeclaration()
+ {
+ PartialMethodEmitData data = CreateBasicEmitData(namespaceName: "MyApp.Models");
+
+ string result = PartialMethodSourceEmitter.Emit(data, "42");
+
+ Assert.That(result, Does.Contain("namespace MyApp.Models;"));
+ }
+
+ [Test]
+ public void Emit_WithoutNamespace_DoesNotContainNamespaceDeclaration()
+ {
+ PartialMethodEmitData data = CreateBasicEmitData(namespaceName: null);
+
+ string result = PartialMethodSourceEmitter.Emit(data, "42");
+
+ Assert.That(result, Does.Not.Contain("namespace"));
+ }
+
+ [Test]
+ public void Emit_ContainsTypeDeclaration()
+ {
+ PartialMethodEmitData data = CreateBasicEmitData(
+ typeModifiers: "static partial",
+ typeKeyword: "class",
+ typeName: "Helper");
+
+ string result = PartialMethodSourceEmitter.Emit(data, "42");
+
+ Assert.That(result, Does.Contain("static partial class Helper"));
+ }
+
+ [Test]
+ public void Emit_ContainsMethodSignature()
+ {
+ PartialMethodEmitData data = CreateBasicEmitData(
+ accessibilityKeyword: "internal",
+ methodModifiers: "static partial",
+ returnTypeName: "int",
+ methodName: "Calculate",
+ parameterList: "int x, string y");
+
+ string result = PartialMethodSourceEmitter.Emit(data, "42");
+
+ Assert.That(result, Does.Contain("internal static partial int Calculate(int x, string y)"));
+ }
+
+ [Test]
+ public void Emit_ContainsClosingBraces()
+ {
+ PartialMethodEmitData data = CreateBasicEmitData();
+
+ string result = PartialMethodSourceEmitter.Emit(data, "42");
+
+ // Should end with method closing brace and type closing brace
+ Assert.That(result, Does.Contain(" }"));
+ Assert.That(result, Does.Contain("}"));
+ }
+
+ [Test]
+ public void Emit_StructType_ContainsStructKeyword()
+ {
+ PartialMethodEmitData data = CreateBasicEmitData(typeKeyword: "struct");
+
+ string result = PartialMethodSourceEmitter.Emit(data, "42");
+
+ Assert.That(result, Does.Contain("partial struct MyClass"));
+ }
+}
From 804f21647f61cbfd4e8096bc7c4fe220626f0288 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 25 Mar 2026 13:01:50 +0000
Subject: [PATCH 4/6] Address review: replace fragile StaticModifier with
MethodModifiers in DummyMethodData
Use consistent MethodModifiers pattern ("partial" / "static partial") in
DummyMethodData, matching PartialMethodEmitData approach and eliminating
the fragile trailing-space convention.
Co-authored-by: dex3r <3155725+dex3r@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dex3r/EasySourceGenerators/sessions/cb205769-20ee-4e1e-9b44-3ef890163968
---
.../DummyImplementationEmitterTests.cs | 20 +++++++++----------
.../DummyImplementationEmitter.cs | 4 ++--
.../SourceEmitting/RoslynSymbolDataMapper.cs | 4 ++--
3 files changed, 14 insertions(+), 14 deletions(-)
diff --git a/EasySourceGenerators.GeneratorTests/DummyImplementationEmitterTests.cs b/EasySourceGenerators.GeneratorTests/DummyImplementationEmitterTests.cs
index 2996408..0b19c46 100644
--- a/EasySourceGenerators.GeneratorTests/DummyImplementationEmitterTests.cs
+++ b/EasySourceGenerators.GeneratorTests/DummyImplementationEmitterTests.cs
@@ -19,7 +19,7 @@ public void Emit_SingleMethodWithNamespace_ContainsNamespaceDeclaration()
{
new DummyMethodData(
AccessibilityKeyword: "public",
- StaticModifier: "",
+ MethodModifiers: "partial",
ReturnTypeName: "string",
MethodName: "GetValue",
ParameterList: "",
@@ -46,7 +46,7 @@ public void Emit_SingleMethodWithNamespace_ContainsClosingNamespaceBrace()
{
new DummyMethodData(
AccessibilityKeyword: "public",
- StaticModifier: "",
+ MethodModifiers: "partial",
ReturnTypeName: "string",
MethodName: "GetValue",
ParameterList: "",
@@ -75,7 +75,7 @@ public void Emit_WithoutNamespace_DoesNotContainNamespaceDeclaration()
{
new DummyMethodData(
AccessibilityKeyword: "public",
- StaticModifier: "",
+ MethodModifiers: "partial",
ReturnTypeName: "void",
MethodName: "DoWork",
ParameterList: "",
@@ -102,7 +102,7 @@ public void Emit_ContainsTypeDeclaration()
{
new DummyMethodData(
AccessibilityKeyword: "public",
- StaticModifier: "static ",
+ MethodModifiers: "static partial",
ReturnTypeName: "int",
MethodName: "Calculate",
ParameterList: "",
@@ -129,7 +129,7 @@ public void Emit_ContainsMethodWithPartialKeyword()
{
new DummyMethodData(
AccessibilityKeyword: "public",
- StaticModifier: "",
+ MethodModifiers: "partial",
ReturnTypeName: "string",
MethodName: "GetValue",
ParameterList: "",
@@ -156,7 +156,7 @@ public void Emit_StaticMethod_ContainsStaticModifier()
{
new DummyMethodData(
AccessibilityKeyword: "public",
- StaticModifier: "static ",
+ MethodModifiers: "static partial",
ReturnTypeName: "int",
MethodName: "Compute",
ParameterList: "int x",
@@ -184,7 +184,7 @@ public void Emit_ContainsBodyStatement()
{
new DummyMethodData(
AccessibilityKeyword: "public",
- StaticModifier: "",
+ MethodModifiers: "partial",
ReturnTypeName: "void",
MethodName: "DoWork",
ParameterList: "",
@@ -211,14 +211,14 @@ public void Emit_MultipleMethodsInSameType_ContainsBothMethods()
{
new DummyMethodData(
AccessibilityKeyword: "public",
- StaticModifier: "",
+ MethodModifiers: "partial",
ReturnTypeName: "string",
MethodName: "First",
ParameterList: "",
BodyStatement: "throw new System.Exception();"),
new DummyMethodData(
AccessibilityKeyword: "internal",
- StaticModifier: "",
+ MethodModifiers: "partial",
ReturnTypeName: "int",
MethodName: "Second",
ParameterList: "int x",
@@ -254,7 +254,7 @@ public void Emit_StructType_ContainsStructKeyword()
{
new DummyMethodData(
AccessibilityKeyword: "public",
- StaticModifier: "",
+ MethodModifiers: "partial",
ReturnTypeName: "int",
MethodName: "GetValue",
ParameterList: "",
diff --git a/EasySourceGenerators.Generators/SourceEmitting/DummyImplementationEmitter.cs b/EasySourceGenerators.Generators/SourceEmitting/DummyImplementationEmitter.cs
index 494b595..e7c913f 100644
--- a/EasySourceGenerators.Generators/SourceEmitting/DummyImplementationEmitter.cs
+++ b/EasySourceGenerators.Generators/SourceEmitting/DummyImplementationEmitter.cs
@@ -18,7 +18,7 @@ internal sealed record DummyTypeGroupData(
///
internal sealed record DummyMethodData(
string AccessibilityKeyword,
- string StaticModifier,
+ string MethodModifiers,
string ReturnTypeName,
string MethodName,
string ParameterList,
@@ -50,7 +50,7 @@ internal static string Emit(IEnumerable typeGroups)
foreach (DummyMethodData method in typeGroup.Methods)
{
- builder.AppendLine($"{method.AccessibilityKeyword} {method.StaticModifier}partial {method.ReturnTypeName} {method.MethodName}({method.ParameterList}) {{");
+ builder.AppendLine($"{method.AccessibilityKeyword} {method.MethodModifiers} {method.ReturnTypeName} {method.MethodName}({method.ParameterList}) {{");
builder.AppendLine(method.BodyStatement);
builder.AppendLine("}");
}
diff --git a/EasySourceGenerators.Generators/SourceEmitting/RoslynSymbolDataMapper.cs b/EasySourceGenerators.Generators/SourceEmitting/RoslynSymbolDataMapper.cs
index fbddd75..71e4df5 100644
--- a/EasySourceGenerators.Generators/SourceEmitting/RoslynSymbolDataMapper.cs
+++ b/EasySourceGenerators.Generators/SourceEmitting/RoslynSymbolDataMapper.cs
@@ -73,7 +73,7 @@ internal static IReadOnlyList ToDummyTypeGroups(IEnumerable<
foreach (IMethodSymbol partialMethod in typeGroup)
{
string accessibility = CSharpAccessibilityKeyword.FromOrEmpty(partialMethod.DeclaredAccessibility);
- string staticModifier = partialMethod.IsStatic ? "static " : "";
+ string methodModifiers = partialMethod.IsStatic ? "static partial" : "partial";
string returnTypeName = partialMethod.ReturnType.ToDisplayString();
string parameterList = string.Join(", ", partialMethod.Parameters.Select(
parameter => $"{parameter.Type.ToDisplayString()} {parameter.Name}"));
@@ -83,7 +83,7 @@ internal static IReadOnlyList ToDummyTypeGroups(IEnumerable<
methods.Add(new DummyMethodData(
AccessibilityKeyword: accessibility,
- StaticModifier: staticModifier,
+ MethodModifiers: methodModifiers,
ReturnTypeName: returnTypeName,
MethodName: partialMethod.Name,
ParameterList: parameterList,
From 174a3e2d3863e5c09836b3f612ab4fe8d6143a92 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 25 Mar 2026 13:15:41 +0000
Subject: [PATCH 5/6] Address review: split files, extract helpers, rename
CSharpAccessibilityKeyword
- Move LoadedAssemblyContext into its own file
- Extract DiagnosticMessageHelper for testable error-joining
- Extract AbstractionsAssemblyResolver from GeneratesMethodExecutionRuntime
- Extract DataGeneratorsFactorySetup from GeneratesMethodExecutionRuntime
- Merge FromOrEmpty into ToKeyword with defaultToPrivate parameter
- Add DiagnosticMessageHelperTests (5 tests)
- Update CSharpAccessibilityKeywordTests for renamed API (12 tests)
Co-authored-by: dex3r <3155725+dex3r@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dex3r/EasySourceGenerators/sessions/7c930db8-e3f7-42d9-98cd-41cf86cdc442
---
.../CSharpAccessibilityKeywordTests.cs | 52 +++----
.../DiagnosticMessageHelperTests.cs | 85 ++++++++++++
.../AbstractionsAssemblyResolver.cs | 123 ++++++++++++++++
.../DataGeneratorsFactorySetup.cs | 37 +++++
.../DiagnosticMessageHelper.cs | 23 +++
.../GeneratesMethodExecutionRuntime.cs | 131 +++---------------
.../GeneratorAssemblyExecutor.cs | 46 +-----
.../LoadedAssemblyContext.cs | 44 ++++++
.../CSharpAccessibilityKeyword.cs | 27 +---
.../SourceEmitting/RoslynSymbolDataMapper.cs | 4 +-
10 files changed, 372 insertions(+), 200 deletions(-)
create mode 100644 EasySourceGenerators.GeneratorTests/DiagnosticMessageHelperTests.cs
create mode 100644 EasySourceGenerators.Generators/IncrementalGenerators/AbstractionsAssemblyResolver.cs
create mode 100644 EasySourceGenerators.Generators/IncrementalGenerators/DataGeneratorsFactorySetup.cs
create mode 100644 EasySourceGenerators.Generators/IncrementalGenerators/DiagnosticMessageHelper.cs
create mode 100644 EasySourceGenerators.Generators/IncrementalGenerators/LoadedAssemblyContext.cs
diff --git a/EasySourceGenerators.GeneratorTests/CSharpAccessibilityKeywordTests.cs b/EasySourceGenerators.GeneratorTests/CSharpAccessibilityKeywordTests.cs
index e98033b..eacca95 100644
--- a/EasySourceGenerators.GeneratorTests/CSharpAccessibilityKeywordTests.cs
+++ b/EasySourceGenerators.GeneratorTests/CSharpAccessibilityKeywordTests.cs
@@ -7,105 +7,105 @@ namespace EasySourceGenerators.GeneratorTests;
public class CSharpAccessibilityKeywordTests
{
// -----------------------------------------------------------------------
- // From (returns "private" as default)
+ // ToKeyword with defaultToPrivate = true (default)
// -----------------------------------------------------------------------
[Test]
- public void From_Public_ReturnsPublic()
+ public void ToKeyword_Public_ReturnsPublic()
{
- string result = CSharpAccessibilityKeyword.From(Accessibility.Public);
+ string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.Public);
Assert.That(result, Is.EqualTo("public"));
}
[Test]
- public void From_Protected_ReturnsProtected()
+ public void ToKeyword_Protected_ReturnsProtected()
{
- string result = CSharpAccessibilityKeyword.From(Accessibility.Protected);
+ string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.Protected);
Assert.That(result, Is.EqualTo("protected"));
}
[Test]
- public void From_Internal_ReturnsInternal()
+ public void ToKeyword_Internal_ReturnsInternal()
{
- string result = CSharpAccessibilityKeyword.From(Accessibility.Internal);
+ string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.Internal);
Assert.That(result, Is.EqualTo("internal"));
}
[Test]
- public void From_ProtectedOrInternal_ReturnsProtectedInternal()
+ public void ToKeyword_ProtectedOrInternal_ReturnsProtectedInternal()
{
- string result = CSharpAccessibilityKeyword.From(Accessibility.ProtectedOrInternal);
+ string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.ProtectedOrInternal);
Assert.That(result, Is.EqualTo("protected internal"));
}
[Test]
- public void From_ProtectedAndInternal_ReturnsPrivateProtected()
+ public void ToKeyword_ProtectedAndInternal_ReturnsPrivateProtected()
{
- string result = CSharpAccessibilityKeyword.From(Accessibility.ProtectedAndInternal);
+ string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.ProtectedAndInternal);
Assert.That(result, Is.EqualTo("private protected"));
}
[Test]
- public void From_Private_ReturnsPrivate()
+ public void ToKeyword_Private_ReturnsPrivate()
{
- string result = CSharpAccessibilityKeyword.From(Accessibility.Private);
+ string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.Private);
Assert.That(result, Is.EqualTo("private"));
}
[Test]
- public void From_NotApplicable_ReturnsPrivate()
+ public void ToKeyword_NotApplicable_ReturnsPrivate()
{
- string result = CSharpAccessibilityKeyword.From(Accessibility.NotApplicable);
+ string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.NotApplicable);
Assert.That(result, Is.EqualTo("private"));
}
// -----------------------------------------------------------------------
- // FromOrEmpty (returns "" as default)
+ // ToKeyword with defaultToPrivate = false
// -----------------------------------------------------------------------
[Test]
- public void FromOrEmpty_Public_ReturnsPublic()
+ public void ToKeyword_DefaultToPrivateFalse_Public_ReturnsPublic()
{
- string result = CSharpAccessibilityKeyword.FromOrEmpty(Accessibility.Public);
+ string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.Public, defaultToPrivate: false);
Assert.That(result, Is.EqualTo("public"));
}
[Test]
- public void FromOrEmpty_Protected_ReturnsProtected()
+ public void ToKeyword_DefaultToPrivateFalse_Protected_ReturnsProtected()
{
- string result = CSharpAccessibilityKeyword.FromOrEmpty(Accessibility.Protected);
+ string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.Protected, defaultToPrivate: false);
Assert.That(result, Is.EqualTo("protected"));
}
[Test]
- public void FromOrEmpty_Internal_ReturnsInternal()
+ public void ToKeyword_DefaultToPrivateFalse_Internal_ReturnsInternal()
{
- string result = CSharpAccessibilityKeyword.FromOrEmpty(Accessibility.Internal);
+ string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.Internal, defaultToPrivate: false);
Assert.That(result, Is.EqualTo("internal"));
}
[Test]
- public void FromOrEmpty_Private_ReturnsEmptyString()
+ public void ToKeyword_DefaultToPrivateFalse_Private_ReturnsEmptyString()
{
- string result = CSharpAccessibilityKeyword.FromOrEmpty(Accessibility.Private);
+ string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.Private, defaultToPrivate: false);
Assert.That(result, Is.EqualTo(""));
}
[Test]
- public void FromOrEmpty_NotApplicable_ReturnsEmptyString()
+ public void ToKeyword_DefaultToPrivateFalse_NotApplicable_ReturnsEmptyString()
{
- string result = CSharpAccessibilityKeyword.FromOrEmpty(Accessibility.NotApplicable);
+ string result = CSharpAccessibilityKeyword.ToKeyword(Accessibility.NotApplicable, defaultToPrivate: false);
Assert.That(result, Is.EqualTo(""));
}
diff --git a/EasySourceGenerators.GeneratorTests/DiagnosticMessageHelperTests.cs b/EasySourceGenerators.GeneratorTests/DiagnosticMessageHelperTests.cs
new file mode 100644
index 0000000..d39d45f
--- /dev/null
+++ b/EasySourceGenerators.GeneratorTests/DiagnosticMessageHelperTests.cs
@@ -0,0 +1,85 @@
+using EasySourceGenerators.Generators.IncrementalGenerators;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace EasySourceGenerators.GeneratorTests;
+
+[TestFixture]
+public class DiagnosticMessageHelperTests
+{
+ [Test]
+ public void JoinErrorDiagnostics_EmptyList_ReturnsEmptyString()
+ {
+ string result = DiagnosticMessageHelper.JoinErrorDiagnostics(Array.Empty());
+
+ Assert.That(result, Is.EqualTo(""));
+ }
+
+ [Test]
+ public void JoinErrorDiagnostics_OnlyWarnings_ReturnsEmptyString()
+ {
+ Diagnostic[] diagnostics = new[]
+ {
+ CreateDiagnostic(DiagnosticSeverity.Warning, "This is a warning")
+ };
+
+ string result = DiagnosticMessageHelper.JoinErrorDiagnostics(diagnostics);
+
+ Assert.That(result, Is.EqualTo(""));
+ }
+
+ [Test]
+ public void JoinErrorDiagnostics_SingleError_ReturnsSingleMessage()
+ {
+ Diagnostic[] diagnostics = new[]
+ {
+ CreateDiagnostic(DiagnosticSeverity.Error, "Something went wrong")
+ };
+
+ string result = DiagnosticMessageHelper.JoinErrorDiagnostics(diagnostics);
+
+ Assert.That(result, Is.EqualTo("Something went wrong"));
+ }
+
+ [Test]
+ public void JoinErrorDiagnostics_MultipleErrors_JoinsWithSemicolon()
+ {
+ Diagnostic[] diagnostics = new[]
+ {
+ CreateDiagnostic(DiagnosticSeverity.Error, "First error"),
+ CreateDiagnostic(DiagnosticSeverity.Error, "Second error")
+ };
+
+ string result = DiagnosticMessageHelper.JoinErrorDiagnostics(diagnostics);
+
+ Assert.That(result, Is.EqualTo("First error; Second error"));
+ }
+
+ [Test]
+ public void JoinErrorDiagnostics_MixedSeverities_ReturnsOnlyErrors()
+ {
+ Diagnostic[] diagnostics = new[]
+ {
+ CreateDiagnostic(DiagnosticSeverity.Warning, "A warning"),
+ CreateDiagnostic(DiagnosticSeverity.Error, "An error"),
+ CreateDiagnostic(DiagnosticSeverity.Info, "An info")
+ };
+
+ string result = DiagnosticMessageHelper.JoinErrorDiagnostics(diagnostics);
+
+ Assert.That(result, Is.EqualTo("An error"));
+ }
+
+ private static Diagnostic CreateDiagnostic(DiagnosticSeverity severity, string message)
+ {
+ DiagnosticDescriptor descriptor = new(
+ id: "TEST001",
+ title: "Test",
+ messageFormat: message,
+ category: "Test",
+ defaultSeverity: severity,
+ isEnabledByDefault: true);
+
+ return Diagnostic.Create(descriptor, Location.None);
+ }
+}
diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/AbstractionsAssemblyResolver.cs b/EasySourceGenerators.Generators/IncrementalGenerators/AbstractionsAssemblyResolver.cs
new file mode 100644
index 0000000..7bd6a4f
--- /dev/null
+++ b/EasySourceGenerators.Generators/IncrementalGenerators/AbstractionsAssemblyResolver.cs
@@ -0,0 +1,123 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Microsoft.CodeAnalysis;
+
+namespace EasySourceGenerators.Generators.IncrementalGenerators;
+
+///
+/// Resolves the EasySourceGenerators.Abstractions assembly from compilation references.
+/// Handles both file-based () and in-memory
+/// () references, such as those provided by Rider's code inspector.
+///
+internal static class AbstractionsAssemblyResolver
+{
+ ///
+ /// Resolves the abstractions assembly from the compilation references into the given
+ /// .
+ ///
+ internal static (Assembly? assembly, string? error) Resolve(
+ LoadedAssemblyContext context,
+ Compilation compilation)
+ {
+ MetadataReference[] matchingReferences = FindAbstractionsReferences(compilation);
+
+ if (matchingReferences.Length == 0)
+ {
+ return (null, BuildNoMatchError(compilation));
+ }
+
+ PortableExecutableReference[] peReferences =
+ matchingReferences.OfType().ToArray();
+ CompilationReference[] compilationReferences =
+ matchingReferences.OfType().ToArray();
+
+ if (peReferences.Length > 0)
+ {
+ return LoadFromPortableExecutableReference(peReferences.First(), context);
+ }
+
+ if (compilationReferences.Length > 0)
+ {
+ return LoadFromCompilationReference(context);
+ }
+
+ string matchesString = string.Join(", ",
+ matchingReferences.Select(reference =>
+ $"{reference.Display} (type: {reference.GetType().Name})"));
+ return (null,
+ $"Found references matching '{Consts.AbstractionsAssemblyName}' but none were PortableExecutableReference or CompilationReference with valid file paths. \nMatching references: {matchesString}");
+ }
+
+ ///
+ /// Finds all compilation references that match the abstractions assembly name.
+ ///
+ private static MetadataReference[] FindAbstractionsReferences(Compilation compilation)
+ {
+ return compilation.References.Where(reference =>
+ reference.Display is not null && (
+ reference.Display.Equals(Consts.AbstractionsAssemblyName, StringComparison.OrdinalIgnoreCase)
+ || (reference is PortableExecutableReference peRef && peRef.FilePath is not null &&
+ Path.GetFileNameWithoutExtension(peRef.FilePath)
+ .Equals(Consts.AbstractionsAssemblyName, StringComparison.OrdinalIgnoreCase))))
+ .ToArray();
+ }
+
+ ///
+ /// Builds an error message when no matching abstractions reference is found.
+ ///
+ private static string BuildNoMatchError(Compilation compilation)
+ {
+ MetadataReference[] closestMatches = compilation.References.Where(reference =>
+ reference.Display is not null
+ && reference.Display.Contains(Consts.SolutionNamespace, StringComparison.OrdinalIgnoreCase))
+ .ToArray();
+
+ string closestMatchesString = string.Join(", ", closestMatches.Select(reference => reference.Display));
+
+ return $"Could not find any reference matching '{Consts.AbstractionsAssemblyName}' in compilation references.\n" +
+ $" Found total references: {compilation.References.Count()}. \nMatching references: {closestMatches.Length}: \n{closestMatchesString}";
+ }
+
+ ///
+ /// Loads the abstractions assembly from a file-based .
+ ///
+ private static (Assembly? assembly, string? error) LoadFromPortableExecutableReference(
+ PortableExecutableReference reference,
+ LoadedAssemblyContext context)
+ {
+ if (string.IsNullOrEmpty(reference.FilePath))
+ {
+ return (null,
+ $"The reference matching '{Consts.AbstractionsAssemblyName}' does not have a valid file path.");
+ }
+
+ string assemblyPath = GeneratorAssemblyExecutor.ResolveImplementationAssemblyPath(reference.FilePath);
+ Assembly assembly = context.LoadContext.LoadFromAssemblyPath(assemblyPath);
+ return (assembly, null);
+ }
+
+ ///
+ /// Loads the abstractions assembly from an in-memory .
+ /// Uses the captured assembly from the load context if available, otherwise emits from bytes.
+ ///
+ private static (Assembly? assembly, string? error) LoadFromCompilationReference(
+ LoadedAssemblyContext context)
+ {
+ if (context.CapturedAbstractionsAssembly != null)
+ {
+ return (context.CapturedAbstractionsAssembly, null);
+ }
+
+ if (context.CompilationReferenceBytes.TryGetValue(Consts.AbstractionsAssemblyName,
+ out byte[]? abstractionBytes))
+ {
+ Assembly assembly = context.LoadContext.LoadFromStream(new MemoryStream(abstractionBytes));
+ return (assembly, null);
+ }
+
+ return (null,
+ $"Found reference matching '{Consts.AbstractionsAssemblyName}' as a CompilationReference, but failed to emit it to a loadable assembly.");
+ }
+}
diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/DataGeneratorsFactorySetup.cs b/EasySourceGenerators.Generators/IncrementalGenerators/DataGeneratorsFactorySetup.cs
new file mode 100644
index 0000000..ff31313
--- /dev/null
+++ b/EasySourceGenerators.Generators/IncrementalGenerators/DataGeneratorsFactorySetup.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Reflection;
+
+namespace EasySourceGenerators.Generators.IncrementalGenerators;
+
+///
+/// Sets up the DataGeneratorsFactory in the loaded execution assembly and
+/// assigns it to Generate.CurrentGenerator in the abstractions assembly,
+/// enabling fluent API usage during generator execution.
+///
+internal static class DataGeneratorsFactorySetup
+{
+ ///
+ /// Creates a DataGeneratorsFactory instance and wires it to the
+ /// Generate.CurrentGenerator static property. Returns an error message
+ /// if the required types or properties cannot be found.
+ ///
+ internal static string? Setup(
+ Assembly executionAssembly,
+ Assembly abstractionsAssembly)
+ {
+ Type? generatorStaticType = abstractionsAssembly.GetType(Consts.GenerateTypeFullName);
+ Type? dataGeneratorsFactoryType = executionAssembly.GetType(Consts.DataGeneratorsFactoryTypeFullName);
+ if (generatorStaticType == null || dataGeneratorsFactoryType == null)
+ {
+ return
+ $"Could not find {Consts.GenerateTypeFullName} or {Consts.DataGeneratorsFactoryTypeFullName} types in compiled assembly";
+ }
+
+ object? dataGeneratorsFactory = Activator.CreateInstance(dataGeneratorsFactoryType);
+ PropertyInfo? currentGeneratorProperty = generatorStaticType.GetProperty(
+ Consts.CurrentGeneratorPropertyName, BindingFlags.NonPublic | BindingFlags.Static);
+ currentGeneratorProperty?.SetValue(null, dataGeneratorsFactory);
+
+ return null;
+ }
+}
diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/DiagnosticMessageHelper.cs b/EasySourceGenerators.Generators/IncrementalGenerators/DiagnosticMessageHelper.cs
new file mode 100644
index 0000000..d04c596
--- /dev/null
+++ b/EasySourceGenerators.Generators/IncrementalGenerators/DiagnosticMessageHelper.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+
+namespace EasySourceGenerators.Generators.IncrementalGenerators;
+
+///
+/// Provides helper methods for formatting diagnostic and error messages
+/// used during generator execution.
+///
+internal static class DiagnosticMessageHelper
+{
+ ///
+ /// Joins error diagnostics from a compilation result into a single semicolon-separated string.
+ /// Only includes diagnostics with severity.
+ ///
+ internal static string JoinErrorDiagnostics(IEnumerable diagnostics)
+ {
+ return string.Join("; ", diagnostics
+ .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .Select(diagnostic => diagnostic.GetMessage()));
+ }
+}
diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodExecutionRuntime.cs b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodExecutionRuntime.cs
index 4c8d5cb..cb8add1 100644
--- a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodExecutionRuntime.cs
+++ b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratesMethodExecutionRuntime.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
@@ -22,7 +21,9 @@ internal sealed record FluentBodyResult(
///
/// Orchestrates the execution of generator methods at compile time.
-/// Delegates compilation and assembly loading to ,
+/// Delegates compilation to ,
+/// abstractions resolution to ,
+/// factory setup to ,
/// and data extraction to .
///
internal static class GeneratesMethodExecutionRuntime
@@ -61,33 +62,25 @@ internal static (FluentBodyResult? result, string? error) ExecuteFluentBodyGener
try
{
(Assembly? abstractionsAssembly, string? abstractionsError) =
- ResolveAbstractionsAssembly(context, compilation);
+ AbstractionsAssemblyResolver.Resolve(context, compilation);
if (abstractionsError != null)
{
return (null, abstractionsError);
}
- string? setupError = SetupDataGeneratorsFactory(context.Assembly, abstractionsAssembly!, context);
+ string? setupError = DataGeneratorsFactorySetup.Setup(context.Assembly, abstractionsAssembly!);
if (setupError != null)
{
return (null, setupError);
}
- string typeName = generatorMethod.ContainingType.ToDisplayString();
- (Type? loadedType, string? typeError) = GeneratorAssemblyExecutor.FindType(context.Assembly, typeName);
- if (typeError != null)
+ (object? methodResult, string? invokeError) =
+ InvokeStaticMethod(context.Assembly, generatorMethod);
+ if (invokeError != null)
{
- return (null, typeError);
+ return (null, invokeError);
}
- (MethodInfo? methodInfo, string? methodError) =
- GeneratorAssemblyExecutor.FindStaticMethod(loadedType!, generatorMethod.Name, typeName);
- if (methodError != null)
- {
- return (null, methodError);
- }
-
- object? methodResult = methodInfo!.Invoke(null, null);
if (methodResult == null)
{
return (null, "Fluent body generator method returned null");
@@ -172,105 +165,27 @@ internal static IReadOnlyList GetAllUnimplementedPartialMethods(C
}
///
- /// Resolves the abstractions assembly from the compilation references.
- /// Handles both (file-based) and
- /// (in-memory, e.g., from Rider's code inspector).
+ /// Locates and invokes a static generator method in the loaded assembly.
///
- private static (Assembly? assembly, string? error) ResolveAbstractionsAssembly(
- LoadedAssemblyContext context,
- Compilation compilation)
+ private static (object? result, string? error) InvokeStaticMethod(
+ Assembly assembly,
+ IMethodSymbol generatorMethod)
{
- MetadataReference[] abstractionsMatchingReferences = compilation.References.Where(reference =>
- reference.Display is not null && (
- reference.Display.Equals(Consts.AbstractionsAssemblyName, StringComparison.OrdinalIgnoreCase)
- || (reference is PortableExecutableReference peRef && peRef.FilePath is not null &&
- Path.GetFileNameWithoutExtension(peRef.FilePath)
- .Equals(Consts.AbstractionsAssemblyName, StringComparison.OrdinalIgnoreCase))))
- .ToArray();
-
- if (abstractionsMatchingReferences.Length == 0)
- {
- MetadataReference[] closestMatches = compilation.References.Where(reference =>
- reference.Display is not null
- && reference.Display.Contains(Consts.SolutionNamespace, StringComparison.OrdinalIgnoreCase))
- .ToArray();
-
- string closestMatchesString = string.Join(", ", closestMatches.Select(reference => reference.Display));
-
- return (null,
- $"Could not find any reference matching '{Consts.AbstractionsAssemblyName}' in compilation references.\n" +
- $" Found total references: {compilation.References.Count()}. \nMatching references: {closestMatches.Length}: \n{closestMatchesString}");
- }
-
- PortableExecutableReference[] peMatchingReferences =
- abstractionsMatchingReferences.OfType().ToArray();
- CompilationReference[] csharpCompilationReference =
- abstractionsMatchingReferences.OfType().ToArray();
-
- if (peMatchingReferences.Length > 0)
+ string typeName = generatorMethod.ContainingType.ToDisplayString();
+ (Type? loadedType, string? typeError) = GeneratorAssemblyExecutor.FindType(assembly, typeName);
+ if (typeError != null)
{
- PortableExecutableReference abstractionsReference = peMatchingReferences.First();
-
- if (string.IsNullOrEmpty(abstractionsReference.FilePath))
- {
- return (null,
- $"The reference matching '{Consts.AbstractionsAssemblyName}' does not have a valid file path.");
- }
-
- string abstractionsAssemblyPath =
- GeneratorAssemblyExecutor.ResolveImplementationAssemblyPath(abstractionsReference.FilePath);
- Assembly abstractionsAssembly = context.LoadContext.LoadFromAssemblyPath(abstractionsAssemblyPath);
- return (abstractionsAssembly, null);
+ return (null, typeError);
}
- if (csharpCompilationReference.Length > 0)
+ (MethodInfo? methodInfo, string? methodError) =
+ GeneratorAssemblyExecutor.FindStaticMethod(loadedType!, generatorMethod.Name, typeName);
+ if (methodError != null)
{
- if (context.CapturedAbstractionsAssembly != null)
- {
- return (context.CapturedAbstractionsAssembly, null);
- }
-
- if (context.CompilationReferenceBytes.TryGetValue(Consts.AbstractionsAssemblyName,
- out byte[]? abstractionBytes))
- {
- Assembly abstractionsAssembly =
- context.LoadContext.LoadFromStream(new MemoryStream(abstractionBytes));
- return (abstractionsAssembly, null);
- }
-
- return (null,
- $"Found reference matching '{Consts.AbstractionsAssemblyName}' as a CompilationReference, but failed to emit it to a loadable assembly.");
+ return (null, methodError);
}
- string matchesString = string.Join(", ",
- abstractionsMatchingReferences.Select(reference =>
- $"{reference.Display} (type: {reference.GetType().Name})"));
- return (null,
- $"Found references matching '{Consts.AbstractionsAssemblyName}' but none were PortableExecutableReference or CompilationReference with valid file paths. \nMatching references: {matchesString}");
- }
-
- ///
- /// Sets up the DataGeneratorsFactory and assigns it to Generate.CurrentGenerator
- /// in the loaded abstractions assembly, enabling fluent API usage during generator execution.
- ///
- private static string? SetupDataGeneratorsFactory(
- Assembly executionAssembly,
- Assembly abstractionsAssembly,
- LoadedAssemblyContext context)
- {
- Type? generatorStaticType = abstractionsAssembly.GetType(Consts.GenerateTypeFullName);
- Type? dataGeneratorsFactoryType = executionAssembly.GetType(Consts.DataGeneratorsFactoryTypeFullName);
- if (generatorStaticType == null || dataGeneratorsFactoryType == null)
- {
- return
- $"Could not find {Consts.GenerateTypeFullName} or {Consts.DataGeneratorsFactoryTypeFullName} types in compiled assembly";
- }
-
- object? dataGeneratorsFactory = Activator.CreateInstance(dataGeneratorsFactoryType);
- PropertyInfo? currentGeneratorProperty = generatorStaticType.GetProperty(
- Consts.CurrentGeneratorPropertyName, BindingFlags.NonPublic | BindingFlags.Static);
- currentGeneratorProperty?.SetValue(null, dataGeneratorsFactory);
-
- return null;
+ object? result = methodInfo!.Invoke(null, null);
+ return (result, null);
}
}
diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratorAssemblyExecutor.cs b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratorAssemblyExecutor.cs
index 86839f7..1582a9c 100644
--- a/EasySourceGenerators.Generators/IncrementalGenerators/GeneratorAssemblyExecutor.cs
+++ b/EasySourceGenerators.Generators/IncrementalGenerators/GeneratorAssemblyExecutor.cs
@@ -12,46 +12,8 @@
namespace EasySourceGenerators.Generators.IncrementalGenerators;
///
-/// Holds the result of compiling and loading a generator assembly in an isolated context.
-/// Implements to ensure the is unloaded.
-///
-internal sealed class LoadedAssemblyContext : IDisposable
-{
- internal Assembly Assembly { get; }
- internal AssemblyLoadContext LoadContext { get; }
- internal Dictionary CompilationReferenceBytes { get; }
-
- ///
- /// The abstractions assembly loaded during assembly resolution, if it was resolved
- /// from a . May be null if the abstractions
- /// assembly was loaded from a file path instead.
- ///
- internal Assembly? CapturedAbstractionsAssembly { get; }
-
- internal LoadedAssemblyContext(
- Assembly assembly,
- AssemblyLoadContext loadContext,
- Dictionary compilationReferenceBytes,
- Assembly? capturedAbstractionsAssembly)
- {
- Assembly = assembly;
- LoadContext = loadContext;
- CompilationReferenceBytes = compilationReferenceBytes;
- CapturedAbstractionsAssembly = capturedAbstractionsAssembly;
- }
-
- ///
- /// Unloads the isolated and all assemblies loaded within it.
- ///
- public void Dispose()
- {
- LoadContext.Unload();
- }
-}
-
-///
-/// Handles the compilation, assembly loading, and method invocation steps
-/// required to execute generator methods at compile time in an isolated context.
+/// Compiles generator source code into an executable assembly and loads it
+/// in an isolated for compile-time execution.
///
internal static class GeneratorAssemblyExecutor
{
@@ -70,9 +32,7 @@ internal static (LoadedAssemblyContext? context, string? error) CompileAndLoadAs
EmitResult emitResult = executableCompilation.Emit(stream);
if (!emitResult.Success)
{
- string errors = string.Join("; ", emitResult.Diagnostics
- .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
- .Select(diagnostic => diagnostic.GetMessage()));
+ string errors = DiagnosticMessageHelper.JoinErrorDiagnostics(emitResult.Diagnostics);
return (null, $"Compilation failed: {errors}");
}
diff --git a/EasySourceGenerators.Generators/IncrementalGenerators/LoadedAssemblyContext.cs b/EasySourceGenerators.Generators/IncrementalGenerators/LoadedAssemblyContext.cs
new file mode 100644
index 0000000..63065f1
--- /dev/null
+++ b/EasySourceGenerators.Generators/IncrementalGenerators/LoadedAssemblyContext.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace EasySourceGenerators.Generators.IncrementalGenerators;
+
+///
+/// Holds the result of compiling and loading a generator assembly in an isolated context.
+/// Implements to ensure the is unloaded.
+///
+internal sealed class LoadedAssemblyContext : IDisposable
+{
+ internal Assembly Assembly { get; }
+ internal AssemblyLoadContext LoadContext { get; }
+ internal Dictionary CompilationReferenceBytes { get; }
+
+ ///
+ /// The abstractions assembly loaded during assembly resolution, if it was resolved
+ /// from a . May be null if the abstractions
+ /// assembly was loaded from a file path instead.
+ ///
+ internal Assembly? CapturedAbstractionsAssembly { get; }
+
+ internal LoadedAssemblyContext(
+ Assembly assembly,
+ AssemblyLoadContext loadContext,
+ Dictionary compilationReferenceBytes,
+ Assembly? capturedAbstractionsAssembly)
+ {
+ Assembly = assembly;
+ LoadContext = loadContext;
+ CompilationReferenceBytes = compilationReferenceBytes;
+ CapturedAbstractionsAssembly = capturedAbstractionsAssembly;
+ }
+
+ ///
+ /// Unloads the isolated and all assemblies loaded within it.
+ ///
+ public void Dispose()
+ {
+ LoadContext.Unload();
+ }
+}
diff --git a/EasySourceGenerators.Generators/SourceEmitting/CSharpAccessibilityKeyword.cs b/EasySourceGenerators.Generators/SourceEmitting/CSharpAccessibilityKeyword.cs
index 9ea6518..4ce6151 100644
--- a/EasySourceGenerators.Generators/SourceEmitting/CSharpAccessibilityKeyword.cs
+++ b/EasySourceGenerators.Generators/SourceEmitting/CSharpAccessibilityKeyword.cs
@@ -8,28 +8,13 @@ namespace EasySourceGenerators.Generators.SourceEmitting;
internal static class CSharpAccessibilityKeyword
{
///
- /// Returns the C# keyword for the given accessibility level.
- /// Returns "private" for and unrecognized values.
- ///
- internal static string From(Accessibility accessibility)
- {
- return accessibility switch
- {
- Accessibility.Public => "public",
- Accessibility.Protected => "protected",
- Accessibility.Internal => "internal",
- Accessibility.ProtectedOrInternal => "protected internal",
- Accessibility.ProtectedAndInternal => "private protected",
- _ => "private"
- };
- }
-
- ///
- /// Returns the C# keyword for the given accessibility level, or an empty string
+ /// Converts a Roslyn value to its C# keyword representation.
+ /// When is true (the default), returns "private"
/// for and unrecognized values.
- /// Used in contexts where the private keyword is implicit (e.g., dummy implementations).
+ /// When false, returns an empty string instead — useful in contexts where
+ /// the private keyword is implicit (e.g., dummy implementations).
///
- internal static string FromOrEmpty(Accessibility accessibility)
+ internal static string ToKeyword(Accessibility accessibility, bool defaultToPrivate = true)
{
return accessibility switch
{
@@ -38,7 +23,7 @@ internal static string FromOrEmpty(Accessibility accessibility)
Accessibility.Internal => "internal",
Accessibility.ProtectedOrInternal => "protected internal",
Accessibility.ProtectedAndInternal => "private protected",
- _ => ""
+ _ => defaultToPrivate ? "private" : ""
};
}
}
diff --git a/EasySourceGenerators.Generators/SourceEmitting/RoslynSymbolDataMapper.cs b/EasySourceGenerators.Generators/SourceEmitting/RoslynSymbolDataMapper.cs
index 71e4df5..c5f171d 100644
--- a/EasySourceGenerators.Generators/SourceEmitting/RoslynSymbolDataMapper.cs
+++ b/EasySourceGenerators.Generators/SourceEmitting/RoslynSymbolDataMapper.cs
@@ -27,7 +27,7 @@ internal static PartialMethodEmitData ToPartialMethodEmitData(
string typeKeyword = CSharpTypeKeyword.From(containingType.TypeKind);
string typeModifiers = containingType.IsStatic ? "static partial" : "partial";
- string accessibility = CSharpAccessibilityKeyword.From(partialMethod.DeclaredAccessibility);
+ string accessibility = CSharpAccessibilityKeyword.ToKeyword(partialMethod.DeclaredAccessibility);
string methodModifiers = partialMethod.IsStatic ? "static partial" : "partial";
string returnTypeName = partialMethod.ReturnType.ToDisplayString();
string parameterList = string.Join(", ", partialMethod.Parameters.Select(
@@ -72,7 +72,7 @@ internal static IReadOnlyList ToDummyTypeGroups(IEnumerable<
List methods = new();
foreach (IMethodSymbol partialMethod in typeGroup)
{
- string accessibility = CSharpAccessibilityKeyword.FromOrEmpty(partialMethod.DeclaredAccessibility);
+ string accessibility = CSharpAccessibilityKeyword.ToKeyword(partialMethod.DeclaredAccessibility, defaultToPrivate: false);
string methodModifiers = partialMethod.IsStatic ? "static partial" : "partial";
string returnTypeName = partialMethod.ReturnType.ToDisplayString();
string parameterList = string.Join(", ", partialMethod.Parameters.Select(
From bccb0c233b62b97e64a77cb6ac946a51c87c0326 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 25 Mar 2026 13:18:40 +0000
Subject: [PATCH 6/6] Fix CSharpAccessibilityKeyword doc to list all defaulting
cases
Co-authored-by: dex3r <3155725+dex3r@users.noreply.github.com>
Agent-Logs-Url: https://github.com/dex3r/EasySourceGenerators/sessions/7c930db8-e3f7-42d9-98cd-41cf86cdc442
---
.../SourceEmitting/CSharpAccessibilityKeyword.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/EasySourceGenerators.Generators/SourceEmitting/CSharpAccessibilityKeyword.cs b/EasySourceGenerators.Generators/SourceEmitting/CSharpAccessibilityKeyword.cs
index 4ce6151..76bc43b 100644
--- a/EasySourceGenerators.Generators/SourceEmitting/CSharpAccessibilityKeyword.cs
+++ b/EasySourceGenerators.Generators/SourceEmitting/CSharpAccessibilityKeyword.cs
@@ -10,7 +10,8 @@ internal static class CSharpAccessibilityKeyword
///
/// Converts a Roslyn value to its C# keyword representation.
/// When is true (the default), returns "private"
- /// for and unrecognized values.
+ /// for , ,
+ /// and any unrecognized values.
/// When false, returns an empty string instead — useful in contexts where
/// the private keyword is implicit (e.g., dummy implementations).
///