diff --git a/AspNetCore.sln b/AspNetCore.sln index 5a54800dae83..57a1bf9f0b3f 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -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 @@ -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 @@ -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} diff --git a/eng/Dependencies.props b/eng/Dependencies.props index af9ef7fa88e3..11229e8c631c 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -68,6 +68,7 @@ and are generated based on the last package release. + diff --git a/eng/Versions.props b/eng/Versions.props index 79edeec22b62..7c5e2dbae14c 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -261,6 +261,7 @@ 3.3.3 1.1.2-beta1.24121.1 1.1.2-beta1.24121.1 + 1.1.2-beta1.24121.1 1.0.0-20230414.1 $(IdentityModelVersion) $(IdentityModelVersion) diff --git a/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj b/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj index 99eb568b96bb..82d88cfd500c 100644 --- a/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj +++ b/src/Framework/App.Ref/src/Microsoft.AspNetCore.App.Ref.csproj @@ -73,6 +73,10 @@ This package is an internal implementation of the .NET Core SDK and is not meant Private="false" ReferenceOutputAssembly="false" /> + + @@ -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/" /> diff --git a/src/Framework/AspNetCoreAnalyzers/src/SourceGenerators/Microsoft.AspNetCore.App.SourceGenerators.csproj b/src/Framework/AspNetCoreAnalyzers/src/SourceGenerators/Microsoft.AspNetCore.App.SourceGenerators.csproj new file mode 100644 index 000000000000..f623f6d11875 --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/src/SourceGenerators/Microsoft.AspNetCore.App.SourceGenerators.csproj @@ -0,0 +1,18 @@ + + + netstandard2.0 + true + false + true + false + enable + true + + + + + + + + + diff --git a/src/Framework/AspNetCoreAnalyzers/src/SourceGenerators/PublicTopLevelProgramGenerator.cs b/src/Framework/AspNetCoreAnalyzers/src/SourceGenerators/PublicTopLevelProgramGenerator.cs new file mode 100644 index 000000000000..4ea333b96252 --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/src/SourceGenerators/PublicTopLevelProgramGenerator.cs @@ -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 = """ +// +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((compilation, cancellationToken) => compilation.GetEntryPoint(cancellationToken)) + // Get the containing symbol of the entry point, this maps to the Program class + .Select((symbol, _) => symbol?.ContainingSymbol) + // If the program class is already public, we don't need to generate anything. + .Select((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((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((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((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); + } + }); + } +} diff --git a/src/Framework/AspNetCoreAnalyzers/test/Microsoft.AspNetCore.App.Analyzers.Test.csproj b/src/Framework/AspNetCoreAnalyzers/test/Microsoft.AspNetCore.App.Analyzers.Test.csproj index 8070e80c0b51..5e75a5d1f451 100644 --- a/src/Framework/AspNetCoreAnalyzers/test/Microsoft.AspNetCore.App.Analyzers.Test.csproj +++ b/src/Framework/AspNetCoreAnalyzers/test/Microsoft.AspNetCore.App.Analyzers.Test.csproj @@ -15,6 +15,7 @@ + @@ -24,6 +25,7 @@ + diff --git a/src/Framework/AspNetCoreAnalyzers/test/SourceGenerators/PublicTopLevelProgramGeneratorTests.cs b/src/Framework/AspNetCoreAnalyzers/test/SourceGenerators/PublicTopLevelProgramGeneratorTests.cs new file mode 100644 index 000000000000..c91a110031a7 --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/test/SourceGenerators/PublicTopLevelProgramGeneratorTests.cs @@ -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; + +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 = """ +// +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); + } +} diff --git a/src/Framework/AspNetCoreAnalyzers/test/Verifiers/CSharpSourceGeneratorVerifier.cs b/src/Framework/AspNetCoreAnalyzers/test/Verifiers/CSharpSourceGeneratorVerifier.cs new file mode 100644 index 000000000000..544adc924754 --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/test/Verifiers/CSharpSourceGeneratorVerifier.cs @@ -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 + where TSourceGenerator : IIncrementalGenerator, new() +{ + public static async Task VerifyAsync(string source, string generatedFileName, string generatedSource) + { + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source.ReplaceLineEndings() }, + OutputKind = OutputKind.ConsoleApplication, + GeneratedSources = + { + (typeof(TSourceGenerator), generatedFileName, SourceText.From(generatedSource, Encoding.UTF8)) + }, + ReferenceAssemblies = CSharpAnalyzerVerifier.GetReferenceAssemblies() + }, + }; + await test.RunAsync(CancellationToken.None); + } + + public static async Task VerifyAsync(string source) + { + var test = new CSharpSourceGeneratorTest + { + TestState = + { + Sources = { source.ReplaceLineEndings() }, + OutputKind = OutputKind.ConsoleApplication, + ReferenceAssemblies = CSharpAnalyzerVerifier.GetReferenceAssemblies() + }, + }; + await test.RunAsync(CancellationToken.None); + } +}