File: CodeFixes\Suppression\AbstractSuppressionCodeFixProvider.PragmaHelpers.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.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.CodeFixes.Suppression;
 
internal abstract partial class AbstractSuppressionCodeFixProvider
{
    /// <summary>
    /// Helper methods for pragma based suppression code actions.
    /// </summary>
    private static class PragmaHelpers
    {
        internal static async Task<Document> GetChangeDocumentWithPragmaAdjustedAsync(
            Document document,
            TextSpan diagnosticSpan,
            SuppressionTargetInfo suppressionTargetInfo,
            Func<SyntaxToken, TextSpan, SyntaxToken> getNewStartToken,
            Func<SyntaxToken, TextSpan, SyntaxToken> getNewEndToken,
            CancellationToken cancellationToken)
        {
            var startToken = suppressionTargetInfo.StartToken;
            var endToken = suppressionTargetInfo.EndToken;
            var nodeWithTokens = suppressionTargetInfo.NodeWithTokens;
            var root = await nodeWithTokens.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
 
            var startAndEndTokenAreTheSame = startToken == endToken;
            var newStartToken = getNewStartToken(startToken, diagnosticSpan);
 
            var newEndToken = endToken;
            if (startAndEndTokenAreTheSame)
            {
                var annotation = new SyntaxAnnotation();
                newEndToken = root.ReplaceToken(startToken, newStartToken.WithAdditionalAnnotations(annotation)).GetAnnotatedTokens(annotation).Single();
                var spanChange = newStartToken.LeadingTrivia.FullSpan.Length - startToken.LeadingTrivia.FullSpan.Length;
                diagnosticSpan = new TextSpan(diagnosticSpan.Start + spanChange, diagnosticSpan.Length);
            }
 
            newEndToken = getNewEndToken(newEndToken, diagnosticSpan);
 
            SyntaxNode newNode;
            if (startAndEndTokenAreTheSame)
            {
                newNode = nodeWithTokens.ReplaceToken(startToken, newEndToken);
            }
            else
            {
                newNode = nodeWithTokens.ReplaceTokens([startToken, endToken], (o, n) => o == startToken ? newStartToken : newEndToken);
            }
 
            var newRoot = root.ReplaceNode(nodeWithTokens, newNode);
            return document.WithSyntaxRoot(newRoot);
        }
 
        private static int GetPositionForPragmaInsertion(ImmutableArray<SyntaxTrivia> triviaList, TextSpan currentDiagnosticSpan, AbstractSuppressionCodeFixProvider fixer, bool isStartToken, out SyntaxTrivia triviaAtIndex)
        {
            // Start token: Insert the #pragma disable directive just **before** the first end of line trivia prior to diagnostic location.
            // End token: Insert the #pragma disable directive just **after** the first end of line trivia after diagnostic location.
 
            int getNextIndex(int cur) => isStartToken ? cur - 1 : cur + 1;
            bool shouldConsiderTrivia(SyntaxTrivia trivia)
                => isStartToken
                    ? trivia.FullSpan.End <= currentDiagnosticSpan.Start
                    : trivia.FullSpan.Start >= currentDiagnosticSpan.End;
 
            var walkedPastDiagnosticSpan = false;
            var seenEndOfLineTrivia = false;
            var index = isStartToken ? triviaList.Length - 1 : 0;
            while (index >= 0 && index < triviaList.Length)
            {
                var trivia = triviaList[index];
 
                walkedPastDiagnosticSpan = walkedPastDiagnosticSpan || shouldConsiderTrivia(trivia);
                seenEndOfLineTrivia = seenEndOfLineTrivia ||
                    IsEndOfLineOrContainsEndOfLine(trivia, fixer);
 
                if (walkedPastDiagnosticSpan && seenEndOfLineTrivia)
                {
                    break;
                }
 
                index = getNextIndex(index);
            }
 
            triviaAtIndex = index >= 0 && index < triviaList.Length
                ? triviaList[index]
                : default;
 
            return index;
        }
 
        internal static SyntaxToken GetNewStartTokenWithAddedPragma(
            SyntaxToken startToken,
            TextSpan currentDiagnosticSpan,
            Diagnostic diagnostic,
            AbstractSuppressionCodeFixProvider fixer,
            Func<SyntaxNode, CancellationToken, SyntaxNode> formatNode,
            bool isRemoveSuppression,
            CancellationToken cancellationToken)
        {
            var trivia = startToken.LeadingTrivia.ToImmutableArray();
            var index = GetPositionForPragmaInsertion(trivia, currentDiagnosticSpan, fixer, isStartToken: true, triviaAtIndex: out var insertAfterTrivia);
            index++;
 
            bool needsLeadingEOL;
            if (index > 0)
            {
                needsLeadingEOL = !IsEndOfLineOrHasTrailingEndOfLine(insertAfterTrivia, fixer);
            }
            else if (startToken.FullSpan.Start == 0)
            {
                needsLeadingEOL = false;
            }
            else
            {
                needsLeadingEOL = true;
            }
 
            var pragmaTrivia = !isRemoveSuppression
                ? fixer.CreatePragmaDisableDirectiveTrivia(diagnostic, formatNode, needsLeadingEOL, needsTrailingEndOfLine: true, cancellationToken)
                : fixer.CreatePragmaRestoreDirectiveTrivia(diagnostic, formatNode, needsLeadingEOL, needsTrailingEndOfLine: true, cancellationToken);
 
            return startToken.WithLeadingTrivia(trivia.InsertRange(index, pragmaTrivia));
        }
 
        private static bool IsEndOfLineOrHasLeadingEndOfLine(SyntaxTrivia trivia, AbstractSuppressionCodeFixProvider fixer)
        {
            return fixer.IsEndOfLine(trivia) ||
                (trivia.HasStructure && fixer.IsEndOfLine(trivia.GetStructure().DescendantTrivia().FirstOrDefault()));
        }
 
        private static bool IsEndOfLineOrHasTrailingEndOfLine(SyntaxTrivia trivia, AbstractSuppressionCodeFixProvider fixer)
        {
            return fixer.IsEndOfLine(trivia) ||
                (trivia.HasStructure && fixer.IsEndOfLine(trivia.GetStructure().DescendantTrivia().LastOrDefault()));
        }
 
        private static bool IsEndOfLineOrContainsEndOfLine(SyntaxTrivia trivia, AbstractSuppressionCodeFixProvider fixer)
        {
            return fixer.IsEndOfLine(trivia) ||
                (trivia.HasStructure && trivia.GetStructure().DescendantTrivia().Any(fixer.IsEndOfLine));
        }
 
        internal static SyntaxToken GetNewEndTokenWithAddedPragma(
            SyntaxToken endToken,
            TextSpan currentDiagnosticSpan,
            Diagnostic diagnostic,
            AbstractSuppressionCodeFixProvider fixer,
            Func<SyntaxNode, CancellationToken, SyntaxNode> formatNode,
            bool isRemoveSuppression,
            CancellationToken cancellationToken)
        {
            ImmutableArray<SyntaxTrivia> trivia;
            var isEOF = fixer.IsEndOfFileToken(endToken);
            if (isEOF)
            {
                trivia = [.. endToken.LeadingTrivia];
            }
            else
            {
                trivia = [.. endToken.TrailingTrivia];
            }
 
            var index = GetPositionForPragmaInsertion(trivia, currentDiagnosticSpan, fixer, isStartToken: false, triviaAtIndex: out var insertBeforeTrivia);
 
            bool needsTrailingEOL;
            if (index < trivia.Length)
            {
                needsTrailingEOL = !IsEndOfLineOrHasLeadingEndOfLine(insertBeforeTrivia, fixer);
            }
            else if (isEOF)
            {
                needsTrailingEOL = false;
            }
            else
            {
                needsTrailingEOL = true;
            }
 
            var pragmaTrivia = !isRemoveSuppression
                ? fixer.CreatePragmaRestoreDirectiveTrivia(diagnostic, formatNode, needsLeadingEndOfLine: true, needsTrailingEndOfLine: needsTrailingEOL, cancellationToken)
                : fixer.CreatePragmaDisableDirectiveTrivia(diagnostic, formatNode, needsLeadingEndOfLine: true, needsTrailingEndOfLine: needsTrailingEOL, cancellationToken);
 
            if (isEOF)
            {
                return endToken.WithLeadingTrivia(trivia.InsertRange(index, pragmaTrivia));
            }
            else
            {
                return endToken.WithTrailingTrivia(trivia.InsertRange(index, pragmaTrivia));
            }
        }
 
        internal static void NormalizeTriviaOnTokens(AbstractSuppressionCodeFixProvider fixer, ref Document document, ref SuppressionTargetInfo suppressionTargetInfo)
        {
            // For pragma suppression fixes, we need to normalize the leading trivia on start token to account for
            // the trailing trivia on its previous token (and similarly normalize trailing trivia for end token).
 
            var startToken = suppressionTargetInfo.StartToken;
            var endToken = suppressionTargetInfo.EndToken;
            var nodeWithTokens = suppressionTargetInfo.NodeWithTokens;
            var startAndEndTokensAreSame = startToken == endToken;
            var isEndTokenEOF = fixer.IsEndOfFileToken(endToken);
 
            var previousOfStart = startToken.GetPreviousToken(includeZeroWidth: true);
            var nextOfEnd = !isEndTokenEOF ? endToken.GetNextToken(includeZeroWidth: true) : default;
            if (!previousOfStart.HasTrailingTrivia && !nextOfEnd.HasLeadingTrivia)
            {
                return;
            }
 
            var root = nodeWithTokens.SyntaxTree.GetRoot();
            var spanEnd = !isEndTokenEOF ? nextOfEnd.FullSpan.End : endToken.FullSpan.End;
            var subtreeRoot = root.FindNode(new TextSpan(previousOfStart.FullSpan.Start, spanEnd - previousOfStart.FullSpan.Start));
 
            var currentStartToken = startToken;
            var currentEndToken = endToken;
            var newStartToken = startToken.WithLeadingTrivia(previousOfStart.TrailingTrivia.Concat(startToken.LeadingTrivia));
 
            var newEndToken = currentEndToken;
            if (startAndEndTokensAreSame)
            {
                newEndToken = newStartToken;
            }
 
            newEndToken = newEndToken.WithTrailingTrivia(endToken.TrailingTrivia.Concat(nextOfEnd.LeadingTrivia));
 
            var newPreviousOfStart = previousOfStart.WithTrailingTrivia();
            var newNextOfEnd = nextOfEnd.WithLeadingTrivia();
 
            var newSubtreeRoot = subtreeRoot.ReplaceTokens([startToken, previousOfStart, endToken, nextOfEnd],
                (o, n) =>
                {
                    if (o == currentStartToken)
                    {
                        return startAndEndTokensAreSame ? newEndToken : newStartToken;
                    }
                    else if (o == previousOfStart)
                    {
                        return newPreviousOfStart;
                    }
                    else if (o == currentEndToken)
                    {
                        return newEndToken;
                    }
                    else if (o == nextOfEnd)
                    {
                        return newNextOfEnd;
                    }
                    else
                    {
                        return n;
                    }
                });
 
            root = root.ReplaceNode(subtreeRoot, newSubtreeRoot);
            document = document.WithSyntaxRoot(root);
            suppressionTargetInfo.StartToken = root.FindToken(startToken.SpanStart);
            suppressionTargetInfo.EndToken = root.FindToken(endToken.SpanStart);
            suppressionTargetInfo.NodeWithTokens = fixer.GetNodeWithTokens(suppressionTargetInfo.StartToken, suppressionTargetInfo.EndToken, root);
        }
    }
}