diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/CodeGeneration/CodeRenderingContextTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/CodeGeneration/CodeRenderingContextTest.cs new file mode 100644 index 00000000000..0df5f6c103d --- /dev/null +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/CodeGeneration/CodeRenderingContextTest.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.AspNetCore.Razor.Language.Intermediate; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration; + +public class CodeRenderingContextTest +{ + [Theory] + [InlineData(0, false)] + [InlineData(11, false)] + [InlineData(12, true)] + [InlineData(9999, true)] + public void GetDiagnostics_FiltersWarningsByLevel(int warningLevel, bool expectDiagnostic) + { + // Arrange + var descriptor = new RazorDiagnosticDescriptor("RZTest", "Test warning for '{0}'", RazorDiagnosticSeverity.Warning, warningLevel: 12); + var diagnostic = RazorDiagnostic.Create(descriptor, SourceSpan.Undefined, "param"); + + var documentNode = new DocumentIntermediateNode(); + documentNode.AddDiagnostic(diagnostic); + + var options = RazorCodeGenerationOptions.Default.WithRazorWarningLevel(warningLevel); + using var context = new CodeRenderingContext( + RuntimeNodeWriter.Instance, + TestRazorSourceDocument.Create(), + documentNode, + options); + + // Act + var diagnostics = context.GetDiagnostics(); + + // Assert + if (expectDiagnostic) + { + Assert.Contains(diagnostics, d => d.Id == "RZTest"); + } + else + { + Assert.DoesNotContain(diagnostics, d => d.Id == "RZTest"); + } + } + + [Fact] + public void GetDiagnostics_AlwaysOnDiagnostics_NotFilteredAtAnyLevel() + { + // Arrange — level 0 diagnostics are always on + var descriptor = new RazorDiagnosticDescriptor("RZAlways", "Always on: '{0}'", RazorDiagnosticSeverity.Warning); + var diagnostic = RazorDiagnostic.Create(descriptor, SourceSpan.Undefined, "param"); + + var documentNode = new DocumentIntermediateNode(); + documentNode.AddDiagnostic(diagnostic); + + var options = RazorCodeGenerationOptions.Default.WithRazorWarningLevel(0); + using var context = new CodeRenderingContext( + RuntimeNodeWriter.Instance, + TestRazorSourceDocument.Create(), + documentNode, + options); + + // Act + var diagnostics = context.GetDiagnostics(); + + // Assert — level 0 diagnostic should be present even at warning level 0 + Assert.Contains(diagnostics, d => d.Id == "RZAlways"); + } + + [Theory] + [InlineData(10)] + [InlineData(11)] + [InlineData(12)] + [InlineData(13)] + public void GetDiagnostics_MultipleLevels_FiltersCorrectly(int warningLevel) + { + // Arrange — diagnostics at levels 0, 11, 12, and 13 + var alwaysOn = RazorDiagnostic.Create( + new RazorDiagnosticDescriptor("RZ0", "Always", RazorDiagnosticSeverity.Warning), + SourceSpan.Undefined); + var level11 = RazorDiagnostic.Create( + new RazorDiagnosticDescriptor("RZ11", "Level 11", RazorDiagnosticSeverity.Warning, warningLevel: 11), + SourceSpan.Undefined); + var level12 = RazorDiagnostic.Create( + new RazorDiagnosticDescriptor("RZ12", "Level 12", RazorDiagnosticSeverity.Warning, warningLevel: 12), + SourceSpan.Undefined); + var level13 = RazorDiagnostic.Create( + new RazorDiagnosticDescriptor("RZ13", "Level 13", RazorDiagnosticSeverity.Warning, warningLevel: 13), + SourceSpan.Undefined); + + var documentNode = new DocumentIntermediateNode(); + documentNode.AddDiagnostic(alwaysOn); + documentNode.AddDiagnostic(level11); + documentNode.AddDiagnostic(level12); + documentNode.AddDiagnostic(level13); + + var options = RazorCodeGenerationOptions.Default.WithRazorWarningLevel(warningLevel); + using var context = new CodeRenderingContext( + RuntimeNodeWriter.Instance, + TestRazorSourceDocument.Create(), + documentNode, + options); + + // Act + var diagnostics = context.GetDiagnostics(); + + // Assert — always-on is always present + Assert.Contains(diagnostics, d => d.Id == "RZ0"); + + // Each level is present only when warningLevel >= that level + Assert.Equal(warningLevel >= 11, diagnostics.Any(d => d.Id == "RZ11")); + Assert.Equal(warningLevel >= 12, diagnostics.Any(d => d.Id == "RZ12")); + Assert.Equal(warningLevel >= 13, diagnostics.Any(d => d.Id == "RZ13")); + } +} diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorDiagnosticDescriptorTest.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorDiagnosticDescriptorTest.cs index d57ad2cc813..67fbdbcc4de 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorDiagnosticDescriptorTest.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/test/RazorDiagnosticDescriptorTest.cs @@ -20,6 +20,20 @@ public void RazorDiagnosticDescriptor_Ctor() Assert.Equal("RZ0001", descriptor.Id); Assert.Equal(RazorDiagnosticSeverity.Error, descriptor.Severity); Assert.Equal("Hello, World!", descriptor.MessageFormat); + Assert.Equal(0, descriptor.WarningLevel); + } + + [Fact] + public void RazorDiagnosticDescriptor_Ctor_WithWarningLevel() + { + // Arrange & Act + var descriptor = new RazorDiagnosticDescriptor("RZ0001", "Hello, World!", RazorDiagnosticSeverity.Warning, warningLevel: 11); + + // Assert + Assert.Equal("RZ0001", descriptor.Id); + Assert.Equal(RazorDiagnosticSeverity.Warning, descriptor.Severity); + Assert.Equal("Hello, World!", descriptor.MessageFormat); + Assert.Equal(11, descriptor.WarningLevel); } [Fact] diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/CodeGeneration/CodeRenderingContext.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/CodeGeneration/CodeRenderingContext.cs index f00fd395f8c..f1aa24ad79d 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/CodeGeneration/CodeRenderingContext.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/CodeGeneration/CodeRenderingContext.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using Microsoft.AspNetCore.Razor.Language.Intermediate; using Microsoft.AspNetCore.Razor.PooledObjects; @@ -92,7 +93,15 @@ public void AddDiagnostic(RazorDiagnostic diagnostic) } public ImmutableArray GetDiagnostics() - => _diagnostics.ToImmutableOrderedBy(static d => d.Span.AbsoluteIndex); + { + var warningLevel = Options.RazorWarningLevel; + + // Filter out diagnostics whose warning level exceeds the configured level. + // Diagnostics with level 0 are always reported regardless of the configured level. + return _diagnostics + .Where(d => d.WarningLevel <= warningLevel) + .OrderByAsArray(static d => d.Span.AbsoluteIndex); + } public void AddSourceMappingFor(IntermediateNode node) { diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.Builder.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.Builder.cs index 812a115c35d..abacfbde00d 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.Builder.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.Builder.cs @@ -27,6 +27,11 @@ public sealed class Builder /// public string? SuppressUniqueIds { get; set; } + /// + /// Gets or sets the warning level for diagnostic filtering. + /// + public int RazorWarningLevel { get; set; } + internal Builder() { IndentSize = DefaultIndentSize; @@ -159,6 +164,6 @@ public bool RemapLinePragmaPathsOnWindows } public RazorCodeGenerationOptions ToOptions() - => new(IndentSize, NewLine, RootNamespace, CssScope, SuppressUniqueIds, _flags); + => new(IndentSize, NewLine, RootNamespace, CssScope, SuppressUniqueIds, RazorWarningLevel, _flags); } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.cs index 3774b8783c1..eb190417843 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorCodeGenerationOptions.cs @@ -16,6 +16,7 @@ public sealed partial class RazorCodeGenerationOptions rootNamespace: null, cssScope: null, suppressUniqueIds: null, + razorWarningLevel: 0, flags: Flags.DefaultFlags); public static RazorCodeGenerationOptions DesignTimeDefault { get; } = new( @@ -24,6 +25,7 @@ public sealed partial class RazorCodeGenerationOptions rootNamespace: null, cssScope: null, suppressUniqueIds: null, + razorWarningLevel: 0, flags: Flags.DefaultDesignTimeFlags); public int IndentSize { get; } @@ -44,6 +46,13 @@ public sealed partial class RazorCodeGenerationOptions /// public string? SuppressUniqueIds { get; } + /// + /// Gets the warning level for diagnostic filtering. Diagnostics with a + /// greater than this value are suppressed. + /// A value of 0 means only always-on diagnostics (level 0) are reported. + /// + public int RazorWarningLevel { get; } + private readonly Flags _flags; private RazorCodeGenerationOptions( @@ -52,6 +61,7 @@ private RazorCodeGenerationOptions( string? rootNamespace, string? cssScope, string? suppressUniqueIds, + int razorWarningLevel, Flags flags) { IndentSize = indentSize; @@ -59,6 +69,7 @@ private RazorCodeGenerationOptions( RootNamespace = rootNamespace; CssScope = cssScope; SuppressUniqueIds = suppressUniqueIds; + RazorWarningLevel = razorWarningLevel; _flags = flags; } @@ -162,27 +173,32 @@ public bool RemapLinePragmaPathsOnWindows public RazorCodeGenerationOptions WithIndentSize(int value) => IndentSize == value ? this - : new(value, NewLine, RootNamespace, CssScope, SuppressUniqueIds, _flags); + : new(value, NewLine, RootNamespace, CssScope, SuppressUniqueIds, RazorWarningLevel, _flags); public RazorCodeGenerationOptions WithNewLine(string value) => NewLine == value ? this - : new(IndentSize, value, RootNamespace, CssScope, SuppressUniqueIds, _flags); + : new(IndentSize, value, RootNamespace, CssScope, SuppressUniqueIds, RazorWarningLevel, _flags); public RazorCodeGenerationOptions WithRootNamespace(string? value) => RootNamespace == value ? this - : new(IndentSize, NewLine, value, CssScope, SuppressUniqueIds, _flags); + : new(IndentSize, NewLine, value, CssScope, SuppressUniqueIds, RazorWarningLevel, _flags); public RazorCodeGenerationOptions WithCssScope(string? value) => CssScope == value ? this - : new(IndentSize, NewLine, RootNamespace, value, SuppressUniqueIds, _flags); + : new(IndentSize, NewLine, RootNamespace, value, SuppressUniqueIds, RazorWarningLevel, _flags); public RazorCodeGenerationOptions WithSuppressUniqueIds(string? value) - => RootNamespace == value + => SuppressUniqueIds == value + ? this + : new(IndentSize, NewLine, RootNamespace, CssScope, value, RazorWarningLevel, _flags); + + public RazorCodeGenerationOptions WithRazorWarningLevel(int value) + => RazorWarningLevel == value ? this - : new(IndentSize, NewLine, RootNamespace, CssScope, value, _flags); + : new(IndentSize, NewLine, RootNamespace, CssScope, SuppressUniqueIds, value, _flags); public RazorCodeGenerationOptions WithFlags( Optional designTime = default, @@ -262,6 +278,6 @@ public RazorCodeGenerationOptions WithFlags( return flags == _flags ? this - : new(IndentSize, NewLine, RootNamespace, CssScope, SuppressUniqueIds, flags); + : new(IndentSize, NewLine, RootNamespace, CssScope, SuppressUniqueIds, RazorWarningLevel, flags); } } 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 e6b1eea1305..35371ec0661 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorConfiguration.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorConfiguration.cs @@ -16,7 +16,8 @@ public sealed record class RazorConfiguration( bool UseConsolidatedMvcViews = true, bool SuppressAddComponentParameter = false, bool UseRoslynTokenizer = false, - ImmutableArray PreprocessorSymbols = default) + ImmutableArray PreprocessorSymbols = default, + int RazorWarningLevel = 0) { public ImmutableArray PreprocessorSymbols { @@ -42,6 +43,7 @@ public bool Equals(RazorConfiguration? other) SuppressAddComponentParameter == other.SuppressAddComponentParameter && UseConsolidatedMvcViews == other.UseConsolidatedMvcViews && UseRoslynTokenizer == other.UseRoslynTokenizer && + RazorWarningLevel == other.RazorWarningLevel && PreprocessorSymbols.SequenceEqual(other.PreprocessorSymbols) && Extensions.SequenceEqual(other.Extensions); @@ -55,6 +57,7 @@ public override int GetHashCode() hash.Add(SuppressAddComponentParameter); hash.Add(UseConsolidatedMvcViews); hash.Add(UseRoslynTokenizer); + hash.Add(RazorWarningLevel); hash.Add(PreprocessorSymbols); return hash; } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorDiagnostic.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorDiagnostic.cs index 351b0c44040..12dd2439886 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorDiagnostic.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorDiagnostic.cs @@ -14,6 +14,7 @@ public sealed class RazorDiagnostic : IEquatable, IFormattable public string Id => _descriptor.Id; public RazorDiagnosticSeverity Severity => _descriptor.Severity; + public int WarningLevel => _descriptor.WarningLevel; public SourceSpan Span { get; } private Checksum? _checksum; diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorDiagnosticDescriptor.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorDiagnosticDescriptor.cs index c6027ef6487..865a27fb97f 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorDiagnosticDescriptor.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorDiagnosticDescriptor.cs @@ -13,7 +13,19 @@ public sealed record RazorDiagnosticDescriptor public string MessageFormat { get; } public RazorDiagnosticSeverity Severity { get; } + /// + /// The warning level at which this diagnostic is reported. Diagnostics with a warning level + /// greater than the configured RazorWarningLevel are suppressed. + /// A value of 0 means the diagnostic is always reported regardless of the configured level. + /// + public int WarningLevel { get; } + public RazorDiagnosticDescriptor(string id, string messageFormat, RazorDiagnosticSeverity severity) + : this(id, messageFormat, severity, warningLevel: 0) + { + } + + public RazorDiagnosticDescriptor(string id, string messageFormat, RazorDiagnosticSeverity severity, int warningLevel) { if (string.IsNullOrEmpty(id)) { @@ -28,9 +40,9 @@ public RazorDiagnosticDescriptor(string id, string messageFormat, RazorDiagnosti Id = id; MessageFormat = messageFormat; Severity = severity; + WarningLevel = warningLevel; } - private string DebuggerToString() => $""" - Error "{Id}": "{MessageFormat}" - """; + private string DebuggerToString() + => $"""Error "{Id}" (level {WarningLevel}): "{MessageFormat}"""; } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorLanguageVersion.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorLanguageVersion.cs index fa8f7a1e977..7bf9c0d447d 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorLanguageVersion.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorLanguageVersion.cs @@ -112,4 +112,11 @@ public int CompareTo(RazorLanguageVersion? other) public static bool operator <=(RazorLanguageVersion x, RazorLanguageVersion y) => x.CompareTo(y) <= 0; public static bool operator >(RazorLanguageVersion x, RazorLanguageVersion y) => x.CompareTo(y) > 0; public static bool operator >=(RazorLanguageVersion x, RazorLanguageVersion y) => x.CompareTo(y) >= 0; + + /// + /// Gets the default warning level for this language version. + /// The warning level corresponds to the major version number + /// (e.g., 11). + /// + public int GetDefaultWarningLevel() => Major; } 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 260b7a2d1a2..1b15ae97448 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Language/RazorProjectEngine.cs @@ -253,7 +253,8 @@ private RazorCodeGenerationOptions ComputeCodeGenerationOptions(string? cssScope var builder = new RazorCodeGenerationOptions.Builder() { CssScope = cssScope, - SuppressAddComponentParameter = configuration.SuppressAddComponentParameter + SuppressAddComponentParameter = configuration.SuppressAddComponentParameter, + RazorWarningLevel = configuration.RazorWarningLevel }; configure?.Invoke(builder); 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..a1cba5c258c 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 @@ -12,93 +12,82 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators { internal static class RazorDiagnostics { - public static readonly DiagnosticDescriptor InvalidRazorLangVersionDescriptor = new DiagnosticDescriptor( - DiagnosticIds.InvalidRazorLangVersionRuleId, - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.InvalidRazorLangTitle), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.InvalidRazorLangMessage), 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)), - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.RecomputingTagHelpersMessage), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - "RazorSourceGenerator", - DiagnosticSeverity.Info, - isEnabledByDefault: true); - - public static readonly DiagnosticDescriptor TargetPathNotProvided = new DiagnosticDescriptor( - DiagnosticIds.TargetPathNotProvidedRuleId, - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.TargetPathNotProvidedTitle), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.TargetPathNotProvidedMessage), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - "RazorSourceGenerator", - DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); - - public static readonly DiagnosticDescriptor GeneratedOutputFullPathNotProvided = new DiagnosticDescriptor( - DiagnosticIds.GeneratedOutputFullPathNotProvidedRuleId, - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.GeneratedOutputFullPathNotProvidedTitle), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.GeneratedOutputFullPathNotProvidedMessage), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - "RazorSourceGenerator", - DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); - - public static readonly DiagnosticDescriptor CurrentCompilationReferenceNotFoundDescriptor = new DiagnosticDescriptor( - DiagnosticIds.CurrentCompilationReferenceNotFoundId, - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.CurrentCompilationReferenceNotFoundTitle), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.CurrentCompilationReferenceNotFoundMessage), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - "RazorSourceGenerator", - DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); - - public static readonly DiagnosticDescriptor SkippingGeneratedFileWriteDescriptor = new DiagnosticDescriptor( - DiagnosticIds.SkippingGeneratedFileWriteId, - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.SkippingGeneratedFileWriteTitle), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.SkippingGeneratedFileWriteMessage), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - "RazorSourceGenerator", - DiagnosticSeverity.Warning, - isEnabledByDefault: true - ); - - public static readonly DiagnosticDescriptor SourceTextNotFoundDescriptor = new DiagnosticDescriptor( - DiagnosticIds.SourceTextNotFoundId, - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.SourceTextNotFoundTitle), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.SourceTextNotFoundMessage), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - "RazorSourceGenerator", - DiagnosticSeverity.Error, - isEnabledByDefault: true - ); - - public static readonly DiagnosticDescriptor UnexpectedProjectItemReadCallDescriptor = new DiagnosticDescriptor( - DiagnosticIds.UnexpectedProjectItemReadCallId, - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.UnexpectedProjectItemReadCallTitle), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.UnexpectedProjectItemReadCallMessage), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - "RazorSourceGenerator", - DiagnosticSeverity.Error, - isEnabledByDefault: true - ); - - public static readonly DiagnosticDescriptor InvalidRazorContextComputedDescriptor = new DiagnosticDescriptor( - DiagnosticIds.InvalidRazorContextComputedId, - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.InvalidRazorContextComputedTitle), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.InvalidRazorContextComputedMessage), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - "RazorSourceGenerator", - DiagnosticSeverity.Info, - isEnabledByDefault: true - ); - - public static readonly DiagnosticDescriptor MetadataReferenceNotProvidedDescriptor = new DiagnosticDescriptor( - DiagnosticIds.MetadataReferenceNotProvidedId, - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.MetadataReferenceNotProvidedTitle), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - new LocalizableResourceString(nameof(RazorSourceGeneratorResources.MetadataReferenceNotProvidedMessage), RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), - "RazorSourceGenerator", - DiagnosticSeverity.Info, - isEnabledByDefault: true - ); + public static readonly DiagnosticDescriptor InvalidRazorLangVersionDescriptor = + CreateDescriptor( + DiagnosticIds.InvalidRazorLangVersionRuleId, + nameof(RazorSourceGeneratorResources.InvalidRazorLangTitle), + nameof(RazorSourceGeneratorResources.InvalidRazorLangMessage), + DiagnosticSeverity.Error); + + public static readonly DiagnosticDescriptor InvalidRazorWarningLevelDescriptor = + CreateDescriptor( + DiagnosticIds.InvalidRazorWarningLevelRuleId, + nameof(RazorSourceGeneratorResources.InvalidRazorWarningLevelTitle), + nameof(RazorSourceGeneratorResources.InvalidRazorWarningLevelMessage), + DiagnosticSeverity.Error); + + public static readonly DiagnosticDescriptor ReComputingTagHelpersDescriptor = + CreateDescriptor( + DiagnosticIds.ReComputingTagHelpersRuleId, + nameof(RazorSourceGeneratorResources.RecomputingTagHelpersTitle), + nameof(RazorSourceGeneratorResources.RecomputingTagHelpersMessage), + DiagnosticSeverity.Info); + + public static readonly DiagnosticDescriptor TargetPathNotProvided = + CreateDescriptor( + DiagnosticIds.TargetPathNotProvidedRuleId, + nameof(RazorSourceGeneratorResources.TargetPathNotProvidedTitle), + nameof(RazorSourceGeneratorResources.TargetPathNotProvidedMessage), + DiagnosticSeverity.Warning); + + public static readonly DiagnosticDescriptor GeneratedOutputFullPathNotProvided = + CreateDescriptor( + DiagnosticIds.GeneratedOutputFullPathNotProvidedRuleId, + nameof(RazorSourceGeneratorResources.GeneratedOutputFullPathNotProvidedTitle), + nameof(RazorSourceGeneratorResources.GeneratedOutputFullPathNotProvidedMessage), + DiagnosticSeverity.Warning); + + public static readonly DiagnosticDescriptor CurrentCompilationReferenceNotFoundDescriptor = + CreateDescriptor( + DiagnosticIds.CurrentCompilationReferenceNotFoundId, + nameof(RazorSourceGeneratorResources.CurrentCompilationReferenceNotFoundTitle), + nameof(RazorSourceGeneratorResources.CurrentCompilationReferenceNotFoundMessage), + DiagnosticSeverity.Warning); + + public static readonly DiagnosticDescriptor SkippingGeneratedFileWriteDescriptor = + CreateDescriptor( + DiagnosticIds.SkippingGeneratedFileWriteId, + nameof(RazorSourceGeneratorResources.SkippingGeneratedFileWriteTitle), + nameof(RazorSourceGeneratorResources.SkippingGeneratedFileWriteMessage), + DiagnosticSeverity.Warning); + + public static readonly DiagnosticDescriptor SourceTextNotFoundDescriptor = + CreateDescriptor( + DiagnosticIds.SourceTextNotFoundId, + nameof(RazorSourceGeneratorResources.SourceTextNotFoundTitle), + nameof(RazorSourceGeneratorResources.SourceTextNotFoundMessage), + DiagnosticSeverity.Error); + + public static readonly DiagnosticDescriptor UnexpectedProjectItemReadCallDescriptor = + CreateDescriptor( + DiagnosticIds.UnexpectedProjectItemReadCallId, + nameof(RazorSourceGeneratorResources.UnexpectedProjectItemReadCallTitle), + nameof(RazorSourceGeneratorResources.UnexpectedProjectItemReadCallMessage), + DiagnosticSeverity.Error); + + public static readonly DiagnosticDescriptor InvalidRazorContextComputedDescriptor = + CreateDescriptor( + DiagnosticIds.InvalidRazorContextComputedId, + nameof(RazorSourceGeneratorResources.InvalidRazorContextComputedTitle), + nameof(RazorSourceGeneratorResources.InvalidRazorContextComputedMessage), + DiagnosticSeverity.Info); + + public static readonly DiagnosticDescriptor MetadataReferenceNotProvidedDescriptor = + CreateDescriptor( + DiagnosticIds.MetadataReferenceNotProvidedId, + nameof(RazorSourceGeneratorResources.MetadataReferenceNotProvidedTitle), + nameof(RazorSourceGeneratorResources.MetadataReferenceNotProvidedMessage), + DiagnosticSeverity.Info); public static Diagnostic AsDiagnostic(this RazorDiagnostic razorDiagnostic) { @@ -137,5 +126,16 @@ public static Diagnostic AsDiagnostic(this RazorDiagnostic razorDiagnostic) return Diagnostic.Create(descriptor, location); } + + private static DiagnosticDescriptor CreateDescriptor(string id, string titleResourceName, string messageResourceName, DiagnosticSeverity defaultSeverity) + { + return new DiagnosticDescriptor( + id, + new LocalizableResourceString(titleResourceName, RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), + new LocalizableResourceString(messageResourceName, RazorSourceGeneratorResources.ResourceManager, typeof(RazorSourceGeneratorResources)), + "RazorSourceGenerator", + defaultSeverity, + isEnabledByDefault: true); + } } } 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 9ce6a9a2f0b..1104ddc87d1 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 @@ -123,6 +123,12 @@ Invalid value '{0}' for RazorLangVersion. Valid values include 'Latest', 'Preview', or a valid version in range 1.0 to {1}. + + Invalid RazorWarningLevel + + + Invalid value '{0}' for RazorWarningLevel. Must be empty or a non-negative 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 01e769c1f3c..6a9be9ce198 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; @@ -52,6 +53,20 @@ internal static IncrementalValueProvider ReportDiagnostics(thi return source.Select((pair, ct) => pair.Item1!); } + + internal static IncrementalValueProvider ReportDiagnostics(this IncrementalValueProvider<(TSource?, ImmutableArray)> source, IncrementalGeneratorInitializationContext context) + { + context.RegisterSourceOutput(source, static (spc, source) => + { + var (_, diagnostics) = source; + foreach (var diagnostic in diagnostics) + { + spc.ReportDiagnostic(diagnostic); + } + }); + + return source.Select(static (pair, ct) => pair.Item1!); + } } internal sealed class LambdaComparer : IEqualityComparer 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 d9759cecc3c..5f3ca8c6ae5 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 @@ -8,6 +8,7 @@ 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; @@ -17,7 +18,7 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators { public partial class RazorSourceGenerator { - private (RazorSourceGenerationOptions?, Diagnostic?) ComputeRazorSourceGeneratorOptions(((AnalyzerConfigOptionsProvider, ParseOptions), ImmutableArray) pair, CancellationToken ct) + private (RazorSourceGenerationOptions?, ImmutableArray) ComputeRazorSourceGeneratorOptions(((AnalyzerConfigOptionsProvider, ParseOptions), ImmutableArray) pair, CancellationToken ct) { var ((options, parseOptions), references) = pair; var globalOptions = options.GlobalOptions; @@ -29,17 +30,10 @@ public partial class RazorSourceGenerator globalOptions.TryGetValue("build_property.SupportLocalizedComponentNames", out var supportLocalizedComponentNames); globalOptions.TryGetValue("build_property.GenerateRazorMetadataSourceChecksumAttributes", out var generateMetadataSourceChecksumAttributes); - Diagnostic? diagnostic = null; - if (!globalOptions.TryGetValue("build_property.RazorLangVersion", out var razorLanguageVersionString) || - !RazorLanguageVersion.TryParse(razorLanguageVersionString, out var razorLanguageVersion)) - { - diagnostic = Diagnostic.Create( - RazorDiagnostics.InvalidRazorLangVersionDescriptor, - Location.None, - razorLanguageVersionString, - RazorLanguageVersion.Preview.ToString()); - razorLanguageVersion = RazorLanguageVersion.Latest; - } + using var diagnostics = new PooledArrayBuilder(capacity: 2); + + var razorLanguageVersion = ParseRazorLanguageVersion(globalOptions, ref diagnostics.AsRef()); + var razorWarningLevel = ParseRazorWarningLevel(globalOptions, razorLanguageVersion, ref diagnostics.AsRef()); var minimalReferences = references .Where(r => r.Display is { } display && display.EndsWith("Microsoft.AspNetCore.Components.dll", StringComparison.Ordinal)) @@ -49,7 +43,7 @@ public partial class RazorSourceGenerator ? false : CSharpCompilation.Create("components", references: minimalReferences).HasAddComponentParameter(); - var razorConfiguration = new RazorConfiguration(razorLanguageVersion, configurationName ?? "default", Extensions: [], UseConsolidatedMvcViews: true, SuppressAddComponentParameter: !isComponentParameterSupported); + var razorConfiguration = new RazorConfiguration(razorLanguageVersion, configurationName ?? "default", Extensions: [], UseConsolidatedMvcViews: true, SuppressAddComponentParameter: !isComponentParameterSupported, RazorWarningLevel: razorWarningLevel); // We use the new tokenizer only when requested for now. var useRoslynTokenizer = parseOptions.UseRoslynTokenizer(); @@ -65,7 +59,50 @@ public partial class RazorSourceGenerator UseRoslynTokenizer = useRoslynTokenizer, }; - return (razorSourceGenerationOptions, diagnostic); + return (razorSourceGenerationOptions, diagnostics.ToImmutableAndClear()); + } + + private static RazorLanguageVersion ParseRazorLanguageVersion(AnalyzerConfigOptions globalOptions, ref PooledArrayBuilder diagnostics) + { + if (!globalOptions.TryGetValue("build_property.RazorLangVersion", out var razorLanguageVersionString) || + !RazorLanguageVersion.TryParse(razorLanguageVersionString, out var razorLanguageVersion)) + { + diagnostics.Add(Diagnostic.Create( + RazorDiagnostics.InvalidRazorLangVersionDescriptor, + Location.None, + razorLanguageVersionString, + RazorLanguageVersion.Preview.ToString())); + return RazorLanguageVersion.Latest; + } + + return razorLanguageVersion; + } + + private static int ParseRazorWarningLevel(AnalyzerConfigOptions globalOptions, RazorLanguageVersion razorLanguageVersion, ref PooledArrayBuilder diagnostics) + { + if (!globalOptions.TryGetValue("build_property.RazorWarningLevel", out var razorWarningLevelString)) + { + // Property not registered - old SDK that doesn't know about warning waves. + // Default to 0 so no wave-gated warnings are reported. + return 0; + } + + if (string.IsNullOrEmpty(razorWarningLevelString)) + { + // Property registered but not set - new SDK, use language version default. + return razorLanguageVersion.GetDefaultWarningLevel(); + } + + if (int.TryParse(razorWarningLevelString, out var parsedLevel) && parsedLevel >= 0) + { + return parsedLevel; + } + + diagnostics.Add(Diagnostic.Create( + RazorDiagnostics.InvalidRazorWarningLevelDescriptor, + Location.None, + razorWarningLevelString)); + return razorLanguageVersion.GetDefaultWarningLevel(); } private static (SourceGeneratorProjectItem?, Diagnostic?) ComputeProjectItems((AdditionalText, AnalyzerConfigOptionsProvider) pair, CancellationToken ct) 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 c4247771986..e164f89cb3f 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 @@ -2726,6 +2726,67 @@ public async Task RazorLangVersion_Incorrect([CombinatorialValues("incorrect", " Assert.Single(result.GeneratedSources); } + [Theory, CombinatorialData] + public async Task RazorWarningLevel_Incorrect( + [CombinatorialValues("incorrect", "1.2", "0x1", "-1")] 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 RZ3601: Invalid value '{0}' for RazorWarningLevel. Must be empty or a non-negative integer. + Diagnostic("RZ3601").WithArguments(warningLevel).WithLocation(1, 1)); + Assert.Single(result.GeneratedSources); + } + + [Theory, CombinatorialData] + public async Task RazorWarningLevel_ValidValues( + [CombinatorialValues("0", "10", "11", "9999")] 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(); + Assert.Single(result.GeneratedSources); + } + + [Fact] + public async Task RazorWarningLevel_Empty_IsValid() + { + 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"] = ""; + }); + + var result = RunGenerator(compilation!, ref driver); + + result.Diagnostics.Verify(); + Assert.Single(result.GeneratedSources); + } + #pragma warning disable RS1041 // This compiler extension should not be implemented in an assembly with target framework '.NET 8.0'. References to other target frameworks will cause the compiler to behave unpredictably. #pragma warning disable RS1038 // This compiler extension should not be implemented in an assembly containing a reference to Microsoft.CodeAnalysis.Workspaces. [Generator]