Skip to content

Commit 1ff45fb

Browse files
authored
Merge pull request #1575 from bash/async-overloads-source-generator
Async overloads source generator
2 parents 11f6cc6 + fa9b7bc commit 1ff45fb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1537
-1819
lines changed

Ix.NET/Source/Ix.NET.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks.System.Interacti
6262
EndProject
6363
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Linq.Async.Ref", "refs\System.Linq.Async.Ref\System.Linq.Async.Ref.csproj", "{1754B36C-D0DB-4E5D-8C30-1F116046DC0F}"
6464
EndProject
65+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Linq.Async.SourceGenerator", "System.Linq.Async.SourceGenerator\System.Linq.Async.SourceGenerator.csproj", "{5C26D649-5ED4-49EE-AFBD-8FA8F12C4AE4}"
66+
EndProject
6567
Global
6668
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6769
Debug|Any CPU = Debug|Any CPU
@@ -136,6 +138,10 @@ Global
136138
{1754B36C-D0DB-4E5D-8C30-1F116046DC0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
137139
{1754B36C-D0DB-4E5D-8C30-1F116046DC0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
138140
{1754B36C-D0DB-4E5D-8C30-1F116046DC0F}.Release|Any CPU.Build.0 = Release|Any CPU
141+
{5C26D649-5ED4-49EE-AFBD-8FA8F12C4AE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
142+
{5C26D649-5ED4-49EE-AFBD-8FA8F12C4AE4}.Debug|Any CPU.Build.0 = Debug|Any CPU
143+
{5C26D649-5ED4-49EE-AFBD-8FA8F12C4AE4}.Release|Any CPU.ActiveCfg = Release|Any CPU
144+
{5C26D649-5ED4-49EE-AFBD-8FA8F12C4AE4}.Release|Any CPU.Build.0 = Release|Any CPU
139145
EndGlobalSection
140146
GlobalSection(SolutionProperties) = preSolution
141147
HideSolutionNode = FALSE
@@ -158,6 +164,7 @@ Global
158164
{2EC0C302-B029-4DDB-AC91-000BF11006AD} = {A3D72E6E-4ADA-42E0-8B2A-055B1F244281}
159165
{5DF341BE-B369-4250-AFD4-604DE8C95E45} = {A3D72E6E-4ADA-42E0-8B2A-055B1F244281}
160166
{1754B36C-D0DB-4E5D-8C30-1F116046DC0F} = {A3D72E6E-4ADA-42E0-8B2A-055B1F244281}
167+
{5C26D649-5ED4-49EE-AFBD-8FA8F12C4AE4} = {80EFE3A1-1414-42EA-949B-1B5370A1B2EA}
161168
EndGlobalSection
162169
GlobalSection(ExtensibilityGlobals) = postSolution
163170
SolutionGuid = {AF70B0C6-C9D9-43B1-9BE4-08720EC1B7B7}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CSharp.Syntax;
3+
4+
namespace System.Linq.Async.SourceGenerator
5+
{
6+
internal sealed record AsyncMethod(IMethodSymbol Symbol, MethodDeclarationSyntax Syntax);
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using System.Collections.Generic;
2+
3+
using Microsoft.CodeAnalysis;
4+
5+
namespace System.Linq.Async.SourceGenerator
6+
{
7+
internal sealed record AsyncMethodGrouping(SyntaxTree SyntaxTree, IEnumerable<AsyncMethod> Methods);
8+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Text;
4+
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CSharp;
6+
using Microsoft.CodeAnalysis.CSharp.Syntax;
7+
8+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
9+
10+
namespace System.Linq.Async.SourceGenerator
11+
{
12+
[Generator]
13+
public sealed class AsyncOverloadsGenerator : ISourceGenerator
14+
{
15+
private const string AttributeSource =
16+
"using System;\n" +
17+
"using System.Diagnostics;\n" +
18+
"namespace System.Linq\n" +
19+
"{\n" +
20+
" [AttributeUsage(AttributeTargets.Method)]\n" +
21+
" [Conditional(\"COMPILE_TIME_ONLY\")]\n" +
22+
" internal sealed class GenerateAsyncOverloadAttribute : Attribute { }\n" +
23+
"}\n";
24+
25+
public void Initialize(GeneratorInitializationContext context)
26+
{
27+
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
28+
context.RegisterForPostInitialization(c => c.AddSource("GenerateAsyncOverloadAttribute", AttributeSource));
29+
}
30+
31+
public void Execute(GeneratorExecutionContext context)
32+
{
33+
if (context.SyntaxReceiver is not SyntaxReceiver syntaxReceiver) return;
34+
35+
var options = GetGenerationOptions(context);
36+
var methodsBySyntaxTree = GetMethodsGroupedBySyntaxTree(context, syntaxReceiver);
37+
38+
foreach (var grouping in methodsBySyntaxTree)
39+
context.AddSource(
40+
$"{Path.GetFileNameWithoutExtension(grouping.SyntaxTree.FilePath)}.AsyncOverloads",
41+
GenerateOverloads(grouping, options));
42+
}
43+
44+
private static GenerationOptions GetGenerationOptions(GeneratorExecutionContext context)
45+
=> new(SupportFlatAsyncApi: context.ParseOptions.PreprocessorSymbolNames.Contains("SUPPORT_FLAT_ASYNC_API"));
46+
47+
private static IEnumerable<AsyncMethodGrouping> GetMethodsGroupedBySyntaxTree(GeneratorExecutionContext context, SyntaxReceiver syntaxReceiver)
48+
=> GetMethodsGroupedBySyntaxTree(
49+
context,
50+
syntaxReceiver,
51+
GetAsyncOverloadAttributeSymbol(context));
52+
53+
private static string GenerateOverloads(AsyncMethodGrouping grouping, GenerationOptions options)
54+
{
55+
var usings = grouping.SyntaxTree.GetRoot() is CompilationUnitSyntax compilationUnit
56+
? compilationUnit.Usings.ToString()
57+
: string.Empty;
58+
59+
var overloads = new StringBuilder();
60+
overloads.AppendLine("#nullable enable");
61+
overloads.AppendLine(usings);
62+
overloads.AppendLine("namespace System.Linq");
63+
overloads.AppendLine("{");
64+
overloads.AppendLine(" partial class AsyncEnumerable");
65+
overloads.AppendLine(" {");
66+
67+
foreach (var method in grouping.Methods)
68+
overloads.AppendLine(GenerateOverload(method, options));
69+
70+
overloads.AppendLine(" }");
71+
overloads.AppendLine("}");
72+
73+
return overloads.ToString();
74+
}
75+
76+
private static string GenerateOverload(AsyncMethod method, GenerationOptions options)
77+
=> MethodDeclaration(method.Syntax.ReturnType, GetMethodName(method.Symbol, options))
78+
.WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)))
79+
.WithTypeParameterList(method.Syntax.TypeParameterList)
80+
.WithParameterList(method.Syntax.ParameterList)
81+
.WithConstraintClauses(method.Syntax.ConstraintClauses)
82+
.WithExpressionBody(ArrowExpressionClause(
83+
InvocationExpression(
84+
IdentifierName(method.Symbol.Name),
85+
ArgumentList(
86+
SeparatedList(
87+
method.Syntax.ParameterList.Parameters
88+
.Select(p => Argument(IdentifierName(p.Identifier))))))))
89+
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken))
90+
.WithLeadingTrivia(method.Syntax.GetLeadingTrivia().Where(t => t.GetStructure() is not DirectiveTriviaSyntax))
91+
.NormalizeWhitespace()
92+
.ToFullString();
93+
94+
private static INamedTypeSymbol GetAsyncOverloadAttributeSymbol(GeneratorExecutionContext context)
95+
=> context.Compilation.GetTypeByMetadataName("System.Linq.GenerateAsyncOverloadAttribute") ?? throw new InvalidOperationException();
96+
97+
private static IEnumerable<AsyncMethodGrouping> GetMethodsGroupedBySyntaxTree(GeneratorExecutionContext context, SyntaxReceiver syntaxReceiver, INamedTypeSymbol attributeSymbol)
98+
=> from candidate in syntaxReceiver.Candidates
99+
group candidate by candidate.SyntaxTree into grouping
100+
let model = context.Compilation.GetSemanticModel(grouping.Key)
101+
select new AsyncMethodGrouping(
102+
grouping.Key,
103+
from methodSyntax in grouping
104+
let methodSymbol = model.GetDeclaredSymbol(methodSyntax) ?? throw new InvalidOperationException()
105+
where methodSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass!, attributeSymbol))
106+
select new AsyncMethod(methodSymbol, methodSyntax));
107+
108+
private static string GetMethodName(IMethodSymbol methodSymbol, GenerationOptions options)
109+
{
110+
var methodName = methodSymbol.Name.Replace("Core", "");
111+
return options.SupportFlatAsyncApi
112+
? methodName.Replace("Await", "").Replace("WithCancellation", "")
113+
: methodName;
114+
}
115+
}
116+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
namespace System.Linq.Async.SourceGenerator
2+
{
3+
internal sealed record GenerationOptions(bool SupportFlatAsyncApi);
4+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Collections.Generic;
2+
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
6+
namespace System.Linq.Async.SourceGenerator
7+
{
8+
internal sealed class SyntaxReceiver : ISyntaxReceiver
9+
{
10+
public IList<MethodDeclarationSyntax> Candidates { get; } = new List<MethodDeclarationSyntax>();
11+
12+
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
13+
{
14+
if (syntaxNode is MethodDeclarationSyntax { AttributeLists: { Count: >0 } } methodDeclarationSyntax)
15+
{
16+
Candidates.Add(methodDeclarationSyntax);
17+
}
18+
}
19+
}
20+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>netstandard2.0</TargetFramework>
4+
<LangVersion>9.0</LangVersion>
5+
</PropertyGroup>
6+
<ItemGroup>
7+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" PrivateAssets="all" />
8+
<PackageReference Include="IsExternalInit" Version="1.0.0" PrivateAssets="all" />
9+
</ItemGroup>
10+
</Project>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"solution": {
3+
"path": "Ix.NET.sln",
4+
"projects": [
5+
"System.Linq.Async\\System.Linq.Async.csproj",
6+
"System.Linq.Async.Tests\\System.Linq.Async.Tests.csproj",
7+
"System.Linq.Async.SourceGenerator\\System.Linq.Async.SourceGenerator.csproj",
8+
"refs\\System.Linq.Async.Ref\\System.Linq.Async.Ref.csproj"
9+
]
10+
}
11+
}

Ix.NET/Source/System.Linq.Async/System.Linq.Async.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<ItemGroup>
2929
<PackageReference Condition="'$(TargetFramework)' != 'netcoreapp3.1' and '$(TargetFramework)' != 'netstandard2.1' " Include="Microsoft.Bcl.AsyncInterfaces" Version="5.0.0" />
3030
<ReferenceAssemblyProjectReference Include="..\refs\System.Linq.Async.Ref\System.Linq.Async.Ref.csproj" />
31+
<ProjectReference Include="..\System.Linq.Async.SourceGenerator\System.Linq.Async.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" Private="false" />
3132
</ItemGroup>
3233

3334
<ItemGroup>

0 commit comments

Comments
 (0)