// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Immutable; using System.Threading; using Microsoft.CodeAnalysis.CodeQuality; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.RemoveUnnecessarySuppressions; internal abstract class AbstractRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer : AbstractCodeQualityDiagnosticAnalyzer { internal const string DocCommentIdKey = nameof(DocCommentIdKey); private static readonly LocalizableResourceString s_localizableTitle = new( nameof(AnalyzersResources.Invalid_global_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); private static readonly LocalizableResourceString s_localizableInvalidScopeMessage = new( nameof(AnalyzersResources.Invalid_scope_for_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); private static readonly LocalizableResourceString s_localizableInvalidOrMissingTargetMessage = new( nameof(AnalyzersResources.Invalid_or_missing_target_for_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); private static readonly DiagnosticDescriptor s_invalidScopeDescriptor = CreateDescriptor( IDEDiagnosticIds.InvalidSuppressMessageAttributeDiagnosticId, EnforceOnBuildValues.InvalidSuppressMessageAttribute, s_localizableTitle, s_localizableInvalidScopeMessage, hasAnyCodeStyleOption: false, isUnnecessary: true); private static readonly DiagnosticDescriptor s_invalidOrMissingTargetDescriptor = CreateDescriptor( IDEDiagnosticIds.InvalidSuppressMessageAttributeDiagnosticId, EnforceOnBuildValues.InvalidSuppressMessageAttribute, s_localizableTitle, s_localizableInvalidOrMissingTargetMessage, hasAnyCodeStyleOption: false, isUnnecessary: true); private static readonly LocalizableResourceString s_localizableLegacyFormatTitle = new( nameof(AnalyzersResources.Avoid_legacy_format_target_in_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); private static readonly LocalizableResourceString s_localizableLegacyFormatMessage = new( nameof(AnalyzersResources.Avoid_legacy_format_target_0_in_SuppressMessageAttribute), AnalyzersResources.ResourceManager, typeof(AnalyzersResources)); internal static readonly DiagnosticDescriptor LegacyFormatTargetDescriptor = CreateDescriptor( IDEDiagnosticIds.LegacyFormatSuppressMessageAttributeDiagnosticId, EnforceOnBuildValues.LegacyFormatSuppressMessageAttribute, s_localizableLegacyFormatTitle, s_localizableLegacyFormatMessage, hasAnyCodeStyleOption: false, isUnnecessary: false); protected AbstractRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer() : base([s_invalidScopeDescriptor, s_invalidOrMissingTargetDescriptor, LegacyFormatTargetDescriptor], GeneratedCodeAnalysisFlags.None) { } protected abstract void RegisterAttributeSyntaxAction(CompilationStartAnalysisContext context, CompilationAnalyzer compilationAnalyzer); public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; protected sealed override void InitializeWorker(AnalysisContext context) { context.RegisterCompilationStartAction(context => { var suppressMessageAttributeType = context.Compilation.SuppressMessageAttributeType(); if (suppressMessageAttributeType == null) { return; } RegisterAttributeSyntaxAction(context, new CompilationAnalyzer(context.Compilation, suppressMessageAttributeType)); }); } protected sealed class CompilationAnalyzer(Compilation compilation, INamedTypeSymbol suppressMessageAttributeType) { private readonly SuppressMessageAttributeState _state = new SuppressMessageAttributeState(compilation, suppressMessageAttributeType); public void AnalyzeAssemblyOrModuleAttribute(SyntaxNode attributeSyntax, SemanticModel model, Action<Diagnostic> reportDiagnostic, CancellationToken cancellationToken) { if (!_state.IsSuppressMessageAttributeWithNamedArguments(attributeSyntax, model, cancellationToken, out var namedAttributeArguments)) { return; } if (!SuppressMessageAttributeState.HasValidScope(namedAttributeArguments, out var targetScope)) { reportDiagnostic(Diagnostic.Create(s_invalidScopeDescriptor, attributeSyntax.GetLocation())); return; } if (!_state.HasValidTarget(namedAttributeArguments, targetScope, out var targetHasDocCommentIdFormat, out var targetSymbolString, out var targetValueOperation, out var resolvedSymbols)) { reportDiagnostic(Diagnostic.Create(s_invalidOrMissingTargetDescriptor, attributeSyntax.GetLocation())); return; } // We want to flag valid target which uses legacy format to update to Roslyn based DocCommentId format. if (resolvedSymbols.Length > 0 && !targetHasDocCommentIdFormat) { RoslynDebug.Assert(!string.IsNullOrEmpty(targetSymbolString)); RoslynDebug.Assert(targetValueOperation != null); var properties = ImmutableDictionary<string, string?>.Empty; if (resolvedSymbols is [var resolvedSymbol]) { // We provide a code fix for the case where the target resolved to a single symbol. var docCommentId = DocumentationCommentId.CreateDeclarationId(resolvedSymbol); if (!string.IsNullOrEmpty(docCommentId)) { // Suppression target has an optional "~" prefix to distinguish it from legacy FxCop suppressions. // IDE suppression code fixes emit this prefix, so we we also add this prefix to new suppression target string. properties = properties.Add(DocCommentIdKey, "~" + docCommentId); } } reportDiagnostic(Diagnostic.Create(LegacyFormatTargetDescriptor, targetValueOperation.Syntax.GetLocation(), properties!, targetSymbolString)); return; } } } } |