Skip to content

Commit

Permalink
Add source generator to emit public Program class definition (#58199)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
captainsafia authored Oct 9, 2024
1 parent 886f6ae commit 15f0a89
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 0 deletions.
19 changes: 19 additions & 0 deletions AspNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -1814,6 +1814,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B32FF7A7-9
EndProject
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}"
EndProject
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}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -10959,6 +10961,22 @@ Global
{B9BBC1A8-7F58-4F43-94C3-5F3CB125CEF7}.Release|x64.Build.0 = Release|Any CPU
{B9BBC1A8-7F58-4F43-94C3-5F3CB125CEF7}.Release|x86.ActiveCfg = Release|Any CPU
{B9BBC1A8-7F58-4F43-94C3-5F3CB125CEF7}.Release|x86.Build.0 = Release|Any CPU
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Debug|arm64.ActiveCfg = Debug|Any CPU
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Debug|arm64.Build.0 = Debug|Any CPU
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Debug|x64.ActiveCfg = Debug|Any CPU
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Debug|x64.Build.0 = Debug|Any CPU
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Debug|x86.ActiveCfg = Debug|Any CPU
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Debug|x86.Build.0 = Debug|Any CPU
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|Any CPU.Build.0 = Release|Any CPU
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|arm64.ActiveCfg = Release|Any CPU
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|arm64.Build.0 = Release|Any CPU
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|x64.ActiveCfg = Release|Any CPU
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|x64.Build.0 = Release|Any CPU
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|x86.ActiveCfg = Release|Any CPU
{C3928C15-1836-46DB-A09D-9EFBCCA33E08}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -11855,6 +11873,7 @@ Global
{757CBDE0-5D0A-4FD8-99F3-6C20BDDD4E63} = {5FE1FBC1-8CE3-4355-9866-44FE1307C5F1}
{B32FF7A7-9CB3-4DCD-AE97-3B2594DB9DAC} = {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B}
{B9BBC1A8-7F58-4F43-94C3-5F3CB125CEF7} = {B32FF7A7-9CB3-4DCD-AE97-3B2594DB9DAC}
{C3928C15-1836-46DB-A09D-9EFBCCA33E08} = {B5D98AEB-9409-4280-8225-9C1EC6A791B2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}
Expand Down
1 change: 1 addition & 0 deletions eng/Dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ and are generated based on the last package release.
<LatestPackageReference Include="Microsoft.Win32.Registry" />
<LatestPackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" />
<LatestPackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" />
<LatestPackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" />
<LatestPackageReference Include="Microsoft.OpenApi" />
<LatestPackageReference Include="Microsoft.OpenApi.Readers" />
<LatestPackageReference Include="System.Buffers" />
Expand Down
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@
<MicrosoftCodeAnalysisPublicApiAnalyzersVersion>3.3.3</MicrosoftCodeAnalysisPublicApiAnalyzersVersion>
<MicrosoftCodeAnalysisCSharpAnalyzerTestingXUnitVersion>1.1.2-beta1.24121.1</MicrosoftCodeAnalysisCSharpAnalyzerTestingXUnitVersion>
<MicrosoftCodeAnalysisCSharpCodeFixTestingXUnitVersion>1.1.2-beta1.24121.1</MicrosoftCodeAnalysisCSharpCodeFixTestingXUnitVersion>
<MicrosoftCodeAnalysisCSharpSourceGeneratorsTestingVersion>1.1.2-beta1.24121.1</MicrosoftCodeAnalysisCSharpSourceGeneratorsTestingVersion>
<MicrosoftCssParserVersion>1.0.0-20230414.1</MicrosoftCssParserVersion>
<MicrosoftIdentityModelLoggingVersion>$(IdentityModelVersion)</MicrosoftIdentityModelLoggingVersion>
<MicrosoftIdentityModelProtocolsOpenIdConnectVersion>$(IdentityModelVersion)</MicrosoftIdentityModelProtocolsOpenIdConnectVersion>
Expand Down
5 changes: 5 additions & 0 deletions src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ This package is an internal implementation of the .NET Core SDK and is not meant
Private="false"
ReferenceOutputAssembly="false" />

<ProjectReference Include="..\..\AspNetCoreAnalyzers\src\SourceGenerators\Microsoft.AspNetCore.App.SourceGenerators.csproj"
Private="false"
ReferenceOutputAssembly="false" />

<ProjectReference Include="$(RepoRoot)src\Components\Analyzers\src\Microsoft.AspNetCore.Components.Analyzers.csproj"
Private="false"
ReferenceOutputAssembly="false" />
Expand Down Expand Up @@ -175,6 +179,7 @@ This package is an internal implementation of the .NET Core SDK and is not meant

<_InitialRefPackContent Include="$(PkgMicrosoft_Internal_Runtime_AspNetCore_Transport)\$(AnalyzersPackagePath)**\*.*" PackagePath="$(AnalyzersPackagePath)" />
<_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.App.Analyzers\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.App.Analyzers.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" />
<_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.App.SourceGenerators\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.App.SourceGenerators.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" />
<_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.Components.Analyzers\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Components.Analyzers.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" />
<_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.App.CodeFixes\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.App.CodeFixes.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" />
<_InitialRefPackContent Include="$(ArtifactsDir)bin\Microsoft.AspNetCore.Http.RequestDelegateGenerator\$(Configuration)\netstandard2.0\Microsoft.AspNetCore.Http.RequestDelegateGenerator.dll" PackagePath="$(AnalyzersPackagePath)dotnet/cs/" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsPackable>false</IsPackable>
<IsAnalyzersProject>true</IsAnalyzersProject>
<AddPublicApiAnalyzers>false</AddPublicApiAnalyzers>
<Nullable>enable</Nullable>
<WarnOnNullable>true</WarnOnNullable>
</PropertyGroup>

<ItemGroup>
<Reference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="All" />
<Reference Include="Microsoft.CodeAnalysis.Common" PrivateAssets="All" />
<Reference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="All" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Microsoft.AspNetCore.SourceGenerators;

[Generator]
public class PublicProgramSourceGenerator : IIncrementalGenerator
{
private const string PublicPartialProgramClassSource = """
// <auto-generated />
public partial class Program { }
""";

public void Initialize(IncrementalGeneratorInitializationContext context)
{
var internalGeneratedProgramClass = context.CompilationProvider
// Get the entry point associated with the compilation, this maps to the Main method definition
.Select(static (compilation, cancellationToken) => compilation.GetEntryPoint(cancellationToken))
// Get the containing symbol of the entry point, this maps to the Program class
.Select(static (symbol, _) => symbol?.ContainingSymbol)
// If the program class is already public, we don't need to generate anything.
.Select(static (symbol, _) => symbol?.DeclaredAccessibility == Accessibility.Public ? null : symbol)
// If the discovered `Program` type is not a class then its not
// generated and has been defined in source, so we can skip it
.Select(static (symbol, _) => symbol is INamedTypeSymbol { TypeKind: TypeKind.Class } ? symbol : null)
// If there are multiple partial declarations, then do nothing since we don't want
// to trample on visibility explicitly set by the user
.Select(static (symbol, _) => symbol is { DeclaringSyntaxReferences: { Length: 1 } declaringSyntaxReferences } ? declaringSyntaxReferences.Single() : null)
// If the `Program` class is already declared in user code, we don't need to generate anything.
.Select(static (declaringSyntaxReference, cancellationToken) => declaringSyntaxReference?.GetSyntax(cancellationToken) is ClassDeclarationSyntax ? null : declaringSyntaxReference);

context.RegisterSourceOutput(internalGeneratedProgramClass, (context, result) =>
{
if (result is not null)
{
context.AddSource("PublicTopLevelProgram.Generated.g.cs", PublicPartialProgramClassSource);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<ItemGroup>
<!-- Also bring in Microsoft.AspNetCore.App.Analyzers. -->
<ProjectReference Include="..\src\CodeFixes\Microsoft.AspNetCore.App.CodeFixes.csproj" />
<ProjectReference Include="..\src\SourceGenerators\Microsoft.AspNetCore.App.SourceGenerators.csproj" />

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

Expand All @@ -24,6 +25,7 @@
<Reference Include="Microsoft.AspNetCore.RateLimiting" />
<Reference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" />
<Reference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.XUnit" />
<Reference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using VerifyCS = Microsoft.AspNetCore.Analyzers.Verifiers.CSharpSourceGeneratorVerifier<Microsoft.AspNetCore.SourceGenerators.PublicProgramSourceGenerator>;

namespace Microsoft.AspNetCore.SourceGenerators.Tests;

public class PublicTopLevelProgramGeneratorTests
{
[Fact]
public async Task GeneratesSource_ProgramWithTopLevelStatements()
{
var source = """
using Microsoft.AspNetCore.Builder;
var app = WebApplication.Create();
app.MapGet("/", () => "Hello, World!");
app.Run();
""";

var expected = """
// <auto-generated />
public partial class Program { }
""";

await VerifyCS.VerifyAsync(source, "PublicTopLevelProgram.Generated.g.cs", expected);
}

[Fact]
public async Task DoesNotGeneratesSource_IfProgramIsAlreadyPublic()
{
var source = """
using Microsoft.AspNetCore.Builder;
var app = WebApplication.Create();
app.MapGet("/", () => "Hello, World!");
app.Run();
public partial class Program { }
""";

await VerifyCS.VerifyAsync(source);
}

[Fact]
public async Task DoesNotGeneratesSource_IfProgramDeclaresExplicitInternalAccess()
{
var source = """
using Microsoft.AspNetCore.Builder;
var app = WebApplication.Create();
app.MapGet("/", () => "Hello, World!");
app.Run();
internal partial class Program { }
""";

await VerifyCS.VerifyAsync(source);
}

[Fact]
public async Task DoesNotGeneratorSource_ExplicitPublicProgramClass()
{
var source = """
using Microsoft.AspNetCore.Builder;
public class Program
{
public static void Main()
{
var app = WebApplication.Create();
app.MapGet("/", () => "Hello, World!");
app.Run();
}
}
""";

await VerifyCS.VerifyAsync(source);
}

[Fact]
public async Task DoesNotGeneratorSource_ExplicitInternalProgramClass()
{
var source = """
using Microsoft.AspNetCore.Builder;
internal class Program
{
public static void Main()
{
var app = WebApplication.Create();
app.MapGet("/", () => "Hello, World!");
app.Run();
}
}
""";

await VerifyCS.VerifyAsync(source);
}

[Theory]
[InlineData("interface")]
[InlineData("struct")]
public async Task DoesNotGeneratorSource_ExplicitInternalProgramType(string type)
{
var source = $$"""
using Microsoft.AspNetCore.Builder;
internal {{type}} Program
{
public static void Main(string[] args)
{
var app = WebApplication.Create();
app.MapGet("/", () => "Hello, World!");
app.Run();
}
}
""";

await VerifyCS.VerifyAsync(source);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;
using Microsoft.AspNetCore.Analyzers.WebApplicationBuilder;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Testing.Verifiers;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.AspNetCore.Analyzers.Verifiers;

public static partial class CSharpSourceGeneratorVerifier<TSourceGenerator>
where TSourceGenerator : IIncrementalGenerator, new()
{
public static async Task VerifyAsync(string source, string generatedFileName, string generatedSource)
{
var test = new CSharpSourceGeneratorTest<TSourceGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source.ReplaceLineEndings() },
OutputKind = OutputKind.ConsoleApplication,
GeneratedSources =
{
(typeof(TSourceGenerator), generatedFileName, SourceText.From(generatedSource, Encoding.UTF8))
},
ReferenceAssemblies = CSharpAnalyzerVerifier<WebApplicationBuilderAnalyzer>.GetReferenceAssemblies()
},
};
await test.RunAsync(CancellationToken.None);
}

public static async Task VerifyAsync(string source)
{
var test = new CSharpSourceGeneratorTest<TSourceGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source.ReplaceLineEndings() },
OutputKind = OutputKind.ConsoleApplication,
ReferenceAssemblies = CSharpAnalyzerVerifier<WebApplicationBuilderAnalyzer>.GetReferenceAssemblies()
},
};
await test.RunAsync(CancellationToken.None);
}
}
1 change: 1 addition & 0 deletions src/Framework/Framework.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"src\\Framework\\AspNetCoreAnalyzers\\samples\\WebAppSample\\WebAppSample.csproj",
"src\\Framework\\AspNetCoreAnalyzers\\src\\Analyzers\\Microsoft.AspNetCore.App.Analyzers.csproj",
"src\\Framework\\AspNetCoreAnalyzers\\src\\CodeFixes\\Microsoft.AspNetCore.App.CodeFixes.csproj",
"src\\Framework\\AspNetCoreAnalyzers\\src\\SourceGenerators\\Microsoft.AspNetCore.App.SourceGenerators.csproj",
"src\\Framework\\AspNetCoreAnalyzers\\test\\Microsoft.AspNetCore.App.Analyzers.Test.csproj",
"src\\Framework\\test\\Microsoft.AspNetCore.App.UnitTests.csproj",
"src\\HealthChecks\\Abstractions\\src\\Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.csproj",
Expand Down

0 comments on commit 15f0a89

Please sign in to comment.