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

Commit

Permalink
Updated implementation. (#5)
Browse files Browse the repository at this point in the history
* Updated namespaces.

* Updated readme.

* Added cancellation tokens.

* Added source generator options.

* Removed options.

* Added support for multiple source files.

* Updated README.
  • Loading branch information
jscarle authored Mar 6, 2024
1 parent 9bf1ae9 commit e3ebc20
Show file tree
Hide file tree
Showing 27 changed files with 369 additions and 186 deletions.
14 changes: 13 additions & 1 deletion AttributeSourceGenerator.sln
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AttributeSourceGenerator", "src\AttributeSourceGenerator\AttributeSourceGenerator.csproj", "{ED92CE9D-902E-445D-8E52-4679C575863E}"
# Visual Studio Version 17
VisualStudioVersion = 17.9.34616.47
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AttributeSourceGenerator", "src\AttributeSourceGenerator\AttributeSourceGenerator.csproj", "{ED92CE9D-902E-445D-8E52-4679C575863E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -13,4 +16,13 @@ Global
{ED92CE9D-902E-445D-8E52-4679C575863E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ED92CE9D-902E-445D-8E52-4679C575863E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0F4E21C8-19DB-41A2-A79A-FA5DCF0B08B6}
EndGlobalSection
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\AttributeSourceGenerator\AttributeSourceGeneratorShared.projitems*{dfd4aa47-b465-4c4b-930c-ec98d65bedc3}*SharedItemsImports = 13
EndGlobalSection
EndGlobal
97 changes: 97 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,99 @@
# AttributeSourceGenerator

A simple attribute-based Roslyn incremental source generator base class for .NET.

### Example generator

```csharp
using AttributeSourceGenerator;
using Microsoft.CodeAnalysis;

namespace SourceGenerators;

[Generator]
public sealed class IdentifierSourceGenerator : AttributeIncrementalGeneratorBase
{
public IdentifierSourceGenerator()
: base(() => new AttributeIncrementalGeneratorConfiguration()
{
MarkerAttributeName = MarkerAttributeName,
MarkerAttributeSource = MarkerAttributeSource,
SymbolFilter = FilterType.Struct,
SourceGenerator = GenerateIdentifier
})
{
}

private const string MarkerAttributeNamespace = "Domain.Common.Attributes";

private const string MarkerAttributeName = $"{MarkerAttributeNamespace}.GeneratedIdentifierAttribute`1";

private static source MarkerAttributeSource = new Source("GeneratedIdentifierAttribute`1", $$"""
namespace {{MarkerAttributeNamespace}};
[AttributeUsage(AttributeTargets.Struct)]
public sealed class GeneratedIdentifierAttribute<TIdentifier> : Attribute;
""";

private static IEnumerable<Source> GenerateIdentifier(Symbol symbol)
{
return [new Source(symbol.Name, $$"""
// <auto-generated/>
#nullable enable
namespace {{symbol.Namespace}};
partial struct {{symbol.Name}} : IEquatable<{{symbol.Name}}>, IComparable<{{symbol.Name}}>, IComparable
{
// Implementation details
}
""")];
}
}
```

### Typical .csproj

```xml
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
<LangVersion>latest</LangVersion>
<AnalysisLevel>latest-All</AnalysisLevel>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" PrivateAssets="all" />
<PackageReference Include="AttributeSourceGenerator" Version="8.0.2" PrivateAssets="all" GeneratePathProperty="true" />
</ItemGroup>

<PropertyGroup>
<IsRoslynComponent>true</IsRoslynComponent>
<IsPublishable>false</IsPublishable>
<IsPackable>true</IsPackable>
<IncludeBuildOutput>false</IncludeBuildOutput>
<NoWarn>$(NoWarn);NU5128</NoWarn>
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>

<ItemGroup>
<None Include="$(PkgAttributeSourceGenerator)/lib/netstandard2.0/AttributeSourceGenerator.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="$(OutputPath)/$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>

<Target Name="GetDependencyTargetPaths">
<ItemGroup>
<TargetPathWithTargetPlatformMoniker Include="$(PkgAttributeSourceGenerator)/lib/netstandard2.0/AttributeSourceGenerator.dll" IncludeRuntimeDependency="false" />
<TargetPathWithTargetPlatformMoniker Include="$(MSBuildThisFileDirectory)bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).dll" IncludeRuntimeDependency="false" />
</ItemGroup>
</Target>

</Project>
```
72 changes: 42 additions & 30 deletions src/AttributeSourceGenerator/AttributeIncrementalGeneratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
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>
Expand Down Expand Up @@ -36,30 +34,29 @@ protected AttributeIncrementalGeneratorBase(Func<AttributeIncrementalGeneratorCo
/// <param name="context">The initialization context.</param>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(initializationContext => AddSource(initializationContext, _configuration.AttributeFullyQualifiedName, _configuration.AttributeSource));

var pipeline = context.SyntaxProvider.ForAttributeWithMetadataName(_configuration.AttributeFullyQualifiedName, (syntaxNode, _) => Filter(syntaxNode, _configuration.SymbolFilter), (syntaxContext, _) => Transform(syntaxContext));
context.RegisterPostInitializationOutput(AddMarkerAttributeSource);

context.RegisterSourceOutput(pipeline, (productionContext, symbol) => GenerateSourceForSymbol(productionContext, symbol, _configuration.SourceGenerator));
var syntaxProvider = context.SyntaxProvider.ForAttributeWithMetadataName(_configuration.MarkerAttributeName, Filter, Transform);
context.RegisterSourceOutput(syntaxProvider, GenerateSourceForSymbol);
}

/// <summary>Adds a source file to the output.</summary>
/// <summary>Adds the marker attribute source 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)
private void AddMarkerAttributeSource(IncrementalGeneratorPostInitializationContext context)
{
if (source?.Length > 0)
context.AddSource($"{name}.g.cs", SourceText.From(source, Encoding.UTF8));
if (_configuration.MarkerAttributeSource?.Text.Length > 0)
context.AddSource($"{_configuration.MarkerAttributeSource.Value.Name}.g.cs", SourceText.From(_configuration.MarkerAttributeSource.Value.Text, Encoding.UTF8));
}

/// <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>
/// <param name="cancellationToken">The cancellation token.</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)
private bool Filter(SyntaxNode syntaxNode, CancellationToken cancellationToken)
{
var filter = filterType == FilterType.None ? FilterType.All : filterType;
cancellationToken.ThrowIfCancellationRequested();

var filter = _configuration.SymbolFilter == FilterType.None ? FilterType.All : _configuration.SymbolFilter;

if (filter.HasFlag(FilterType.Interface) && syntaxNode is InterfaceDeclarationSyntax)
return true;
Expand All @@ -79,35 +76,50 @@ private static bool Filter(SyntaxNode syntaxNode, FilterType filterType)

/// <summary>Transforms a generator attribute syntax context into a symbol for source generation.</summary>
/// <param name="context">The generator attribute syntax context.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The transformed symbol.</returns>
private static Symbol Transform(GeneratorAttributeSyntaxContext context)
private static Symbol Transform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

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 markerAttribute = context.GetMarkerAttribute(cancellationToken);
var containingDeclarations = targetSymbol.GetContainingDeclarations(cancellationToken);
var symbolType = targetSymbol.GetSymbolType(cancellationToken);
var symbolName = targetSymbol.Name;

EquatableReadOnlyList<string> genericTypeParameters;
EquatableReadOnlyList<ConstructorParameter> constructorParameters;
EquatableReadOnlyList<MethodParameter> constructorParameters;
string returnType;
switch (targetSymbol)
{
case INamedTypeSymbol namedTypeSymbol:
genericTypeParameters = namedTypeSymbol.GetGenericTypeParameters();
constructorParameters = EquatableReadOnlyList<ConstructorParameter>.Empty;
genericTypeParameters = namedTypeSymbol.GetGenericTypeParameters(cancellationToken);
constructorParameters = EquatableReadOnlyList<MethodParameter>.Empty;
returnType = "";
/*
TODO: Analyze members.
var members = namedTypeSymbol.GetMembers();
foreach (var member in members)
switch (member)
{
case IPropertySymbol propertySymbol:
break;
case IMethodSymbol methodSymbol:
break;
}
*/
break;
case IMethodSymbol methodSymbol:
genericTypeParameters = methodSymbol.GetGenericTypeParameters();
constructorParameters = methodSymbol.GetConstructorParameters();
genericTypeParameters = methodSymbol.GetGenericTypeParameters(cancellationToken);
constructorParameters = methodSymbol.GetMethodParameters(cancellationToken);
returnType = methodSymbol.ReturnType.ToDisplayString();
break;
default:
genericTypeParameters = EquatableReadOnlyList<string>.Empty;
constructorParameters = EquatableReadOnlyList<ConstructorParameter>.Empty;
constructorParameters = EquatableReadOnlyList<MethodParameter>.Empty;
returnType = "";
break;
}
Expand All @@ -120,10 +132,10 @@ private static Symbol Transform(GeneratorAttributeSyntaxContext context)
/// <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)
private void GenerateSourceForSymbol(SourceProductionContext context, Symbol symbol)
{
var sourceText = generate(symbol);
context.AddSource($"{symbol.FullyQualifiedName}.g.cs", sourceText);
var sources = _configuration.SourceGenerator(symbol);
foreach (var source in sources)
context.AddSource($"{source.Name}.g.cs", source.Text);
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
// ReSharper disable CheckNamespace

namespace AttributeSourceGenerator;
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 fully qualified name of the marker attribute.</summary>
public required string MarkerAttributeName { get; init; }

/// <summary>The source for the attribute.</summary>
public string? AttributeSource { get; init; }
/// <summary>The source for the marker attribute.</summary>
public Source? MarkerAttributeSource { 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; }
public required Func<Symbol, IEnumerable<Source>> SourceGenerator { get; init; }

/// <summary>Initializes a new instance of the <see cref="AttributeIncrementalGeneratorConfiguration" /> class</summary>
public AttributeIncrementalGeneratorConfiguration()
{
}
}
}
5 changes: 5 additions & 0 deletions src/AttributeSourceGenerator/AttributeSourceGenerator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
<Optimize>true</Optimize>
</PropertyGroup>

<ItemGroup>
<None Remove="AttributeSourceGenerator.shproj"/>
<None Remove="AttributeSourceGenerator.projitems"/>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" PrivateAssets="all"/>
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.1" PrivateAssets="all"/>
Expand Down
35 changes: 35 additions & 0 deletions src/AttributeSourceGenerator/AttributeSourceGenerator.projitems
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects Condition="'$(MSBuildVersion)' == '' Or '$(MSBuildVersion)' &lt; '16.0'">$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>dfd4aa47-b465-4c4b-930c-ec98d65bedc3</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>AttributeSourceGenerator</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Common\DeclarationExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Common\EquatableReadOnlyList.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Common\EquatableReadOnlyList`1.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Common\GeneratorAttributeSyntaxContextExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Common\MethodSymbolExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Common\NamedSymbolExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Common\SymbolExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Common\TypeSymbolExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Compiler\CompilerServices.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\ConstructorArgument.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\Declaration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\GenericTypeArgument.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\MarkerAttributeData.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\MethodParameter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Models\NamedArgument.cs" />
<Compile Include="$(MSBuildThisFileDirectory)AttributeIncrementalGeneratorBase.cs" />
<Compile Include="$(MSBuildThisFileDirectory)AttributeIncrementalGeneratorConfiguration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)DeclarationType.cs" />
<Compile Include="$(MSBuildThisFileDirectory)FilterType.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Source.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Symbol.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SymbolType.cs" />
</ItemGroup>
</Project>
13 changes: 13 additions & 0 deletions src/AttributeSourceGenerator/AttributeSourceGenerator.shproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>dfd4aa47-b465-4c4b-930c-ec98d65bedc3</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')"/>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props"/>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props"/>
<PropertyGroup/>
<Import Project="AttributeSourceGenerator.projitems" Label="Shared"/>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets"/>
</Project>
4 changes: 1 addition & 3 deletions src/AttributeSourceGenerator/Common/DeclarationExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using System.Text;
using AttributeSourceGenerator.Models;

// ReSharper disable CheckNamespace

namespace AttributeSourceGenerator.Common;

/// <summary>Provides extension methods for working with declarations.</summary>
Expand Down Expand Up @@ -55,4 +53,4 @@ public static string ToFullyQualifiedName(this EquatableReadOnlyList<Declaration

return builder.ToString();
}
}
}
6 changes: 2 additions & 4 deletions src/AttributeSourceGenerator/Common/EquatableReadOnlyList.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
// ReSharper disable CheckNamespace

namespace AttributeSourceGenerator.Common;
namespace AttributeSourceGenerator.Common;

/// <summary>Provides extension methods to convert various collections to an <see cref="EquatableReadOnlyList{T}" />.</summary>
internal static class EquatableReadOnlyList
Expand All @@ -20,4 +18,4 @@ public static EquatableReadOnlyList<T> ToEquatableReadOnlyList<T>(this IEnumerab
{
return new EquatableReadOnlyList<T>(enumerable.ToArray());
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
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>
Expand Down Expand Up @@ -88,4 +86,4 @@ IEnumerator IEnumerable.GetEnumerator()
{
return Collection.GetEnumerator();
}
}
}
Loading

0 comments on commit e3ebc20

Please sign in to comment.