Skip to content

Commit

Permalink
Further improve docs and comment code for posterity
Browse files Browse the repository at this point in the history
  • Loading branch information
kzu committed Dec 22, 2024
1 parent 24f281b commit a727008
Show file tree
Hide file tree
Showing 9 changed files with 88 additions and 44 deletions.
26 changes: 17 additions & 9 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,13 @@ file record struct TValue;
```

The `TValue` is subsequently defined as a file-local type where you can
specify whether it's a struct or a class and any interfaces it implements.
These are used to constrain the template expansion to only apply to struct ids,
such as those whose `TValue` is a struct above.
specify any interfaces it implements. If no constraints need to apply to
`TValue`, you can just leave the declaration empty, meaning "any value type".

> NOTE: The type of declaration (struct, class, record, etc.) of `TValue` is not checked,
> since in many cases you'd end up having to create two versions of the same template,
> one for structs and another for strings, since they are not value types and have no
> common declaration type.
Here's another example from the built-in templates that uses this technique to
apply to all struct ids whose `TValue` implements `IComparable<TValue>`:
Expand Down Expand Up @@ -306,14 +310,18 @@ This automatically covers not only all built-in value types, but also any custom
types that implement the interface, making the code generation much more flexible
and powerful.

> NOTE: if you need to exclude just the string type from applying to the `TValue`,
> you can use the inline comment `/*!string*/` in the primary constructor parameter
> type, as in `TSelf(/*!string*/ TValue Value)`.
In addition to constraining on the `TValue` type, you can also constrain on the
the struct id/`TSelf` itself by declaring the inheritance requirements in a partial
class of `TSelf` in the template. For example, the following (built-in) template
ensures it's only applied/expanded for struct ids whose `TValue` is [Ulid](https://github.com/Cysharp/Ulid)
and implement `INewable<TSelf, Ulid>`. Its usefulness in this case is that
the given interface constraint allows us to use the `TSelf.New(Ulid)` static interface
ensures it's only applied to struct ids whose `TValue` is [Ulid](https://github.com/Cysharp/Ulid)
and implement `INewable<TSelf, Ulid>`. This is useful in this case since the given
interface constraint allows us to use the `TSelf.New(Ulid)` static interface
factory method and have it recognized by the C# compiler as valid code as part of the
implementation of the parameterless `New()` factory method:
implementation of introduced parameterless `New()` factory method provided by the template:

```csharp
[TStructId]
Expand All @@ -329,8 +337,8 @@ file partial record struct TSelf : INewable<TSelf, Ulid>
}
```

> NOTE: the built-in templates will always provide an implementation of
> `INewable<TSelf, TValue>`.
> NOTE: the built-in templates will always emit an implementation of
> `INewable<TSelf, TValue>` for all struct ids.
Here you can see that the constraint that the value type must be `Ulid` is enforced by
the `TValue` constructor parameter type, while the interface constraint in the partial
Expand Down
4 changes: 4 additions & 0 deletions src/StructId.Analyzer/DapperExtensions.sbn
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,25 @@ public static partial class DapperExtensions
/// </summary>
public static TConnection UseStructId<TConnection>(this TConnection connection) where TConnection : IDbConnection
{
// Built-in supported TValues
{{~ for id in Ids ~}}
if (!SqlMapper.HasTypeHandler(typeof({{ id.TSelf }})))
SqlMapper.AddTypeHandler(new DapperTypeHandler{{ id.TValue }}<{{ id.TSelf }}>());

{{~ end ~}}
// Custom TValue handlers via pass-through type handler for the struct id
{{~ for id in CustomIds ~}}
if (!SqlMapper.HasTypeHandler(typeof({{ id.TSelf }})))
SqlMapper.AddTypeHandler(new DapperTypeHandler<{{ id.TSelf }}, {{ id.TValue }}, {{ id.THandler }}>());

{{~ end ~}}
// Custom TValue handlers that may not be used in struct ids at all
{{~ for handler in CustomValues ~}}
if (!SqlMapper.HasTypeHandler(typeof({{ handler.TValue }})))
SqlMapper.AddTypeHandler(new {{ handler.THandler }}());

{{~ end ~}}
// Templatized TValue handlers
{{~ for handler in TemplatizedValueHandlers ~}}
if (!SqlMapper.HasTypeHandler(typeof({{ handler.TValue }})))
SqlMapper.AddTypeHandler(new {{ handler.THandler }}());
Expand Down
12 changes: 9 additions & 3 deletions src/StructId.Analyzer/DapperGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ protected override IncrementalValuesProvider<TemplateArgs> OnInitialize(Incremen

var builtInHandled = source.Where(x => IsBuiltIn(x.TValue.ToFullName()));

// Any type in the compilation that inherits from Dapper.SqlMapper.TypeHandler<T> is also picked up,
// unless its a value template
var customHandlers = context.CompilationProvider
.SelectMany((x, _) => x.Assembly.GetAllTypes().OfType<INamedTypeSymbol>())
.Combine(context.CompilationProvider.Select((x, _) => x.GetTypeByMetadataName("Dapper.SqlMapper+TypeHandler`1")))
Expand All @@ -41,17 +43,21 @@ protected override IncrementalValuesProvider<TemplateArgs> OnInitialize(Incremen
.Select((x, _) => x.Left)
.Collect();

// Non built-in value types can be templatized by using [TValue] templates. These would necessarily be
// file-local types which are not registered as handlers themselves but applied to each struct id TValue in turn.
var templatizedValues = context.SelectTemplatizedValues()
.Where(x => !IsBuiltIn(x.TValue.ToFullName()))
.Combine(context.CompilationProvider.Select((x, _) => x.GetTypeByMetadataName("Dapper.SqlMapper+TypeHandler`1")))
.Where(x => x.Left.Template.TTemplate.Is(x.Right))
.Select((x, _) => x.Left);

// If there are custom type handlers for value types that are in turn used in struct ids, we need to register them
// as handlers that pass-through to the value handler itself.
var customHandled = source
.Combine(customHandlers.Combine(templatizedValues.Collect()))
.Select((x, _) =>
{
(TemplateArgs args, (ImmutableArray<INamedTypeSymbol> handlers, ImmutableArray<TValueTemplate> templatized)) = x;
(TemplateArgs args, (ImmutableArray<INamedTypeSymbol> handlers, ImmutableArray<TemplatizedTValue> templatized)) = x;

var handlerType = args.ReferenceType.Construct(args.TValue);
var handler = handlers.FirstOrDefault(x => x.Is(handlerType, false));
Expand Down Expand Up @@ -84,7 +90,7 @@ protected override IncrementalValuesProvider<TemplateArgs> OnInitialize(Incremen
return source.Where(x => false);
}

void GenerateHandlers(SourceProductionContext context, ((ImmutableArray<TemplateArgs> builtInHandled, ImmutableArray<TemplateArgs> customHandled), ImmutableArray<TValueTemplate> templatizedValues) source)
void GenerateHandlers(SourceProductionContext context, ((ImmutableArray<TemplateArgs> builtInHandled, ImmutableArray<TemplateArgs> customHandled), ImmutableArray<TemplatizedTValue> templatizedValues) source)
{
var ((builtInHandled, customHandled), templatizedValues) = source;
if (builtInHandled.Length == 0 && customHandled.Length == 0 && templatizedValues.Length == 0)
Expand Down Expand Up @@ -133,7 +139,7 @@ record ValueHandlerModel(string TValue, string THandler);

class ValueHandlerModelCode
{
public ValueHandlerModelCode(TValueTemplate template)
public ValueHandlerModelCode(TemplatizedTValue template)
{
var declaration = template.Template.Syntax.ApplyValue(template.TValue)
.DescendantNodes()
Expand Down
4 changes: 2 additions & 2 deletions src/StructId.Analyzer/EntityFrameworkGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ protected override SyntaxNode SelectTemplate(TemplateArgs args)
return idTemplate ??= CodeTemplate.Parse(ThisAssembly.Resources.Templates.EntityFramework.Text, args.KnownTypes.Compilation.GetParseOptions());
}

void GenerateValueSelector(SourceProductionContext context, ((ImmutableArray<TemplateArgs>, ImmutableArray<INamedTypeSymbol>), ImmutableArray<TValueTemplate>) args)
void GenerateValueSelector(SourceProductionContext context, ((ImmutableArray<TemplateArgs>, ImmutableArray<INamedTypeSymbol>), ImmutableArray<TemplatizedTValue>) args)
{
((var structIds, var customConverters), var templatizedConverters) = args;

Expand Down Expand Up @@ -119,7 +119,7 @@ record ConverterModel(string TModel, string TProvider, string TConverter);

class TemplatizedModel
{
public TemplatizedModel(TValueTemplate template)
public TemplatizedModel(TemplatizedTValue template)
{
var declaration = template.Template.Syntax.ApplyValue(template.TValue)
.DescendantNodes()
Expand Down
35 changes: 27 additions & 8 deletions src/StructId.Analyzer/TemplatedGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,41 @@ namespace StructId;
public partial class TemplatedGenerator : IIncrementalGenerator
{
/// <summary>
/// Represents a template for struct ids.
/// Represents an instantiation of a struct id template for a specific combination
/// of a <paramref name="TSelf"/> and <paramref name="TValue"/>.
/// </summary>
/// <param name="StructId">The struct id type, either IStructId or IStructId{T}.</param>
/// <param name="TSelf">The struct id type, either IStructId or IStructId{T}.</param>
/// <param name="TValue">The type of value the struct id holds, such as Guid or string.</param>
/// <param name="Template">The template to apply to it.</param>
record IdTemplate(INamedTypeSymbol StructId, INamedTypeSymbol TValue, Template Template);
record TemplatizedStructId(INamedTypeSymbol TSelf, INamedTypeSymbol TValue, Template Template);

/// <summary>
/// Represents the template that will be applied to a struct id.
/// </summary>
/// <param name="TSelf">Declaration and potential constraints to check on struct ids for the template to apply.</param>
/// <param name="TValue">Target value type, potentially containing a file-local declaration with further constraints to apply.</param>
/// <param name="Attribute">The <c>[TStructId]</c> attribute applied to the <paramref name="TSelf"/>.</param>
/// <param name="KnownTypes">Useful known compilation types used at template expansion time.</param>
record Template(INamedTypeSymbol TSelf, INamedTypeSymbol TValue, AttributeData Attribute, KnownTypes KnownTypes)
{
/// <summary>
/// Originally declared TValue type in the primary constructor itself.
/// </summary>
public INamedTypeSymbol? OriginalTValue { get; init; }

// A custom TValue is a file-local type declaration.
/// <summary>
/// Whether the a custom TValue is a file-local type declaration providing further constraints.
/// </summary>
public bool IsLocalTValue => OriginalTValue?.IsFileLocal == true;

/// <summary>
/// The syntax tree root of the template file.
/// </summary>
public SyntaxNode Syntax { get; } = TSelf.DeclaringSyntaxReferences[0].GetSyntax().SyntaxTree.GetRoot();

/// <summary>
/// Whether the template should not be applied to string value types.
/// </summary>
public bool NoString { get; } = new NoStringSyntaxWalker().Accept(
TSelf.DeclaringSyntaxReferences[0].GetSyntax().SyntaxTree.GetRoot());

Expand Down Expand Up @@ -131,18 +150,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
// If the TValue/Value implements or inherits from the template base type and/or its interfaces
return templates
.Where(template => template.AppliesTo(tid))
.Select(template => new IdTemplate(id, tid, template));
.Select(template => new TemplatizedStructId(id, tid, template));
});

context.RegisterSourceOutput(ids, GenerateCode);
}

void GenerateCode(SourceProductionContext context, IdTemplate source)
void GenerateCode(SourceProductionContext context, TemplatizedStructId source)
{
var templateFile = Path.GetFileNameWithoutExtension(source.Template.Syntax.SyntaxTree.FilePath);
var hintName = $"{source.StructId.ToFileName()}/{templateFile}.cs";
var hintName = $"{source.TSelf.ToFileName()}/{templateFile}.cs";

var applied = source.Template.Syntax.Apply(source.StructId);
var applied = source.Template.Syntax.Apply(source.TSelf);
var output = applied.ToFullString();

context.AddSource(hintName, SourceText.From(output, Encoding.UTF8));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
namespace StructId;

/// <summary>
/// Represents a template for the value type of struct ids.
/// Represents a templatized value type of a struct ids.
/// </summary>
/// <param name="TValue">The type of value the struct id holds, such as Guid or string.</param>
/// <param name="TValue">The type of value the a struct id holds, such as Guid or string.</param>
/// <param name="Template">The template to apply to it.</param>
record TValueTemplate(INamedTypeSymbol TValue, TValueTemplateInfo Template)
record TemplatizedTValue(INamedTypeSymbol TValue, TValueTemplate Template)
{
SyntaxNode? applied;

Expand All @@ -26,10 +26,26 @@ record TValueTemplate(INamedTypeSymbol TValue, TValueTemplateInfo Template)
public string Render() => Declaration.ToFullString();
}

record TValueTemplateInfo(INamedTypeSymbol TTemplate, KnownTypes KnownTypes)
/// <summary>
/// Represents a generic file-local template that applies to TValues that match the template
/// constraints.
/// </summary>
/// <param name="TTemplate">The declared symbol of the template in the compilation.</param>
/// <param name="KnownTypes">Useful known types for use when applying the template.</param>
record TValueTemplate(INamedTypeSymbol TTemplate, KnownTypes KnownTypes)
{
/// <summary>
/// Syntax root of the file declaring the template.
/// </summary>
public SyntaxNode Syntax { get; } = TTemplate.DeclaringSyntaxReferences[0].GetSyntax().SyntaxTree.GetRoot();

/// <summary>
/// Whether the template should not be applied to string value types.
/// </summary>
/// <remarks>
/// Since strings implement also a bunch of interfaces, an easy way to exclude them
/// from matching a struct value template that has a restriction on just
/// </remarks>
public bool NoString { get; } = new NoStringSyntaxWalker().Accept(
TTemplate.DeclaringSyntaxReferences[0].GetSyntax().SyntaxTree.GetRoot());

Expand Down Expand Up @@ -66,9 +82,12 @@ public bool AppliesTo(INamedTypeSymbol valueType)
}
}

static class TValueTemplateExtensions
static class TemplatizedTValueExtensions
{
public static IncrementalValuesProvider<TValueTemplate> SelectTemplatizedValues(this IncrementalGeneratorInitializationContext context)
/// <summary>
/// Gets all instantiations of TValue templates that apply to the struct ids in the compilation.
/// </summary>
public static IncrementalValuesProvider<TemplatizedTValue> SelectTemplatizedValues(this IncrementalGeneratorInitializationContext context)
{
var structIdNamespace = context.AnalyzerConfigOptionsProvider.GetStructIdNamespace();

Expand All @@ -87,7 +106,7 @@ public static IncrementalValuesProvider<TValueTemplate> SelectTemplatizedValues(
r => r.GetSyntax() is TypeDeclarationSyntax declaration && x.GetAttributes().Any(
a => a.IsValueTemplate())))
.Combine(known)
.Select((x, cancellation) => new TValueTemplateInfo(x.Left, x.Right))
.Select((x, cancellation) => new TValueTemplate(x.Left, x.Right))
.Collect();

var values = context.CompilationProvider
Expand All @@ -104,20 +123,9 @@ public static IncrementalValuesProvider<TValueTemplate> SelectTemplatizedValues(
var tvalue = (INamedTypeSymbol)structId.TypeArguments[0];
return templates
.Where(template => template.AppliesTo(tvalue))
.Select(template => new TValueTemplate(tvalue, template));
.Select(template => new TemplatizedTValue(tvalue, template));
});

return values;
}

//void GenerateCode(SourceProductionContext context, TIdTemplate source)
//{
// var templateFile = Path.GetFileNameWithoutExtension(source.Template.Syntax.SyntaxTree.FilePath);
// var hintName = $"{source.TValue.ToFileName()}/{templateFile}.cs";

// var applied = source.Template.Syntax.Apply(source.TValue);
// var output = applied.ToFullString();

// context.AddSource(hintName, SourceText.From(output, Encoding.UTF8));
//}
}
1 change: 0 additions & 1 deletion src/StructId/Templates/DapperTypeHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.Diagnostics.CodeAnalysis;
using StructId;

// TODO: pending making it conditionally included at compile-time
[TValue]
file class TValue_TypeHandler : Dapper.SqlMapper.TypeHandler<TValue>
{
Expand Down
2 changes: 1 addition & 1 deletion src/StructId/Templates/ParsableT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? prov
}

// This will be removed when applying the template to each user-defined struct id.
file record struct TValue : IParsable<TValue>
file struct TValue : IParsable<TValue>
{
public static TValue Parse(string s, IFormatProvider? provider) => throw new NotImplementedException();
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TValue result) => throw new NotImplementedException();
Expand Down
2 changes: 1 addition & 1 deletion src/StructId/Templates/SpanParsable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ file partial record struct TSelf
}

// This will be removed when applying the template to each user-defined struct id.
file record struct TValue : ISpanParsable<TValue>
file struct TValue : ISpanParsable<TValue>
{
public static TValue Parse(ReadOnlySpan<char> s, IFormatProvider? provider) => throw new NotImplementedException();
public static TValue Parse(string s, IFormatProvider? provider) => throw new NotImplementedException();
Expand Down

0 comments on commit a727008

Please sign in to comment.