diff --git a/src/Framework/AspNetCoreAnalyzers/src/SourceGenerators/PublicTopLevelProgramGenerator.cs b/src/Framework/AspNetCoreAnalyzers/src/SourceGenerators/PublicTopLevelProgramGenerator.cs index e7048722e8a6..7e3f3eacd95d 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/SourceGenerators/PublicTopLevelProgramGenerator.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/SourceGenerators/PublicTopLevelProgramGenerator.cs @@ -12,7 +12,9 @@ public class PublicProgramSourceGenerator : IIncrementalGenerator { private const string PublicPartialProgramClassSource = """ // +#pragma warning disable CS1591 public partial class Program { } +#pragma warning restore CS1591 """; public void Initialize(IncrementalGeneratorInitializationContext context) @@ -21,17 +23,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Get the entry point associated with the compilation, this maps to the Main method definition // Get the containing symbol of the entry point, this maps to the Program class compilation.GetEntryPoint(cancellationToken)?.ContainingSymbol is INamedTypeSymbol - { - // If the discovered `Program` type is not a class then its not - // generated and has been defined in source, so we can skip it - // If the program class is already public, we don't need to generate anything. - DeclaredAccessibility: not Accessibility.Public, - TypeKind: TypeKind.Class, - // If there are multiple partial declarations, then do nothing since we don't want - // to trample on visibility explicitly set by the user - DeclaringSyntaxReferences: { Length: 1 } declaringSyntaxReferences - } && - // If the `Program` class is already declared in user code, we don't need to generate anything. + { + // If the program class is already public, we don't need to generate anything + DeclaredAccessibility: not Accessibility.Public, + // If the discovered `Program` type is not a class then its not + // generated and has been defined in source, so we can skip it + TypeKind: TypeKind.Class, + // If there are multiple partial declarations, then do nothing since we don't want + // to trample on visibility explicitly set by the user + DeclaringSyntaxReferences: { Length: 1 } declaringSyntaxReferences + } && + // If the `Program` class is already declared in user code, we don't need to generate anything declaringSyntaxReferences.Single().GetSyntax(cancellationToken) is not ClassDeclarationSyntax ); diff --git a/src/Framework/AspNetCoreAnalyzers/test/SourceGenerators/PublicTopLevelProgramGeneratorTests.cs b/src/Framework/AspNetCoreAnalyzers/test/SourceGenerators/PublicTopLevelProgramGeneratorTests.cs index c91a110031a7..173dfa7469f8 100644 --- a/src/Framework/AspNetCoreAnalyzers/test/SourceGenerators/PublicTopLevelProgramGeneratorTests.cs +++ b/src/Framework/AspNetCoreAnalyzers/test/SourceGenerators/PublicTopLevelProgramGeneratorTests.cs @@ -7,6 +7,13 @@ namespace Microsoft.AspNetCore.SourceGenerators.Tests; public class PublicTopLevelProgramGeneratorTests { + private const string ExpectedGeneratedSource = """ +// +#pragma warning disable CS1591 +public partial class Program { } +#pragma warning restore CS1591 +"""; + [Fact] public async Task GeneratesSource_ProgramWithTopLevelStatements() { @@ -20,12 +27,33 @@ public async Task GeneratesSource_ProgramWithTopLevelStatements() app.Run(); """; - var expected = """ -// -public partial class Program { } + await VerifyCS.VerifyAsync(source, "PublicTopLevelProgram.Generated.g.cs", ExpectedGeneratedSource); + } + + // The compiler synthesizes a Program class in the global namespace due to top-level statements + // The Foo.Program class is completely unrelated to the entry point and is just as any regular type + // Hence, we will expect to see the source generated in these scenarios + [Theory] + [InlineData("public partial class Program { }")] + [InlineData("internal partial class Program { }")] + public async Task GeneratesSource_IfProgramDefinedInANamespace (string declaration) + { + var source = $$""" +using Microsoft.AspNetCore.Builder; + +var app = WebApplication.Create(); + +app.MapGet("/", () => "Hello, World!"); + +app.Run(); + +namespace Foo +{ + {{declaration}} +} """; - await VerifyCS.VerifyAsync(source, "PublicTopLevelProgram.Generated.g.cs", expected); + await VerifyCS.VerifyAsync(source, "PublicTopLevelProgram.Generated.g.cs", ExpectedGeneratedSource); } [Fact] @@ -86,6 +114,31 @@ public static void Main() await VerifyCS.VerifyAsync(source); } + [Fact] + public async Task DoesNotGeneratorSource_ExplicitPublicProgramClassInNamespace() + { + var source = """ +using Microsoft.AspNetCore.Builder; + +namespace Foo +{ + 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() { @@ -108,6 +161,31 @@ public static void Main() await VerifyCS.VerifyAsync(source); } + [Fact] + public async Task DoesNotGeneratorSource_ExplicitInternalProgramClassInNamespace() + { + var source = """ +using Microsoft.AspNetCore.Builder; + +namespace Foo +{ + 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")] @@ -127,6 +205,33 @@ public static void Main(string[] args) app.Run(); } } +"""; + + await VerifyCS.VerifyAsync(source); + } + + [Theory] + [InlineData("interface")] + [InlineData("struct")] + public async Task DoesNotGeneratorSource_ExplicitInternalProgramTypeInNamespace(string type) + { + var source = $$""" +using Microsoft.AspNetCore.Builder; + +namespace Foo +{ + internal {{type}} Program + { + public static void Main(string[] args) + { + var app = WebApplication.Create(); + + app.MapGet("/", () => "Hello, World!"); + + app.Run(); + } + } +} """; await VerifyCS.VerifyAsync(source);