-
-
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.
Templates are regular C# code, annotated with `[StructId]`. The template must provide the `[TId] Value` primary constructor, so that operations for the provided members can use the value just as it will for generated `IStructId` implementations. The type of the value determines the target ids that will get the template applied: they must match the TId type. The provided functional sample showcases how it works. Say you have a custom interface in your app, `IId` which you use to retrieve a GUID that represents the ID of your domain objects. You can have all struct ids that have a TId of Guid implement this interface by defining the template as: ```csharp [TStructId] file partial record struct IIdTemplate(Guid Value) : IId { public Guid Id => Value; } ``` What happens at compile-time: 1. The attribute triggers template processing for this type 2. The `Guid Value` parameter is used to filter struct ids that implement `IStructId<Guid>` (which will therefore have a matching constructor and value type). 3. The emitted partial is added to whichever namespace the target ID is, so the template does not need a namespace at all (should use global namespace). 4. The `file` keyword is used so that this type does not pollute the API surface of the project containing the template. Templates can come from referenced projects too, as long as the generator can locate the original syntax tree for it (to apply it to the target id).
- Loading branch information
Showing
6 changed files
with
234 additions
and
1 deletion.
There are no files selected for viewing
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,174 @@ | ||
using System.Linq; | ||
using System.Text; | ||
using System.Text.RegularExpressions; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Text; | ||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; | ||
|
||
namespace StructId; | ||
|
||
[Generator(LanguageNames.CSharp)] | ||
public class TemplatedGenerator : IIncrementalGenerator | ||
{ | ||
record KnownTypes(INamedTypeSymbol String, INamedTypeSymbol? IStructId, INamedTypeSymbol? TStructId, INamedTypeSymbol? TStructIdT); | ||
record IdTemplate(INamedTypeSymbol StructId, Template Template); | ||
record Template(INamedTypeSymbol TSelf, ITypeSymbol TId, AttributeData Attribute, bool IsGenericTId) | ||
{ | ||
public Regex NameExpr { get; } = new Regex($@"\b{TSelf.Name}\b", RegexOptions.Compiled | RegexOptions.Multiline); | ||
|
||
public string Text { get; } = GetTemplateCode(TSelf, TId, Attribute); | ||
|
||
static string GetTemplateCode(INamedTypeSymbol self, ITypeSymbol tid, AttributeData attribute) | ||
{ | ||
if (self.DeclaringSyntaxReferences[0].GetSyntax() is not TypeDeclarationSyntax declaration) | ||
return ""; | ||
|
||
// Remove the TId/TValue if present in the same syntax tree. | ||
var toremove = tid.DeclaringSyntaxReferences.Select(x => x.GetSyntax()).ToList(); | ||
// Also the [TStructId<T>] attribute applied to the template itself | ||
if (attribute.ApplicationSyntaxReference?.GetSyntax().FirstAncestorOrSelf<AttributeListSyntax>() is { } attr) | ||
toremove.Add(attr); | ||
// And the primary constructor if present, since that's generated for the struct id already | ||
if (declaration.ParameterList != null) | ||
toremove.Add(declaration.ParameterList); | ||
|
||
var root = declaration.SyntaxTree.GetRoot() | ||
.RemoveNodes(toremove, SyntaxRemoveOptions.KeepLeadingTrivia)!; | ||
|
||
var update = root.DescendantNodes().OfType<TypeDeclarationSyntax>().First(x => x.Identifier.Text == self.Name); | ||
|
||
// Remove file-scoped modifier if present | ||
if (update.Modifiers.FirstOrDefault(x => x.IsKind(SyntaxKind.FileKeyword)) is { } file) | ||
{ | ||
var updated = update.WithModifiers(update.Modifiers.Remove(file)); | ||
// Preserve trivia, i.e. newline from original file modifier | ||
if (updated.Modifiers.Count > 0) | ||
updated = updated.ReplaceToken(updated.Modifiers[0], updated.Modifiers[0].WithLeadingTrivia(file.LeadingTrivia)); | ||
|
||
root = root.ReplaceNode(update, updated); | ||
} | ||
|
||
return root.SyntaxTree.GetRoot().ToFullString().Trim(); | ||
} | ||
} | ||
|
||
public void Initialize(IncrementalGeneratorInitializationContext context) | ||
{ | ||
var targetNamespace = context.AnalyzerConfigOptionsProvider | ||
.Select((x, _) => x.GlobalOptions.TryGetValue("build_property.StructIdNamespace", out var ns) ? ns : "StructId"); | ||
|
||
var known = context.CompilationProvider | ||
.Combine(targetNamespace) | ||
.Select((x, _) => new KnownTypes( | ||
// get string known type | ||
x.Left.GetTypeByMetadataName("System.String")!, | ||
x.Left.GetTypeByMetadataName($"{x.Right}.IStructId`1"), | ||
x.Left.GetTypeByMetadataName($"{x.Right}.TStructIdAttribute"), | ||
x.Left.GetTypeByMetadataName($"{x.Right}.TStructIdAttribute`1"))); | ||
|
||
var templates = context.CompilationProvider | ||
.SelectMany((x, _) => x.GetAllTypes(includeReferenced: true).OfType<INamedTypeSymbol>()) | ||
.Combine(known) | ||
.Where(x => | ||
// Ensure template is a partial record struct | ||
x.Left.TypeKind == TypeKind.Struct && x.Left.IsRecord && | ||
// We can only work with templates where we have the actual syntax tree. | ||
x.Left.DeclaringSyntaxReferences.Length == 1 && | ||
// The declaring syntax reference has a primary constructor with a single parameter named Value | ||
// This would be enforced by an analyzer/codefix pair. | ||
x.Left.DeclaringSyntaxReferences[0].GetSyntax() is TypeDeclarationSyntax declaration && | ||
declaration.ParameterList?.Parameters.Count == 1 && | ||
declaration.ParameterList.Parameters[0].Identifier.Text == "Value" && | ||
// And we can locate the TStructIdAttribute type that should be applied to it. | ||
x.Right.TStructId != null && x.Right.TStructIdT != null && | ||
x.Left.GetAttributes().Any(a => a.AttributeClass != null && | ||
// The attribute should either be the generic or regular TStructIdAttribute | ||
(a.AttributeClass.Is(x.Right.TStructId) || a.AttributeClass.Is(x.Right.TStructIdT)))) | ||
.Select((x, _) => | ||
{ | ||
var (structId, known) = x; | ||
var attribute = structId.GetAttributes().FirstOrDefault(a => a.AttributeClass != null && a.AttributeClass.Is(known.TStructIdT)); | ||
if (attribute != null) | ||
return new Template(structId, attribute.AttributeClass!.TypeArguments[0], attribute, true); | ||
// If we don't have the generic attribute, infer the idType from the required | ||
// primary constructor Value parameter type | ||
var idType = structId.GetMembers().OfType<IPropertySymbol>().First(p => p.Name == "Value").Type; | ||
attribute = structId.GetAttributes().First(a => a.AttributeClass != null && a.AttributeClass.Is(known.TStructId)); | ||
return new Template(structId, idType, attribute, false); | ||
}) | ||
.Collect(); | ||
|
||
var ids = context.CompilationProvider | ||
.SelectMany((x, _) => x.Assembly.GetAllTypes().OfType<INamedTypeSymbol>()) | ||
.Combine(known) | ||
.Where(x => x.Right.IStructId != null && x.Left.Is(x.Right.IStructId) && x.Left.IsPartial()) | ||
.Combine(templates) | ||
.Where(x => | ||
{ | ||
var ((id, known), templates) = x; | ||
var structId = id.AllInterfaces.FirstOrDefault(i => i.Is(known.IStructId)); | ||
return structId != null; | ||
}) | ||
.SelectMany((x, _) => | ||
{ | ||
var ((id, known), templates) = x; | ||
// Locate the IStructId<TId> interface implemented by the id | ||
var structId = id.AllInterfaces.First(i => i.Is(known.IStructId)); | ||
var tid = structId.TypeArguments[0]; | ||
// If the current struct id (which will be a generic) implements or inherits from | ||
// the template base type and/or its interfaces | ||
return templates | ||
// check struct id's value type against the template's TId for compatibility | ||
.Where(template => | ||
tid.Equals(template.TId, SymbolEqualityComparer.Default) || | ||
tid.Is(template.TId) || | ||
// If the template had a generic attribute, we'd be looking at an intermediate | ||
// type (typically TValue or TId) being used to define multiple constraints on | ||
// the struct id's value type, such as implementing multiple interfaces. In | ||
// this case, the tid would never equal or inherit from the template's TId, | ||
// but we want instead to check for base type compatibility plus all interfaces. | ||
(template.IsGenericTId && | ||
// TId is a derived class of the template's TId base type (i.e. object or ValueType) | ||
tid.Is(template.TId.BaseType) && | ||
// All template provided TId interfaces must be implemented by the struct id's TId | ||
template.TId.AllInterfaces.All(iface => | ||
tid.AllInterfaces.Any(tface => tface.Is(iface))))) | ||
.Select(template => new IdTemplate(id, template)); | ||
}); | ||
|
||
context.RegisterSourceOutput(ids, GenerateCode); | ||
} | ||
|
||
void GenerateCode(SourceProductionContext context, IdTemplate source) | ||
{ | ||
var hintName = $"{source.StructId.ToFileName()}-{source.Template.TSelf.Name}.cs"; | ||
var output = source.Template.NameExpr.Replace(source.Template.Text, source.StructId.Name); | ||
|
||
if (source.StructId.ContainingNamespace.Equals(source.StructId.ContainingModule.GlobalNamespace, SymbolEqualityComparer.Default)) | ||
{ | ||
// No need to tweak target namespace. | ||
context.AddSource(hintName, SourceText.From(output, Encoding.UTF8)); | ||
return; | ||
} | ||
|
||
// parse template into a C# compilation unit | ||
var syntax = CSharpSyntaxTree.ParseText(output).GetCompilationUnitRoot(); | ||
|
||
// if we got a ns, move all members after a file-scoped namespace declaration | ||
var members = syntax.Members; | ||
var fsns = FileScopedNamespaceDeclaration(ParseName(source.StructId.ContainingNamespace.ToDisplayString()) | ||
.WithLeadingTrivia(Whitespace(" "))) | ||
.WithLeadingTrivia(LineFeed) | ||
.WithTrailingTrivia(LineFeed, LineFeed) | ||
.WithMembers(members); | ||
|
||
syntax = syntax.WithMembers(SingletonList<MemberDeclarationSyntax>(fsns)); | ||
|
||
output = syntax.ToFullString(); | ||
context.AddSource(hintName, SourceText.From(output, Encoding.UTF8)); | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
|
||
namespace StructId; | ||
|
||
/// <summary> | ||
/// Showcases a custom interface that we want implemented by all | ||
/// guid-based struct ids. | ||
/// </summary> | ||
public interface IId | ||
{ | ||
public Guid Id { get; } | ||
} |
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,12 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using StructId; | ||
|
||
[TStructId] | ||
file partial record struct IIdTemplate(Guid Value) : IId | ||
{ | ||
public Guid Id => Value; | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
using System; | ||
|
||
namespace StructId; | ||
|
||
/// <summary> | ||
/// Attribute for marking a template type for a struct id based on | ||
/// a generic type parameter, which would implement <see cref="IStructId{TId}"/>. | ||
/// </summary> | ||
/// <typeparam name="TId">Template for the TId to replace in the <see cref="IStructId{TId}"/.></typeparam> | ||
[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class)] | ||
public class TStructIdAttribute<TId> : Attribute | ||
{ | ||
} | ||
|
||
/// <summary> | ||
/// Attribute for marking a template type for a struct id based on a string value, | ||
/// which would implement <see cref="IStructId"/>. | ||
/// </summary> | ||
[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class)] | ||
public class TStructIdAttribute : Attribute | ||
{ | ||
} |