Skip to content

Commit

Permalink
Refactor code templates processing, move more to compiled
Browse files Browse the repository at this point in the history
Refactor resource and compiled templates to use a unified CodeTemplate implementation that simplifies the application of a template for a given target struct id.

This allowed to also simplify quite a bit the tests.

Most templates are now simple compiled ones, except for the ones that need custom logic that isn't just checking target value type (TId) compatiblity.

In particular: ctor generation is dynamic since users can provide their own ctor, so we can't just apply them as compiled templates.

EF, Dapper and Newtonsoft.Json need conditional checking on type presense, so they cannot be ported either.

Regardless, we now have a single unified way of authoring templates for either scenario. This infolves using always file-only types, which removes cross-template dependencies we had with a hardcoded TSelf/TId pair.
  • Loading branch information
kzu committed Dec 9, 2024
1 parent 8050bb1 commit 91deeaf
Show file tree
Hide file tree
Showing 37 changed files with 712 additions and 537 deletions.
5 changes: 4 additions & 1 deletion src/StructId.Analyzer/AnalysisExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static class AnalysisExtensions
public static SymbolDisplayFormat FullNameNullable { get; } = new(
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.ExpandNullable);
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.ExpandNullable | SymbolDisplayMiscellaneousOptions.UseSpecialTypes);

public static string ToFullName(this ISymbol symbol) => symbol.ToDisplayString(FullNameNullable);

Expand Down Expand Up @@ -149,6 +149,9 @@ public static string ToFileName(this ITypeSymbol type)

public static bool IsStructId(this ITypeSymbol type) => type.AllInterfaces.Any(x => x.Name == "IStructId");

public static bool IsStructIdTemplate(this AttributeSyntax attribute)
=> attribute.Name.ToString() == "TStructId" || attribute.Name.ToString() == "TStructIdAttribute";

public static bool IsPartial(this ITypeSymbol node) => node.DeclaringSyntaxReferences.Any(
r => r.GetSyntax() is TypeDeclarationSyntax { Modifiers: { } modifiers } &&
modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)));
Expand Down
40 changes: 9 additions & 31 deletions src/StructId.Analyzer/BaseGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public enum ReferenceCheck

public abstract class BaseGenerator(string referenceType, string stringTemplate, string typeTemplate, ReferenceCheck referenceCheck = ReferenceCheck.ValueIsType) : IIncrementalGenerator
{
SyntaxNode? stringSyntax;
SyntaxNode? typedSyntax;

protected record struct TemplateArgs(string StructIdNamespace, INamedTypeSymbol StructId, INamedTypeSymbol ValueType, INamedTypeSymbol ReferenceType, INamedTypeSymbol StringType);

public virtual void Initialize(IncrementalGeneratorInitializationContext context)
Expand Down Expand Up @@ -67,39 +70,14 @@ public virtual void Initialize(IncrementalGeneratorInitializationContext context

void GenerateCode(SourceProductionContext context, TemplateArgs args) => AddFromTemplate(
context, args, $"{args.StructId.ToFileName()}.cs",
args.ValueType.Equals(args.StringType, SymbolEqualityComparer.Default) ? stringTemplate : typeTemplate);
args.ValueType.Equals(args.StringType, SymbolEqualityComparer.Default) ?
(stringSyntax ??= CodeTemplate.Parse(stringTemplate)) :
(typedSyntax ??= CodeTemplate.Parse(typeTemplate)));

protected static void AddFromTemplate(SourceProductionContext context, TemplateArgs args, string hintName, string template)
protected static void AddFromTemplate(SourceProductionContext context, TemplateArgs args, string hintName, SyntaxNode template)
{
var ns = args.StructId.ContainingNamespace.Equals(args.StructId.ContainingModule.GlobalNamespace, SymbolEqualityComparer.Default)
? null
: args.StructId.ContainingNamespace.ToDisplayString();

// replace tokens in the template
var replaced = template
// Adjust to current target namespace
.Replace("namespace StructId;", $"namespace {args.StructIdNamespace};")
.Replace("using StructId;", $"using {args.StructIdNamespace};")
// Simple names suffices since we emit a partial in the same namespace
.Replace("TSelf", args.StructId.Name)
.Replace("Self", args.StructId.Name)
.Replace("TId", args.ValueType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));

// parse template into a C# compilation unit
var syntax = CSharpSyntaxTree.ParseText(replaced).GetCompilationUnitRoot();

// if we got a ns, move all members after a file-scoped namespace declaration
if (ns != null)
{
var members = syntax.Members;
var fsns = FileScopedNamespaceDeclaration(ParseName(ns).WithLeadingTrivia(Whitespace(" ")))
.WithLeadingTrivia(LineFeed)
.WithTrailingTrivia(LineFeed)
.WithMembers(members);
syntax = syntax.WithMembers(SingletonList<MemberDeclarationSyntax>(fsns));
}

var output = syntax.ToFullString();
var applied = template.Apply(args.StructId);
var output = applied.ToFullString();

context.AddSource(hintName, SourceText.From(output, Encoding.UTF8));
}
Expand Down
183 changes: 183 additions & 0 deletions src/StructId.Analyzer/CodeTemplate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace StructId;

public static class CodeTemplate
{
public static SyntaxNode Parse(string template)
{
var tree = CSharpSyntaxTree.ParseText(template,
CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest));

return tree.GetRoot();
}

public static string Apply(string template, string structIdType, string valueType)
{
var targetNamespace = structIdType.Contains('.') ? structIdType.Substring(0, structIdType.LastIndexOf('.')) : null;
structIdType = structIdType.Contains('.') ? structIdType.Substring(structIdType.LastIndexOf('.') + 1) : structIdType;

return ApplyImpl(Parse(template), structIdType, valueType, targetNamespace).ToFullString();
}

public static SyntaxNode Apply(this SyntaxNode node, INamedTypeSymbol structId)
{
var root = node.SyntaxTree.GetCompilationUnitRoot();
if (root == null)
return node;

// determine namespace of the IStructId/IStructId<T> interface implemented by structId
var iface = structId.Interfaces.FirstOrDefault(x => x.Name == "IStructId");
if (iface == null)
return root;

var tid = iface.TypeArguments.FirstOrDefault()?.ToFullName() ?? "string";
var corens = iface.ContainingNamespace.ToFullName();
var targetNamespace = structId.ContainingNamespace != null && !structId.ContainingNamespace.IsGlobalNamespace ?
structId.ContainingNamespace.ToDisplayString() : null;

return ApplyImpl(root, structId.Name, tid, targetNamespace, corens);
}

static SyntaxNode ApplyImpl(this SyntaxNode node, string structIdType, string valueType, string? targetNamespace = default, string coreNamespace = "StructId")
{
var root = node.SyntaxTree.GetCompilationUnitRoot();
if (root == null)
return node;

// If we got a ns, move all members after a file-scoped namespace declaration
if (targetNamespace != null)
{
var members = root.Members;
var fsns = FileScopedNamespaceDeclaration(ParseName(targetNamespace)
.WithLeadingTrivia(node.GetLeadingTrivia())
.WithLeadingTrivia(Whitespace(" ")))
.WithLeadingTrivia(LineFeed)
.WithTrailingTrivia(LineFeed, LineFeed)
.WithMembers(members);

root = root.WithMembers(SingletonList<MemberDeclarationSyntax>(fsns));
}

var usings = root.DescendantNodes().OfType<UsingDirectiveSyntax>().ToList();
// There should be NO namespace declared in the template itself, since we enforce file-local
usings.Add(UsingDirective(ParseName(coreNamespace)).NormalizeWhitespace());

// deduplicate usings just in case
var unique = new HashSet<string>();
root = root.ReplaceNodes(usings, (old, _) =>
{
// replace 'StructId' > StructIdNamespace
if (old.Name?.ToString() == "StructId")
{
unique.Add(coreNamespace);
return old.WithName(ParseName(coreNamespace));
}

if (unique.Add(old.Name?.ToString() ?? ""))
return old;

return null!;
});

node = new TemplateRewriter(structIdType, valueType).Visit(root)!;

return node;
}

class TemplateRewriter(string tself, string tid) : CSharpSyntaxRewriter
{
public override SyntaxNode? VisitRecordDeclaration(RecordDeclarationSyntax node)
{
// remove file-local records that aren't annotated with [TStructId]
if (node.Modifiers.Any(x => x.IsKind(SyntaxKind.FileKeyword)) &&
!node.AttributeLists.Any(list => list.Attributes.Any(a => a.IsStructIdTemplate())))
return null;

// If the record has the [TStructId] attribute, remove parameter list
if (node.AttributeLists.Any(list => list.Attributes.Any(a => a.IsStructIdTemplate())) &&
node.ParameterList is { } parameters)
{
// Check if the open paren trivia contains the text '🙏' and remove it
if (parameters.OpenParenToken.GetAllTrivia().Any(x => x.ToString().Contains("🙏")))
node = node.WithParameterList(parameters
.WithOpenParenToken(parameters.OpenParenToken.WithoutTrivia()));
else
node = node.WithParameterList(null);
}

var visited = (RecordDeclarationSyntax)base.VisitRecordDeclaration(node)!;

// remove file modifier from type declarations
if (visited.Modifiers.FirstOrDefault(x => x.IsKind(SyntaxKind.FileKeyword)) is { } file)
// Preserve trivia, i.e. newline from original file modifier
return visited
.WithLeadingTrivia(file.LeadingTrivia)
.WithModifiers(visited.Modifiers.Remove(file));

return visited;
}

public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node)
{
// remove file-local structs that aren't annotated with [TStructId]
if (node.Modifiers.Any(x => x.IsKind(SyntaxKind.FileKeyword)) &&
!node.AttributeLists.Any(list => list.Attributes.Any(a => a.IsStructIdTemplate())))
return null;

return base.VisitStructDeclaration(node);
}

public override SyntaxNode? VisitAttributeList(AttributeListSyntax node)
{
node = (AttributeListSyntax)base.VisitAttributeList(node)!;
if (node.Attributes.Count == 0)
return null;

return node;
}

public override SyntaxNode? VisitAttribute(AttributeSyntax node)
{
if (node.IsStructIdTemplate())
return null;

return base.VisitAttribute(node);
}

// rewrite references to the original type with the target type
public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node)
{
if (node.Identifier.Text == "TSelf")
return IdentifierName(tself)
.WithLeadingTrivia(node.Identifier.LeadingTrivia)
.WithTrailingTrivia(node.Identifier.TrailingTrivia);
else if (node.Identifier.Text == "TId")
return IdentifierName(tid)
.WithLeadingTrivia(node.Identifier.LeadingTrivia)
.WithTrailingTrivia(node.Identifier.TrailingTrivia);

return base.VisitIdentifierName(node);
}

public override SyntaxToken VisitToken(SyntaxToken token)
{
// if token is an identifier token, rewrite it
if (token.IsKind(SyntaxKind.IdentifierToken) && token.Text == "TSelf")
return Identifier(tself)
.WithLeadingTrivia(token.LeadingTrivia)
.WithTrailingTrivia(token.TrailingTrivia);
else if (token.IsKind(SyntaxKind.IdentifierToken) && token.Text == "TId")
return Identifier(tid)
.WithLeadingTrivia(token.LeadingTrivia)
.WithTrailingTrivia(token.TrailingTrivia);

return base.VisitToken(token);
}
}
}
3 changes: 3 additions & 0 deletions src/StructId.Analyzer/DapperGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ protected override IncrementalValuesProvider<TemplateArgs> OnInitialize(Incremen
"System.Guid" => true,
"System.Int32" => true,
"System.Int64" => true,
"string" => true,
"int" => true,
"long" => true,
_ => false
});

Expand Down
26 changes: 0 additions & 26 deletions src/StructId.Analyzer/NewableGenerator.cs

This file was deleted.

9 changes: 0 additions & 9 deletions src/StructId.Analyzer/ParsableGenerator.cs

This file was deleted.

1 change: 1 addition & 0 deletions src/StructId.Analyzer/StructId.Analyzer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

<ItemGroup>
<TemplateCode Include="..\StructId\ResourceTemplates\*.cs" Link="StructId\%(Filename)%(Extension)" />
<TemplateCode Include="..\StructId\Templates\*.cs" Link="StructId\%(Filename)%(Extension)" />
<EmbeddedResource Include="*.sbn" Kind="Text" />
<UpToDateCheck Include="@(TemplateCode);@(EmbeddedResource)" />
</ItemGroup>
Expand Down
Loading

0 comments on commit 91deeaf

Please sign in to comment.