Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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