This repository has been archived by the owner on Sep 7, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
5 changed files
with
339 additions
and
164 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
/// <summary> | ||
/// <para> | ||
/// <b>Note:</b> this structure must be marked as partial. | ||
/// </para> | ||
/// </summary> | ||
[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> withPropertyData) | ||
{ | ||
using var output = new StringWriter(); | ||
using var writer = new IndentedTextWriter(output); | ||
|
||
writer.WriteLine("// <auto-generated/>"); | ||
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("/// <summary>"); | ||
writer.WriteLine("/// <para>"); | ||
writer.WriteLine($"/// <b>(Auto-generated)</b> from <see cref=\"{entry.TargetName}\" />"); | ||
writer.WriteLine("/// </para>"); | ||
writer.WriteLine("/// </summary>"); | ||
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<Diagnostic> Diagnostics; | ||
|
||
public WithPropertyData(string containerFQN, string targetName, string? requestedPropertyName, string typeFQN, | ||
string containerName, string? containingNamespace, ImmutableArray<Diagnostic> diagnostics) | ||
{ | ||
// Find all fields marked as [WithProperty] | ||
var propertyFields = syntaxTree.GetRoot() | ||
.DescendantNodes() | ||
.OfType<FieldDeclarationSyntax>() | ||
.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<Diagnostic> 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} | ||
{{ | ||
/// <summary> | ||
/// <para> | ||
/// <b>(Auto-generated)</b> from <see cref=""{fieldName}"" /> | ||
/// </para> | ||
/// </summary> | ||
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(); | ||
} | ||
} | ||
} |
Oops, something went wrong.