diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentLoweringPass.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentLoweringPass.cs index 7302deb9c69..5eed179c277 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentLoweringPass.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Components/ComponentLoweringPass.cs @@ -12,9 +12,16 @@ namespace Microsoft.AspNetCore.Razor.Language.Components; internal class ComponentLoweringPass : ComponentIntermediateNodePassBase, IRazorOptimizationPass { + private readonly int? _razorWarningLevel; + // This pass runs earlier than our other passes that 'lower' specific kinds of attributes. public override int Order => 0; + public ComponentLoweringPass(RazorConfiguration configuration) + { + _razorWarningLevel = configuration.RazorWarningLevel; + } + protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) { if (!IsComponentDocument(documentNode)) @@ -137,7 +144,7 @@ static TagHelperDescriptor GetTagHelperOrAddDiagnostic(TagHelperIntermediateNode } } - private static ComponentIntermediateNode RewriteAsComponent(TagHelperIntermediateNode node, TagHelperDescriptor tagHelper) + private ComponentIntermediateNode RewriteAsComponent(TagHelperIntermediateNode node, TagHelperDescriptor tagHelper) { var component = new ComponentIntermediateNode() { @@ -214,8 +221,13 @@ static bool IsPresentAsAttribute(string attributeName, ComponentIntermediateNode } } - private static void WarnForUnnecessaryAt(ComponentIntermediateNode component) + private void WarnForUnnecessaryAt(ComponentIntermediateNode component) { + if (_razorWarningLevel is not >= 9) + { + return; + } + foreach (var attribute in component.Attributes) { // IntParam="@x" has unnecessary `@`, can just use IntParam="x" -> warn diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorConfiguration.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorConfiguration.cs index 33f6e744b0e..960731a1005 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorConfiguration.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorConfiguration.cs @@ -12,7 +12,8 @@ public sealed record class RazorConfiguration( string ConfigurationName, ImmutableArray Extensions, LanguageServerFlags? LanguageServerFlags = null, - bool UseConsolidatedMvcViews = false) + bool UseConsolidatedMvcViews = false, + int? RazorWarningLevel = null) { public static readonly RazorConfiguration Default = new( RazorLanguageVersion.Latest, @@ -27,6 +28,7 @@ public bool Equals(RazorConfiguration? other) ConfigurationName == other.ConfigurationName && LanguageServerFlags == other.LanguageServerFlags && UseConsolidatedMvcViews == other.UseConsolidatedMvcViews && + RazorWarningLevel == other.RazorWarningLevel && Extensions.SequenceEqual(other.Extensions); public override int GetHashCode() @@ -37,6 +39,7 @@ public override int GetHashCode() hash.Add(Extensions); hash.Add(UseConsolidatedMvcViews); hash.Add(LanguageServerFlags); + hash.Add(RazorWarningLevel); return hash; } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs index a0412d29635..3c181b5ddc1 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs @@ -363,7 +363,7 @@ public static RazorProjectEngine Create( NamespaceDirective.Register(builder); AttributeDirective.Register(builder); - AddComponentFeatures(builder, configuration.LanguageVersion); + AddComponentFeatures(builder, configuration); } configure?.Invoke(builder); @@ -448,8 +448,10 @@ private static void AddDefaultFeatures(ImmutableArray.Builder fea }); } - private static void AddComponentFeatures(RazorProjectEngineBuilder builder, RazorLanguageVersion razorLanguageVersion) + private static void AddComponentFeatures(RazorProjectEngineBuilder builder, RazorConfiguration configuration) { + RazorLanguageVersion razorLanguageVersion = configuration.LanguageVersion; + // Project Engine Features builder.Features.Add(new ComponentImportProjectFeature()); @@ -486,7 +488,7 @@ private static void AddComponentFeatures(RazorProjectEngineBuilder builder, Razo // Optimization builder.Features.Add(new ComponentComplexAttributeContentPass()); - builder.Features.Add(new ComponentLoweringPass()); + builder.Features.Add(new ComponentLoweringPass(configuration)); builder.Features.Add(new ComponentEventHandlerLoweringPass()); builder.Features.Add(new ComponentKeyLoweringPass()); builder.Features.Add(new ComponentReferenceCaptureLoweringPass()); 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..8002e8b3c5c 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 @@ -6,6 +6,7 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators internal static class DiagnosticIds { public const string InvalidRazorLangVersionRuleId = "RZ3600"; + public const string InvalidRazorWarningLevelRuleId = "RZ3601"; public const string ReComputingTagHelpersRuleId = "RSG001"; public const string TargetPathNotProvidedRuleId = "RSG002"; public const string GeneratedOutputFullPathNotProvidedRuleId = "RSG003"; 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..6a81ae75b84 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 @@ -20,6 +20,14 @@ internal static class RazorDiagnostics DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor InvalidRazorWarningLevelDescriptor = new DiagnosticDescriptor( + DiagnosticIds.InvalidRazorWarningLevelRuleId, + new LocalizableResourceString(nameof(RazorSourceGeneratorResources.InvalidRazorWarningLevelTitle), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), + new LocalizableResourceString(nameof(RazorSourceGeneratorResources.InvalidRazorWarningLevelMessage), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), + "RazorSourceGenerator", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + public static readonly DiagnosticDescriptor ReComputingTagHelpersDescriptor = new DiagnosticDescriptor( DiagnosticIds.ReComputingTagHelpersRuleId, new LocalizableResourceString(nameof(RazorSourceGeneratorResources.RecomputingTagHelpersTitle), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), 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..f5edaab0765 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 @@ -121,7 +121,13 @@ Invalid RazorLangVersion - Invalid value '{0}'' for RazorLangVersion. Valid values include 'Latest' or a valid version in range 1.0 to 8.0. + Invalid value '{0}' for RazorLangVersion. Valid values include 'Latest' or a valid version in range 1.0 to 8.0. + + + Invalid RazorWarningLevel + + + Invalid value '{0}' for RazorWarningLevel. Must be empty or an integer. Recomputing tag helpers diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/IncrementalValueProviderExtensions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/IncrementalValueProviderExtensions.cs index b1745bbbf9a..266237f0564 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/IncrementalValueProviderExtensions.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/IncrementalValueProviderExtensions.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Microsoft.AspNetCore.Razor; using Microsoft.CodeAnalysis; @@ -39,12 +40,12 @@ internal static IncrementalValuesProvider ReportDiagnostics(th return source.Where((pair) => pair.Item1 != null).Select((pair, ct) => pair.Item1!); } - internal static IncrementalValueProvider ReportDiagnostics(this IncrementalValueProvider<(TSource?, Diagnostic?)> source, IncrementalGeneratorInitializationContext context) + internal static IncrementalValueProvider ReportDiagnostics(this IncrementalValueProvider<(TSource?, ImmutableArray)> source, IncrementalGeneratorInitializationContext context) { context.RegisterSourceOutput(source, (spc, source) => { - var (_, diagnostic) = source; - if (diagnostic != null) + var (_, diagnostics) = source; + foreach (var diagnostic in diagnostics) { spc.ReportDiagnostic(diagnostic); } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs index 7daf0ba4abf..33e02e336b7 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs @@ -2,10 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Immutable; using System.IO; using System.Text; using System.Threading; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; @@ -14,7 +16,7 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators { public partial class RazorSourceGenerator { - private (RazorSourceGenerationOptions?, Diagnostic?) ComputeRazorSourceGeneratorOptions(((AnalyzerConfigOptionsProvider, ParseOptions), bool) pair, CancellationToken ct) + private (RazorSourceGenerationOptions?, ImmutableArray) ComputeRazorSourceGeneratorOptions(((AnalyzerConfigOptionsProvider, ParseOptions), bool) pair, CancellationToken ct) { var ((options, parseOptions), isSuppressed) = pair; var globalOptions = options.GlobalOptions; @@ -31,18 +33,42 @@ public partial class RazorSourceGenerator globalOptions.TryGetValue("build_property.SupportLocalizedComponentNames", out var supportLocalizedComponentNames); globalOptions.TryGetValue("build_property.GenerateRazorMetadataSourceChecksumAttributes", out var generateMetadataSourceChecksumAttributes); - Diagnostic? diagnostic = null; + using var diagnostics = new PooledArrayBuilder(); if (!globalOptions.TryGetValue("build_property.RazorLangVersion", out var razorLanguageVersionString) || !RazorLanguageVersion.TryParse(razorLanguageVersionString, out var razorLanguageVersion)) { - diagnostic = Diagnostic.Create( + diagnostics.Add(Diagnostic.Create( RazorDiagnostics.InvalidRazorLangVersionDescriptor, Location.None, - razorLanguageVersionString); + razorLanguageVersionString)); razorLanguageVersion = RazorLanguageVersion.Latest; } - var razorConfiguration = new RazorConfiguration(razorLanguageVersion, configurationName ?? "default", Extensions: [], UseConsolidatedMvcViews: true); + globalOptions.TryGetValue("build_property.RazorWarningLevel", out var razorWarningLevelString); + int? razorWarningLevel; + if (string.IsNullOrEmpty(razorWarningLevelString)) + { + razorWarningLevel = null; + } + else if (!int.TryParse(razorWarningLevelString, out var razorWarningLevelInt)) + { + diagnostics.Add(Diagnostic.Create( + RazorDiagnostics.InvalidRazorWarningLevelDescriptor, + Location.None, + razorWarningLevelString)); + razorWarningLevel = null; + } + else + { + razorWarningLevel = razorWarningLevelInt; + } + + var razorConfiguration = new RazorConfiguration( + razorLanguageVersion, + configurationName ?? "default", + Extensions: [], + UseConsolidatedMvcViews: true, + RazorWarningLevel: razorWarningLevel); var razorSourceGenerationOptions = new RazorSourceGenerationOptions() { @@ -54,7 +80,7 @@ public partial class RazorSourceGenerator TestSuppressUniqueIds = _testSuppressUniqueIds, }; - return (razorSourceGenerationOptions, diagnostic); + return (razorSourceGenerationOptions, diagnostics.DrainToImmutable()); } private static (SourceGeneratorProjectItem?, Diagnostic?) ComputeProjectItems((AdditionalText, AnalyzerConfigOptionsProvider) pair, CancellationToken ct) diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorComponentTests.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorComponentTests.cs index 1553b8ca2aa..745a292f8e4 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorComponentTests.cs +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorComponentTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -274,8 +275,14 @@ public static class RuntimeHelpers Assert.DoesNotContain("AddComponentParameter", source.SourceText.ToString()); } - [Fact] - public async Task ComponentParameter_UnnecessaryAt() + [Theory] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData("8", false)] + [InlineData("9", true)] + [InlineData("10", true)] + [InlineData("999", true)] + public async Task ComponentParameter_UnnecessaryAt(string warningLevel, bool hasWarnings) { // Arrange var project = CreateTestProject(new() @@ -308,22 +315,35 @@ public async Task ComponentParameter_UnnecessaryAt() """ }); var compilation = await project.GetCompilationAsync(); - var driver = await GetDriverAsync(project); + var driver = await GetDriverAsync(project, options => + { + if (warningLevel != null) + { + options.TestGlobalOptions["build_property.RazorWarningLevel"] = warningLevel; + } + }); // Act var result = RunGenerator(compilation!, ref driver, out compilation); // Assert - result.Diagnostics.VerifyRazor(project, - // Shared/Component1.razor(7,15): warning RZ2013: The '@' prefix is not necessary for component parameters whose type is not string. - // IntParam="@(43)" - Diagnostic("RZ2013", "@").WithLocation(7, 15), - // Shared/Component1.razor(10,15): warning RZ2013: The '@' prefix is not necessary for component parameters whose type is not string. - // IntParam="@x" - Diagnostic("RZ2013", "@").WithLocation(10, 15), - // Shared/Component1.razor(13,22): warning RZ2013: The '@' prefix is not necessary for component parameters whose type is not string. - // - Diagnostic("RZ2013", "@").WithLocation(13, 22)); + if (hasWarnings) + { + result.Diagnostics.VerifyRazor(project, + // Shared/Component1.razor(7,15): warning RZ2013: The '@' prefix is not necessary for component parameters whose type is not string. + // IntParam="@(43)" + Diagnostic("RZ2013", "@").WithLocation(7, 15), + // Shared/Component1.razor(10,15): warning RZ2013: The '@' prefix is not necessary for component parameters whose type is not string. + // IntParam="@x" + Diagnostic("RZ2013", "@").WithLocation(10, 15), + // Shared/Component1.razor(13,22): warning RZ2013: The '@' prefix is not necessary for component parameters whose type is not string. + // + Diagnostic("RZ2013", "@").WithLocation(13, 22)); + } + else + { + result.Diagnostics.VerifyRazor(project); + } Assert.Equal(3, result.GeneratedSources.Length); await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index"); } diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs index f85188fc36a..a916938901c 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs @@ -3073,11 +3073,33 @@ public async Task RazorLangVersion_Incorrect([CombinatorialValues("incorrect", " var result = RunGenerator(compilation!, ref driver); result.Diagnostics.Verify( - // error RZ3600: Invalid value '{0}'' for RazorLangVersion. Valid values include 'Latest' or a valid version in range 1.0 to 8.0. + // error RZ3600: Invalid value '{0}' for RazorLangVersion. Valid values include 'Latest' or a valid version in range 1.0 to 8.0. Diagnostic("RZ3600").WithArguments(langVersion).WithLocation(1, 1)); Assert.Single(result.GeneratedSources); } + [Theory, CombinatorialData] + public async Task RazorWarningLevel_Incorrect( + [CombinatorialValues("incorrect", "1.2", "0x1")] string warningLevel) + { + var project = CreateTestProject(new() + { + ["Pages/Index.razor"] = "

Hello world

", + }); + var compilation = await project.GetCompilationAsync(); + var driver = await GetDriverAsync(project, options => + { + options.TestGlobalOptions["build_property.RazorWarningLevel"] = warningLevel; + }); + + var result = RunGenerator(compilation!, ref driver); + + result.Diagnostics.Verify( + // error RZ3600: Invalid value '{0}' for RazorWarningLevel. Must be empty or an integer. + Diagnostic("RZ3601").WithArguments(warningLevel).WithLocation(1, 1)); + Assert.Single(result.GeneratedSources); + } + [Fact] public async Task Test_WhenEmptyOrCached() {