From 11e6f0cd15141cb3e9e6a109adf9fa829143d39e Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Mon, 6 May 2024 14:01:33 +0200 Subject: [PATCH 1/5] Add a test --- .../RazorSourceGeneratorComponentTests.cs | 40 +++++++++++++++++++ .../Views_Home_Index.html | 4 ++ 2 files changed, 44 insertions(+) create mode 100644 src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ComponentParameter_UnnecessaryAt/Views_Home_Index.html 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 a65aef15797..41e44c21bce 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 @@ -274,6 +274,46 @@ public static class RuntimeHelpers Assert.DoesNotContain("AddComponentParameter", source.SourceText.ToString()); } + [Fact] + public async Task ComponentParameter_UnnecessaryAt() + { + // Arrange + var project = CreateTestProject(new() + { + ["Views/Home/Index.cshtml"] = """ + @(await Html.RenderComponentAsync(RenderMode.Static)) + """, + ["Shared/Component1.razor"] = """ + @{ + var hi = "str"; + var x = 21; + } + + + + + """, + ["Shared/Component2.razor"] = """ + I: @(IntParam + 1), S: @StrParam.ToUpperInvariant() + + @code { + [Parameter] public int IntParam { get; set; } + [Parameter] public required string StrParam { get; set; } + } + """ + }); + var compilation = await project.GetCompilationAsync(); + var driver = await GetDriverAsync(project); + + // Act + var result = RunGenerator(compilation!, ref driver, out compilation); + + // Assert + Assert.Empty(result.Diagnostics); + Assert.Equal(3, result.GeneratedSources.Length); + await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index"); + } + [Fact, WorkItem("https://github.com/dotnet/razor/issues/8660")] public async Task TypeArgumentsCannotBeInferred() { diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ComponentParameter_UnnecessaryAt/Views_Home_Index.html b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ComponentParameter_UnnecessaryAt/Views_Home_Index.html new file mode 100644 index 00000000000..b36997e69c6 --- /dev/null +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ComponentParameter_UnnecessaryAt/Views_Home_Index.html @@ -0,0 +1,4 @@ +I: 43, S: HI +I: 44, S: STR +I: 22, S: LIT +I: 64, S: STRHEY \ No newline at end of file From 46d7dca822ff8badce1126f95ec5cde935fed840 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Mon, 6 May 2024 15:01:31 +0200 Subject: [PATCH 2/5] Add utility for verifying razor diagnostics --- .../RazorSourceGeneratorTestsBase.cs | 14 +++++++ .../DiagnosticExtensions.cs | 38 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/Shared/Microsoft.AspNetCore.Razor.Test.Common/DiagnosticExtensions.cs diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTestsBase.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTestsBase.cs index 18609e8ea59..0f8c43fc4d4 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTestsBase.cs +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTestsBase.cs @@ -545,4 +545,18 @@ public static void AssertPair(this RazorEventListener.RazorEvent e, string expec Assert.Equal(payload1, e.Payload[0]); Assert.Equal(payload2, e.Payload[1]); } + + public static void VerifyRazor( + this IEnumerable diagnostics, + Project project, + params DiagnosticDescription[] expected) + { + var mappedDiagnostics = diagnostics.PretendTheseAreCSharpDiagnostics(path => + { + var document = project.AdditionalDocuments.Single(d => d.Name == path); + Assert.True(document.TryGetText(out var text)); + return text; + }); + mappedDiagnostics.Verify(expected); + } } diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/DiagnosticExtensions.cs b/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/DiagnosticExtensions.cs new file mode 100644 index 00000000000..e7cc52fa994 --- /dev/null +++ b/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/DiagnosticExtensions.cs @@ -0,0 +1,38 @@ +// 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.Linq; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis; + +public static class RazorDiagnosticExtensions +{ + /// + /// Provides better experience when verifying diagnostics in tests - includes squiggled text and code snippet. + /// + public static IEnumerable PretendTheseAreCSharpDiagnostics( + this IEnumerable diagnostics, + Func filePathToContent) + { + var texts = new Dictionary(); + return diagnostics.Select(d => + { + if (d.Location is { Kind: LocationKind.ExternalFile } originalLocation) + { + var mappedSpan = originalLocation.GetMappedLineSpan(); + var path = mappedSpan.Path; + var text = texts.GetOrAdd(path, filePathToContent); + var syntaxTree = CSharpSyntaxTree.ParseText(text, path: path); + var span = text.Lines.GetTextSpan(mappedSpan.Span); + var location = Location.Create(syntaxTree, span); + return Diagnostic.Create(d.Descriptor, location); + } + + return d; + }); + } +} From fd61d29cf38e75b2a8c5b6971c29bf2e36e19c63 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Mon, 6 May 2024 15:15:01 +0200 Subject: [PATCH 3/5] Add warning for unnecessary at --- .../Language/Components/ComponentLoweringPass.cs | 15 +++++++++++++++ .../src/Language/RazorDiagnosticFactory.cs | 8 ++++++++ .../src/Language/Resources.resx | 3 +++ .../RazorSourceGeneratorComponentTests.cs | 8 +++++++- 4 files changed, 33 insertions(+), 1 deletion(-) 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 e01340e82cf..a648f6d8197 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 @@ -163,6 +163,7 @@ private static ComponentIntermediateNode RewriteAsComponent(TagHelperIntermediat } ValidateRequiredAttributes(node, tagHelper, component); + WarnForUnnecessaryAt(component); return component; } @@ -213,6 +214,20 @@ static bool IsPresentAsAttribute(string attributeName, ComponentIntermediateNode } } + private static void WarnForUnnecessaryAt(ComponentIntermediateNode component) + { + foreach (var attribute in component.Attributes) + { + // IntParam="@x" has unnecessary `@`, can just use IntParam="x" -> warn + // StrParam="@x" is different than StrParam="x" -> don't warn + if (!attribute.BoundAttribute.IsStringProperty && + attribute.Children is [CSharpExpressionIntermediateNode]) + { + attribute.Diagnostics.Add(RazorDiagnosticFactory.CreateComponentParameter_UnnecessaryAt(attribute.Source)); + } + } + } + private static MarkupElementIntermediateNode RewriteAsElement(TagHelperIntermediateNode node) { var result = new MarkupElementIntermediateNode() diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorDiagnosticFactory.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorDiagnosticFactory.cs index 8c36e2fe36d..cbae448d525 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorDiagnosticFactory.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorDiagnosticFactory.cs @@ -480,6 +480,14 @@ public static RazorDiagnostic CreateTagHelper_InconsistentTagStructure(SourceSpa public static RazorDiagnostic CreateComponent_EditorRequiredParameterNotSpecified(SourceSpan? location, string tagName, string parameterName) => RazorDiagnostic.Create(Component_EditorRequiredParameterNotSpecified, location, tagName, parameterName); + internal static readonly RazorDiagnosticDescriptor ComponentParameter_UnnecessaryAt = + new($"{DiagnosticPrefix}2013", + Resources.ComponentParameter_UnnecessaryAt, + RazorDiagnosticSeverity.Warning); + + public static RazorDiagnostic CreateComponentParameter_UnnecessaryAt(SourceSpan? location) + => RazorDiagnostic.Create(ComponentParameter_UnnecessaryAt, location); + #endregion #region TagHelper Errors diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Resources.resx b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Resources.resx index f469eb79b96..144ce8dc525 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Resources.resx +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/Resources.resx @@ -574,6 +574,9 @@ Component '{0}' expects a value for the parameter '{1}', but a value may not have been provided. + + The '@' prefix is not necessary for component parameters whose type is not string. + An item with the same key has already been added. Key: {0} 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 41e44c21bce..f466c40f351 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 @@ -309,7 +309,13 @@ public async Task ComponentParameter_UnnecessaryAt() var result = RunGenerator(compilation!, ref driver, out compilation); // Assert - Assert.Empty(result.Diagnostics); + result.Diagnostics.VerifyRazor(project, + // Shared/Component1.razor(6,23): warning RZ2013: The '@' prefix is not necessary for component parameters whose type is not string. + // + Diagnostic("RZ2013", "@(43)").WithLocation(6, 23), + // Shared/Component1.razor(7,23): warning RZ2013: The '@' prefix is not necessary for component parameters whose type is not string. + // + Diagnostic("RZ2013", "@x").WithLocation(7, 23)); Assert.Equal(3, result.GeneratedSources.Length); await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index"); } From fa4d3e67176b2c2ea85f15199b394842d28562d8 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Mon, 6 May 2024 15:22:28 +0200 Subject: [PATCH 4/5] Report warning at the `@` only --- .../Components/ComponentLoweringPass.cs | 4 ++- .../RazorSourceGeneratorComponentTests.cs | 28 ++++++++++++------- .../Views_Home_Index.html | 5 ++-- 3 files changed, 24 insertions(+), 13 deletions(-) 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 a648f6d8197..7302deb9c69 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 @@ -221,9 +221,11 @@ private static void WarnForUnnecessaryAt(ComponentIntermediateNode component) // IntParam="@x" has unnecessary `@`, can just use IntParam="x" -> warn // StrParam="@x" is different than StrParam="x" -> don't warn if (!attribute.BoundAttribute.IsStringProperty && + attribute.Source is { } originalSource && attribute.Children is [CSharpExpressionIntermediateNode]) { - attribute.Diagnostics.Add(RazorDiagnosticFactory.CreateComponentParameter_UnnecessaryAt(attribute.Source)); + var source = originalSource.With(length: 1, endCharacterIndex: originalSource.CharacterIndex + 1); + attribute.Diagnostics.Add(RazorDiagnosticFactory.CreateComponentParameter_UnnecessaryAt(source)); } } } 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 f466c40f351..1553b8ca2aa 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 @@ -289,16 +289,21 @@ public async Task ComponentParameter_UnnecessaryAt() var x = 21; } - - + + + """, ["Shared/Component2.razor"] = """ - I: @(IntParam + 1), S: @StrParam.ToUpperInvariant() + I: @(IntParam + 1), S: @StrParam?.ToUpperInvariant() @code { [Parameter] public int IntParam { get; set; } - [Parameter] public required string StrParam { get; set; } + [Parameter] public string? StrParam { get; set; } } """ }); @@ -310,12 +315,15 @@ public async Task ComponentParameter_UnnecessaryAt() // Assert result.Diagnostics.VerifyRazor(project, - // Shared/Component1.razor(6,23): warning RZ2013: The '@' prefix is not necessary for component parameters whose type is not string. - // - Diagnostic("RZ2013", "@(43)").WithLocation(6, 23), - // Shared/Component1.razor(7,23): warning RZ2013: The '@' prefix is not necessary for component parameters whose type is not string. - // - Diagnostic("RZ2013", "@x").WithLocation(7, 23)); + // 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)); Assert.Equal(3, result.GeneratedSources.Length); await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index"); } diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ComponentParameter_UnnecessaryAt/Views_Home_Index.html b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ComponentParameter_UnnecessaryAt/Views_Home_Index.html index b36997e69c6..84e2c57c811 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ComponentParameter_UnnecessaryAt/Views_Home_Index.html +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ComponentParameter_UnnecessaryAt/Views_Home_Index.html @@ -1,4 +1,5 @@ -I: 43, S: HI +I: 43, S: HI I: 44, S: STR I: 22, S: LIT -I: 64, S: STRHEY \ No newline at end of file +I: 64, S: STRHEY +I: 22, S: From 96baca0ace325fb9df69f4c5a612faeb9e997c8e Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Mon, 6 May 2024 15:56:07 +0200 Subject: [PATCH 5/5] Add RazorWarningLevel support --- .../Components/ComponentLoweringPass.cs | 16 ++++++- .../src/Language/RazorConfiguration.cs | 5 +- .../src/Language/RazorProjectEngine.cs | 8 ++-- .../Diagnostics/DiagnosticIds.cs | 1 + .../Diagnostics/RazorDiagnostics.cs | 8 ++++ .../RazorSourceGeneratorResources.resx | 8 +++- .../IncrementalValueProviderExtensions.cs | 7 +-- .../RazorSourceGenerator.RazorProviders.cs | 38 ++++++++++++--- .../RazorSourceGeneratorComponentTests.cs | 46 +++++++++++++------ .../RazorSourceGeneratorTests.cs | 24 +++++++++- 10 files changed, 131 insertions(+), 30 deletions(-) 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() {