File: src\Analyzers\Core\Analyzers\RemoveUnnecessarySuppressions\AbstractRemoveUnnecessaryAttributeSuppressionsDiagnosticAnalyzer.cs
Web Access
Project: src\src\CodeStyle\Core\Analyzers\Microsoft.CodeAnalysis.CodeStyle.csproj (Microsoft.CodeAnalysis.CodeStyle)
// 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;
            }
        }
    }
}