From 8f0c1163b57137306efc8e4b2e866451addf14df Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 4 Jun 2024 14:30:13 -0400 Subject: [PATCH] Switch to IIncrementalGenerators (#2) * Enable nullables and latest lang on Mocha.Generators * Rewrite PropertyGenerator as IIncrementalGenerator * Move WithPropertyAttribute to generator * Minor changes to PropertyGenerator.cs * Add constraints to ResourceAttribute * Make WithPropertyData readonly * Rewrite ResourceGenerator as IIncrementalGenerator * Move ResourceAttribute to generator * Explicitly define fully qualified names * dotnet format Mocha.Generators --- .../Attributes/ResourceAttribute.cs | 17 -- .../Attributes/WithPropertyAttribute.cs | 22 -- .../Mocha.Generators/Mocha.Generators.csproj | 2 + Source/Mocha.Generators/PropertyGenerator.cs | 238 +++++++++++++----- Source/Mocha.Generators/ResourceGenerator.cs | 224 ++++++++++++----- 5 files changed, 339 insertions(+), 164 deletions(-) delete mode 100644 Source/Mocha.Common/Attributes/ResourceAttribute.cs delete mode 100644 Source/Mocha.Common/Attributes/WithPropertyAttribute.cs diff --git a/Source/Mocha.Common/Attributes/ResourceAttribute.cs b/Source/Mocha.Common/Attributes/ResourceAttribute.cs deleted file mode 100644 index a123a6b..0000000 --- a/Source/Mocha.Common/Attributes/ResourceAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Mocha; - -/// -/// -/// Specifies that this can be (de-)serialized through JSON. -/// -/// -/// Will codegen a static Load( string filePath ) function, which loads using . -/// -/// -/// Note: this structure must be marked as partial. -/// -/// -[AttributeUsage( AttributeTargets.Struct )] -public class ResourceAttribute : Attribute -{ -} diff --git a/Source/Mocha.Common/Attributes/WithPropertyAttribute.cs b/Source/Mocha.Common/Attributes/WithPropertyAttribute.cs deleted file mode 100644 index 2c4b4c7..0000000 --- a/Source/Mocha.Common/Attributes/WithPropertyAttribute.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Mocha; - -/// -/// -/// Note: this structure must be marked as partial. -/// -/// -[AttributeUsage( AttributeTargets.Field )] -public class WithPropertyAttribute : Attribute -{ - public string? Name { get; } - - public WithPropertyAttribute() - { - - } - - public WithPropertyAttribute( string name ) - { - Name = name; - } -} diff --git a/Source/Mocha.Generators/Mocha.Generators.csproj b/Source/Mocha.Generators/Mocha.Generators.csproj index 8609a91..1a0ae21 100644 --- a/Source/Mocha.Generators/Mocha.Generators.csproj +++ b/Source/Mocha.Generators/Mocha.Generators.csproj @@ -2,6 +2,8 @@ netstandard2.0 + enable + latest AnyCPU;x64 true diff --git a/Source/Mocha.Generators/PropertyGenerator.cs b/Source/Mocha.Generators/PropertyGenerator.cs index 24462b4..5683c4d 100644 --- a/Source/Mocha.Generators/PropertyGenerator.cs +++ b/Source/Mocha.Generators/PropertyGenerator.cs @@ -1,95 +1,205 @@ using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; +using System.CodeDom.Compiler; +using System.Collections.Immutable; +using System.IO; using System.Linq; using System.Text; +using System.Threading; namespace Mocha.Generators { - [Generator] - public class PropertyGenerator : ISourceGenerator + [Generator(LanguageNames.CSharp)] + public class PropertyGenerator : IIncrementalGenerator { - public void Initialize( GeneratorInitializationContext context ) + private const string WithPropertyAttributeHint = "WithPropertyAttribute.g.cs"; + private const string OutputFileHint = "WithPropertyGen.g.cs"; + + private const string WithPropertyAttribute = "Mocha.WithPropertyAttribute"; + + public void Initialize(IncrementalGeneratorInitializationContext context) { - // Nothing to do here + context.RegisterPostInitializationOutput(PostInitialize); + + var provider = context.SyntaxProvider.ForAttributeWithMetadataName(WithPropertyAttribute, SyntaxPredicate, TransformWithPropertyAttribute) + .Where(obj => obj != default); + + context.RegisterSourceOutput(provider.Collect(), Execute); } - private static string GetPropertyName( string fieldName ) + private void PostInitialize(IncrementalGeneratorPostInitializationContext context) { - var sb = new StringBuilder(); + context.AddSource(WithPropertyAttributeHint, """ + namespace Mocha; + + /// + /// + /// Note: this structure must be marked as partial. + /// + /// + [global::System.AttributeUsage( global::System.AttributeTargets.Field, AllowMultiple = false, Inherited = false )] + public class WithPropertyAttribute : global::System.Attribute + { + public string? Name { get; } + + public WithPropertyAttribute() + { + + } + + public WithPropertyAttribute( string name ) + { + Name = name; + } + } + """); + } + + private static void Execute(SourceProductionContext context, ImmutableArray withPropertyData) + { + using var output = new StringWriter(); + using var writer = new IndentedTextWriter(output); + + writer.WriteLine("// "); + writer.WriteLine(); - for ( int i = 0; i < fieldName.Length; i++ ) + var groups = withPropertyData.GroupBy(entry => entry.ContainerFQN); + + foreach (var group in groups) { - char c = fieldName[i]; - - if ( c == '_' ) + var firstEntry = group.First(); + + var containingNamespace = firstEntry.ContainingNamespace; + var containerName = firstEntry.ContainerName; + + // Start group namespace + if (!string.IsNullOrWhiteSpace(containingNamespace)) { - sb.Append( char.ToUpper( fieldName[i + 1] ) ); - i++; + writer.WriteLine($"namespace {containingNamespace}"); + writer.WriteLine("{"); + writer.Indent++; } - else + + // Start group containing type(s) + writer.WriteLine($"partial class {containerName}"); + writer.WriteLine("{"); + writer.Indent++; + + foreach (var entry in group) + { + var propertyName = entry.RequestedPropertyName; + if (propertyName is null) + propertyName = GetPropertyName(entry.TargetName); + else + propertyName = propertyName.Substring(1, propertyName.Length - 2); + + writer.WriteLine("/// "); + writer.WriteLine("/// "); + writer.WriteLine($"/// (Auto-generated) from "); + writer.WriteLine("/// "); + writer.WriteLine("/// "); + writer.WriteLine($"public {entry.TypeFQN} {propertyName} => {entry.TargetName};"); + } + + writer.Indent--; + writer.WriteLine("}"); + // End group containing type(s) + + if (!string.IsNullOrWhiteSpace(containingNamespace)) { - sb.Append( c ); + writer.Indent--; + writer.WriteLine("}"); } + // End group namespace } - - return sb.ToString(); + + context.CancellationToken.ThrowIfCancellationRequested(); + context.AddSource(OutputFileHint, output.ToString()); + } + + // Will always be true because of the AttributeUsage constraint. + private static bool SyntaxPredicate(SyntaxNode node, CancellationToken token) => true; + private static WithPropertyData TransformWithPropertyAttribute(GeneratorAttributeSyntaxContext context, CancellationToken token) + { + if (context.TargetSymbol is not IFieldSymbol symbol) + return default; + + var containerFQN = symbol.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var targetName = symbol.Name; + var typeFQN = symbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + // FIXME: Support nested types + var containerName = symbol.ContainingType.Name; + var containingNamespace = symbol.ContainingNamespace.IsGlobalNamespace + ? null + : symbol.ContainingNamespace.ToDisplayString(); + + var attribute = context.Attributes.First(); + // NOTE: What is this used for? + var requestedPropertyName = attribute.ConstructorArguments.FirstOrDefault().Value as string; + + return new WithPropertyData(containerFQN, targetName, requestedPropertyName, typeFQN, containerName, + containingNamespace, []); } - public void Execute( GeneratorExecutionContext context ) + private readonly record struct WithPropertyData { - foreach ( var syntaxTree in context.Compilation.SyntaxTrees ) + public readonly string ContainerFQN; + public readonly string TargetName; + public readonly string? RequestedPropertyName; + public readonly string TypeFQN; + public readonly string ContainerName; + public readonly string? ContainingNamespace; + + public readonly bool IsError; + public readonly ImmutableArray Diagnostics; + + public WithPropertyData(string containerFQN, string targetName, string? requestedPropertyName, string typeFQN, + string containerName, string? containingNamespace, ImmutableArray diagnostics) { - // Find all fields marked as [WithProperty] - var propertyFields = syntaxTree.GetRoot() - .DescendantNodes() - .OfType() - .Where( s => s.AttributeLists.Any( al => al.Attributes.Any( a => a.Name.ToString() == "WithProperty" ) ) ); - - var semanticModel = context.Compilation.GetSemanticModel( syntaxTree ); - - // Iterate over the resource structs and generate code - foreach ( var field in propertyFields ) - { - var fieldSymbol = semanticModel.GetDeclaredSymbol( field.Declaration.Variables.First() ) as IFieldSymbol; - - var fieldType = fieldSymbol.Type.ToDisplayString( SymbolDisplayFormat.FullyQualifiedFormat ); - var fieldName = fieldSymbol.Name.ToString(); + ContainerFQN = containerFQN; + TargetName = targetName; + RequestedPropertyName = requestedPropertyName; + TypeFQN = typeFQN; + ContainerName = containerName; + ContainingNamespace = containingNamespace; - var className = fieldSymbol.ContainingType.Name.ToString(); - var namespaceName = fieldSymbol.ContainingNamespace.ToDisplayString(); + IsError = false; + Diagnostics = diagnostics; + } - var attributeSyntax = field.AttributeLists - .SelectMany( al => al.Attributes ) - .First( a => a.Name.ToString() == "WithProperty" ); + public WithPropertyData(ImmutableArray diagnostics) + { + ContainerFQN = string.Empty; + TargetName = string.Empty; + RequestedPropertyName = null; + TypeFQN = string.Empty; + ContainerName = string.Empty; + ContainingNamespace = null; - string propertyName = attributeSyntax.ArgumentList?.Arguments.FirstOrDefault()?.ToString(); + IsError = true; + Diagnostics = diagnostics; + } + } - if ( propertyName == null ) - propertyName = GetPropertyName( fieldName ); - else - propertyName = propertyName.Substring( 1, propertyName.Length - 2 ); - - var sourceBuilder = new StringBuilder( $@" -using System.Text.Json; - -namespace {namespaceName} -{{ - partial class {className} - {{ - /// - /// - /// (Auto-generated) from - /// - /// - public {fieldType} {propertyName} => {fieldName}; - }} -}}" ); - - context.AddSource( $"{className}_{propertyName}Property.generated.cs", SourceText.From( sourceBuilder.ToString(), Encoding.UTF8 ) ); + private static string GetPropertyName(string fieldName) + { + var sb = new StringBuilder(); + + for (int i = 0; i < fieldName.Length; i++) + { + char c = fieldName[i]; + + if (c == '_') + { + sb.Append(char.ToUpper(fieldName[i + 1])); + i++; + } + else + { + sb.Append(c); } } + + return sb.ToString(); } } } diff --git a/Source/Mocha.Generators/ResourceGenerator.cs b/Source/Mocha.Generators/ResourceGenerator.cs index 33d0e8e..1a71362 100644 --- a/Source/Mocha.Generators/ResourceGenerator.cs +++ b/Source/Mocha.Generators/ResourceGenerator.cs @@ -1,77 +1,179 @@ using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; +using System.CodeDom.Compiler; +using System.Collections.Immutable; +using System.IO; using System.Linq; -using System.Text; +using System.Threading; namespace Mocha.Generators { - [Generator] - public class ResourceGenerator : ISourceGenerator + [Generator(LanguageNames.CSharp)] + public class ResourceGenerator : IIncrementalGenerator { - public void Initialize( GeneratorInitializationContext context ) + private const string ResourceAttributeHint = "ResourceAttribute.g.cs"; + private const string OutputFileHint = "ResourceGen.g.cs"; + + private const string ResourceAttribute = "Mocha.ResourceAttribute"; + private const string FileSystemContent = "global::Mocha.FileSystem.Content"; + private const string FileSystemReadAllTextMethod = "ReadAllText"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(PostInitialize); + + var provider = context.SyntaxProvider.ForAttributeWithMetadataName(ResourceAttribute, SyntaxPredicate, TransformResourceAttribute) + .Where(obj => obj != default); + + context.RegisterSourceOutput(provider.Collect(), Execute); + } + + private void PostInitialize(IncrementalGeneratorPostInitializationContext context) { - // Nothing to do here + context.AddSource(ResourceAttributeHint, $$""" + namespace Mocha; + + /// + /// + /// Specifies that this can be (de-)serialized through JSON. + /// + /// + /// Will codegen a static Load( string filePath ) function, which loads using . + /// + /// + /// Note: this structure must be marked as partial. + /// + /// + [global::System.AttributeUsage( global::System.AttributeTargets.Struct, AllowMultiple = false, Inherited = false )] + public class ResourceAttribute : global::System.Attribute + { + } + """); } - public void Execute( GeneratorExecutionContext context ) + private void Execute(SourceProductionContext context, ImmutableArray resourceData) { - // Iterate over all syntax trees in the compilation - foreach ( var syntaxTree in context.Compilation.SyntaxTrees ) + using var output = new StringWriter(); + using var writer = new IndentedTextWriter(output); + + writer.WriteLine("// "); + writer.WriteLine(); + + var groups = resourceData.GroupBy(resource => resource.ContainingNamespace); + + foreach (var group in groups) { - // Find all struct declarations that are annotated with [Resource] - var resourceStructs = syntaxTree.GetRoot() - .DescendantNodes() - .OfType() - .Where( s => s.AttributeLists - .Any( al => al.Attributes - .Any( a => a.Name.ToString() == "Resource" ) ) ); - - // Iterate over the resource structs and generate code - foreach ( var resourceStruct in resourceStructs ) + var firstEntry = group.First(); + var containingNamespace = firstEntry.ContainingNamespace; + + // Start group namespace + if (!string.IsNullOrWhiteSpace(containingNamespace)) + { + writer.WriteLine($"namespace {containingNamespace}"); + writer.WriteLine("{"); + writer.Indent++; + } + + foreach (var entry in group) + { + // Start entry type + writer.WriteLine($"partial struct {entry.TypeName}"); + writer.WriteLine("{"); + writer.Indent++; + + // Start Load(string) + writer.WriteLine("/// "); + writer.WriteLine("/// "); + writer.WriteLine($"/// Loads a structure from a file using "); + writer.WriteLine("/// "); + writer.WriteLine("/// "); + writer.WriteLine("/// (Auto-generated)"); + writer.WriteLine("/// "); + writer.WriteLine("/// "); + + writer.WriteLine($"public static {entry.TypeName} Load( string filePath )"); + writer.WriteLine("{"); + writer.Indent++; + + writer.WriteLine($"var file = {FileSystemContent}.{FileSystemReadAllTextMethod}( filePath );"); + writer.WriteLine($"return global::System.Text.Json.JsonSerializer.Deserialize<{entry.TypeName}>( file );"); + + writer.Indent--; + writer.WriteLine("}"); + // End Load(string) + + // Start Load(byte[]) + writer.WriteLine("/// "); + writer.WriteLine("/// "); + writer.WriteLine($"/// Loads a structure from a data array."); + writer.WriteLine("/// "); + writer.WriteLine("/// "); + writer.WriteLine("/// (Auto-generated)"); + writer.WriteLine("/// "); + writer.WriteLine("/// "); + + writer.WriteLine($"public static {entry.TypeName} Load( byte[] data )"); + writer.WriteLine("{"); + writer.Indent++; + + writer.WriteLine($"return global::System.Text.Json.JsonSerializer.Deserialize<{entry.TypeName}>( data );"); + + writer.Indent--; + writer.WriteLine("}"); + // End Load(byte[]) + + writer.Indent--; + writer.WriteLine("}"); + // End entry type + } + + if (!string.IsNullOrWhiteSpace(containingNamespace)) { - var namespaceName = (resourceStruct.Parent as NamespaceDeclarationSyntax)?.Name.ToString() ?? "Mocha"; - var structName = resourceStruct.Identifier.Text; - - var sourceBuilder = new StringBuilder( $@" -using System.Text.Json; - -namespace {namespaceName} -{{ - partial struct {structName} - {{ - /// - /// - /// Loads a structure from a file using - /// - /// - /// (Auto-generated) - /// - /// - public static {structName} Load( string filePath ) - {{ - var file = FileSystem.Content.ReadAllText( filePath ); - return JsonSerializer.Deserialize<{structName}>( file ); - }} - - /// - /// - /// Loads a structure from a data array. - /// - /// - /// (Auto-generated) - /// - /// - public static {structName} Load( byte[] data ) - {{ - return JsonSerializer.Deserialize<{structName}>( data ); - }} - }} -}}" ); - - context.AddSource( $"{structName}_Load.generated.cs", SourceText.From( sourceBuilder.ToString(), Encoding.UTF8 ) ); + writer.Indent--; + writer.WriteLine("}"); } + // End group namespace + } + + context.CancellationToken.ThrowIfCancellationRequested(); + context.AddSource(OutputFileHint, output.ToString()); + } + + // Will always be true because of the AttributeUsage constraint. + private bool SyntaxPredicate(SyntaxNode node, CancellationToken token) => true; + private ResourceData TransformResourceAttribute(GeneratorAttributeSyntaxContext context, CancellationToken token) + { + var containingNamespace = context.TargetSymbol.ContainingNamespace.IsGlobalNamespace + ? null + : context.TargetSymbol.ContainingNamespace.ToDisplayString(); + var typeName = context.TargetSymbol.Name; + + return new ResourceData(containingNamespace, typeName, []); + } + + private readonly record struct ResourceData + { + public readonly string? ContainingNamespace; + public readonly string TypeName; + + public readonly bool IsError; + public readonly ImmutableArray Diagnostics; + + public ResourceData(string? containingNamespace, string typeName, ImmutableArray diagnostics) + { + ContainingNamespace = containingNamespace; + TypeName = typeName; + + IsError = false; + Diagnostics = diagnostics; + } + + public ResourceData(ImmutableArray diagnostics) + { + ContainingNamespace = null; + TypeName = string.Empty; + + IsError = true; + Diagnostics = diagnostics; } } }