From 30aacef5d0baad256b7f6d956e529e222127169c Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Fri, 14 Feb 2025 15:03:18 -0800 Subject: [PATCH 1/4] Move to testing library 1.1.2 --- Directory.Packages.props | 4 +--- .../Razor.Diagnostics.Analyzers.Test.csproj | 3 +-- .../Verifiers/CSharpAnalyzerVerifier`1+Test.cs | 4 ++-- .../Verifiers/CSharpAnalyzerVerifier`1.cs | 7 +++---- .../Microsoft.AspNetCore.Razor.Test.Common.Tooling.csproj | 2 +- 5 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0767cdeb9c1..3bdc4add56a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -32,13 +32,12 @@ - + - @@ -50,7 +49,6 @@ - diff --git a/src/Analyzers/Razor.Diagnostics.Analyzers.Test/Razor.Diagnostics.Analyzers.Test.csproj b/src/Analyzers/Razor.Diagnostics.Analyzers.Test/Razor.Diagnostics.Analyzers.Test.csproj index d8977ed8c0f..ab94ba1cf89 100644 --- a/src/Analyzers/Razor.Diagnostics.Analyzers.Test/Razor.Diagnostics.Analyzers.Test.csproj +++ b/src/Analyzers/Razor.Diagnostics.Analyzers.Test/Razor.Diagnostics.Analyzers.Test.csproj @@ -8,8 +8,7 @@ - - + diff --git a/src/Analyzers/Razor.Diagnostics.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier`1+Test.cs b/src/Analyzers/Razor.Diagnostics.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier`1+Test.cs index d59cd6bfe2e..f01ea1f7665 100644 --- a/src/Analyzers/Razor.Diagnostics.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier`1+Test.cs +++ b/src/Analyzers/Razor.Diagnostics.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier`1+Test.cs @@ -3,14 +3,14 @@ using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Testing.Verifiers; +using Microsoft.CodeAnalysis.Testing; namespace Razor.Diagnostics.Analyzers.Test; public static partial class CSharpAnalyzerVerifier where TAnalyzer : DiagnosticAnalyzer, new() { - public class Test : CSharpAnalyzerTest + public class Test : CSharpAnalyzerTest { public Test() { diff --git a/src/Analyzers/Razor.Diagnostics.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier`1.cs b/src/Analyzers/Razor.Diagnostics.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier`1.cs index a24421ec5b5..56194eec436 100644 --- a/src/Analyzers/Razor.Diagnostics.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier`1.cs +++ b/src/Analyzers/Razor.Diagnostics.Analyzers.Test/Verifiers/CSharpAnalyzerVerifier`1.cs @@ -7,7 +7,6 @@ using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; -using Microsoft.CodeAnalysis.Testing.Verifiers; namespace Razor.Diagnostics.Analyzers.Test; @@ -16,15 +15,15 @@ public static partial class CSharpAnalyzerVerifier { /// public static DiagnosticResult Diagnostic() - => CSharpAnalyzerVerifier.Diagnostic(); + => CSharpAnalyzerVerifier.Diagnostic(); /// public static DiagnosticResult Diagnostic(string diagnosticId) - => CSharpAnalyzerVerifier.Diagnostic(diagnosticId); + => CSharpAnalyzerVerifier.Diagnostic(diagnosticId); /// public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor) - => CSharpAnalyzerVerifier.Diagnostic(descriptor); + => CSharpAnalyzerVerifier.Diagnostic(descriptor); /// public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Microsoft.AspNetCore.Razor.Test.Common.Tooling.csproj b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Microsoft.AspNetCore.Razor.Test.Common.Tooling.csproj index ec3d2ab0f63..92c0373c802 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Microsoft.AspNetCore.Razor.Test.Common.Tooling.csproj +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Microsoft.AspNetCore.Razor.Test.Common.Tooling.csproj @@ -44,7 +44,7 @@ - + From 20ef8d5b89b78f89bf59ad351f52feffc3c22032 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Fri, 14 Feb 2025 15:04:16 -0800 Subject: [PATCH 2/4] Add an analyzer to detect when a duplicate razor file is provided This can cause other source generator errors, so we should make this easier to diagnose by including an analyzer that explicitly tells the user that this is an error scenario. --- .../DuplicateRazorFileIncludedAnalyzer.cs | 51 ++++++++ .../Diagnostics/DiagnosticIds.cs | 1 + .../Diagnostics/RazorDiagnostics.cs | 9 ++ .../RazorSourceGeneratorResources.resx | 62 ++++----- .../DuplicateRazorFileIncludedAnalyzerTest.cs | 118 ++++++++++++++++++ ...NET.Sdk.Razor.SourceGenerators.Test.csproj | 1 + 6 files changed, 214 insertions(+), 28 deletions(-) create mode 100644 src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Analyzers/DuplicateRazorFileIncludedAnalyzer.cs create mode 100644 src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/DuplicateRazorFileIncludedAnalyzerTest.cs diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Analyzers/DuplicateRazorFileIncludedAnalyzer.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Analyzers/DuplicateRazorFileIncludedAnalyzer.cs new file mode 100644 index 00000000000..2568502dde1 --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Analyzers/DuplicateRazorFileIncludedAnalyzer.cs @@ -0,0 +1,51 @@ + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using Microsoft.NET.Sdk.Razor.SourceGenerators; + +namespace Microsoft.AspNetCore.Razor.SourceGenerators; + +#pragma warning disable RS1041 // Compiler extensions should be implemented in assemblies targeting netstandard2.0 +[DiagnosticAnalyzer(LanguageNames.CSharp)] +#pragma warning restore RS1041 // Compiler extensions should be implemented in assemblies targeting netstandard2.0 +public class DuplicateRazorFileIncludedAnalyzer : DiagnosticAnalyzer +{ + private static readonly ImmutableArray s_supportedDiagnostics = ImmutableArray.Create(RazorDiagnostics.DuplicateRazorFileIncludedDescriptor); + + public override ImmutableArray SupportedDiagnostics => s_supportedDiagnostics; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(static compStartAction => + { + var includedFiles = new HashSet(StringComparer.Ordinal); + + compStartAction.RegisterAdditionalFileAction(additionalFilesContext => + { + var additionalFile = additionalFilesContext.AdditionalFile; + var fileName = Path.GetFileName(additionalFile.Path); + + if (additionalFile.Path.EndsWith(".cshtml", StringComparison.Ordinal) || + additionalFile.Path.EndsWith(".razor", StringComparison.Ordinal)) + { + if (!includedFiles.Add(additionalFile.Path)) + { + var diagnostic = Diagnostic.Create( + RazorDiagnostics.DuplicateRazorFileIncludedDescriptor, + Location.Create(additionalFile.Path, new TextSpan(0, 0), new LinePositionSpan(LinePosition.Zero, LinePosition.Zero)), + additionalFile.Path); + + additionalFilesContext.ReportDiagnostic(diagnostic); + } + } + }); + }); + } +} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Diagnostics/DiagnosticIds.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Diagnostics/DiagnosticIds.cs index d5cdb1480d2..8090dbf0fc3 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Diagnostics/DiagnosticIds.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Diagnostics/DiagnosticIds.cs @@ -15,5 +15,6 @@ internal static class DiagnosticIds public const string UnexpectedProjectItemReadCallId = "RSG007"; public const string InvalidRazorContextComputedId = "RSG008"; public const string MetadataReferenceNotProvidedId = "RSG009"; + public const string DuplicateRazorFileFound = "RSG010"; } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Diagnostics/RazorDiagnostics.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Diagnostics/RazorDiagnostics.cs index a44f75aa970..45d0cb16e72 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Diagnostics/RazorDiagnostics.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Diagnostics/RazorDiagnostics.cs @@ -100,6 +100,15 @@ internal static class RazorDiagnostics isEnabledByDefault: true ); + public static readonly DiagnosticDescriptor DuplicateRazorFileIncludedDescriptor = new DiagnosticDescriptor( + DiagnosticIds.DuplicateRazorFileFound, + new LocalizableResourceString(nameof(RazorSourceGeneratorResources.DuplicateRazorFileIncludedTitle), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), + new LocalizableResourceString(nameof(RazorSourceGeneratorResources.DuplicateRazorFileIncludedMessage), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), + "RazorSourceGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true + ); + public static Diagnostic AsDiagnostic(this RazorDiagnostic razorDiagnostic) { var descriptor = new DiagnosticDescriptor( diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Diagnostics/RazorSourceGeneratorResources.resx b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Diagnostics/RazorSourceGeneratorResources.resx index f589cf9f855..593724cc171 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Diagnostics/RazorSourceGeneratorResources.resx +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Diagnostics/RazorSourceGeneratorResources.resx @@ -1,17 +1,17 @@  - @@ -177,4 +177,10 @@ Expected a valid MetadataReference, but found none. - \ No newline at end of file + + Razor file was included twice + + + File '{0}' was included in the build twice. This will cause generator build issues. + + diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/DuplicateRazorFileIncludedAnalyzerTest.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/DuplicateRazorFileIncludedAnalyzerTest.cs new file mode 100644 index 00000000000..ec1c8be47a3 --- /dev/null +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/DuplicateRazorFileIncludedAnalyzerTest.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.SourceGenerators; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Xunit; + +namespace Microsoft.NET.Sdk.Razor.SourceGenerators.Tests; + +public class DuplicateRazorFileIncludedAnalyzerTest +{ + [Theory] + [InlineData("Duplicate.cshtml")] + [InlineData("Duplicate.razor")] + public async Task Analyzer_ReportsDiagnostic_WhenDuplicateRazorFileIsIncluded(string fileName) + { + // Arrange + var test = new CSharpAnalyzerTest + { + TestState = + { + Sources = + { + // Need a non-empty source file to make the test helper happy + ("Test.cs", "public class Test {}"), + }, + AdditionalFiles = + { + (fileName, "

Duplicate

"), + (fileName, "

Duplicate

"), + }, + }, + ExpectedDiagnostics = + { + new DiagnosticResult(RazorDiagnostics.DuplicateRazorFileIncludedDescriptor.Id, DiagnosticSeverity.Error) + .WithLocation(fileName, 1, 1) + .WithMessageFormat(RazorDiagnostics.DuplicateRazorFileIncludedDescriptor.MessageFormat.ToString()) + .WithArguments(fileName), + }, + }; + + // Act & Assert + await test.RunAsync(); + } + + [Theory] + [InlineData("Duplicate.cshtml", "duplicate.cshtml")] + [InlineData("Duplicate.razor", "duplicate.razor")] + public async Task Analyzer_NoDiagnostic_WhenDuplicateRazorFileIsIncluded_DifferentCase(string fileName1, string fileName2) + { + // Arrange + var test = new CSharpAnalyzerTest + { + TestState = + { + Sources = + { + // Need a non-empty source file to make the test helper happy + ("Test.cs", "public class Test {}"), + }, + AdditionalFiles = + { + (fileName1, "

Duplicate

"), + (fileName2, "

Duplicate

"), + }, + }, + ExpectedDiagnostics = + { + }, + }; + + // Act & Assert + await test.RunAsync(); + } + + [Theory] + [InlineData("Duplicate.cshtml")] + [InlineData("Duplicate.razor")] + public async Task Analyzer_ReportsDiagnostic_WhenThreeDuplicateRazorFilesAreIncluded(string fileName) + { + // Arrange + var test = new CSharpAnalyzerTest + { + TestState = + { + Sources = + { + // Need a non-empty source file to make the test helper happy + ("Test.cs", "public class Test {}"), + }, + AdditionalFiles = + { + (fileName, "

Duplicate

"), + (fileName, "

Duplicate

"), + (fileName, "

Duplicate

"), + }, + }, + ExpectedDiagnostics = + { + new DiagnosticResult(RazorDiagnostics.DuplicateRazorFileIncludedDescriptor.Id, DiagnosticSeverity.Error) + .WithLocation(fileName, 1, 1) + .WithMessageFormat(RazorDiagnostics.DuplicateRazorFileIncludedDescriptor.MessageFormat.ToString()) + .WithArguments(fileName), + new DiagnosticResult(RazorDiagnostics.DuplicateRazorFileIncludedDescriptor.Id, DiagnosticSeverity.Error) + .WithLocation(fileName, 1, 1) + .WithMessageFormat(RazorDiagnostics.DuplicateRazorFileIncludedDescriptor.MessageFormat.ToString()) + .WithArguments(fileName), + }, + }; + + // Act & Assert + await test.RunAsync(); + } +} + diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Microsoft.NET.Sdk.Razor.SourceGenerators.Test.csproj b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Microsoft.NET.Sdk.Razor.SourceGenerators.Test.csproj index 581f9841034..e2456ee3c44 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Microsoft.NET.Sdk.Razor.SourceGenerators.Test.csproj +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Microsoft.NET.Sdk.Razor.SourceGenerators.Test.csproj @@ -27,6 +27,7 @@ + From 5fda4128ea9ddfa98b43651f37616abb395ae69a Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Fri, 14 Feb 2025 15:08:54 -0800 Subject: [PATCH 3/4] Missing file license --- .../Analyzers/DuplicateRazorFileIncludedAnalyzer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Analyzers/DuplicateRazorFileIncludedAnalyzer.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Analyzers/DuplicateRazorFileIncludedAnalyzer.cs index 2568502dde1..79e36bdd82a 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Analyzers/DuplicateRazorFileIncludedAnalyzer.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Analyzers/DuplicateRazorFileIncludedAnalyzer.cs @@ -1,4 +1,6 @@ - +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Collections.Generic; using System.Collections.Immutable; From 23d5cee5a0482a5d79e5a7afa0481aff6745200e Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Fri, 14 Feb 2025 15:09:38 -0800 Subject: [PATCH 4/4] Remove unused variable --- .../Analyzers/DuplicateRazorFileIncludedAnalyzer.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Analyzers/DuplicateRazorFileIncludedAnalyzer.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Analyzers/DuplicateRazorFileIncludedAnalyzer.cs index 79e36bdd82a..e0a943f3ccb 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Analyzers/DuplicateRazorFileIncludedAnalyzer.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Analyzers/DuplicateRazorFileIncludedAnalyzer.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.IO; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; @@ -32,7 +31,6 @@ public override void Initialize(AnalysisContext context) compStartAction.RegisterAdditionalFileAction(additionalFilesContext => { var additionalFile = additionalFilesContext.AdditionalFile; - var fileName = Path.GetFileName(additionalFile.Path); if (additionalFile.Path.EndsWith(".cshtml", StringComparison.Ordinal) || additionalFile.Path.EndsWith(".razor", StringComparison.Ordinal))