diff --git a/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs index e6b5ec23cb21f2edd98f026fd15490d264cba47c..7d13678c91bc30d790bd1f46e46406195e9079da 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnnecessarySuppressions/AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs @@ -99,10 +99,8 @@ protected sealed override void InitializeWorker(AnalysisContext context) // Bail out if analyzer is suppressed on this file or project. // NOTE: Normally, we would not require this check in the analyzer as the analyzer driver has this optimization. // However, this is a special analyzer that is directly invoked by the analysis host (IDE), so we do this check here. - ReportDiagnostic severity; - if ( - compilationWithAnalyzers.Compilation.Options.SyntaxTreeOptionsProvider != null && - compilationWithAnalyzers.Compilation.Options.SyntaxTreeOptionsProvider.TryGetDiagnosticValue(tree, IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId, cancellationToken, out severity) || + if (compilationWithAnalyzers.Compilation.Options.SyntaxTreeOptionsProvider != null && + compilationWithAnalyzers.Compilation.Options.SyntaxTreeOptionsProvider.TryGetDiagnosticValue(tree, IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId, cancellationToken, out var severity) || compilationWithAnalyzers.Compilation.Options.SpecificDiagnosticOptions.TryGetValue(IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId, out severity)) { if (severity == ReportDiagnostic.Suppress) @@ -114,7 +112,7 @@ protected sealed override void InitializeWorker(AnalysisContext context) // Bail out if analyzer has been turned off through options. var option = compilationWithAnalyzers.AnalysisOptions.Options?.GetOption( CodeStyleOptions2.RemoveUnnecessarySuppressionExclusions, tree, cancellationToken).Trim(); - var (userExclusions, analyzerDisabled) = ParseUserExclusions(option); + var (userIdExclusions, userCategoryExclusions, analyzerDisabled) = ParseUserExclusions(option); if (analyzerDisabled) { return; @@ -161,14 +159,14 @@ protected sealed override void InitializeWorker(AnalysisContext context) using var _3 = PooledDictionary.GetInstance(out var pragmasToIsUsedMap); using var _4 = PooledHashSet.GetInstance(out var compilerDiagnosticIds); var hasPragmaInAnalysisSpan = ProcessPragmaDirectives(root, span, idToPragmasMap, - pragmasToIsUsedMap, sortedPragmasWithIds, compilerDiagnosticIds, userExclusions); + pragmasToIsUsedMap, sortedPragmasWithIds, compilerDiagnosticIds, userIdExclusions); cancellationToken.ThrowIfCancellationRequested(); using var _5 = PooledDictionary>.GetInstance(out var idToSuppressMessageAttributesMap); using var _6 = PooledDictionary.GetInstance(out var suppressMessageAttributesToIsUsedMap); var hasAttributeInAnalysisSpan = await ProcessSuppressMessageAttributesAsync(root, semanticModel, span, - idToSuppressMessageAttributesMap, suppressMessageAttributesToIsUsedMap, userExclusions, cancellationToken).ConfigureAwait(false); + idToSuppressMessageAttributesMap, suppressMessageAttributesToIsUsedMap, userIdExclusions, userCategoryExclusions, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); @@ -345,35 +343,47 @@ private static bool IsSupportedAnalyzerDiagnosticId(string id) } } - private static (ImmutableArray userExclusions, bool analyzerDisabled) ParseUserExclusions(string? userExclusions) + private static (ImmutableArray userIdExclusions, ImmutableArray userCategoryExclusions, bool analyzerDisabled) ParseUserExclusions(string? userExclusions) { - // Option value must be a comma separate list of diagnostic IDs to exclude from unnecessary pragma analysis. + // Option value must be a comma separate list of diagnostic IDs or categories (with a "category:" prefix) to exclude from unnecessary pragma analysis. // We also allow a special keyword "all" to disable the analyzer completely. switch (userExclusions) { case "": case null: - return (userExclusions: ImmutableArray.Empty, analyzerDisabled: false); + return (userIdExclusions: ImmutableArray.Empty, userCategoryExclusions: ImmutableArray.Empty, analyzerDisabled: false); case "all": - return (userExclusions: ImmutableArray.Empty, analyzerDisabled: true); + return (userIdExclusions: ImmutableArray.Empty, userCategoryExclusions: ImmutableArray.Empty, analyzerDisabled: true); default: // Default string representation for unconfigured option value should be treated as no exclusions. if (userExclusions == CodeStyleOptions2.RemoveUnnecessarySuppressionExclusions.DefaultValue) - return (userExclusions: ImmutableArray.Empty, analyzerDisabled: false); + return (userIdExclusions: ImmutableArray.Empty, userCategoryExclusions: ImmutableArray.Empty, analyzerDisabled: false); break; } - using var _ = ArrayBuilder.GetInstance(out var builder); + // We allow excluding category of diagnostics with a category prefix, for example "category: ExcludedCategory". + const string categoryPrefix = "category:"; + + using var _1 = ArrayBuilder.GetInstance(out var idBuilder); + using var _2 = ArrayBuilder.GetInstance(out var categoryBuilder); foreach (var part in userExclusions.Split(',')) { var trimmedPart = part.Trim(); - builder.Add(trimmedPart); + if (trimmedPart.StartsWith(categoryPrefix, StringComparison.OrdinalIgnoreCase)) + { + trimmedPart = trimmedPart[categoryPrefix.Length..].Trim(); + categoryBuilder.Add(trimmedPart); + } + else + { + idBuilder.Add(trimmedPart); + } } - return (userExclusions: builder.ToImmutable(), analyzerDisabled: false); + return (userIdExclusions: idBuilder.ToImmutable(), userCategoryExclusions: categoryBuilder.ToImmutable(), analyzerDisabled: false); } private static async Task<(ImmutableArray reportedDiagnostics, ImmutableArray unhandledIds)> GetReportedDiagnosticsForIdsAsync( @@ -695,7 +705,8 @@ private static (ImmutableArray userExclusions, bool analyzerDisabled) Pa TextSpan? span, PooledDictionary> idToSuppressMessageAttributesMap, PooledDictionary suppressMessageAttributesToIsUsedMap, - ImmutableArray userExclusions, + ImmutableArray userIdExclusions, + ImmutableArray userCategoryExclusions, CancellationToken cancellationToken) { var suppressMessageAttributeType = semanticModel.Compilation.SuppressMessageAttributeType(); @@ -742,11 +753,12 @@ private static (ImmutableArray userExclusions, bool analyzerDisabled) Pa foreach (var attribute in symbol.GetAttributes()) { if (attribute.ApplicationSyntaxReference != null && - TryGetSuppressedDiagnosticId(attribute, suppressMessageAttributeType, out var id)) + TryGetSuppressedDiagnosticId(attribute, suppressMessageAttributeType, out var id, out var category)) { // Ignore unsupported IDs and those excluded through user option. if (!IsSupportedAnalyzerDiagnosticId(id) || - userExclusions.Contains(id, StringComparer.OrdinalIgnoreCase)) + userIdExclusions.Contains(id, StringComparer.OrdinalIgnoreCase) || + category?.Length > 0 && userCategoryExclusions.Contains(category, StringComparer.OrdinalIgnoreCase)) { continue; } @@ -779,21 +791,38 @@ private static (ImmutableArray userExclusions, bool analyzerDisabled) Pa private static bool TryGetSuppressedDiagnosticId( AttributeData attribute, INamedTypeSymbol suppressMessageAttributeType, - [NotNullWhen(returnValue: true)] out string? id) + [NotNullWhen(returnValue: true)] out string? id, + out string? category) { + category = null; + if (suppressMessageAttributeType.Equals(attribute.AttributeClass) && attribute.AttributeConstructor?.Parameters.Length >= 2 && attribute.AttributeConstructor.Parameters[1].Name == "checkId" && attribute.AttributeConstructor.Parameters[1].Type.SpecialType == SpecialType.System_String && attribute.ConstructorArguments.Length >= 2 && - attribute.ConstructorArguments[1] is { } typedConstant && - typedConstant.Kind == TypedConstantKind.Primitive && - typedConstant.Value is string checkId) + attribute.ConstructorArguments[1] is + { + Kind: TypedConstantKind.Primitive, + Value: string checkId + }) { // CheckId represents diagnostic ID, followed by an option ':' and name. // For example, "CA1801:ReviewUnusedParameters" var index = checkId.IndexOf(':'); id = index > 0 ? checkId.Substring(0, index) : checkId; + + if (attribute.AttributeConstructor.Parameters[0].Name == "category" && + attribute.AttributeConstructor.Parameters[0].Type.SpecialType == SpecialType.System_String && + attribute.ConstructorArguments[0] is + { + Kind: TypedConstantKind.Primitive, + Value: string categoryArg + }) + { + category = categoryArg; + } + return id.Length > 0; } diff --git a/src/EditorFeatures/CSharpTest/Diagnostics/Suppression/RemoveUnnecessaryPragmaSuppressionsTests.cs b/src/EditorFeatures/CSharpTest/Diagnostics/Suppression/RemoveUnnecessaryPragmaSuppressionsTests.cs index 88cdc4f8a5fd7121b3f5920e13f33dbdc9ee8c46..b23f35ef392f8333530aa02f6ca245721aaf27bd 100644 --- a/src/EditorFeatures/CSharpTest/Diagnostics/Suppression/RemoveUnnecessaryPragmaSuppressionsTests.cs +++ b/src/EditorFeatures/CSharpTest/Diagnostics/Suppression/RemoveUnnecessaryPragmaSuppressionsTests.cs @@ -412,6 +412,33 @@ void M() |]", new TestParameters(options: options)); } + [Fact, WorkItem(47288, "https://github.com/dotnet/roslyn/issues/47288")] + public async Task TestDoNotRemoveExcludedDiagnosticCategorySuppression() + { + var options = new OptionsCollection(LanguageNames.CSharp) + { + { CodeStyleOptions2.RemoveUnnecessarySuppressionExclusions, "category: ExcludedCategory" } + }; + + await TestMissingInRegularAndScriptAsync( + $@" +[| +class Class +{{ + [System.Diagnostics.CodeAnalysis.SuppressMessage(""ExcludedCategory"", ""{VariableDeclaredButNotUsedDiagnosticId}"")] + [System.Diagnostics.CodeAnalysis.SuppressMessage(""ExcludedCategory"", ""{VariableAssignedButNotUsedDiagnosticId}"")] + void M() + {{ + int y; + y = 1; + + int z = 1; + z++; + }} +}} +|]", new TestParameters(options: options)); + } + [Fact] public async Task TestDoNotRemoveDiagnosticSuppression_Attribute_OnPartialDeclarations() {