File: CodeFixes\Suppression\AbstractSuppressionCodeFixProvider.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.AddImport;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CodeFixes.Suppression;
 
internal abstract partial class AbstractSuppressionCodeFixProvider : IConfigurationFixProvider
{
    public const string SuppressMessageAttributeName = "System.Diagnostics.CodeAnalysis.SuppressMessage";
    private const string s_globalSuppressionsFileName = "GlobalSuppressions";
    private const string s_suppressionsFileCommentTemplate =
@"{0} This file is used by Code Analysis to maintain SuppressMessage
{0} attributes that are applied to this project.
{0} Project-level suppressions either have no target or are given
{0} a specific target and scoped to a namespace, type, member, etc.
 
";
    protected AbstractSuppressionCodeFixProvider()
    {
    }
 
    public FixAllProvider GetFixAllProvider()
        => SuppressionFixAllProvider.Instance;
 
    public bool IsFixableDiagnostic(Diagnostic diagnostic)
        => SuppressionHelpers.CanBeSuppressed(diagnostic) || SuppressionHelpers.CanBeUnsuppressed(diagnostic);
 
    protected abstract SyntaxTriviaList CreatePragmaDisableDirectiveTrivia(Diagnostic diagnostic, Func<SyntaxNode, CancellationToken, SyntaxNode> formatNode, bool needsLeadingEndOfLine, bool needsTrailingEndOfLine, CancellationToken cancellationToken);
    protected abstract SyntaxTriviaList CreatePragmaRestoreDirectiveTrivia(Diagnostic diagnostic, Func<SyntaxNode, CancellationToken, SyntaxNode> formatNode, bool needsLeadingEndOfLine, bool needsTrailingEndOfLine, CancellationToken cancellationToken);
 
    protected abstract SyntaxNode AddGlobalSuppressMessageAttribute(
        SyntaxNode newRoot,
        ISymbol targetSymbol,
        INamedTypeSymbol suppressMessageAttribute,
        Diagnostic diagnostic,
        SolutionServices services,
        SyntaxFormattingOptions options,
        IAddImportsService addImportsService,
        CancellationToken cancellationToken);
 
    protected abstract SyntaxNode AddLocalSuppressMessageAttribute(
        SyntaxNode targetNode, ISymbol targetSymbol, INamedTypeSymbol suppressMessageAttribute, Diagnostic diagnostic);
 
    protected abstract string DefaultFileExtension { get; }
    protected abstract string SingleLineCommentStart { get; }
    protected abstract bool IsAttributeListWithAssemblyAttributes(SyntaxNode node);
    protected abstract bool IsEndOfLine(SyntaxTrivia trivia);
    protected abstract bool IsEndOfFileToken(SyntaxToken token);
    protected abstract bool IsSingleAttributeInAttributeList(SyntaxNode attribute);
    protected abstract bool IsAnyPragmaDirectiveForId(SyntaxTrivia trivia, string id, out bool enableDirective, out bool hasMultipleIds);
    protected abstract SyntaxTrivia TogglePragmaDirective(SyntaxTrivia trivia);
 
    protected string GlobalSuppressionsFileHeaderComment
    {
        get
        {
            return string.Format(s_suppressionsFileCommentTemplate, SingleLineCommentStart);
        }
    }
 
    protected static string GetOrMapDiagnosticId(Diagnostic diagnostic, out bool includeTitle)
    {
        if (diagnostic.Id == IDEDiagnosticIds.FormattingDiagnosticId)
        {
            includeTitle = false;
            return FormattingDiagnosticIds.FormatDocumentControlDiagnosticId;
        }
 
        includeTitle = true;
        return diagnostic.Id;
    }
 
    protected abstract SyntaxNode GetContainingStatement(SyntaxToken token);
    protected abstract bool TokenHasTrailingLineContinuationChar(SyntaxToken token);
 
    protected SyntaxToken GetAdjustedTokenForPragmaDisable(SyntaxToken token, SyntaxNode root, TextLineCollection lines)
    {
        var containingStatement = GetContainingStatement(token);
 
        // The containing statement might not start on the same line as the token, but we don't want to split
        // a statement in the middle, so we actually want to use the first token on the line that has the first token
        // of the statement.
        //
        // eg, given: public void M() { int x = 1; }
        //
        // When trying to suppress an "unused local" for x, token would be "x", the first token
        // of the containing statement is "int", but we want the pragma before "public".
        if (containingStatement is not null && containingStatement.GetFirstToken() != token)
        {
            var indexOfLine = lines.IndexOf(containingStatement.GetFirstToken().SpanStart);
            var line = lines[indexOfLine];
            token = root.FindToken(line.Start);
        }
 
        return token;
    }
 
    private SyntaxToken GetAdjustedTokenForPragmaRestore(SyntaxToken token, SyntaxNode root, TextLineCollection lines, int indexOfLine)
    {
        var containingStatement = GetContainingStatement(token);
 
        // As per above, the last token of the statement might not be the last token on the line
        if (containingStatement is not null && containingStatement.GetLastToken() != token)
        {
            indexOfLine = lines.IndexOf(containingStatement.GetLastToken().SpanStart);
        }
 
        var line = lines[indexOfLine];
        token = root.FindToken(line.End);
 
        // VB has line continuation characters that can explicitly extend the line beyond the last
        // token, so allow for that by just skipping over them
        while (TokenHasTrailingLineContinuationChar(token))
        {
            indexOfLine = indexOfLine + 1;
            token = root.FindToken(lines[indexOfLine].End, findInsideTrivia: true);
        }
 
        return token;
    }
 
    public Task<ImmutableArray<CodeFix>> GetFixesAsync(
        TextDocument textDocument, TextSpan span, IEnumerable<Diagnostic> diagnostics, CancellationToken cancellationToken)
    {
        if (textDocument is not Document document)
            return Task.FromResult(ImmutableArray<CodeFix>.Empty);
 
        return GetSuppressionsAsync(document, span, diagnostics, skipSuppressMessage: false, skipUnsuppress: false, cancellationToken: cancellationToken);
    }
 
    internal async Task<ImmutableArray<PragmaWarningCodeAction>> GetPragmaSuppressionsAsync(Document document, TextSpan span, IEnumerable<Diagnostic> diagnostics, CancellationToken cancellationToken)
    {
        var codeFixes = await GetSuppressionsAsync(document, span, diagnostics, skipSuppressMessage: true, skipUnsuppress: true, cancellationToken: cancellationToken).ConfigureAwait(false);
        return codeFixes.SelectMany(fix => fix.Action.NestedActions)
                        .OfType<PragmaWarningCodeAction>()
                        .ToImmutableArray();
    }
 
    private async Task<ImmutableArray<CodeFix>> GetSuppressionsAsync(
        Document document, TextSpan span, IEnumerable<Diagnostic> diagnostics, bool skipSuppressMessage, bool skipUnsuppress, CancellationToken cancellationToken)
    {
        var suppressionTargetInfo = await GetSuppressionTargetInfoAsync(document, span, cancellationToken).ConfigureAwait(false);
        if (suppressionTargetInfo == null)
        {
            return [];
        }
 
        return await GetSuppressionsAsync(
            document, document.Project, diagnostics, suppressionTargetInfo, skipSuppressMessage, skipUnsuppress, cancellationToken).ConfigureAwait(false);
    }
 
    public async Task<ImmutableArray<CodeFix>> GetFixesAsync(
        Project project, IEnumerable<Diagnostic> diagnostics, CancellationToken cancellationToken)
    {
        if (!project.SupportsCompilation)
        {
            return [];
        }
 
        var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
        var suppressionTargetInfo = new SuppressionTargetInfo() { TargetSymbol = compilation.Assembly };
        return await GetSuppressionsAsync(
            documentOpt: null, project, diagnostics, suppressionTargetInfo,
            skipSuppressMessage: false, skipUnsuppress: false,
            cancellationToken).ConfigureAwait(false);
    }
 
    private async Task<ImmutableArray<CodeFix>> GetSuppressionsAsync(
        Document documentOpt, Project project, IEnumerable<Diagnostic> diagnostics, SuppressionTargetInfo suppressionTargetInfo, bool skipSuppressMessage, bool skipUnsuppress, CancellationToken cancellationToken)
    {
        // We only care about diagnostics that can be suppressed/unsuppressed.
        diagnostics = diagnostics.Where(IsFixableDiagnostic);
        if (diagnostics.IsEmpty())
        {
            return [];
        }
 
        INamedTypeSymbol suppressMessageAttribute = null;
        if (!skipSuppressMessage)
        {
            var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
            suppressMessageAttribute = compilation.SuppressMessageAttributeType();
            skipSuppressMessage = suppressMessageAttribute == null || !suppressMessageAttribute.IsAttribute();
        }
 
        var lazyFormattingOptions = (SyntaxFormattingOptions)null;
        var result = ArrayBuilder<CodeFix>.GetInstance();
        foreach (var diagnostic in diagnostics)
        {
            if (!diagnostic.IsSuppressed)
            {
                var nestedActions = ArrayBuilder<NestedSuppressionCodeAction>.GetInstance();
                if (diagnostic.Location.IsInSource && documentOpt != null)
                {
                    // pragma warning disable.
                    lazyFormattingOptions ??= await documentOpt.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
                    nestedActions.Add(PragmaWarningCodeAction.Create(suppressionTargetInfo, documentOpt, lazyFormattingOptions, diagnostic, this));
                }
 
                // SuppressMessageAttribute suppression is not supported for compiler diagnostics.
                if (!skipSuppressMessage && SuppressionHelpers.CanBeSuppressedWithAttribute(diagnostic))
                {
                    // global assembly-level suppress message attribute.
                    nestedActions.Add(new GlobalSuppressMessageCodeAction(
                        suppressionTargetInfo.TargetSymbol, suppressMessageAttribute, project, diagnostic, this));
 
                    // local suppress message attribute
                    // please note that in order to avoid issues with existing unit tests referencing the code fix
                    // by their index this needs to be the last added to nestedActions
                    if (suppressionTargetInfo.TargetMemberNode != null && suppressionTargetInfo.TargetSymbol.Kind != SymbolKind.Namespace)
                    {
                        nestedActions.Add(new LocalSuppressMessageCodeAction(
                            this, suppressionTargetInfo.TargetSymbol, suppressMessageAttribute, suppressionTargetInfo.TargetMemberNode, documentOpt, diagnostic));
                    }
                }
 
                if (nestedActions.Count > 0)
                {
                    var codeAction = new TopLevelSuppressionCodeAction(
                        diagnostic, nestedActions.ToImmutableAndFree());
                    result.Add(new CodeFix(project, codeAction, diagnostic));
                }
            }
            else if (!skipUnsuppress)
            {
                var codeAction = await RemoveSuppressionCodeAction.CreateAsync(suppressionTargetInfo, documentOpt, project, diagnostic, this, cancellationToken).ConfigureAwait(false);
                if (codeAction != null)
                {
                    result.Add(new CodeFix(project, codeAction, diagnostic));
                }
            }
        }
 
        return result.ToImmutableAndFree();
    }
 
    internal sealed class SuppressionTargetInfo
    {
        public ISymbol TargetSymbol { get; set; }
        public SyntaxToken StartToken { get; set; }
        public SyntaxToken EndToken { get; set; }
        public SyntaxNode NodeWithTokens { get; set; }
        public SyntaxNode TargetMemberNode { get; set; }
    }
 
    private async Task<SuppressionTargetInfo> GetSuppressionTargetInfoAsync(Document document, TextSpan span, CancellationToken cancellationToken)
    {
        var syntaxTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
        if (syntaxTree.GetLineVisibility(span.Start, cancellationToken) == LineVisibility.Hidden)
        {
            return null;
        }
 
        // Find the start token to attach leading pragma disable warning directive.
        var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
        var lines = syntaxTree.GetText(cancellationToken).Lines;
        var indexOfLine = lines.IndexOf(span.Start);
        var lineAtPos = lines[indexOfLine];
        var startToken = root.FindToken(lineAtPos.Start);
        startToken = GetAdjustedTokenForPragmaDisable(startToken, root, lines);
 
        // Find the end token to attach pragma restore warning directive.
        var spanEnd = Math.Max(startToken.Span.End, span.End);
        indexOfLine = lines.IndexOf(spanEnd);
        lineAtPos = lines[indexOfLine];
        var endToken = root.FindToken(lineAtPos.End);
        endToken = GetAdjustedTokenForPragmaRestore(endToken, root, lines, indexOfLine);
 
        var nodeWithTokens = GetNodeWithTokens(startToken, endToken, root);
 
        var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
 
        var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
 
        ISymbol targetSymbol = null;
        var targetMemberNode = syntaxFacts.GetContainingMemberDeclaration(root, nodeWithTokens.SpanStart);
        if (targetMemberNode != null)
        {
            targetSymbol = semanticModel.GetDeclaredSymbol(targetMemberNode, cancellationToken);
 
            if (targetSymbol == null)
            {
                var analyzerDriverService = document.GetLanguageService<IAnalyzerDriverService>();
 
                // targetMemberNode could be a declaration node with multiple decls (e.g. field declaration defining multiple variables).
                // Let us compute all the declarations intersecting the span.
                var declsBuilder = ArrayBuilder<DeclarationInfo>.GetInstance();
                analyzerDriverService.ComputeDeclarationsInSpan(semanticModel, span, true, declsBuilder, cancellationToken);
                var decls = declsBuilder.ToImmutableAndFree();
 
                if (!decls.IsEmpty)
                {
                    var containedDecls = decls.Where(d => span.Contains(d.DeclaredNode.Span));
                    if (containedDecls.Count() == 1)
                    {
                        // Single containing declaration, use this symbol.
                        var decl = containedDecls.Single();
                        targetSymbol = decl.DeclaredSymbol;
                    }
                    else
                    {
                        // Otherwise, use the most enclosing declaration.
                        TextSpan? minContainingSpan = null;
                        foreach (var decl in decls)
                        {
                            var declSpan = decl.DeclaredNode.Span;
                            if (declSpan.Contains(span) &&
                                (!minContainingSpan.HasValue || minContainingSpan.Value.Contains(declSpan)))
                            {
                                minContainingSpan = declSpan;
                                targetSymbol = decl.DeclaredSymbol;
                            }
                        }
                    }
                }
            }
        }
 
        // Outside of a member declaration, suppress diagnostic for the entire assembly.
        targetSymbol ??= semanticModel.Compilation.Assembly;
 
        return new SuppressionTargetInfo() { TargetSymbol = targetSymbol, NodeWithTokens = nodeWithTokens, StartToken = startToken, EndToken = endToken, TargetMemberNode = targetMemberNode };
    }
 
    internal SyntaxNode GetNodeWithTokens(SyntaxToken startToken, SyntaxToken endToken, SyntaxNode root)
    {
        if (IsEndOfFileToken(endToken))
        {
            return root;
        }
        else
        {
            return startToken.GetCommonRoot(endToken);
        }
    }
 
    protected static string GetScopeString(SymbolKind targetSymbolKind)
    {
        switch (targetSymbolKind)
        {
            case SymbolKind.Event:
            case SymbolKind.Field:
            case SymbolKind.Method:
            case SymbolKind.Property:
                return "member";
 
            case SymbolKind.NamedType:
                return "type";
 
            case SymbolKind.Namespace:
                return "namespace";
 
            default:
                return null;
        }
    }
 
    protected static string GetTargetString(ISymbol targetSymbol)
        => "~" + DocumentationCommentId.CreateDeclarationId(targetSymbol);
}