Skip to content
This repository was archived by the owner on Mar 22, 2024. It is now read-only.

Commit 109c0a9

Browse files
authored
Improved the symbol model and API. (#2)
* First draft of improvements. * Second draft of improvements. * Added symbol type. * Third draft. * Updated version.
1 parent 26ebbe2 commit 109c0a9

22 files changed

Lines changed: 796 additions & 156 deletions
Lines changed: 91 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,125 +1,129 @@
11
using System.Text;
22
using AttributeSourceGenerator.Common;
3+
using AttributeSourceGenerator.Models;
34
using Microsoft.CodeAnalysis;
45
using Microsoft.CodeAnalysis.CSharp;
56
using Microsoft.CodeAnalysis.CSharp.Syntax;
67
using Microsoft.CodeAnalysis.Text;
78

9+
// ReSharper disable CheckNamespace
10+
811
namespace AttributeSourceGenerator;
912

13+
/// <summary>Provides a base class for incremental source generators that generate source using marker attributes.</summary>
1014
public abstract class AttributeIncrementalGeneratorBase : IIncrementalGenerator
1115
{
12-
protected abstract string AttributeFullName { get; }
13-
protected abstract string AttributeSource { get; }
14-
protected abstract FilterType AttributeFilter { get; }
15-
protected abstract Func<Symbol, string> GenerateSourceForSymbol { get; }
16+
private readonly AttributeIncrementalGeneratorConfiguration _configuration;
1617

17-
public void Initialize(IncrementalGeneratorInitializationContext context)
18+
/// <summary>Initializes a new instance of the <see cref="AttributeIncrementalGeneratorBase" /> class with the given configuration initializer.</summary>
19+
/// <param name="configuration">The configuration for the generator.</param>
20+
protected AttributeIncrementalGeneratorBase(AttributeIncrementalGeneratorConfiguration configuration)
1821
{
19-
context.RegisterPostInitializationOutput(AddSource);
20-
21-
var pipeline = context.SyntaxProvider.ForAttributeWithMetadataName(AttributeFullName, Filter, Transform);
22-
23-
context.RegisterSourceOutput(pipeline, GenerateSource);
22+
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
2423
}
2524

26-
private void AddSource(IncrementalGeneratorPostInitializationContext ctx)
25+
/// <summary>Initializes a new instance of the <see cref="AttributeIncrementalGeneratorBase" /> class with the given configuration initializer.</summary>
26+
/// <param name="initializer">A function that provides the configuration for the generator.</param>
27+
protected AttributeIncrementalGeneratorBase(Func<AttributeIncrementalGeneratorConfiguration> initializer)
2728
{
28-
ctx.AddSource($"{AttributeFullName}.g.cs", SourceText.From(AttributeSource, Encoding.UTF8));
29-
}
29+
if (initializer is null)
30+
throw new ArgumentNullException(nameof(initializer));
3031

31-
private bool Filter(SyntaxNode syntaxNode, CancellationToken _)
32-
{
33-
return AttributeFilter switch
34-
{
35-
FilterType.Interface => syntaxNode is InterfaceDeclarationSyntax,
36-
FilterType.Class => syntaxNode is ClassDeclarationSyntax,
37-
FilterType.Record => syntaxNode is RecordDeclarationSyntax syntax && syntax.Kind() == SyntaxKind.ClassDeclaration,
38-
FilterType.Struct => syntaxNode is StructDeclarationSyntax,
39-
FilterType.RecordStruct => syntaxNode is RecordDeclarationSyntax syntax && syntax.Kind() == SyntaxKind.StructDeclaration,
40-
FilterType.Method => syntaxNode is MethodDeclarationSyntax,
41-
_ => true,
42-
};
32+
_configuration = initializer();
4333
}
4434

45-
private static Symbol Transform(GeneratorAttributeSyntaxContext context, CancellationToken _)
35+
/// <summary>Initializes the incremental generator.</summary>
36+
/// <param name="context">The initialization context.</param>
37+
public void Initialize(IncrementalGeneratorInitializationContext context)
4638
{
47-
var symbol = context.TargetSymbol;
48-
var containingDeclarations = BuildHierarchy(symbol);
49-
var symbolName = symbol.Name;
50-
return new Symbol(symbolName, containingDeclarations);
51-
}
39+
context.RegisterPostInitializationOutput(initializationContext => AddSource(initializationContext, _configuration.AttributeFullyQualifiedName, _configuration.AttributeSource));
5240

53-
private void GenerateSource(SourceProductionContext context, Symbol symbol)
54-
{
55-
var sourceText = GenerateSourceForSymbol(symbol);
56-
context.AddSource($"{symbol.FullName}.g.cs", sourceText);
41+
var pipeline = context.SyntaxProvider.ForAttributeWithMetadataName(_configuration.AttributeFullyQualifiedName, (syntaxNode, _) => Filter(syntaxNode, _configuration.SymbolFilter), (syntaxContext, _) => Transform(syntaxContext));
42+
43+
context.RegisterSourceOutput(pipeline, (productionContext, symbol) => GenerateSourceForSymbol(productionContext, symbol, _configuration.SourceGenerator));
5744
}
5845

59-
private static EquatableReadOnlyList<Declaration> BuildHierarchy(ISymbol symbol)
46+
/// <summary>Adds a source file to the output.</summary>
47+
/// <param name="context">The post-initialization context.</param>
48+
/// <param name="name">The name of the source file.</param>
49+
/// <param name="source">The source code for the file.</param>
50+
private static void AddSource(IncrementalGeneratorPostInitializationContext context, string name, string? source)
6051
{
61-
var declarations = new Stack<Declaration>();
62-
BuildContainingSymbolHierarchy(symbol, in declarations);
63-
return declarations.ToEquatableReadOnlyList();
52+
if (source?.Length > 0)
53+
context.AddSource($"{name}.g.cs", SourceText.From(source, Encoding.UTF8));
6454
}
6555

66-
private static void BuildContainingSymbolHierarchy(ISymbol symbol, in Stack<Declaration> declarations)
56+
/// <summary>Determines whether a syntax node should be included based on the filter settings.</summary>
57+
/// <param name="syntaxNode">The syntax node to filter.</param>
58+
/// <param name="filterType">The filter configuration.</param>
59+
/// <returns><see langword="true" /> if the syntax node should be included, otherwise <see langword="false" />.</returns>
60+
private static bool Filter(SyntaxNode syntaxNode, FilterType filterType)
6761
{
68-
if (symbol.ContainingType is not null)
69-
BuildTypeHierarchy(symbol.ContainingType, in declarations);
70-
else if (symbol.ContainingNamespace is not null)
71-
BuildNamespaceHierarchy(symbol.ContainingNamespace, declarations);
62+
var filter = filterType == FilterType.None ? FilterType.All : filterType;
63+
64+
if (filter.HasFlag(FilterType.Interface) && syntaxNode is InterfaceDeclarationSyntax)
65+
return true;
66+
if (filter.HasFlag(FilterType.Class) && syntaxNode is ClassDeclarationSyntax)
67+
return true;
68+
if (filter.HasFlag(FilterType.Record) && syntaxNode is RecordDeclarationSyntax recordDeclaration && recordDeclaration.Kind() == SyntaxKind.ClassDeclaration)
69+
return true;
70+
if (filter.HasFlag(FilterType.Struct) && syntaxNode is StructDeclarationSyntax)
71+
return true;
72+
if (filter.HasFlag(FilterType.RecordStruct) && syntaxNode is RecordDeclarationSyntax recordStructDeclaration && recordStructDeclaration.Kind() == SyntaxKind.StructDeclaration)
73+
return true;
74+
if (filter.HasFlag(FilterType.Method) && syntaxNode is MethodDeclarationSyntax)
75+
return true;
76+
77+
return false;
7278
}
7379

74-
private static void BuildTypeHierarchy(INamedTypeSymbol symbol, in Stack<Declaration> declarations)
80+
/// <summary>Transforms a generator attribute syntax context into a symbol for source generation.</summary>
81+
/// <param name="context">The generator attribute syntax context.</param>
82+
/// <returns>The transformed symbol.</returns>
83+
private static Symbol Transform(GeneratorAttributeSyntaxContext context)
7584
{
76-
DeclarationType? declarationType = null;
77-
78-
79-
if (symbol.IsReferenceType)
80-
{
81-
if (symbol.TypeKind == TypeKind.Interface)
82-
declarationType = DeclarationType.Interface;
83-
else if (symbol.IsRecord)
84-
declarationType = DeclarationType.Record;
85-
else
86-
declarationType = DeclarationType.Class;
87-
}
88-
else if (symbol.IsValueType)
85+
var targetSymbol = context.TargetSymbol;
86+
if (targetSymbol is not INamedTypeSymbol && targetSymbol is not IMethodSymbol)
87+
throw new InvalidOperationException($"{nameof(AttributeIncrementalGeneratorBase)} unexpectedly tried to transform a {nameof(context.TargetSymbol)} that was not an {nameof(INamedTypeSymbol)} or a {nameof(IMethodSymbol)}.");
88+
89+
var markerAttribute = context.GetMarkerAttribute();
90+
var containingDeclarations = targetSymbol.GetContainingDeclarations();
91+
var symbolType = targetSymbol.GetSymbolType();
92+
var symbolName = targetSymbol.Name;
93+
EquatableReadOnlyList<string> genericTypeParameters;
94+
EquatableReadOnlyList<ConstructorParameter> constructorParameters;
95+
string returnType;
96+
switch (targetSymbol)
8997
{
90-
if (symbol.IsRecord)
91-
declarationType = DeclarationType.RecordStruct;
92-
else
93-
declarationType = DeclarationType.Struct;
98+
case INamedTypeSymbol namedTypeSymbol:
99+
genericTypeParameters = namedTypeSymbol.GetGenericTypeParameters();
100+
constructorParameters = EquatableReadOnlyList<ConstructorParameter>.Empty;
101+
returnType = "";
102+
break;
103+
case IMethodSymbol methodSymbol:
104+
genericTypeParameters = methodSymbol.GetGenericTypeParameters();
105+
constructorParameters = methodSymbol.GetConstructorParameters();
106+
returnType = methodSymbol.ReturnType.ToDisplayString();
107+
break;
108+
default:
109+
genericTypeParameters = EquatableReadOnlyList<string>.Empty;
110+
constructorParameters = EquatableReadOnlyList<ConstructorParameter>.Empty;
111+
returnType = "";
112+
break;
94113
}
95114

96-
if (declarationType is null)
97-
return;
115+
var symbol = new Symbol(markerAttribute, containingDeclarations, symbolType, symbolName, genericTypeParameters, constructorParameters, returnType);
98116

99-
var genericParameters = EquatableReadOnlyList<string>.Empty;
100-
if (symbol.IsGenericType)
101-
{
102-
var typeParameters = new List<string>();
103-
foreach (var typeParameter in symbol.TypeParameters)
104-
typeParameters.Add(typeParameter.Name);
105-
genericParameters = new EquatableReadOnlyList<string>(typeParameters);
106-
}
107-
108-
var typeDeclaration = new Declaration(declarationType.Value, symbol.Name, genericParameters);
109-
declarations.Push(typeDeclaration);
110-
111-
BuildContainingSymbolHierarchy(symbol, declarations);
117+
return symbol;
112118
}
113119

114-
private static void BuildNamespaceHierarchy(INamespaceSymbol symbol, in Stack<Declaration> declarations)
120+
/// <summary>Generates source code for a given symbol.</summary>
121+
/// <param name="context">The source production context.</param>
122+
/// <param name="symbol">The symbol to generate source for.</param>
123+
/// <param name="generate">A function that generates the source code for a symbol.</param>
124+
private static void GenerateSourceForSymbol(SourceProductionContext context, Symbol symbol, Func<Symbol, string> generate)
115125
{
116-
if (!symbol.IsGlobalNamespace)
117-
{
118-
var namespaceDeclaration = new Declaration(DeclarationType.Namespace, symbol.Name, EquatableReadOnlyList<string>.Empty);
119-
declarations.Push(namespaceDeclaration);
120-
}
121-
122-
if (symbol.ContainingNamespace is not null && !symbol.ContainingNamespace.IsGlobalNamespace)
123-
BuildNamespaceHierarchy(symbol.ContainingNamespace, declarations);
126+
var sourceText = generate(symbol);
127+
context.AddSource($"{symbol.FullyQualifiedName}.g.cs", sourceText);
124128
}
125-
}
129+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// ReSharper disable CheckNamespace
2+
3+
namespace AttributeSourceGenerator;
4+
5+
/// <summary>Defines the configuration for an incremental attribute generator.</summary>
6+
public sealed class AttributeIncrementalGeneratorConfiguration
7+
{
8+
/// <summary>The fully qualified name of the attribute.</summary>
9+
public required string AttributeFullyQualifiedName { get; init; }
10+
11+
/// <summary>The source for the attribute.</summary>
12+
public string? AttributeSource { get; init; }
13+
14+
/// <summary>The filter to apply to symbols.</summary>
15+
public FilterType SymbolFilter { get; init; } = FilterType.All;
16+
17+
/// <summary>The function that generates the source code for the attribute.</summary>
18+
public required Func<Symbol, string> SourceGenerator { get; init; }
19+
20+
/// <summary>Initializes a new instance of the <see cref="AttributeIncrementalGeneratorConfiguration" /> class</summary>
21+
public AttributeIncrementalGeneratorConfiguration()
22+
{
23+
}
24+
}

src/AttributeSourceGenerator/AttributeSourceGenerator.csproj

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<Nullable>enable</Nullable>
77
<RootNamespace>AttributeSourceGenerator</RootNamespace>
88
<LangVersion>latest</LangVersion>
9-
<Version>8.0.1</Version>
9+
<Version>8.0.2</Version>
1010
<Title>AttributeSourceGenerator</Title>
1111
<Authors>Jean-Sebastien Carle</Authors>
1212
<Description>A simple attribute-based Roslyn incremental source generator base class for .NET.</Description>
@@ -18,8 +18,8 @@
1818
<RepositoryUrl>https://github.com/jscarle/AttributeSourceGenerator</RepositoryUrl>
1919
<RepositoryType>git</RepositoryType>
2020
<PackageTags>attribute source-generator attribute-based source-generators</PackageTags>
21-
<AssemblyVersion>8.0.1.0</AssemblyVersion>
22-
<FileVersion>8.0.1.0</FileVersion>
21+
<AssemblyVersion>8.0.2.0</AssemblyVersion>
22+
<FileVersion>8.0.2.0</FileVersion>
2323
<NeutralLanguage>en-US</NeutralLanguage>
2424
<IncludeSymbols>true</IncludeSymbols>
2525
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
@@ -31,12 +31,12 @@
3131
</PropertyGroup>
3232

3333
<ItemGroup>
34-
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="[4.3.1,]"/>
35-
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.1"/>
34+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" PrivateAssets="all"/>
35+
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.1" PrivateAssets="all"/>
3636
</ItemGroup>
3737

3838
<ItemGroup>
39-
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
39+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="all"/>
4040
<None Include="..\..\LICENSE.md">
4141
<Pack>True</Pack>
4242
<PackagePath>\</PackagePath>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System.Text;
2+
using AttributeSourceGenerator.Models;
3+
4+
// ReSharper disable CheckNamespace
5+
6+
namespace AttributeSourceGenerator.Common;
7+
8+
/// <summary>Provides extension methods for working with declarations.</summary>
9+
internal static class DeclarationExtensions
10+
{
11+
/// <summary>Converts a list of declarations to their corresponding namespace.</summary>
12+
/// <param name="declarations">The list of declarations to convert.</param>
13+
/// <returns>The namespace represented by the declarations.</returns>
14+
public static string ToNamespace(this EquatableReadOnlyList<Declaration> declarations)
15+
{
16+
var builder = new StringBuilder();
17+
18+
// ReSharper disable once ForCanBeConvertedToForeach
19+
for (var index = 0; index < declarations.Count; index++)
20+
{
21+
var declaration = declarations[index];
22+
if (declaration.DeclarationType != DeclarationType.Namespace)
23+
continue;
24+
25+
if (builder.Length > 0)
26+
builder.Append('.');
27+
builder.Append(declaration.Name);
28+
}
29+
30+
return builder.ToString();
31+
}
32+
33+
/// <summary>Converts a list of declarations to their fully qualified name.</summary>
34+
/// <param name="declarations">The list of declarations to convert.</param>
35+
/// <returns>The fully qualified name represented by the declarations.</returns>
36+
public static string ToFullyQualifiedName(this EquatableReadOnlyList<Declaration> declarations)
37+
{
38+
var builder = new StringBuilder();
39+
40+
// ReSharper disable once ForCanBeConvertedToForeach
41+
for (var index = 0; index < declarations.Count; index++)
42+
{
43+
var declaration = declarations[index];
44+
45+
if (builder.Length > 0)
46+
builder.Append('.');
47+
builder.Append(declaration.Name);
48+
49+
if (declaration.GenericParameters.Count <= 0)
50+
continue;
51+
52+
builder.Append('`');
53+
builder.Append(declaration.GenericParameters.Count);
54+
}
55+
56+
return builder.ToString();
57+
}
58+
}

src/AttributeSourceGenerator/Common/EquatableReadOnlyList.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
namespace AttributeSourceGenerator.Common;
1+
// ReSharper disable CheckNamespace
2+
3+
namespace AttributeSourceGenerator.Common;
24

35
/// <summary>Provides extension methods to convert various collections to an <see cref="EquatableReadOnlyList{T}" />.</summary>
4-
public static class EquatableReadOnlyList
6+
internal static class EquatableReadOnlyList
57
{
68
/// <summary>Converts an <see cref="IReadOnlyList{T}" /> to an <see cref="EquatableReadOnlyList{T}" />.</summary>
79
/// <param name="list">The <see cref="IReadOnlyList{T}" /> to convert.</param>

src/AttributeSourceGenerator/Common/EquatableReadOnlyList`1.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
using System.Collections;
22

3+
// ReSharper disable CheckNamespace
4+
35
namespace AttributeSourceGenerator.Common;
46

57
/// <summary>A read-only list that implements <see cref="IEquatable{T}" /> for value-based equality comparisons.</summary>
68
/// <typeparam name="T">The type of elements in the list.</typeparam>
79
public readonly struct EquatableReadOnlyList<T> : IEquatable<EquatableReadOnlyList<T>>, IReadOnlyList<T>
810
{
911
/// <summary>Gets an empty <see cref="EquatableReadOnlyList{T}" />.</summary>
10-
public static EquatableReadOnlyList<T> Empty { get; } = new([]);
12+
internal static EquatableReadOnlyList<T> Empty { get; } = new([]);
13+
14+
/// <summary>Gets the number of elements in the list.</summary>
15+
public int Count => Collection.Count;
1116

1217
/// <summary>Gets the element at the specified index.</summary>
1318
/// <param name="index">The index of the element to get.</param>
1419
/// <returns>The element at the specified index.</returns>
1520
public T this[int index] => Collection[index];
1621

17-
/// <summary>Gets the number of elements in the list.</summary>
18-
public int Count => Collection.Count;
19-
22+
private IReadOnlyList<T> Collection => _collection ?? [];
2023
private readonly IReadOnlyList<T>? _collection;
2124

2225
/// <summary>Creates a new <see cref="EquatableReadOnlyList{T}" /> from an existing <see cref="IReadOnlyList{T}" />.</summary>
@@ -26,8 +29,6 @@ internal EquatableReadOnlyList(IReadOnlyList<T>? collection)
2629
_collection = collection;
2730
}
2831

29-
private IReadOnlyList<T> Collection => _collection ?? [];
30-
3132
/// <summary>Determines whether this instance and another object are equal.</summary>
3233
/// <param name="other">The object to compare with this instance.</param>
3334
/// <returns>True if the objects are equal, false otherwise.</returns>
@@ -68,7 +69,8 @@ public override int GetHashCode()
6869
{
6970
var hashCode = new HashCode();
7071

71-
foreach (var item in Collection) hashCode.Add(item);
72+
foreach (var item in Collection)
73+
hashCode.Add(item);
7274

7375
return hashCode.ToHashCode();
7476
}

0 commit comments

Comments
 (0)