Skip to content
This repository has been archived by the owner on Sep 7, 2024. It is now read-only.

Commit

Permalink
Switch to IIncrementalGenerators (#2)
Browse files Browse the repository at this point in the history
* 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
peter-r-g authored Jun 4, 2024
1 parent 7e585f6 commit 8f0c116
Show file tree
Hide file tree
Showing 5 changed files with 339 additions and 164 deletions.
17 changes: 0 additions & 17 deletions Source/Mocha.Common/Attributes/ResourceAttribute.cs

This file was deleted.

22 changes: 0 additions & 22 deletions Source/Mocha.Common/Attributes/WithPropertyAttribute.cs

This file was deleted.

2 changes: 2 additions & 0 deletions Source/Mocha.Generators/Mocha.Generators.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<Platforms>AnyCPU;x64</Platforms>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
Expand Down
238 changes: 174 additions & 64 deletions Source/Mocha.Generators/PropertyGenerator.cs
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();
}
}
}
Loading

0 comments on commit 8f0c116

Please sign in to comment.