Skip to content

Commit b8cdb25

Browse files
authored
Source generator row adaptors for records (#187)
1 parent c1e798c commit b8cdb25

File tree

12 files changed

+276
-12
lines changed

12 files changed

+276
-12
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System;
2+
using JetBrains.Annotations;
3+
4+
namespace NexusMods.HyperDuck;
5+
6+
[PublicAPI]
7+
[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
8+
public class QueryResultAttribute : Attribute { }
9+

src/NexusMods.MnemonicDB.SourceGenerator/NexusMods.MnemonicDB.SourceGenerator.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@
2727

2828
<ItemGroup>
2929
<WeaveTemplate Include="Template.weave" />
30+
3031
<None Remove="AmbientSqlTemplate.weave" />
3132
<WeaveTemplate Include="AmbientSqlTemplate.weave" />
33+
34+
<None Remove="ResultGeneratorTemplate.weave" />
35+
<WeaveTemplate Include="ResultGeneratorTemplate.weave" />
3236
</ItemGroup>
3337

3438
<Import Project="$([MSBuild]::GetPathOfFileAbove('NuGet.Build.props', '$(MSBuildThisFileDirectory)../'))"/>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
@namespace NexusMods.MnemonicDB.SourceGenerator
2+
@methodname Render
3+
@model SqlResultGenerator.ResultData
4+
5+
#nullable enable
6+
7+
public class {{= model.Symbol.Name }}AdapterFactory : global::NexusMods.HyperDuck.Adaptor.IRowAdaptorFactory
8+
{
9+
private static readonly global::System.Type[] ElementTypes = [{{each parameter in model.Parameters}}typeof({{= parameter.DisplayString}}){{delimit}},{{/each}}];
10+
11+
public bool TryExtractElementTypes(global::System.ReadOnlySpan<global::NexusMods.HyperDuck.Result.ColumnInfo> result, global::System.Type resultType, out global::System.Type[] elementTypes, out int priority)
12+
{
13+
if (resultType != typeof({{= model.DisplayString}}))
14+
{
15+
elementTypes = [];
16+
priority = int.MinValue;
17+
return false;
18+
}
19+
20+
elementTypes = ElementTypes;
21+
priority = int.MaxValue;
22+
return true;
23+
}
24+
25+
public global::System.Type CreateType(global::System.Type resultType, global::System.Type[] elementTypes, global::System.Type[] elementAdaptors)
26+
{
27+
return typeof({{= model.Symbol.Name }}Adapter<{{= new string(',', count: model.Parameters.Length)}}>).MakeGenericType(elementAdaptors);
28+
}
29+
}
30+
31+
public class {{= model.Symbol.Name }}Adapter<{{each parameter in model.Parameters}}TParam{{= parameter.Index }}Adaptor{{delimit}},{{/each}}>
32+
: global::NexusMods.HyperDuck.Adaptor.IRowAdaptor<{{= model.DisplayString }}>
33+
{{each parameter in model.Parameters}}
34+
where TParam{{= parameter.Index }}Adaptor : global::NexusMods.HyperDuck.Adaptor.IValueAdaptor<{{= parameter.DisplayString }}>
35+
{{/each}}
36+
{
37+
public static bool Adapt(global::NexusMods.HyperDuck.Adaptor.RowCursor cursor, ref {{= model.DisplayString }} value)
38+
{
39+
var valueCursor = new global::NexusMods.HyperDuck.Adaptor.ValueCursor(cursor);
40+
41+
{{each parameter in model.Parameters}}
42+
var param{{= parameter.Index }} = value.{{= parameter.Identifier.ValueText }};
43+
var param{{= parameter.Index }}Eq = TParam{{= parameter.Index }}Adaptor.Adapt(valueCursor, ref param{{= parameter.Index }})
44+
if (!param{{= parameter.Index }}Eq) value.{{= parameter.Identifier.ValueText }} = param{{= parameter.Index }};
45+
valueCursor.ColumnIndex++;
46+
{{delimit}}
47+
48+
{{/each}}
49+
50+
return {{each parameter in model.Parameters}} param{{= parameter.Index }}Eq {{delimit}} || {{/each}};
51+
}
52+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System.Text;
2+
using System.Threading;
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Text;
6+
7+
namespace NexusMods.MnemonicDB.SourceGenerator;
8+
9+
[Generator(LanguageNames.CSharp)]
10+
public class SqlResultGenerator : IIncrementalGenerator
11+
{
12+
public void Initialize(IncrementalGeneratorInitializationContext context)
13+
{
14+
var records = context.SyntaxProvider.ForAttributeWithMetadataName(
15+
fullyQualifiedMetadataName: "NexusMods.HyperDuck.QueryResultAttribute",
16+
predicate: (node, _) => FilterSyntaxNode(node),
17+
transform: TransformSyntaxContext
18+
).Where(static x => x.HasValue);
19+
20+
context.RegisterSourceOutput(records, (productionContext, data) => Execute(productionContext, data!.Value));
21+
}
22+
23+
private static void Execute(SourceProductionContext productionContext, ResultData data)
24+
{
25+
var writer = new System.IO.StringWriter();
26+
Templates.Render(data, writer);
27+
28+
productionContext.AddSource(hintName: $"QueryDataResult.{data.Symbol.Name}.g.cs", SourceText.From(writer.ToString(), Encoding.UTF8));
29+
}
30+
31+
private static bool FilterSyntaxNode(SyntaxNode node) => node is RecordDeclarationSyntax;
32+
33+
private static ResultData? TransformSyntaxContext(GeneratorAttributeSyntaxContext syntaxContext, CancellationToken cancellationToken)
34+
{
35+
if (syntaxContext.TargetNode is not RecordDeclarationSyntax declarationSyntax) return null;
36+
if (syntaxContext.SemanticModel.GetDeclaredSymbol(declarationSyntax, cancellationToken) is not INamedTypeSymbol recordSymbol) return null;
37+
38+
var parameterList = declarationSyntax.ParameterList;
39+
if (parameterList is null) return null;
40+
41+
var parameters = new ParameterData[parameterList.Parameters.Count];
42+
for (var i = 0; i < parameterList.Parameters.Count; i++)
43+
{
44+
var parameter = parameterList.Parameters[0];
45+
var identifier = parameter.Identifier;
46+
47+
var type = parameter.Type;
48+
if (type is null) return null;
49+
50+
var symbolInfo = syntaxContext.SemanticModel.GetSymbolInfo(type, cancellationToken);
51+
52+
var symbol = symbolInfo.Symbol;
53+
if (symbol is null) return null;
54+
55+
parameters[i] = new ParameterData(i, identifier, symbol, symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
56+
}
57+
58+
return new ResultData(recordSymbol, parameters, recordSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
59+
}
60+
61+
internal record struct ResultData(INamedTypeSymbol Symbol, ParameterData[] Parameters, string DisplayString);
62+
63+
internal record struct ParameterData(int Index, SyntaxToken Identifier, ISymbol Symbol, string DisplayString);
64+
}

tests/NexusMods.HyperDuck.Tests/NexusMods.HyperDuck.Tests.csproj

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@
77
<Nullable>enable</Nullable>
88
</PropertyGroup>
99

10-
<ItemGroup>
11-
<Content Include="HyperDuck.Test.csproj" />
12-
</ItemGroup>
13-
1410
<ItemGroup>
1511
<PackageReference Include="Microsoft.Extensions.Hosting" />
1612
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />

tests/NexusMods.MnemonicDB.SourceGenerator.Tests/ArrayTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ namespace NexusMods.MnemonicDB.SourceGenerator.Tests;
33
public class ArrayTest
44
{
55
[Test]
6-
public async Task TestModel() => await Helper.TestGenerator();
6+
public async Task TestModel() => await Helper.TestSourceGenerator<ModelGenerator>();
77
}

tests/NexusMods.MnemonicDB.SourceGenerator.Tests/BasicTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ namespace NexusMods.MnemonicDB.SourceGenerator.Tests;
33
public class BasicTest
44
{
55
[Test]
6-
public async Task TestModel() => await Helper.TestGenerator();
6+
public async Task TestModel() => await Helper.TestSourceGenerator<ModelGenerator>();
77
}

tests/NexusMods.MnemonicDB.SourceGenerator.Tests/Helper.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
11
using System.Runtime.CompilerServices;
22
using Microsoft.CodeAnalysis;
33
using Microsoft.CodeAnalysis.CSharp;
4+
using NexusMods.HyperDuck;
45
using NexusMods.MnemonicDB.Abstractions.Models;
56
using NexusMods.Paths;
67

78
namespace NexusMods.MnemonicDB.SourceGenerator.Tests;
89

910
public static class Helper
1011
{
11-
public static async Task TestGenerator(
12-
[CallerFilePath] string sourceFile = "",
13-
params Type[] requiredTypes)
12+
private static async Task TestGenerator(CSharpGeneratorDriver driver, string sourceFile, Type[] requiredTypes)
1413
{
15-
var generator = new ModelGenerator();
16-
var driver = CSharpGeneratorDriver.Create(generator);
17-
1814
var path = FileSystem.Shared.FromUnsanitizedFullPath(sourceFile);
1915
var inputPath = path.ReplaceExtension(new Extension(".input.cs"));
2016
await Assert.That(inputPath.FileExists).IsTrue();
@@ -31,6 +27,7 @@ public static async Task TestGenerator(
3127
.Select(t => t.Assembly.Location)
3228
.Prepend(typeof(object).Assembly.Location) // .NET library
3329
.Prepend(typeof(IModelDefinition).Assembly.Location) // Abstraction library
30+
.Prepend(typeof(DuckDB).Assembly.Location) // HyperDuck library
3431
.Distinct()
3532
.Select(location => MetadataReference.CreateFromFile(location))
3633
.ToArray()
@@ -42,4 +39,20 @@ public static async Task TestGenerator(
4239
// ReSharper disable once ExplicitCallerInfoArgument
4340
await Verify(result, sourceFile: sourceFile).UseFileName(path.GetFileNameWithoutExtension());
4441
}
42+
43+
public static Task TestSourceGenerator<TGenerator>([CallerFilePath] string sourceFile = "", params Type[] requiredTypes)
44+
where TGenerator : ISourceGenerator, new()
45+
{
46+
var generator = new TGenerator();
47+
var driver = CSharpGeneratorDriver.Create(generator);
48+
return TestGenerator(driver, sourceFile, requiredTypes);
49+
}
50+
51+
public static Task TestIncrementalGenerator<TGenerator>([CallerFilePath] string sourceFile = "", params Type[] requiredTypes)
52+
where TGenerator : IIncrementalGenerator, new()
53+
{
54+
var generator = new TGenerator();
55+
var driver = CSharpGeneratorDriver.Create(generator);
56+
return TestGenerator(driver, sourceFile, requiredTypes);
57+
}
4558
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//HintName: QueryDataResult.MyClass.g.cs
2+
#nullable enable
3+
4+
public class MyClassAdapterFactory : global::NexusMods.HyperDuck.Adaptor.IRowAdaptorFactory
5+
{
6+
private static readonly global::System.Type[] ElementTypes = [typeof(string),typeof(string),typeof(string)];
7+
8+
public bool TryExtractElementTypes(global::System.ReadOnlySpan<global::NexusMods.HyperDuck.Result.ColumnInfo> result, global::System.Type resultType, out global::System.Type[] elementTypes, out int priority)
9+
{
10+
if (resultType != typeof(global::NexusMods.MnemonicDB.SourceGenerator.Tests.MyClass))
11+
{
12+
elementTypes = [];
13+
priority = int.MinValue;
14+
return false;
15+
}
16+
17+
elementTypes = ElementTypes;
18+
priority = int.MaxValue;
19+
return true;
20+
}
21+
22+
public global::System.Type CreateType(global::System.Type resultType, global::System.Type[] elementTypes, global::System.Type[] elementAdaptors)
23+
{
24+
return typeof(MyClassAdapter<,,,>).MakeGenericType(elementAdaptors);
25+
}
26+
}
27+
28+
public class MyClassAdapter<TParam0Adaptor,TParam1Adaptor,TParam2Adaptor>
29+
: global::NexusMods.HyperDuck.Adaptor.IRowAdaptor<global::NexusMods.MnemonicDB.SourceGenerator.Tests.MyClass>
30+
where TParam0Adaptor : global::NexusMods.HyperDuck.Adaptor.IValueAdaptor<string>
31+
where TParam1Adaptor : global::NexusMods.HyperDuck.Adaptor.IValueAdaptor<string>
32+
where TParam2Adaptor : global::NexusMods.HyperDuck.Adaptor.IValueAdaptor<string>
33+
{
34+
public static bool Adapt(global::NexusMods.HyperDuck.Adaptor.RowCursor cursor, ref global::NexusMods.MnemonicDB.SourceGenerator.Tests.MyClass value)
35+
{
36+
var valueCursor = new global::NexusMods.HyperDuck.Adaptor.ValueCursor(cursor);
37+
38+
var param0 = value.Foo;
39+
var param0Eq = TParam0Adaptor.Adapt(valueCursor, ref param0)
40+
if (!param0Eq) value.Foo = param0;
41+
valueCursor.ColumnIndex++;
42+
43+
var param1 = value.Foo;
44+
var param1Eq = TParam1Adaptor.Adapt(valueCursor, ref param1)
45+
if (!param1Eq) value.Foo = param1;
46+
valueCursor.ColumnIndex++;
47+
48+
var param2 = value.Foo;
49+
var param2Eq = TParam2Adaptor.Adapt(valueCursor, ref param2)
50+
if (!param2Eq) value.Foo = param2;
51+
valueCursor.ColumnIndex++;
52+
53+
return param0Eq || param1Eq || param2Eq ;
54+
}
55+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//HintName: QueryDataResult.MyStruct.g.cs
2+
#nullable enable
3+
4+
public class MyStructAdapterFactory : global::NexusMods.HyperDuck.Adaptor.IRowAdaptorFactory
5+
{
6+
private static readonly global::System.Type[] ElementTypes = [typeof(string),typeof(string),typeof(string)];
7+
8+
public bool TryExtractElementTypes(global::System.ReadOnlySpan<global::NexusMods.HyperDuck.Result.ColumnInfo> result, global::System.Type resultType, out global::System.Type[] elementTypes, out int priority)
9+
{
10+
if (resultType != typeof(global::NexusMods.MnemonicDB.SourceGenerator.Tests.MyStruct))
11+
{
12+
elementTypes = [];
13+
priority = int.MinValue;
14+
return false;
15+
}
16+
17+
elementTypes = ElementTypes;
18+
priority = int.MaxValue;
19+
return true;
20+
}
21+
22+
public global::System.Type CreateType(global::System.Type resultType, global::System.Type[] elementTypes, global::System.Type[] elementAdaptors)
23+
{
24+
return typeof(MyStructAdapter<,,,>).MakeGenericType(elementAdaptors);
25+
}
26+
}
27+
28+
public class MyStructAdapter<TParam0Adaptor,TParam1Adaptor,TParam2Adaptor>
29+
: global::NexusMods.HyperDuck.Adaptor.IRowAdaptor<global::NexusMods.MnemonicDB.SourceGenerator.Tests.MyStruct>
30+
where TParam0Adaptor : global::NexusMods.HyperDuck.Adaptor.IValueAdaptor<string>
31+
where TParam1Adaptor : global::NexusMods.HyperDuck.Adaptor.IValueAdaptor<string>
32+
where TParam2Adaptor : global::NexusMods.HyperDuck.Adaptor.IValueAdaptor<string>
33+
{
34+
public static bool Adapt(global::NexusMods.HyperDuck.Adaptor.RowCursor cursor, ref global::NexusMods.MnemonicDB.SourceGenerator.Tests.MyStruct value)
35+
{
36+
var valueCursor = new global::NexusMods.HyperDuck.Adaptor.ValueCursor(cursor);
37+
38+
var param0 = value.Foo;
39+
var param0Eq = TParam0Adaptor.Adapt(valueCursor, ref param0)
40+
if (!param0Eq) value.Foo = param0;
41+
valueCursor.ColumnIndex++;
42+
43+
var param1 = value.Foo;
44+
var param1Eq = TParam1Adaptor.Adapt(valueCursor, ref param1)
45+
if (!param1Eq) value.Foo = param1;
46+
valueCursor.ColumnIndex++;
47+
48+
var param2 = value.Foo;
49+
var param2Eq = TParam2Adaptor.Adapt(valueCursor, ref param2)
50+
if (!param2Eq) value.Foo = param2;
51+
valueCursor.ColumnIndex++;
52+
53+
return param0Eq || param1Eq || param2Eq ;
54+
}
55+
}

0 commit comments

Comments
 (0)