Skip to content

Commit 430e623

Browse files
committed
Handle generator dependencies without ILRepack
1 parent da367fb commit 430e623

File tree

7 files changed

+141
-29
lines changed

7 files changed

+141
-29
lines changed

NetFabric.Hyperlinq.SourceGenerator.UnitTests/GenerateSourceTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ public static TheoryData<string[], string> GeneratorSources
8383
// "TestData/Results/Count.Span.cs"
8484
//},
8585
{
86-
new[] { "TestData/Source/Count.TestEnumerableWithValueTypeEnumerator.cs" },
87-
"TestData/Results/Count.TestEnumerableWithValueTypeEnumerator.cs"
86+
new[] { "TestData/Source/AsEnumerable.TestEnumerableWithValueTypeEnumerator.cs" },
87+
"TestData/Results/AsEnumerable.TestEnumerableWithValueTypeEnumerator.cs"
8888
},
8989
};
9090

NetFabric.Hyperlinq.SourceGenerator.UnitTests/NetFabric.Hyperlinq.SourceGenerator.UnitTests.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929

3030
<ItemGroup>
3131
<ProjectReference Include="..\NetFabric.Hyperlinq.SourceGenerator\NetFabric.Hyperlinq.SourceGenerator.csproj" />
32-
<ProjectReference Include="..\NetFabric.Hyperlinq\NetFabric.Hyperlinq.csproj" />
3332
</ItemGroup>
3433

3534
<ItemGroup>

NetFabric.Hyperlinq.SourceGenerator.UnitTests/TestData/Results/Count.Array.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ namespace NetFabric.Hyperlinq
99
{
1010
static partial class GeneratedExtensionMethods
1111
{
12+
public static int Count(this ArrayExtensions.ArraySegmentValueEnumerable<int> source)
13+
=> source.Count();
1214
}
1315
}

NetFabric.Hyperlinq.SourceGenerator/Generator.AsValueEnumerable.cs

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ namespace NetFabric.Hyperlinq.SourceGenerator
1111
{
1212
public partial class Generator
1313
{
14-
static ValueEnumerableType? GenerateAsValueEnumerable(Compilation compilation, SemanticModel semanticModel, TypeSymbolsCache typeSymbolsCache, MemberAccessExpressionSyntax expressionSyntax, CodeBuilder builder, HashSet<MethodSignature> generatedMethods, CancellationToken cancellationToken, bool isUnitTest)
14+
static ValueEnumerableType? GenerateAsValueEnumerable(Compilation compilation, SemanticModel semanticModel, TypeSymbolsCache typeSymbolsCache, MemberAccessExpressionSyntax expressionSyntax, CodeBuilder builder, Dictionary<MethodSignature, ValueEnumerableType> generatedMethods, CancellationToken cancellationToken, bool isUnitTest)
1515
{
1616
// Check if the method is already defined in the project source
17-
if (semanticModel.GetSymbolInfo(expressionSyntax, cancellationToken).Symbol is IMethodSymbol methodSymbol and not null)
18-
return new ValueEnumerableType(Name: methodSymbol.ReturnType.Name);
17+
if (semanticModel.GetSymbolInfo(expressionSyntax, cancellationToken).Symbol is IMethodSymbol { } methodSymbol)
18+
return new ValueEnumerableType(
19+
Name: methodSymbol.ReturnType.Name,
20+
IsCollection: methodSymbol.ReturnType.ImplementsInterface(typeSymbolsCache["NetFabric.Hyperlinq.IValueReadOnlyCollection`2"]!, out var _),
21+
IsList: methodSymbol.ReturnType.ImplementsInterface(typeSymbolsCache["NetFabric.Hyperlinq.IValueReadOnlyList`2"]!, out var _));
1922

2023
// Get the type this operator is applied to
2124
var receiverTypeSymbol = semanticModel.GetTypeInfo(expressionSyntax.Expression, cancellationToken).Type;
@@ -32,28 +35,41 @@ public partial class Generator
3235
|| SymbolEqualityComparer.Default.Equals(receiverTypeSymbol.OriginalDefinition, typeSymbolsCache[typeof(List<>)])
3336
|| SymbolEqualityComparer.Default.Equals(receiverTypeSymbol.OriginalDefinition, typeSymbolsCache[typeof(ImmutableArray<>)])
3437
)
35-
return null; // no need to generate an implementation
36-
37-
var receiverTypeString = receiverTypeSymbol.ToDisplayString();
38+
return null; // Do generate an implementation. The 'using NetFabric.Hyperlinq;' statement should be added instead.
3839

3940
// Receiver type implements IValueEnumerable<,>
4041

41-
if (IsValueEnumerable(receiverTypeSymbol, typeSymbolsCache))
42+
var valueEnumerableType = AsValueEnumerable(expressionSyntax, compilation, semanticModel, typeSymbolsCache, expressionSyntax, builder, generatedMethods, cancellationToken, isUnitTest);
43+
if (valueEnumerableType is not null)
4244
{
45+
// Check if the method is already defined by this generator
46+
var methodSignature = new MethodSignature("AsValueEnumerable", valueEnumerableType.Name);
47+
if (generatedMethods.TryGetValue(methodSignature, out var returnType))
48+
return returnType;
49+
4350
// Receiver instance returns itself
4451
_ = builder
4552
.AppendLine()
4653
.AppendLine("[MethodImpl(MethodImplOptions.AggressiveInlining)]")
47-
.AppendLine($"public static {receiverTypeString} AsValueEnumerable(this {receiverTypeString} source)")
54+
.AppendLine($"public static {valueEnumerableType.Name} AsValueEnumerable(this {valueEnumerableType.Name} source)")
4855
.AppendIdentation().AppendLine($"=> source;");
4956

50-
return new ValueEnumerableType(Name: receiverTypeString);
57+
// A new AsValueEnumerable() method has been generated
58+
generatedMethods.Add(methodSignature, valueEnumerableType);
59+
return valueEnumerableType;
5160
}
5261

5362
// Receiver type is an enumerable
5463

5564
if (receiverTypeSymbol.IsEnumerable(compilation, out var enumerableSymbols))
5665
{
66+
var receiverTypeString = receiverTypeSymbol.ToDisplayString();
67+
68+
// Check if the method is already defined by this generator
69+
var methodSignature = new MethodSignature("AsValueEnumerable", receiverTypeString);
70+
if (generatedMethods.TryGetValue(methodSignature, out var returnType))
71+
return returnType;
72+
5773
// Use an unique identifier to avoid name clashing
5874
var uniqueIdString = isUnitTest
5975
? receiverTypeString.Replace('.', '_').Replace(',', '_').Replace('<', '_').Replace('>', '_').Replace('`', '_')
@@ -371,9 +387,13 @@ public partial class Generator
371387
}
372388
}
373389

374-
// A new AsValueEnumerable method has been generated
375-
_ = generatedMethods.Add(new MethodSignature("AsValueEnumerable", receiverTypeString));
376-
return new ValueEnumerableType(Name: valueEnumerableTypeName);
390+
// A new AsValueEnumerable() method has been generated
391+
valueEnumerableType = new ValueEnumerableType(
392+
Name: valueEnumerableTypeName,
393+
IsCollection: enumerableImplementsICollection || enumerableImplementsIReadOnlyCollection,
394+
IsList: enumerableImplementsIList || enumerableImplementsIReadOnlyList);
395+
generatedMethods.Add(methodSignature, valueEnumerableType);
396+
return valueEnumerableType;
377397
}
378398
}
379399

NetFabric.Hyperlinq.SourceGenerator/Generator.cs

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ public void Execute(GeneratorExecutionContext context)
127127

128128
internal static void GenerateSource(Compilation compilation, TypeSymbolsCache typeSymbolsCache, List<MemberAccessExpressionSyntax> memberAccessExpressions, CodeBuilder builder, CancellationToken cancellationToken, bool isUnitTest = false)
129129
{
130-
var generatedMethods = new HashSet<MethodSignature>();
130+
var generatedMethods = new Dictionary<MethodSignature, ValueEnumerableType>();
131131

132132
_ = builder
133133
.AppendLine("#nullable enable")
@@ -143,29 +143,95 @@ internal static void GenerateSource(Compilation compilation, TypeSymbolsCache ty
143143
{
144144
foreach (var expressionSyntax in memberAccessExpressions)
145145
{
146+
cancellationToken.ThrowIfCancellationRequested();
147+
146148
var semanticModel = compilation.GetSemanticModel(expressionSyntax.SyntaxTree);
147149

148150
_ = GenerateSource(compilation, semanticModel, typeSymbolsCache, expressionSyntax, builder, generatedMethods, cancellationToken, isUnitTest);
149151
}
150152
}
151153
}
152154

153-
static ValueEnumerableType? GenerateSource(Compilation compilation, SemanticModel semanticModel, TypeSymbolsCache typeSymbolsCache, MemberAccessExpressionSyntax expressionSyntax, CodeBuilder builder, HashSet<MethodSignature> generatedMethods, CancellationToken cancellationToken, bool isUnitTest)
154-
=> expressionSyntax.Name.ToString() switch
155+
static ValueEnumerableType? AsValueEnumerable(MemberAccessExpressionSyntax memberAccessExpressionSyntax, Compilation compilation, SemanticModel semanticModel, TypeSymbolsCache typeSymbolsCache, MemberAccessExpressionSyntax expressionSyntax, CodeBuilder builder, Dictionary<MethodSignature, ValueEnumerableType> generatedMethods, CancellationToken cancellationToken, bool isUnitTest)
156+
{
157+
var typeSymbol = semanticModel.GetTypeInfo(memberAccessExpressionSyntax.Expression, cancellationToken).Type;
158+
if (typeSymbol is null)
159+
return null;
160+
161+
// Check if the receiver type implements IValueEnumerable<,>
162+
if (typeSymbol.ImplementsInterface(typeSymbolsCache["NetFabric.Hyperlinq.IValueEnumerable`2"]!, out var _))
163+
return new ValueEnumerableType(
164+
Name: typeSymbol.ToDisplayString(),
165+
IsCollection: typeSymbol.ImplementsInterface(typeSymbolsCache["NetFabric.Hyperlinq.IValueReadOnlyCollection`2"]!, out var _),
166+
IsList: typeSymbol.ImplementsInterface(typeSymbolsCache["NetFabric.Hyperlinq.IValueReadOnlyList`2"]!, out var _));
167+
168+
// Go up one layer. Generate method is required.
169+
if (expressionSyntax.Expression is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax receiverSyntax })
155170
{
156-
"AsValueEnumerable" => GenerateAsValueEnumerable(compilation, semanticModel, typeSymbolsCache, expressionSyntax, builder, generatedMethods, cancellationToken, isUnitTest),
157-
_ => GenerateOperationSource(compilation, semanticModel, expressionSyntax, builder, generatedMethods, cancellationToken, isUnitTest),
158-
};
171+
if (GenerateSource(compilation, semanticModel, typeSymbolsCache, receiverSyntax, builder, generatedMethods, cancellationToken, isUnitTest) is { } valueEnumerableType)
172+
return valueEnumerableType; // Receiver type implements IValueEnumerable<,>
173+
}
174+
175+
// Receiver type does not implement IValueEnumerable<,> so nothing else needs to be done
176+
return null;
177+
}
159178

160-
static ValueEnumerableType? GenerateOperationSource(Compilation compilation, SemanticModel semanticModel, MemberAccessExpressionSyntax expressionSyntax, CodeBuilder builder, HashSet<MethodSignature> generatedMethods, CancellationToken cancellationToken, bool isUnitTest)
179+
static ValueEnumerableType? GenerateSource(Compilation compilation, SemanticModel semanticModel, TypeSymbolsCache typeSymbolsCache, MemberAccessExpressionSyntax expressionSyntax, CodeBuilder builder, Dictionary<MethodSignature, ValueEnumerableType> generatedMethods, CancellationToken cancellationToken, bool isUnitTest)
180+
=> expressionSyntax.Name.ToString() switch
181+
{
182+
"AsValueEnumerable" => GenerateAsValueEnumerable(compilation, semanticModel, typeSymbolsCache, expressionSyntax, builder, generatedMethods, cancellationToken, isUnitTest),
183+
_ => GenerateOperationSource(compilation, semanticModel, typeSymbolsCache, expressionSyntax, builder, generatedMethods, cancellationToken, isUnitTest),
184+
};
185+
186+
static ValueEnumerableType? GenerateOperationSource(Compilation compilation, SemanticModel semanticModel, TypeSymbolsCache typeSymbolsCache, MemberAccessExpressionSyntax expressionSyntax, CodeBuilder builder, Dictionary<MethodSignature, ValueEnumerableType> generatedMethods, CancellationToken cancellationToken, bool isUnitTest)
161187
{
162-
// Get the type this operator is applied to
163-
var receiverTypeSymbol = semanticModel.GetTypeInfo(expressionSyntax.Expression, cancellationToken).Type;
188+
var valueEnumerableType = AsValueEnumerable(expressionSyntax, compilation, semanticModel, typeSymbolsCache, expressionSyntax, builder, generatedMethods, cancellationToken, isUnitTest);
189+
if (valueEnumerableType is null)
190+
return null;
191+
192+
var symbol = semanticModel.GetSymbolInfo(expressionSyntax, cancellationToken).Symbol;
193+
if (symbol is IMethodSymbol methodSymbol)
194+
{
195+
// Check if the generator already generated this method
196+
var parameters = new string[] { valueEnumerableType.Name }.Concat(methodSymbol.Parameters.Select(parameter => parameter.Type.ToDisplayString())).ToArray();
197+
var methodSignature = new MethodSignature(expressionSyntax.Name.ToString(), parameters);
198+
if (generatedMethods.TryGetValue(methodSignature, out var returnType))
199+
return returnType;
200+
201+
// Generate the extension method
202+
_ = builder
203+
.AppendLine()
204+
.AppendLine("[MethodImpl(MethodImplOptions.AggressiveInlining)]")
205+
.AppendLine($"public static {methodSymbol.ReturnType.ToDisplayString()} {methodSymbol.Name}(this {valueEnumerableType.Name} source)")
206+
.AppendIdentation().AppendLine($"=> source.{methodSymbol.Name}();");
207+
208+
generatedMethods.Add(methodSignature, returnType);
209+
return returnType;
210+
}
211+
212+
213+
// TODO: when 'using System.Linq;' is not used...
214+
if (expressionSyntax.Parent is InvocationExpressionSyntax invocation)
215+
{
216+
217+
var type = semanticModel.GetTypeInfo(invocation.ArgumentList.Arguments[0], cancellationToken).Type;
218+
var symbol2 = semanticModel.GetSymbolInfo(invocation, cancellationToken).Symbol;
219+
220+
221+
222+
223+
// Check if the source already provides the implementation as an instance method
224+
225+
// Check if the source already provides the implementation as an extension method
226+
227+
// Generate the extension method
228+
229+
_ = builder
230+
.AppendLine()
231+
.AppendLine("// TODO");
232+
}
164233

165234
return null;
166235
}
167-
168-
static bool IsValueEnumerable(ITypeSymbol symbol, TypeSymbolsCache typeSymbolsCache)
169-
=> symbol.ImplementsInterface(typeSymbolsCache["NetFabric.Hyperlinq.IValueEnumerable`2"]!, out var _);
170236
}
171237
}

NetFabric.Hyperlinq.SourceGenerator/NetFabric.Hyperlinq.SourceGenerator.csproj

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,23 @@
22

33
<PropertyGroup>
44
<TargetFramework>netstandard2.0</TargetFramework>
5+
<PackageId>NetFabric.Hyperlinq.SourceGenerator</PackageId>
6+
<Title>NetFabric.Hyperlinq.SourceGenerator</Title>
7+
<Description> High performance LINQ implementation with minimal heap allocations. Supports enumerables, async enumerables, Memory, and Span.</Description>
8+
<Version>3.0.0-beta45</Version>
9+
<PackageIcon>Icon.png</PackageIcon>
10+
<PackageLicenseFile>LICENSE</PackageLicenseFile>
11+
<PackageTags>netfabric, hyperlinq, linq, enumeration, extensions, performance</PackageTags>
12+
<PublishRepositoryUrl>true</PublishRepositoryUrl>
513
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <!-- Generates a package at build -->
614
<IncludeBuildOutput>false</IncludeBuildOutput> <!-- Do not include the generator as a lib dependency -->
715
</PropertyGroup>
8-
16+
17+
<ItemGroup>
18+
<!-- Take a public dependency on NetFabric.Hyperlinq. Consumers of this generator will get a reference to this package -->
19+
<ProjectReference Include="..\NetFabric.Hyperlinq\NetFabric.Hyperlinq.csproj" />
20+
</ItemGroup>
21+
922
<ItemGroup>
1023
<!-- Take a private dependency on Ben.TypeDictionary (PrivateAssets=all) Consumers of this generator will not reference it.
1124
Set GeneratePathProperty=true so we can reference the binaries via the PKGBen_TypeDictionary property -->
@@ -17,6 +30,14 @@
1730

1831
<!-- Package the generator in the analyzer directory of the nuget package -->
1932
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
33+
<None Include="..\Icon.png" Link="Icon.png">
34+
<PackagePath></PackagePath>
35+
<Pack>true</Pack>
36+
</None>
37+
<None Include="..\LICENSE" Link="LICENSE">
38+
<PackagePath></PackagePath>
39+
<Pack>true</Pack>
40+
</None>
2041

2142
<!-- Package the Ben.TypeDictionary dependency alongside the generator assembly -->
2243
<None Include="$(PKGBen_TypeDictionary)\lib\netstandard2.0\*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
@@ -29,6 +50,10 @@
2950
<PrivateAssets>all</PrivateAssets>
3051
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
3152
</PackageReference>
53+
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0">
54+
<PrivateAssets>all</PrivateAssets>
55+
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
56+
</PackageReference>
3257
<PackageReference Include="IsExternalInit" Version="1.0.1">
3358
<PrivateAssets>all</PrivateAssets>
3459
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

NetFabric.Hyperlinq.SourceGenerator/ValueEnumerableType.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
namespace NetFabric.Hyperlinq.SourceGenerator
44
{
5-
record ValueEnumerableType(string Name);
5+
record ValueEnumerableType(string Name, bool IsCollection, bool IsList);
66
}

0 commit comments

Comments
 (0)