Skip to content

Commit

Permalink
Split SpanFormattable from Formattable
Browse files Browse the repository at this point in the history
This makes the templates more granular.
  • Loading branch information
kzu committed Dec 9, 2024
1 parent a533c32 commit c4361e7
Show file tree
Hide file tree
Showing 6 changed files with 45 additions and 24 deletions.
8 changes: 8 additions & 0 deletions src/StructId.Analyzer/AnalysisExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,18 @@ 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 AttributeData attribute)
=> attribute.AttributeClass?.Name == "TStructId" ||
attribute.AttributeClass?.Name == "TStructIdAttribute";

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)));

public static bool IsFileLocal(this ITypeSymbol? node) => node != null && node.DeclaringSyntaxReferences.All(
r => r.GetSyntax() is TypeDeclarationSyntax { Modifiers: { } modifiers } &&
modifiers.Any(m => m.IsKind(SyntaxKind.FileKeyword)));
}
28 changes: 9 additions & 19 deletions src/StructId.Analyzer/TemplatedGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
using System;
using System.IO;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Scriban;

namespace StructId;

Expand Down Expand Up @@ -51,8 +49,7 @@ record Template(INamedTypeSymbol TSelf, INamedTypeSymbol TId, AttributeData Attr
public INamedTypeSymbol? OriginalTId { get; init; }

// A custom TId is a file-local type declaration.
public bool IsLocalTId => OriginalTId?.DeclaringSyntaxReferences
.All(x => x.GetSyntax() is TypeDeclarationSyntax decl && decl.Modifiers.Any(m => m.IsKind(SyntaxKind.FileKeyword))) == true;
public bool IsLocalTId => OriginalTId?.IsFileLocal == true;

public SyntaxNode Syntax { get; } = TSelf.DeclaringSyntaxReferences[0].GetSyntax().SyntaxTree.GetRoot();

Expand Down Expand Up @@ -119,22 +116,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context)

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 &&
// Ensure template is a file-local partial record struct
x.TypeKind == TypeKind.Struct && x.IsRecord && x.IsFileLocal &&
// 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.Left.GetAttributes().Any(a => a.AttributeClass != null &&
// The attribute should either be the generic or regular TStructIdAttribute
(a.AttributeClass.Is(x.Right.TStructId))))
x.DeclaringSyntaxReferences.Any(
// And we can locate the TStructIdAttribute type that should be applied to it.
r => r.GetSyntax() is TypeDeclarationSyntax declaration && x.GetAttributes().Any(
a => a.IsStructIdTemplate())))
.Combine(known)
.Select((x, cancellation) =>
{
var (structId, known) = x;
Expand Down
8 changes: 8 additions & 0 deletions src/StructId.FunctionalTests/Functional.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,14 @@ public void GuidImplementUtf8SpanFormattable()
}
}

[Fact]
public void ImplementsFormattable()
{
Assert.IsAssignableFrom<IFormattable>(ProductId.New());
Assert.IsAssignableFrom<IFormattable>(UserId.New(123));
Assert.IsNotAssignableFrom<IFormattable>(WalletId.New("foo"));
}

public class Context : DbContext
{
public Context(DbContextOptions<Context> options) : base(options) { }
Expand Down
13 changes: 13 additions & 0 deletions src/StructId/Templates/Formattable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// <auto-generated />
#nullable enable

using System;
using StructId;

[TStructId]
file partial record struct TSelf(IFormattable Value) : IFormattable
{
/// <inheritdoc/>
public readonly string ToString(string? format, IFormatProvider? formatProvider)
=> ((IFormattable)Value).ToString(format, formatProvider);
}
10 changes: 6 additions & 4 deletions src/StructId/Templates/SpanFormattable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
[TStructId]
file partial record struct TSelf(ISpanFormattable Value) : ISpanFormattable
{
/// <inheritdoc/>
public readonly string ToString(string? format, IFormatProvider? formatProvider)
=> ((ISpanFormattable)Value).ToString(format, formatProvider);

/// <inheritdoc/>
public readonly bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
=> ((ISpanFormattable)Value).TryFormat(destination, out charsWritten, format, provider);
}

file partial record struct TSelf
{
// This partial is provided by Formattable template
public string ToString(string? format, IFormatProvider? formatProvider) => throw new NotImplementedException();
}
2 changes: 1 addition & 1 deletion src/StructId/Templates/SpanParsable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using StructId;

[TStructId]
file readonly partial record struct TSelf(TId Value) : ISpanParsable<TSelf>
file readonly partial record struct TSelf(/*!string*/ TId Value) : ISpanParsable<TSelf>
{
/// <inheritdoc cref="ISpanParsable{TSelf}"/>
public static TSelf Parse(ReadOnlySpan<char> input, IFormatProvider? provider) => new(TId.Parse(input, provider));
Expand Down

0 comments on commit c4361e7

Please sign in to comment.