Skip to content

Commit 7163cb0

Browse files
committed
Add source generator to emit public Program class definition
1 parent 3b2df64 commit 7163cb0

File tree

9 files changed

+235
-0
lines changed

9 files changed

+235
-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: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
public void Initialize(IncrementalGeneratorInitializationContext context)
14+
{
15+
var internalGeneratedProgramClass = context.CompilationProvider.Select((compilation, cancellationToken) =>
16+
{
17+
var program = compilation.GetTypeByMetadataName("Program");
18+
// If the program class is already public, we don't need to generate anything.
19+
if (program is null || program.DeclaredAccessibility == Accessibility.Public)
20+
{
21+
return null;
22+
}
23+
if (program.DeclaringSyntaxReferences.Length > 1)
24+
{
25+
return null;
26+
}
27+
// If the `Program` class is already declared in user code, we don't need to generate anything.
28+
if (program.DeclaringSyntaxReferences.SingleOrDefault()?.GetSyntax(cancellationToken) is ClassDeclarationSyntax)
29+
{
30+
return null;
31+
}
32+
return program;
33+
});
34+
35+
context.RegisterSourceOutput(internalGeneratedProgramClass, (context, symbol) =>
36+
{
37+
if (symbol is null)
38+
{
39+
return;
40+
}
41+
42+
var output = """
43+
// <auto-generated />
44+
public partial class Program { }
45+
""";
46+
context.AddSource("PublicTopLevelProgram.Generated.cs", output);
47+
});
48+
}
49+
}

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: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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.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 DoesNotGeneratorSource_ExplicitPublicProgramClass()
51+
{
52+
var source = """
53+
using Microsoft.AspNetCore.Builder;
54+
55+
public class Program
56+
{
57+
public static void Main()
58+
{
59+
var app = WebApplication.Create();
60+
61+
app.MapGet("/", () => "Hello, World!");
62+
63+
app.Run();
64+
}
65+
}
66+
""";
67+
68+
await VerifyCS.VerifyAsync(source);
69+
}
70+
71+
[Fact]
72+
public async Task DoesNotGeneratorSource_ExplicitInternalProgramClass()
73+
{
74+
var source = """
75+
using Microsoft.AspNetCore.Builder;
76+
77+
internal class Program
78+
{
79+
public static void Main()
80+
{
81+
var app = WebApplication.Create();
82+
83+
app.MapGet("/", () => "Hello, World!");
84+
85+
app.Run();
86+
}
87+
}
88+
""";
89+
90+
await VerifyCS.VerifyAsync(source);
91+
}
92+
}
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+
}

0 commit comments

Comments
 (0)