Skip to content

Commit 2816185

Browse files
committed
Add source generator to emit public Program class definition (#58199)
* Add source generator to emit public Program class definition * Add more checks and test cases * Use GetEntrypoint API and transformations for better caching * Address feedback
1 parent b931a51 commit 2816185

10 files changed

+273
-0
lines changed

AspNetCore.sln

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1814,6 +1814,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B32FF7A7-9
18141814
EndProject
18151815
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenApi.Tests", "src\OpenApi\test\Microsoft.AspNetCore.OpenApi.Tests\Microsoft.AspNetCore.OpenApi.Tests.csproj", "{B9BBC1A8-7F58-4F43-94C3-5F3CB125CEF7}"
18161816
EndProject
1817+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.App.SourceGenerators", "src\Framework\AspNetCoreAnalyzers\src\SourceGenerators\Microsoft.AspNetCore.App.SourceGenerators.csproj", "{C3928C15-1836-46DB-A09D-9EFBCCA33E08}"
1818+
EndProject
18171819
Global
18181820
GlobalSection(SolutionConfigurationPlatforms) = preSolution
18191821
Debug|Any CPU = Debug|Any CPU
@@ -10959,6 +10961,22 @@ Global
1095910961
{B9BBC1A8-7F58-4F43-94C3-5F3CB125CEF7}.Release|x64.Build.0 = Release|Any CPU
1096010962
{B9BBC1A8-7F58-4F43-94C3-5F3CB125CEF7}.Release|x86.ActiveCfg = Release|Any CPU
1096110963
{B9BBC1A8-7F58-4F43-94C3-5F3CB125CEF7}.Release|x86.Build.0 = Release|Any CPU
10964+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
10965+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Debug|Any CPU.Build.0 = Debug|Any CPU
10966+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Debug|arm64.ActiveCfg = Debug|Any CPU
10967+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Debug|arm64.Build.0 = Debug|Any CPU
10968+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Debug|x64.ActiveCfg = Debug|Any CPU
10969+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Debug|x64.Build.0 = Debug|Any CPU
10970+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Debug|x86.ActiveCfg = Debug|Any CPU
10971+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Debug|x86.Build.0 = Debug|Any CPU
10972+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|Any CPU.ActiveCfg = Release|Any CPU
10973+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|Any CPU.Build.0 = Release|Any CPU
10974+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|arm64.ActiveCfg = Release|Any CPU
10975+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|arm64.Build.0 = Release|Any CPU
10976+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|x64.ActiveCfg = Release|Any CPU
10977+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|x64.Build.0 = Release|Any CPU
10978+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|x86.ActiveCfg = Release|Any CPU
10979+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|x86.Build.0 = Release|Any CPU
1096210980
EndGlobalSection
1096310981
GlobalSection(SolutionProperties) = preSolution
1096410982
HideSolutionNode = FALSE
@@ -11855,6 +11873,7 @@ Global
1185511873
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63} = {5FE1FBC1-8CE3-4355-9866-44FE1307C5F1}
1185611874
{B32FF7A7-9CB3-4DCD-AE97-3B2594DB9DAC} = {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B}
1185711875
{B9BBC1A8-7F58-4F43-94C3-5F3CB125CEF7} = {B32FF7A7-9CB3-4DCD-AE97-3B2594DB9DAC}
11876+
{C3928C15-1836-46DB-A09D-9EFBCCA33E08} = {B5D98AEB-9409-4280-8225-9C1EC6A791B2}
1185811877
EndGlobalSection
1185911878
GlobalSection(ExtensibilityGlobals) = postSolution
1186011879
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

eng/Dependencies.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ and are generated based on the last package release.
6868
<LatestPackageReference Include="Microsoft.Win32.Registry" />
6969
<LatestPackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" />
7070
<LatestPackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" />
71+
<LatestPackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" />
7172
<LatestPackageReference Include="Microsoft.OpenApi" />
7273
<LatestPackageReference Include="Microsoft.OpenApi.Readers" />
7374
<LatestPackageReference Include="System.Buffers" />

eng/Versions.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@
261261
<MicrosoftCodeAnalysisPublicApiAnalyzersVersion>3.3.3</MicrosoftCodeAnalysisPublicApiAnalyzersVersion>
262262
<MicrosoftCodeAnalysisCSharpAnalyzerTestingXUnitVersion>1.1.2-beta1.24121.1</MicrosoftCodeAnalysisCSharpAnalyzerTestingXUnitVersion>
263263
<MicrosoftCodeAnalysisCSharpCodeFixTestingXUnitVersion>1.1.2-beta1.24121.1</MicrosoftCodeAnalysisCSharpCodeFixTestingXUnitVersion>
264+
<MicrosoftCodeAnalysisCSharpSourceGeneratorsTestingVersion>1.1.2-beta1.24121.1</MicrosoftCodeAnalysisCSharpSourceGeneratorsTestingVersion>
264265
<MicrosoftCssParserVersion>1.0.0-20230414.1</MicrosoftCssParserVersion>
265266
<MicrosoftIdentityModelLoggingVersion>$(IdentityModelVersion)</MicrosoftIdentityModelLoggingVersion>
266267
<MicrosoftIdentityModelProtocolsOpenIdConnectVersion>$(IdentityModelVersion)</MicrosoftIdentityModelProtocolsOpenIdConnectVersion>

src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ This package is an internal implementation of the .NET Core SDK and is not meant
7373
Private="false"
7474
ReferenceOutputAssembly="false" />
7575

76+
<ProjectReference Include="..\..\AspNetCoreAnalyzers\src\SourceGenerators\Microsoft.AspNetCore.App.SourceGenerators.csproj"
77+
Private="false"
78+
ReferenceOutputAssembly="false" />
79+
7680
<ProjectReference Include="$(RepoRoot)src\Components\Analyzers\src\Microsoft.AspNetCore.Components.Analyzers.csproj"
7781
Private="false"
7882
ReferenceOutputAssembly="false" />
@@ -175,6 +179,7 @@ This package is an internal implementation of the .NET Core SDK and is not meant
175179

176180
<_InitialRefPackContent Include="$(PkgMicrosoft_Internal_Runtime_AspNetCore_Transport)\$(AnalyzersPackagePath)**\*.*" PackagePath="$(AnalyzersPackagePath)" />
177181
<_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.App.Analyzers\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.App.Analyzers.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" />
182+
<_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.App.SourceGenerators\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.App.SourceGenerators.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" />
178183
<_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.Components.Analyzers\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Components.Analyzers.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" />
179184
<_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.App.CodeFixes\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.App.CodeFixes.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" />
180185
<_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.Http.RequestDelegateGenerator\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Http.RequestDelegateGenerator.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" />
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>netstandard2.0</TargetFramework>
4+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
5+
<IsPackable>false</IsPackable>
6+
<IsAnalyzersProject>true</IsAnalyzersProject>
7+
<AddPublicApiAnalyzers>false</AddPublicApiAnalyzers>
8+
<Nullable>enable</Nullable>
9+
<WarnOnNullable>true</WarnOnNullable>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<Reference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="All" />
14+
<Reference Include="Microsoft.CodeAnalysis.Common" PrivateAssets="All" />
15+
<Reference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="All" />
16+
</ItemGroup>
17+
18+
</Project>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Linq;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp.Syntax;
7+
8+
namespace Microsoft.AspNetCore.SourceGenerators;
9+
10+
[Generator]
11+
public class PublicProgramSourceGenerator : IIncrementalGenerator
12+
{
13+
private const string PublicPartialProgramClassSource = """
14+
// <auto-generated />
15+
public partial class Program { }
16+
""";
17+
18+
public void Initialize(IncrementalGeneratorInitializationContext context)
19+
{
20+
var internalGeneratedProgramClass = context.CompilationProvider
21+
// Get the entry point associated with the compilation, this maps to the Main method definition
22+
.Select(static (compilation, cancellationToken) => compilation.GetEntryPoint(cancellationToken))
23+
// Get the containing symbol of the entry point, this maps to the Program class
24+
.Select(static (symbol, _) => symbol?.ContainingSymbol)
25+
// If the program class is already public, we don't need to generate anything.
26+
.Select(static (symbol, _) => symbol?.DeclaredAccessibility == Accessibility.Public ? null : symbol)
27+
// If the discovered `Program` type is not a class then its not
28+
// generated and has been defined in source, so we can skip it
29+
.Select(static (symbol, _) => symbol is INamedTypeSymbol { TypeKind: TypeKind.Class } ? symbol : null)
30+
// If there are multiple partial declarations, then do nothing since we don't want
31+
// to trample on visibility explicitly set by the user
32+
.Select(static (symbol, _) => symbol is { DeclaringSyntaxReferences: { Length: 1 } declaringSyntaxReferences } ? declaringSyntaxReferences.Single() : null)
33+
// If the `Program` class is already declared in user code, we don't need to generate anything.
34+
.Select(static (declaringSyntaxReference, cancellationToken) => declaringSyntaxReference?.GetSyntax(cancellationToken) is ClassDeclarationSyntax ? null : declaringSyntaxReference);
35+
36+
context.RegisterSourceOutput(internalGeneratedProgramClass, (context, result) =>
37+
{
38+
if (result is not null)
39+
{
40+
context.AddSource("PublicTopLevelProgram.Generated.g.cs", PublicPartialProgramClassSource);
41+
}
42+
});
43+
}
44+
}

src/Framework/AspNetCoreAnalyzers/test/Microsoft.AspNetCore.App.Analyzers.Test.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<ItemGroup>
1616
<!-- Also bring in Microsoft.AspNetCore.App.Analyzers. -->
1717
<ProjectReference Include="..\src\CodeFixes\Microsoft.AspNetCore.App.CodeFixes.csproj" />
18+
<ProjectReference Include="..\src\SourceGenerators\Microsoft.AspNetCore.App.SourceGenerators.csproj" />
1819

1920
<ProjectReference Include="$(RepoRoot)src\Analyzers\Microsoft.AspNetCore.Analyzer.Testing\src\Microsoft.AspNetCore.Analyzer.Testing.csproj" />
2021

@@ -24,6 +25,7 @@
2425
<Reference Include="Microsoft.AspNetCore.RateLimiting" />
2526
<Reference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" />
2627
<Reference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" />
28+
<Reference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" />
2729
</ItemGroup>
2830

2931
<ItemGroup>
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using VerifyCS = Microsoft.AspNetCore.Analyzers.Verifiers.CSharpSourceGeneratorVerifier<Microsoft.AspNetCore.SourceGenerators.PublicProgramSourceGenerator>;
5+
6+
namespace Microsoft.AspNetCore.SourceGenerators.Tests;
7+
8+
public class PublicTopLevelProgramGeneratorTests
9+
{
10+
[Fact]
11+
public async Task GeneratesSource_ProgramWithTopLevelStatements()
12+
{
13+
var source = """
14+
using Microsoft.AspNetCore.Builder;
15+
16+
var app = WebApplication.Create();
17+
18+
app.MapGet("/", () => "Hello, World!");
19+
20+
app.Run();
21+
""";
22+
23+
var expected = """
24+
// <auto-generated />
25+
public partial class Program { }
26+
""";
27+
28+
await VerifyCS.VerifyAsync(source, "PublicTopLevelProgram.Generated.g.cs", expected);
29+
}
30+
31+
[Fact]
32+
public async Task DoesNotGeneratesSource_IfProgramIsAlreadyPublic()
33+
{
34+
var source = """
35+
using Microsoft.AspNetCore.Builder;
36+
37+
var app = WebApplication.Create();
38+
39+
app.MapGet("/", () => "Hello, World!");
40+
41+
app.Run();
42+
43+
public partial class Program { }
44+
""";
45+
46+
await VerifyCS.VerifyAsync(source);
47+
}
48+
49+
[Fact]
50+
public async Task DoesNotGeneratesSource_IfProgramDeclaresExplicitInternalAccess()
51+
{
52+
var source = """
53+
using Microsoft.AspNetCore.Builder;
54+
55+
var app = WebApplication.Create();
56+
57+
app.MapGet("/", () => "Hello, World!");
58+
59+
app.Run();
60+
61+
internal partial class Program { }
62+
""";
63+
64+
await VerifyCS.VerifyAsync(source);
65+
}
66+
67+
[Fact]
68+
public async Task DoesNotGeneratorSource_ExplicitPublicProgramClass()
69+
{
70+
var source = """
71+
using Microsoft.AspNetCore.Builder;
72+
73+
public class Program
74+
{
75+
public static void Main()
76+
{
77+
var app = WebApplication.Create();
78+
79+
app.MapGet("/", () => "Hello, World!");
80+
81+
app.Run();
82+
}
83+
}
84+
""";
85+
86+
await VerifyCS.VerifyAsync(source);
87+
}
88+
89+
[Fact]
90+
public async Task DoesNotGeneratorSource_ExplicitInternalProgramClass()
91+
{
92+
var source = """
93+
using Microsoft.AspNetCore.Builder;
94+
95+
internal class Program
96+
{
97+
public static void Main()
98+
{
99+
var app = WebApplication.Create();
100+
101+
app.MapGet("/", () => "Hello, World!");
102+
103+
app.Run();
104+
}
105+
}
106+
""";
107+
108+
await VerifyCS.VerifyAsync(source);
109+
}
110+
111+
[Theory]
112+
[InlineData("interface")]
113+
[InlineData("struct")]
114+
public async Task DoesNotGeneratorSource_ExplicitInternalProgramType(string type)
115+
{
116+
var source = $$"""
117+
using Microsoft.AspNetCore.Builder;
118+
119+
internal {{type}} Program
120+
{
121+
public static void Main(string[] args)
122+
{
123+
var app = WebApplication.Create();
124+
125+
app.MapGet("/", () => "Hello, World!");
126+
127+
app.Run();
128+
}
129+
}
130+
""";
131+
132+
await VerifyCS.VerifyAsync(source);
133+
}
134+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Text;
5+
using Microsoft.AspNetCore.Analyzers.WebApplicationBuilder;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CSharp.Testing;
8+
using Microsoft.CodeAnalysis.Testing;
9+
using Microsoft.CodeAnalysis.Testing.Verifiers;
10+
using Microsoft.CodeAnalysis.Text;
11+
12+
namespace Microsoft.AspNetCore.Analyzers.Verifiers;
13+
14+
public static partial class CSharpSourceGeneratorVerifier<TSourceGenerator>
15+
where TSourceGenerator : IIncrementalGenerator, new()
16+
{
17+
public static async Task VerifyAsync(string source, string generatedFileName, string generatedSource)
18+
{
19+
var test = new CSharpSourceGeneratorTest<TSourceGenerator, DefaultVerifier>
20+
{
21+
TestState =
22+
{
23+
Sources = { source.ReplaceLineEndings() },
24+
OutputKind = OutputKind.ConsoleApplication,
25+
GeneratedSources =
26+
{
27+
(typeof(TSourceGenerator), generatedFileName, SourceText.From(generatedSource, Encoding.UTF8))
28+
},
29+
ReferenceAssemblies = CSharpAnalyzerVerifier<WebApplicationBuilderAnalyzer>.GetReferenceAssemblies()
30+
},
31+
};
32+
await test.RunAsync(CancellationToken.None);
33+
}
34+
35+
public static async Task VerifyAsync(string source)
36+
{
37+
var test = new CSharpSourceGeneratorTest<TSourceGenerator, DefaultVerifier>
38+
{
39+
TestState =
40+
{
41+
Sources = { source.ReplaceLineEndings() },
42+
OutputKind = OutputKind.ConsoleApplication,
43+
ReferenceAssemblies = CSharpAnalyzerVerifier<WebApplicationBuilderAnalyzer>.GetReferenceAssemblies()
44+
},
45+
};
46+
await test.RunAsync(CancellationToken.None);
47+
}
48+
}

src/Framework/Framework.slnf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"src\\Framework\\AspNetCoreAnalyzers\\samples\\WebAppSample\\WebAppSample.csproj",
2727
"src\\Framework\\AspNetCoreAnalyzers\\src\\Analyzers\\Microsoft.AspNetCore.App.Analyzers.csproj",
2828
"src\\Framework\\AspNetCoreAnalyzers\\src\\CodeFixes\\Microsoft.AspNetCore.App.CodeFixes.csproj",
29+
"src\\Framework\\AspNetCoreAnalyzers\\src\\SourceGenerators\\Microsoft.AspNetCore.App.SourceGenerators.csproj",
2930
"src\\Framework\\AspNetCoreAnalyzers\\test\\Microsoft.AspNetCore.App.Analyzers.Test.csproj",
3031
"src\\Framework\\test\\Microsoft.AspNetCore.App.UnitTests.csproj",
3132
"src\\HealthChecks\\Abstractions\\src\\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj",

0 commit comments

Comments
 (0)