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/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..e0a943f3ccb --- /dev/null +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/Analyzers/DuplicateRazorFileIncludedAnalyzer.cs @@ -0,0 +1,51 @@ +// 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; +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; + + 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 @@ + 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 @@ - +