File: src\roslyn\src\Analyzers\Core\Analyzers\RemoveUnnecessarySuppressions\AbstractRemoveUnnecessaryPragmaSuppressionsDiagnosticAnalyzer.cs
Web Access
Project: src\roslyn\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.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeQuality;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.RemoveUnnecessarySuppressions;

internal abstract class AbstractRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer
    : AbstractCodeQualityDiagnosticAnalyzer, IPragmaSuppressionsAnalyzer
{
    private static readonly LocalizableResourceString s_localizableRemoveUnnecessarySuppression = new(
       nameof(AnalyzersResources.Remove_unnecessary_suppression), AnalyzersResources.ResourceManager, typeof(AnalyzersResources));
    internal static readonly DiagnosticDescriptor s_removeUnnecessarySuppressionDescriptor = CreateDescriptor(
        IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId,
        EnforceOnBuildValues.RemoveUnnecessaryPragmaSuppression,
        s_localizableRemoveUnnecessarySuppression, s_localizableRemoveUnnecessarySuppression,
        hasAnyCodeStyleOption: false, isUnnecessary: true);

    private readonly Lazy<ImmutableHashSet<int>> _lazySupportedCompilerErrorCodes;

    protected AbstractRemoveUnnecessaryInlineSuppressionsDiagnosticAnalyzer()
        : base([s_removeUnnecessarySuppressionDescriptor], GeneratedCodeAnalysisFlags.None)
    {
        _lazySupportedCompilerErrorCodes = new Lazy<ImmutableHashSet<int>>(GetSupportedCompilerErrorCodes);
    }

    protected abstract string CompilerErrorCodePrefix { get; }
    protected abstract int CompilerErrorCodeDigitCount { get; }
    protected abstract ISyntaxFacts SyntaxFacts { get; }
    protected abstract ISemanticFacts SemanticFacts { get; }
    protected abstract (Assembly assembly, string typeName) GetCompilerDiagnosticAnalyzerInfo();
    protected abstract bool ContainsPragmaDirective(SyntaxNode root);

    private ImmutableHashSet<int> GetSupportedCompilerErrorCodes()
    {
        try
        {
            // Use reflection to fetch compiler diagnostic IDs that are supported in IDE live analysis.
            // Note that the unit test projects have IVT access to compiler layer, and hence can access this API.
            // We have unit tests that guard this reflection based logic and will fail if the API is changed
            // without updating the below code.

            var (assembly, compilerAnalyzerTypeName) = GetCompilerDiagnosticAnalyzerInfo();
            var compilerAnalyzerType = assembly.GetType(compilerAnalyzerTypeName)!;
            var methodInfo = compilerAnalyzerType.GetMethod("GetSupportedErrorCodes", BindingFlags.Instance | BindingFlags.NonPublic)!;
            var compilerAnalyzerInstance = Activator.CreateInstance(compilerAnalyzerType);
            var supportedCodes = methodInfo.Invoke(compilerAnalyzerInstance, []) as IEnumerable<int>;
            return supportedCodes?.ToImmutableHashSet() ?? [];
        }
        catch (Exception ex)
        {
            Debug.Fail(ex.Message);
            return [];
        }
    }

    public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory() => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis;

    protected sealed override void InitializeWorker(AnalysisContext context)
    {
        // We do not register any normal analyzer actions as we need 'CompilationWithAnalyzers'
        // context to analyze unused suppressions using reported compiler and analyzer diagnostics.
        // Instead, the analyzer defines a special 'AnalyzeAsync' method that should be invoked
        // by the host with CompilationWithAnalyzers input to compute unused suppression diagnostics.
    }

    public async Task AnalyzeAsync(
        SemanticModel semanticModel,
        TextSpan? span,
        CompilationWithAnalyzers compilationWithAnalyzers,
        Func<DiagnosticAnalyzer, ImmutableArray<DiagnosticDescriptor>> getSupportedDiagnostics,
        Action<Diagnostic> reportDiagnostic,
        CancellationToken cancellationToken)
    {
        // We need compilation with suppressed diagnostics for this feature.
        if (!compilationWithAnalyzers.Compilation.Options.ReportSuppressedDiagnostics)
        {
            return;
        }

        var tree = semanticModel.SyntaxTree;

        // 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.
        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)
            {
                return;
            }
        }

        // Bail out if analyzer has been turned off through options.
        var option = compilationWithAnalyzers.AnalysisOptions.Options?.GetAnalyzerOptions(tree).RemoveUnnecessarySuppressionExclusions.Trim();
        var (userIdExclusions, userCategoryExclusions, analyzerDisabled) = ParseUserExclusions(option);
        if (analyzerDisabled)
        {
            return;
        }

        // Bail out for generated code.
        if (tree.IsGeneratedCode(compilationWithAnalyzers.AnalysisOptions.Options, SyntaxFacts, cancellationToken))
        {
            return;
        }

        var root = tree.GetRoot(cancellationToken);

        // Bail out if tree has syntax errors.
        if (root.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error))
        {
            return;
        }

        // Process pragma directives and inline SuppressMessageAttributes in the tree.
        // The core algorithm is as follows:
        //  1. Iterate through all the active pragmas and local SuppressMessageAttributes in the source file and
        //     identify the pragmas and local SuppressMessageAttributes
        //     with diagnostics IDs for which we support unnecessary suppression analysis.
        //  2. Build the following data structures during this loop:
        //      a. A map from diagnostic ID to list of pragmas for the ID. This map tracks supported diagnostic IDs for this tree's pragmas.
        //      b. A array of tuples of candidate pragmas sorted by span, along with associated IDs and enable/disable flag.
        //         This sorted array allows mapping an unnecessary pragma to the corresponding toggling pragma pair for removal.
        //      c. A map from pragmas to a boolean indicating if the pragma was used or not.
        //      d. A map from diagnostic ID to list of SuppressMessageAttribute nodes for the ID.
        //         This map tracks supported diagnostic IDs for this tree's SuppressMessageAttribute nodes.
        //      e. A map from SuppressMessageAttribute nodes to a boolean indicating if the attribute was used or not.
        //      f. A set of supported compiler diagnostic IDs that are used in pragmas or SuppressMessageAttributes in this file.
        //  3. Map the set of candidate diagnostic IDs to the analyzers that can report diagnostics with these IDs.
        //  4. Execute these analyzers to compute the diagnostics reported by these analyzers in this file.
        //  5. Iterate through the suppressed diagnostics from this list and do the following:
        //     a. If the diagnostic was suppressed with a pragma, mark the closest preceding disable pragma
        //        which suppresses this ID as used/necessary. Also mark the matching restore pragma as used.
        //     b. Otherwise, if the diagnostic was suppressed with SuppressMessageAttribute, mark the attribute as used. 
        //  6. Finally, report a diagnostic all the pragmas and SuppressMessageAttributes which have not been marked as used.

        using var _1 = PooledDictionary<string, List<(SyntaxTrivia pragma, bool isDisable)>>.GetInstance(out var idToPragmasMap);
        using var _2 = ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray<string> ids, bool isDisable)>.GetInstance(out var sortedPragmasWithIds);
        using var _3 = PooledDictionary<SyntaxTrivia, bool>.GetInstance(out var pragmasToIsUsedMap);
        using var _4 = PooledHashSet<string>.GetInstance(out var compilerDiagnosticIds);
        var hasPragmaInAnalysisSpan = ProcessPragmaDirectives(root, span, idToPragmasMap,
            pragmasToIsUsedMap, sortedPragmasWithIds, compilerDiagnosticIds, userIdExclusions);

        cancellationToken.ThrowIfCancellationRequested();

        using var _5 = PooledDictionary<string, List<SyntaxNode>>.GetInstance(out var idToSuppressMessageAttributesMap);
        using var _6 = PooledDictionary<SyntaxNode, bool>.GetInstance(out var suppressMessageAttributesToIsUsedMap);
        var hasAttributeInAnalysisSpan = await ProcessSuppressMessageAttributesAsync(root, semanticModel, span,
            idToSuppressMessageAttributesMap, suppressMessageAttributesToIsUsedMap, userIdExclusions, userCategoryExclusions, cancellationToken).ConfigureAwait(false);

        cancellationToken.ThrowIfCancellationRequested();

        // Bail out if we have no pragma directives or SuppressMessageAttributes to analyze.
        if (!hasPragmaInAnalysisSpan && !hasAttributeInAnalysisSpan)
        {
            return;
        }

        using var _8 = PooledHashSet<string>.GetInstance(out var idsToAnalyzeBuilder);
        idsToAnalyzeBuilder.AddAll(idToPragmasMap.Keys);
        idsToAnalyzeBuilder.AddAll(idToSuppressMessageAttributesMap.Keys);
        var idsToAnalyze = idsToAnalyzeBuilder.ToImmutableHashSet();

        // Compute all the reported compiler and analyzer diagnostics for diagnostic IDs corresponding to pragmas in the tree.
        var (diagnostics, unhandledIds) = await GetReportedDiagnosticsForIdsAsync(
            idsToAnalyze, root, semanticModel, compilationWithAnalyzers,
            getSupportedDiagnostics, compilerDiagnosticIds, cancellationToken).ConfigureAwait(false);

        cancellationToken.ThrowIfCancellationRequested();

        // Iterate through reported diagnostics which are suppressed in source through pragmas and mark the corresponding pragmas as used.
        await ProcessReportedDiagnosticsAsync(diagnostics, tree, compilationWithAnalyzers, idToPragmasMap,
            pragmasToIsUsedMap, idToSuppressMessageAttributesMap, suppressMessageAttributesToIsUsedMap, cancellationToken).ConfigureAwait(false);

        cancellationToken.ThrowIfCancellationRequested();

        // Remove entries for unhandled diagnostic ids.
        foreach (var id in unhandledIds)
        {
            if (idToPragmasMap.TryGetValue(id, out var pragmas))
            {
                foreach (var (pragma, _) in pragmas)
                {
                    pragmasToIsUsedMap.Remove(pragma);
                }
            }

            if (idToSuppressMessageAttributesMap.TryGetValue(id, out var attributeNodes))
            {
                foreach (var attributeNode in attributeNodes)
                {
                    suppressMessageAttributesToIsUsedMap.Remove(attributeNode);
                }

                idToSuppressMessageAttributesMap.Remove(id);
            }
        }

        // Finally, report the unnecessary suppressions.
        var effectiveSeverity = severity.ToDiagnosticSeverity() ?? s_removeUnnecessarySuppressionDescriptor.DefaultSeverity;
        ReportUnnecessarySuppressions(pragmasToIsUsedMap, sortedPragmasWithIds,
            suppressMessageAttributesToIsUsedMap, reportDiagnostic, effectiveSeverity, compilationWithAnalyzers.Compilation);
    }

    private bool ProcessPragmaDirectives(
        SyntaxNode root,
        TextSpan? span,
        PooledDictionary<string, List<(SyntaxTrivia pragma, bool isDisable)>> idToPragmasMap,
        PooledDictionary<SyntaxTrivia, bool> pragmasToIsUsedMap,
        ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray<string> ids, bool isDisable)> sortedPragmasWithIds,
        PooledHashSet<string> compilerDiagnosticIds,
        ImmutableArray<string> userExclusions)
    {
        if (!ContainsPragmaDirective(root))
        {
            return false;
        }

        using var _ = ArrayBuilder<string>.GetInstance(out var idsBuilder);
        var hasPragmaInAnalysisSpan = false;
        foreach (var trivia in root.DescendantTrivia(node => node.ContainsDirectives))
        {
            // Check if this is an active pragma with at least one applicable diagnostic ID/error code.
            // Note that a pragma can have multiple error codes, such as '#pragma warning disable ID0001, ID0002'
            if (SyntaxFacts.IsPragmaDirective(trivia, out var isDisable, out var isActive, out var errorCodeNodes) &&
                isActive &&
                errorCodeNodes.Count > 0)
            {
                // Iterate through each ID for this pragma and build the supported IDs.
                idsBuilder.Clear();
                foreach (var errorCodeNode in errorCodeNodes)
                {
                    // Ignore unsupported IDs and those excluded through user option.
                    if (!IsSupportedId(errorCodeNode, out var id, out var isCompilerDiagnosticId) ||
                        userExclusions.Contains(id, StringComparer.OrdinalIgnoreCase))
                    {
                        continue;
                    }

                    idsBuilder.Add(id);
                    if (isCompilerDiagnosticId)
                    {
                        compilerDiagnosticIds.Add(id);
                    }

                    // Add entry to idToPragmasMap
                    // Insert the pragmas in reverse order for easier processing later.
                    if (!idToPragmasMap.TryGetValue(id, out var pragmasForIdInReverseOrder))
                    {
                        pragmasForIdInReverseOrder = [];
                        idToPragmasMap.Add(id, pragmasForIdInReverseOrder);
                    }

                    pragmasForIdInReverseOrder.Insert(0, (trivia, isDisable));
                }

                if (idsBuilder.Count == 0)
                {
                    // No supported ID in this pragma.
                    continue;
                }

                hasPragmaInAnalysisSpan = hasPragmaInAnalysisSpan || !span.HasValue || span.Value.OverlapsWith(trivia.Span);

                sortedPragmasWithIds.Add((trivia, idsBuilder.ToImmutable(), isDisable));

                // Pragma directive is initialized as unnecessary at the start of the algorithm (value = false).
                // We will subsequently find required/used pragmas and update the entries in this map (value = true).
                pragmasToIsUsedMap.Add(trivia, false);
            }
        }

        return hasPragmaInAnalysisSpan;
    }

    private bool IsSupportedId(
        SyntaxNode idNode,
        [NotNullWhen(returnValue: true)] out string? id,
        out bool isCompilerDiagnosticId)
    {
        id = idNode.ToString();

        // Compiler diagnostic pragma suppressions allow specifying just the integral ID.
        // For example:
        //      "#pragma warning disable 0168" OR "#pragma warning disable 168"
        // is equivalent to
        //      "#pragma warning disable CS0168"
        // We handle all the three supported formats for compiler diagnostic pragmas.

        var idWithoutPrefix = id.StartsWith(CompilerErrorCodePrefix) && id.Length == CompilerErrorCodePrefix.Length + CompilerErrorCodeDigitCount
            ? id[CompilerErrorCodePrefix.Length..]
            : id;

        // ID without prefix should parse as an integer for compiler diagnostics.
        if (int.TryParse(idWithoutPrefix, out var errorCode))
        {
            // Normalize the ID to always be in the format with prefix.
            id = CompilerErrorCodePrefix + errorCode.ToString($"D{CompilerErrorCodeDigitCount}");
            isCompilerDiagnosticId = true;
            return _lazySupportedCompilerErrorCodes.Value.Contains(errorCode);
        }

        isCompilerDiagnosticId = false;
        return IsSupportedAnalyzerDiagnosticId(id) &&
            idWithoutPrefix == id;
    }

    private static bool IsSupportedAnalyzerDiagnosticId(string id)
    {
        switch (id)
        {
            case IDEDiagnosticIds.RemoveUnnecessarySuppressionDiagnosticId:
                // Not supported as this would lead to recursion in computation.
                return false;

            case "format":
            case IDEDiagnosticIds.FormattingDiagnosticId:
                // Formatting analyzer is not supported as the analyzer does not seem to return suppressed IDE0055 diagnostics.
                return false;

            default:
                return true;
        }
    }

    private static (ImmutableArray<string> userIdExclusions, ImmutableArray<string> userCategoryExclusions, bool analyzerDisabled) ParseUserExclusions(string? userExclusions)
    {
        // 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 (userIdExclusions: [], userCategoryExclusions: [], analyzerDisabled: false);

            case "all":
                return (userIdExclusions: [], userCategoryExclusions: [], analyzerDisabled: true);

            default:
                // Default string representation for unconfigured option value should be treated as no exclusions.
                if (userExclusions == CodeStyleOptions2.RemoveUnnecessarySuppressionExclusions.DefaultValue)
                    return (userIdExclusions: [], userCategoryExclusions: [], analyzerDisabled: false);

                break;
        }

        // We allow excluding category of diagnostics with a category prefix, for example "category: ExcludedCategory".
        const string categoryPrefix = "category:";

        using var _1 = ArrayBuilder<string>.GetInstance(out var idBuilder);
        using var _2 = ArrayBuilder<string>.GetInstance(out var categoryBuilder);
        foreach (var part in userExclusions.Split(','))
        {
            var trimmedPart = part.Trim();
            if (trimmedPart.StartsWith(categoryPrefix, StringComparison.OrdinalIgnoreCase))
            {
                trimmedPart = trimmedPart[categoryPrefix.Length..].Trim();
                categoryBuilder.Add(trimmedPart);
            }
            else
            {
                idBuilder.Add(trimmedPart);
            }
        }

        return (userIdExclusions: idBuilder.ToImmutable(), userCategoryExclusions: categoryBuilder.ToImmutable(), analyzerDisabled: false);
    }

    private static async Task<(ImmutableArray<Diagnostic> reportedDiagnostics, ImmutableArray<string> unhandledIds)> GetReportedDiagnosticsForIdsAsync(
        ImmutableHashSet<string> idsToAnalyze,
        SyntaxNode root,
        SemanticModel semanticModel,
        CompilationWithAnalyzers compilationWithAnalyzers,
        Func<DiagnosticAnalyzer, ImmutableArray<DiagnosticDescriptor>> getSupportedDiagnostics,
        PooledHashSet<string> compilerDiagnosticIds,
        CancellationToken cancellationToken)
    {
        using var _1 = ArrayBuilder<DiagnosticAnalyzer>.GetInstance(out var analyzersBuilder);
        using var _2 = ArrayBuilder<string>.GetInstance(out var unhandledIds);

        // First, we compute the relevant analyzers whose reported diagnostics need to be computed.
        var addedCompilerAnalyzer = false;
        var hasNonCompilerAnalyzers = idsToAnalyze.Count > compilerDiagnosticIds.Count;
        foreach (var analyzer in compilationWithAnalyzers.Analyzers)
        {
            if (!addedCompilerAnalyzer &&
                analyzer.IsCompilerAnalyzer())
            {
                addedCompilerAnalyzer = true;
                analyzersBuilder.Add(analyzer);

                if (!hasNonCompilerAnalyzers)
                {
                    break;
                }

                continue;
            }

            if (hasNonCompilerAnalyzers)
            {
                Debug.Assert(!analyzer.IsCompilerAnalyzer());

                bool? lazyIsUnhandledAnalyzer = null;
                foreach (var descriptor in getSupportedDiagnostics(analyzer))
                {
                    if (!idsToAnalyze.Contains(descriptor.Id))
                    {
                        continue;
                    }

                    lazyIsUnhandledAnalyzer ??= descriptor.IsCompilationEnd() || analyzer is IPragmaSuppressionsAnalyzer;
                    if (lazyIsUnhandledAnalyzer.Value)
                    {
                        unhandledIds.Add(descriptor.Id);
                    }
                }

                if (lazyIsUnhandledAnalyzer.HasValue && !lazyIsUnhandledAnalyzer.Value)
                {
                    analyzersBuilder.Add(analyzer);
                }
            }
        }

        // Then, we execute these analyzers on the current file to fetch these diagnostics.
        // Note that if an analyzer has already executed, then this will be just a cache access
        // as computed analyzer diagnostics are cached on CompilationWithAnalyzers instance.

        using var _3 = ArrayBuilder<Diagnostic>.GetInstance(out var reportedDiagnostics);
        if (!addedCompilerAnalyzer && compilerDiagnosticIds.Count > 0)
        {
            // Special case when compiler analyzer could not be found.
            Debug.Assert(semanticModel.Compilation.Options.ReportSuppressedDiagnostics);
            reportedDiagnostics.AddRange(root.GetDiagnostics());
            reportedDiagnostics.AddRange(semanticModel.GetDiagnostics(cancellationToken: cancellationToken));
            cancellationToken.ThrowIfCancellationRequested();
        }

        if (analyzersBuilder.Count > 0)
        {
            var analyzers = analyzersBuilder.ToImmutable();

            var analysisResult = await compilationWithAnalyzers.GetAnalysisResultAsync(semanticModel.SyntaxTree, analyzers, cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            if (analysisResult.SyntaxDiagnostics.TryGetValue(semanticModel.SyntaxTree, out var diagnostics))
            {
                AddAllDiagnostics(diagnostics, reportedDiagnostics);
            }

            analysisResult = await compilationWithAnalyzers.GetAnalysisResultAsync(semanticModel, filterSpan: null, analyzers, cancellationToken).ConfigureAwait(false);
            cancellationToken.ThrowIfCancellationRequested();
            if (analysisResult.SemanticDiagnostics.TryGetValue(semanticModel.SyntaxTree, out diagnostics))
            {
                AddAllDiagnostics(diagnostics, reportedDiagnostics);
            }

            AddAllCompilationDiagnosticsForTree(analysisResult, semanticModel.SyntaxTree, reportedDiagnostics);
        }

        return (reportedDiagnostics.ToImmutable(), unhandledIds.ToImmutable());

        static void AddAllDiagnostics(ImmutableDictionary<DiagnosticAnalyzer, ImmutableArray<Diagnostic>> diagnostics, ArrayBuilder<Diagnostic> reportedDiagnostics)
        {
            foreach (var perAnalyzerDiagnostics in diagnostics.Values)
            {
                reportedDiagnostics.AddRange(perAnalyzerDiagnostics);
            }
        }

        static void AddAllCompilationDiagnosticsForTree(AnalysisResult analysisResult, SyntaxTree tree, ArrayBuilder<Diagnostic> reportedDiagnostics)
        {
            foreach (var perAnalyzerDiagnostics in analysisResult.CompilationDiagnostics.Values)
            {
                foreach (var diagnostic in perAnalyzerDiagnostics)
                {
                    if (diagnostic.Location.SourceTree == tree)
                    {
                        reportedDiagnostics.Add(diagnostic);
                    }
                }
            }
        }
    }

    private static async Task ProcessReportedDiagnosticsAsync(
        ImmutableArray<Diagnostic> diagnostics,
        SyntaxTree tree,
        CompilationWithAnalyzers compilationWithAnalyzers,
        PooledDictionary<string, List<(SyntaxTrivia pragma, bool isDisable)>> idToPragmasMap,
        PooledDictionary<SyntaxTrivia, bool> pragmasToIsUsedMap,
        PooledDictionary<string, List<SyntaxNode>> idToSuppressMessageAttributesMap,
        PooledDictionary<SyntaxNode, bool> suppressMessageAttributesToIsUsedMap,
        CancellationToken cancellationToken)
    {
        foreach (var diagnostic in diagnostics)
        {
            if (!diagnostic.IsSuppressed)
            {
                continue;
            }

            var suppressionInfo = diagnostic.GetSuppressionInfo(compilationWithAnalyzers.Compilation);
            if (suppressionInfo == null || !suppressionInfo.ProgrammaticSuppressions.IsEmpty)
            {
                // Skip diagnostics that are not suppressed in source or suppressed via programmatic suppressions from suppressors.
                continue;
            }

            if (suppressionInfo.Attribute is { } attribute)
            {
                await ProcessAttributeSuppressionsAsync(diagnostic, attribute,
                    idToSuppressMessageAttributesMap, suppressMessageAttributesToIsUsedMap, cancellationToken).ConfigureAwait(false);
            }
            else
            {
                ProcessPragmaSuppressions(diagnostic, tree, idToPragmasMap, pragmasToIsUsedMap);
            }
        }

        return;

        static void ProcessPragmaSuppressions(
            Diagnostic diagnostic,
            SyntaxTree tree,
            PooledDictionary<string, List<(SyntaxTrivia pragma, bool isDisable)>> idToPragmasMap,
            PooledDictionary<SyntaxTrivia, bool> pragmasToIsUsedMap)
        {
            if (!idToPragmasMap.TryGetValue(diagnostic.Id, out var pragmasForIdInReverseOrder))
            {
                return;
            }

            Debug.Assert(diagnostic.Location.IsInSource);
            Debug.Assert(diagnostic.Location.SourceTree == tree);

            // Process the pragmas for the document bottom-up,
            // finding the first disable pragma directive before the diagnostic span.
            // Mark this pragma and the corresponding enable pragma directive as used.
            SyntaxTrivia? lastEnablePragma = null;
            foreach (var (pragma, isDisable) in pragmasForIdInReverseOrder)
            {
                if (isDisable)
                {
                    if (pragma.Span.End <= diagnostic.Location.SourceSpan.Start)
                    {
                        pragmasToIsUsedMap[pragma] = true;
                        if (lastEnablePragma.HasValue)
                        {
                            pragmasToIsUsedMap[lastEnablePragma.Value] = true;
                        }

                        break;
                    }
                }
                else
                {
                    lastEnablePragma = pragma;
                }
            }
        }

        static async Task ProcessAttributeSuppressionsAsync(
            Diagnostic diagnostic,
            AttributeData attribute,
            PooledDictionary<string, List<SyntaxNode>> idToSuppressMessageAttributesMap,
            PooledDictionary<SyntaxNode, bool> suppressMessageAttributesToIsUsedMap,
            CancellationToken cancellationToken)
        {
            if (attribute.ApplicationSyntaxReference == null ||
                !idToSuppressMessageAttributesMap.TryGetValue(diagnostic.Id, out var suppressMessageAttributesForId))
            {
                return;
            }

            var attributeNode = await attribute.ApplicationSyntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false);
            foreach (var node in suppressMessageAttributesForId)
            {
                if (attributeNode == node)
                {
                    suppressMessageAttributesToIsUsedMap[attributeNode] = true;
                    return;
                }
            }
        }
    }

    private static void ReportUnnecessarySuppressions(
        PooledDictionary<SyntaxTrivia, bool> pragmasToIsUsedMap,
        ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray<string> ids, bool isDisable)> sortedPragmasWithIds,
        PooledDictionary<SyntaxNode, bool> suppressMessageAttributesToIsUsedMap,
        Action<Diagnostic> reportDiagnostic,
        DiagnosticSeverity severity,
        Compilation compilation)
    {
        using var _ = ArrayBuilder<Diagnostic>.GetInstance(out var diagnosticsBuilder);
        AddUnnecessaryPragmaDiagnostics(diagnosticsBuilder, pragmasToIsUsedMap, sortedPragmasWithIds, severity);
        AddUnnecessarySuppressMessageAttributeDiagnostics(diagnosticsBuilder, suppressMessageAttributesToIsUsedMap, severity);

        // Apply the diagnostic filtering
        var effectiveDiagnostics = CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnosticsBuilder, compilation);
        foreach (var diagnostic in effectiveDiagnostics)
        {
            reportDiagnostic(diagnostic);
        }

        return;

        static void AddUnnecessaryPragmaDiagnostics(
            ArrayBuilder<Diagnostic> diagnosticsBuilder,
            PooledDictionary<SyntaxTrivia, bool> pragmasToIsUsedMap,
            ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray<string> ids, bool isDisable)> sortedPragmasWithIds,
            DiagnosticSeverity severity)
        {
            foreach (var (pragma, isUsed) in pragmasToIsUsedMap)
            {
                if (!isUsed)
                {
                    // We found an unnecessary pragma directive.
                    // Try to find a matching disable/restore counterpart that toggles the pragma state.
                    // This enables the code fix to simultaneously remove both the disable and restore directives.
                    // If we don't find a matching pragma, report just the current pragma.
                    ImmutableArray<Location> additionalLocations;
                    if (TryGetTogglingPragmaDirective(pragma, sortedPragmasWithIds, out var togglePragma) &&
                        pragmasToIsUsedMap.TryGetValue(togglePragma, out var isToggleUsed) &&
                        !isToggleUsed)
                    {
                        additionalLocations = [togglePragma.GetLocation()];
                    }
                    else
                    {
                        additionalLocations = [];
                    }

                    var diagnostic = Diagnostic.Create(s_removeUnnecessarySuppressionDescriptor, pragma.GetLocation(), severity, additionalLocations, properties: null);
                    diagnosticsBuilder.Add(diagnostic);
                }
            }
        }

        static void AddUnnecessarySuppressMessageAttributeDiagnostics(
            ArrayBuilder<Diagnostic> diagnosticsBuilder,
            PooledDictionary<SyntaxNode, bool> suppressMessageAttributesToIsUsedMap,
            DiagnosticSeverity severity)
        {
            foreach (var (attribute, isUsed) in suppressMessageAttributesToIsUsedMap)
            {
                if (!isUsed)
                {
                    var diagnostic = Diagnostic.Create(s_removeUnnecessarySuppressionDescriptor, attribute.GetLocation(), severity, additionalLocations: null, properties: null);
                    diagnosticsBuilder.Add(diagnostic);
                }
            }
        }
    }

    private static bool TryGetTogglingPragmaDirective(
        SyntaxTrivia pragma,
        ArrayBuilder<(SyntaxTrivia pragma, ImmutableArray<string> ids, bool isDisable)> sortedPragmasWithIds,
        out SyntaxTrivia togglePragma)
    {
        var indexOfPragma = sortedPragmasWithIds.FindIndex(p => p.pragma == pragma);
        var idsForPragma = sortedPragmasWithIds[indexOfPragma].ids;
        var isDisable = sortedPragmasWithIds[indexOfPragma].isDisable;
        var incrementOrDecrement = isDisable ? 1 : -1;
        var matchingPragmaStackCount = 0;
        for (var i = indexOfPragma + incrementOrDecrement; i >= 0 && i < sortedPragmasWithIds.Count; i += incrementOrDecrement)
        {
            var (nextPragma, nextPragmaIds, nextPragmaIsDisable) = sortedPragmasWithIds[i];
            var intersect = nextPragmaIds.Intersect(idsForPragma).ToImmutableArray();
            if (intersect.IsEmpty)
            {
                // Unrelated pragma
                continue;
            }

            if (intersect.Length != idsForPragma.Length)
            {
                // Partial intersection of IDs - bail out.
                togglePragma = default;
                return false;
            }

            // Found a pragma with same IDs.
            // Check if this is a pragma of same kind (disable/restore) or not.
            if (isDisable == nextPragmaIsDisable)
            {
                // Same pragma kind, increment the stack count
                matchingPragmaStackCount++;
            }
            else
            {
                // Found a pragma of opposite kind.
                if (matchingPragmaStackCount > 0)
                {
                    // Not matching one for the input pragma, decrement stack count
                    matchingPragmaStackCount--;
                }
                else
                {
                    // Found the match.
                    togglePragma = nextPragma;
                    return true;
                }
            }
        }

        togglePragma = default;
        return false;
    }

    private async Task<bool> ProcessSuppressMessageAttributesAsync(
        SyntaxNode root,
        SemanticModel semanticModel,
        TextSpan? span,
        PooledDictionary<string, List<SyntaxNode>> idToSuppressMessageAttributesMap,
        PooledDictionary<SyntaxNode, bool> suppressMessageAttributesToIsUsedMap,
        ImmutableArray<string> userIdExclusions,
        ImmutableArray<string> userCategoryExclusions,
        CancellationToken cancellationToken)
    {
        var suppressMessageAttributeType = semanticModel.Compilation.SuppressMessageAttributeType();
        if (suppressMessageAttributeType == null)
        {
            return false;
        }

        // Specifies false for discardLargeInstances as these objects commonly exceed the default ArrayBuilder capacity threshold.
        using var _1 = ArrayBuilder<SyntaxNode>.GetInstance(discardLargeInstances: false, out var declarationNodes);
        this.SyntaxFacts.AddTopLevelAndMethodLevelMembers(root, declarationNodes);

        using var _2 = PooledHashSet<ISymbol>.GetInstance(out var processedPartialSymbols);
        if (declarationNodes.Count > 0)
        {
            foreach (var node in declarationNodes)
            {
                if (span.HasValue && !node.FullSpan.Contains(span.Value))
                {
                    continue;
                }

                // In the case of declaration nodes that can have more than one symbol e.g. fields and events,
                // the attributes are shared between then. Given this, we only need to inspect the first symbol
                // of the node.
                var symbol = SemanticFacts
                    .GetDeclaredSymbols(semanticModel, node, cancellationToken)
                    .FirstOrDefault();

                // If we somehow do not have a symbol, we can't do anything. Otherwise, check if our symbol is
                // a partial definition. If it is, skip it in favor of checking the implementation.
                if (symbol is null or
                    IMethodSymbol { IsPartialDefinition: true } or
                    IPropertySymbol { IsPartialDefinition: true })
                {
                    continue;
                }

                switch (symbol?.Kind)
                {
                    // Local SuppressMessageAttributes are only applicable for types and members.
                    case SymbolKind.NamedType:
                    case SymbolKind.Method:
                    case SymbolKind.Field:
                    case SymbolKind.Property:
                    case SymbolKind.Event:
                        break;

                    default:
                        continue;
                }

                // Skip already processed symbols from partial declarations
                var isPartial = symbol.Locations.Length > 1;

                if (isPartial && !processedPartialSymbols.Add(symbol))
                {
                    continue;
                }

                foreach (var attribute in symbol.GetAttributes())
                {
                    if (attribute.ApplicationSyntaxReference != null &&
                        TryGetSuppressedDiagnosticId(attribute, suppressMessageAttributeType, out var id, out var category))
                    {
                        // Ignore unsupported IDs and those excluded through user option.
                        if (!IsSupportedAnalyzerDiagnosticId(id) ||
                            userIdExclusions.Contains(id, StringComparer.OrdinalIgnoreCase) ||
                            category?.Length > 0 && userCategoryExclusions.Contains(category, StringComparer.OrdinalIgnoreCase))
                        {
                            continue;
                        }

                        if (!idToSuppressMessageAttributesMap.TryGetValue(id, out var nodesForId))
                        {
                            nodesForId = [];
                            idToSuppressMessageAttributesMap.Add(id, nodesForId);
                        }

                        var attributeNode = await attribute.ApplicationSyntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false);
                        nodesForId.Add(attributeNode);

                        // Initialize the attribute node as unnecessary at the start of the algorithm.
                        // Later processing will identify attributes which are indeed responsible for suppressing diagnostics
                        // and mark them as used.
                        // NOTE: For attributes on partial symbols with multiple declarations, we conservatively
                        // consider them as used and avoid unnecessary attribute analysis because that would potentially
                        // require analysis across multiple files, which can be expensive from a performance standpoint.
                        suppressMessageAttributesToIsUsedMap.Add(attributeNode, isPartial);
                    }
                }

                // Individual variables within a variable declaration cannot be decorated with distinct attributes, so we
                // should avoid looking at any of the subsequent symbols for this node as they will be the same.
            }
        }

        return idToSuppressMessageAttributesMap.Count > 0;
    }

    private static bool TryGetSuppressedDiagnosticId(
        AttributeData attribute,
        INamedTypeSymbol suppressMessageAttributeType,
        [NotNullWhen(returnValue: true)] out string? id,
        out string? category)
    {
        category = null;

        if (suppressMessageAttributeType.Equals(attribute.AttributeClass) &&
            attribute.AttributeConstructor?.Parameters is [_, { Name: "checkId", Type.SpecialType: SpecialType.System_String }, ..] &&
            attribute.ConstructorArguments 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[..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;
        }

        id = null;
        return false;
    }
}