This repository has been archived by the owner on Mar 22, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improved the symbol model and API. (#2)
* First draft of improvements. * Second draft of improvements. * Added symbol type. * Third draft. * Updated version.
- Loading branch information
Showing
22 changed files
with
796 additions
and
156 deletions.
There are no files selected for viewing
178 changes: 91 additions & 87 deletions
178
src/AttributeSourceGenerator/AttributeIncrementalGeneratorBase.cs
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,125 +1,129 @@ | ||
using System.Text; | ||
using AttributeSourceGenerator.Common; | ||
using AttributeSourceGenerator.Models; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Text; | ||
|
||
// ReSharper disable CheckNamespace | ||
|
||
namespace AttributeSourceGenerator; | ||
|
||
/// <summary>Provides a base class for incremental source generators that generate source using marker attributes.</summary> | ||
public abstract class AttributeIncrementalGeneratorBase : IIncrementalGenerator | ||
{ | ||
protected abstract string AttributeFullName { get; } | ||
protected abstract string AttributeSource { get; } | ||
protected abstract FilterType AttributeFilter { get; } | ||
protected abstract Func<Symbol, string> GenerateSourceForSymbol { get; } | ||
private readonly AttributeIncrementalGeneratorConfiguration _configuration; | ||
|
||
public void Initialize(IncrementalGeneratorInitializationContext context) | ||
/// <summary>Initializes a new instance of the <see cref="AttributeIncrementalGeneratorBase" /> class with the given configuration initializer.</summary> | ||
/// <param name="configuration">The configuration for the generator.</param> | ||
protected AttributeIncrementalGeneratorBase(AttributeIncrementalGeneratorConfiguration configuration) | ||
{ | ||
context.RegisterPostInitializationOutput(AddSource); | ||
|
||
var pipeline = context.SyntaxProvider.ForAttributeWithMetadataName(AttributeFullName, Filter, Transform); | ||
|
||
context.RegisterSourceOutput(pipeline, GenerateSource); | ||
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); | ||
} | ||
|
||
private void AddSource(IncrementalGeneratorPostInitializationContext ctx) | ||
/// <summary>Initializes a new instance of the <see cref="AttributeIncrementalGeneratorBase" /> class with the given configuration initializer.</summary> | ||
/// <param name="initializer">A function that provides the configuration for the generator.</param> | ||
protected AttributeIncrementalGeneratorBase(Func<AttributeIncrementalGeneratorConfiguration> initializer) | ||
{ | ||
ctx.AddSource($"{AttributeFullName}.g.cs", SourceText.From(AttributeSource, Encoding.UTF8)); | ||
} | ||
if (initializer is null) | ||
throw new ArgumentNullException(nameof(initializer)); | ||
|
||
private bool Filter(SyntaxNode syntaxNode, CancellationToken _) | ||
{ | ||
return AttributeFilter switch | ||
{ | ||
FilterType.Interface => syntaxNode is InterfaceDeclarationSyntax, | ||
FilterType.Class => syntaxNode is ClassDeclarationSyntax, | ||
FilterType.Record => syntaxNode is RecordDeclarationSyntax syntax && syntax.Kind() == SyntaxKind.ClassDeclaration, | ||
FilterType.Struct => syntaxNode is StructDeclarationSyntax, | ||
FilterType.RecordStruct => syntaxNode is RecordDeclarationSyntax syntax && syntax.Kind() == SyntaxKind.StructDeclaration, | ||
FilterType.Method => syntaxNode is MethodDeclarationSyntax, | ||
_ => true, | ||
}; | ||
_configuration = initializer(); | ||
} | ||
|
||
private static Symbol Transform(GeneratorAttributeSyntaxContext context, CancellationToken _) | ||
/// <summary>Initializes the incremental generator.</summary> | ||
/// <param name="context">The initialization context.</param> | ||
public void Initialize(IncrementalGeneratorInitializationContext context) | ||
{ | ||
var symbol = context.TargetSymbol; | ||
var containingDeclarations = BuildHierarchy(symbol); | ||
var symbolName = symbol.Name; | ||
return new Symbol(symbolName, containingDeclarations); | ||
} | ||
context.RegisterPostInitializationOutput(initializationContext => AddSource(initializationContext, _configuration.AttributeFullyQualifiedName, _configuration.AttributeSource)); | ||
|
||
private void GenerateSource(SourceProductionContext context, Symbol symbol) | ||
{ | ||
var sourceText = GenerateSourceForSymbol(symbol); | ||
context.AddSource($"{symbol.FullName}.g.cs", sourceText); | ||
var pipeline = context.SyntaxProvider.ForAttributeWithMetadataName(_configuration.AttributeFullyQualifiedName, (syntaxNode, _) => Filter(syntaxNode, _configuration.SymbolFilter), (syntaxContext, _) => Transform(syntaxContext)); | ||
|
||
context.RegisterSourceOutput(pipeline, (productionContext, symbol) => GenerateSourceForSymbol(productionContext, symbol, _configuration.SourceGenerator)); | ||
} | ||
|
||
private static EquatableReadOnlyList<Declaration> BuildHierarchy(ISymbol symbol) | ||
/// <summary>Adds a source file to the output.</summary> | ||
/// <param name="context">The post-initialization context.</param> | ||
/// <param name="name">The name of the source file.</param> | ||
/// <param name="source">The source code for the file.</param> | ||
private static void AddSource(IncrementalGeneratorPostInitializationContext context, string name, string? source) | ||
{ | ||
var declarations = new Stack<Declaration>(); | ||
BuildContainingSymbolHierarchy(symbol, in declarations); | ||
return declarations.ToEquatableReadOnlyList(); | ||
if (source?.Length > 0) | ||
context.AddSource($"{name}.g.cs", SourceText.From(source, Encoding.UTF8)); | ||
} | ||
|
||
private static void BuildContainingSymbolHierarchy(ISymbol symbol, in Stack<Declaration> declarations) | ||
/// <summary>Determines whether a syntax node should be included based on the filter settings.</summary> | ||
/// <param name="syntaxNode">The syntax node to filter.</param> | ||
/// <param name="filterType">The filter configuration.</param> | ||
/// <returns><see langword="true" /> if the syntax node should be included, otherwise <see langword="false" />.</returns> | ||
private static bool Filter(SyntaxNode syntaxNode, FilterType filterType) | ||
{ | ||
if (symbol.ContainingType is not null) | ||
BuildTypeHierarchy(symbol.ContainingType, in declarations); | ||
else if (symbol.ContainingNamespace is not null) | ||
BuildNamespaceHierarchy(symbol.ContainingNamespace, declarations); | ||
var filter = filterType == FilterType.None ? FilterType.All : filterType; | ||
|
||
if (filter.HasFlag(FilterType.Interface) && syntaxNode is InterfaceDeclarationSyntax) | ||
return true; | ||
if (filter.HasFlag(FilterType.Class) && syntaxNode is ClassDeclarationSyntax) | ||
return true; | ||
if (filter.HasFlag(FilterType.Record) && syntaxNode is RecordDeclarationSyntax recordDeclaration && recordDeclaration.Kind() == SyntaxKind.ClassDeclaration) | ||
return true; | ||
if (filter.HasFlag(FilterType.Struct) && syntaxNode is StructDeclarationSyntax) | ||
return true; | ||
if (filter.HasFlag(FilterType.RecordStruct) && syntaxNode is RecordDeclarationSyntax recordStructDeclaration && recordStructDeclaration.Kind() == SyntaxKind.StructDeclaration) | ||
return true; | ||
if (filter.HasFlag(FilterType.Method) && syntaxNode is MethodDeclarationSyntax) | ||
return true; | ||
|
||
return false; | ||
} | ||
|
||
private static void BuildTypeHierarchy(INamedTypeSymbol symbol, in Stack<Declaration> declarations) | ||
/// <summary>Transforms a generator attribute syntax context into a symbol for source generation.</summary> | ||
/// <param name="context">The generator attribute syntax context.</param> | ||
/// <returns>The transformed symbol.</returns> | ||
private static Symbol Transform(GeneratorAttributeSyntaxContext context) | ||
{ | ||
DeclarationType? declarationType = null; | ||
|
||
|
||
if (symbol.IsReferenceType) | ||
{ | ||
if (symbol.TypeKind == TypeKind.Interface) | ||
declarationType = DeclarationType.Interface; | ||
else if (symbol.IsRecord) | ||
declarationType = DeclarationType.Record; | ||
else | ||
declarationType = DeclarationType.Class; | ||
} | ||
else if (symbol.IsValueType) | ||
var targetSymbol = context.TargetSymbol; | ||
if (targetSymbol is not INamedTypeSymbol && targetSymbol is not IMethodSymbol) | ||
throw new InvalidOperationException($"{nameof(AttributeIncrementalGeneratorBase)} unexpectedly tried to transform a {nameof(context.TargetSymbol)} that was not an {nameof(INamedTypeSymbol)} or a {nameof(IMethodSymbol)}."); | ||
|
||
var markerAttribute = context.GetMarkerAttribute(); | ||
var containingDeclarations = targetSymbol.GetContainingDeclarations(); | ||
var symbolType = targetSymbol.GetSymbolType(); | ||
var symbolName = targetSymbol.Name; | ||
EquatableReadOnlyList<string> genericTypeParameters; | ||
EquatableReadOnlyList<ConstructorParameter> constructorParameters; | ||
string returnType; | ||
switch (targetSymbol) | ||
{ | ||
if (symbol.IsRecord) | ||
declarationType = DeclarationType.RecordStruct; | ||
else | ||
declarationType = DeclarationType.Struct; | ||
case INamedTypeSymbol namedTypeSymbol: | ||
genericTypeParameters = namedTypeSymbol.GetGenericTypeParameters(); | ||
constructorParameters = EquatableReadOnlyList<ConstructorParameter>.Empty; | ||
returnType = ""; | ||
break; | ||
case IMethodSymbol methodSymbol: | ||
genericTypeParameters = methodSymbol.GetGenericTypeParameters(); | ||
constructorParameters = methodSymbol.GetConstructorParameters(); | ||
returnType = methodSymbol.ReturnType.ToDisplayString(); | ||
break; | ||
default: | ||
genericTypeParameters = EquatableReadOnlyList<string>.Empty; | ||
constructorParameters = EquatableReadOnlyList<ConstructorParameter>.Empty; | ||
returnType = ""; | ||
break; | ||
} | ||
|
||
if (declarationType is null) | ||
return; | ||
var symbol = new Symbol(markerAttribute, containingDeclarations, symbolType, symbolName, genericTypeParameters, constructorParameters, returnType); | ||
|
||
var genericParameters = EquatableReadOnlyList<string>.Empty; | ||
if (symbol.IsGenericType) | ||
{ | ||
var typeParameters = new List<string>(); | ||
foreach (var typeParameter in symbol.TypeParameters) | ||
typeParameters.Add(typeParameter.Name); | ||
genericParameters = new EquatableReadOnlyList<string>(typeParameters); | ||
} | ||
|
||
var typeDeclaration = new Declaration(declarationType.Value, symbol.Name, genericParameters); | ||
declarations.Push(typeDeclaration); | ||
|
||
BuildContainingSymbolHierarchy(symbol, declarations); | ||
return symbol; | ||
} | ||
|
||
private static void BuildNamespaceHierarchy(INamespaceSymbol symbol, in Stack<Declaration> declarations) | ||
/// <summary>Generates source code for a given symbol.</summary> | ||
/// <param name="context">The source production context.</param> | ||
/// <param name="symbol">The symbol to generate source for.</param> | ||
/// <param name="generate">A function that generates the source code for a symbol.</param> | ||
private static void GenerateSourceForSymbol(SourceProductionContext context, Symbol symbol, Func<Symbol, string> generate) | ||
{ | ||
if (!symbol.IsGlobalNamespace) | ||
{ | ||
var namespaceDeclaration = new Declaration(DeclarationType.Namespace, symbol.Name, EquatableReadOnlyList<string>.Empty); | ||
declarations.Push(namespaceDeclaration); | ||
} | ||
|
||
if (symbol.ContainingNamespace is not null && !symbol.ContainingNamespace.IsGlobalNamespace) | ||
BuildNamespaceHierarchy(symbol.ContainingNamespace, declarations); | ||
var sourceText = generate(symbol); | ||
context.AddSource($"{symbol.FullyQualifiedName}.g.cs", sourceText); | ||
} | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
src/AttributeSourceGenerator/AttributeIncrementalGeneratorConfiguration.cs
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 |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// ReSharper disable CheckNamespace | ||
|
||
namespace AttributeSourceGenerator; | ||
|
||
/// <summary>Defines the configuration for an incremental attribute generator.</summary> | ||
public sealed class AttributeIncrementalGeneratorConfiguration | ||
{ | ||
/// <summary>The fully qualified name of the attribute.</summary> | ||
public required string AttributeFullyQualifiedName { get; init; } | ||
|
||
/// <summary>The source for the attribute.</summary> | ||
public string? AttributeSource { get; init; } | ||
|
||
/// <summary>The filter to apply to symbols.</summary> | ||
public FilterType SymbolFilter { get; init; } = FilterType.All; | ||
|
||
/// <summary>The function that generates the source code for the attribute.</summary> | ||
public required Func<Symbol, string> SourceGenerator { get; init; } | ||
|
||
/// <summary>Initializes a new instance of the <see cref="AttributeIncrementalGeneratorConfiguration" /> class</summary> | ||
public AttributeIncrementalGeneratorConfiguration() | ||
{ | ||
} | ||
} |
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
58 changes: 58 additions & 0 deletions
58
src/AttributeSourceGenerator/Common/DeclarationExtensions.cs
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 |
---|---|---|
@@ -0,0 +1,58 @@ | ||
using System.Text; | ||
using AttributeSourceGenerator.Models; | ||
|
||
// ReSharper disable CheckNamespace | ||
|
||
namespace AttributeSourceGenerator.Common; | ||
|
||
/// <summary>Provides extension methods for working with declarations.</summary> | ||
internal static class DeclarationExtensions | ||
{ | ||
/// <summary>Converts a list of declarations to their corresponding namespace.</summary> | ||
/// <param name="declarations">The list of declarations to convert.</param> | ||
/// <returns>The namespace represented by the declarations.</returns> | ||
public static string ToNamespace(this EquatableReadOnlyList<Declaration> declarations) | ||
{ | ||
var builder = new StringBuilder(); | ||
|
||
// ReSharper disable once ForCanBeConvertedToForeach | ||
for (var index = 0; index < declarations.Count; index++) | ||
{ | ||
var declaration = declarations[index]; | ||
if (declaration.DeclarationType != DeclarationType.Namespace) | ||
continue; | ||
|
||
if (builder.Length > 0) | ||
builder.Append('.'); | ||
builder.Append(declaration.Name); | ||
} | ||
|
||
return builder.ToString(); | ||
} | ||
|
||
/// <summary>Converts a list of declarations to their fully qualified name.</summary> | ||
/// <param name="declarations">The list of declarations to convert.</param> | ||
/// <returns>The fully qualified name represented by the declarations.</returns> | ||
public static string ToFullyQualifiedName(this EquatableReadOnlyList<Declaration> declarations) | ||
{ | ||
var builder = new StringBuilder(); | ||
|
||
// ReSharper disable once ForCanBeConvertedToForeach | ||
for (var index = 0; index < declarations.Count; index++) | ||
{ | ||
var declaration = declarations[index]; | ||
|
||
if (builder.Length > 0) | ||
builder.Append('.'); | ||
builder.Append(declaration.Name); | ||
|
||
if (declaration.GenericParameters.Count <= 0) | ||
continue; | ||
|
||
builder.Append('`'); | ||
builder.Append(declaration.GenericParameters.Count); | ||
} | ||
|
||
return builder.ToString(); | ||
} | ||
} |
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
Oops, something went wrong.