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

Commit

Permalink
Improved the symbol model and API. (#2)
Browse files Browse the repository at this point in the history
* First draft of improvements.

* Second draft of improvements.

* Added symbol type.

* Third draft.

* Updated version.
  • Loading branch information
jscarle authored Feb 26, 2024
1 parent 26ebbe2 commit 109c0a9
Show file tree
Hide file tree
Showing 22 changed files with 796 additions and 156 deletions.
178 changes: 91 additions & 87 deletions src/AttributeSourceGenerator/AttributeIncrementalGeneratorBase.cs
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);
}
}
}
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()
{
}
}
12 changes: 6 additions & 6 deletions src/AttributeSourceGenerator/AttributeSourceGenerator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<RootNamespace>AttributeSourceGenerator</RootNamespace>
<LangVersion>latest</LangVersion>
<Version>8.0.1</Version>
<Version>8.0.2</Version>
<Title>AttributeSourceGenerator</Title>
<Authors>Jean-Sebastien Carle</Authors>
<Description>A simple attribute-based Roslyn incremental source generator base class for .NET.</Description>
Expand All @@ -18,8 +18,8 @@
<RepositoryUrl>https://github.com/jscarle/AttributeSourceGenerator</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>attribute source-generator attribute-based source-generators</PackageTags>
<AssemblyVersion>8.0.1.0</AssemblyVersion>
<FileVersion>8.0.1.0</FileVersion>
<AssemblyVersion>8.0.2.0</AssemblyVersion>
<FileVersion>8.0.2.0</FileVersion>
<NeutralLanguage>en-US</NeutralLanguage>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
Expand All @@ -31,12 +31,12 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="[4.3.1,]"/>
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.1"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" PrivateAssets="all"/>
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.1" PrivateAssets="all"/>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="all"/>
<None Include="..\..\LICENSE.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
Expand Down
58 changes: 58 additions & 0 deletions src/AttributeSourceGenerator/Common/DeclarationExtensions.cs
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();
}
}
6 changes: 4 additions & 2 deletions src/AttributeSourceGenerator/Common/EquatableReadOnlyList.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
namespace AttributeSourceGenerator.Common;
// ReSharper disable CheckNamespace

namespace AttributeSourceGenerator.Common;

/// <summary>Provides extension methods to convert various collections to an <see cref="EquatableReadOnlyList{T}" />.</summary>
public static class EquatableReadOnlyList
internal static class EquatableReadOnlyList
{
/// <summary>Converts an <see cref="IReadOnlyList{T}" /> to an <see cref="EquatableReadOnlyList{T}" />.</summary>
/// <param name="list">The <see cref="IReadOnlyList{T}" /> to convert.</param>
Expand Down
16 changes: 9 additions & 7 deletions src/AttributeSourceGenerator/Common/EquatableReadOnlyList`1.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
using System.Collections;

// ReSharper disable CheckNamespace

namespace AttributeSourceGenerator.Common;

/// <summary>A read-only list that implements <see cref="IEquatable{T}" /> for value-based equality comparisons.</summary>
/// <typeparam name="T">The type of elements in the list.</typeparam>
public readonly struct EquatableReadOnlyList<T> : IEquatable<EquatableReadOnlyList<T>>, IReadOnlyList<T>
{
/// <summary>Gets an empty <see cref="EquatableReadOnlyList{T}" />.</summary>
public static EquatableReadOnlyList<T> Empty { get; } = new([]);
internal static EquatableReadOnlyList<T> Empty { get; } = new([]);

/// <summary>Gets the number of elements in the list.</summary>
public int Count => Collection.Count;

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

/// <summary>Gets the number of elements in the list.</summary>
public int Count => Collection.Count;

private IReadOnlyList<T> Collection => _collection ?? [];
private readonly IReadOnlyList<T>? _collection;

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

private IReadOnlyList<T> Collection => _collection ?? [];

/// <summary>Determines whether this instance and another object are equal.</summary>
/// <param name="other">The object to compare with this instance.</param>
/// <returns>True if the objects are equal, false otherwise.</returns>
Expand Down Expand Up @@ -68,7 +69,8 @@ public override int GetHashCode()
{
var hashCode = new HashCode();

foreach (var item in Collection) hashCode.Add(item);
foreach (var item in Collection)
hashCode.Add(item);

return hashCode.ToHashCode();
}
Expand Down
Loading

0 comments on commit 109c0a9

Please sign in to comment.