File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\Core\Utilities\CommonFormattingHelpers.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.Generic;
using System.Text;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Shared.Utilities;
 
internal static class CommonFormattingHelpers
{
    public static readonly Comparison<SuppressOperation> SuppressOperationComparer = (o1, o2) =>
    {
        return o1.TextSpan.Start - o2.TextSpan.Start;
    };
 
    public static readonly Comparison<IndentBlockOperation> IndentBlockOperationComparer = (o1, o2) =>
    {
        // smaller one goes left
        var d = o1.TextSpan.Start - o2.TextSpan.Start;
        if (d != 0)
            return d;
 
        // bigger one goes left
        d = o2.TextSpan.End - o1.TextSpan.End;
        if (d != 0)
            return d;
 
        if (o1.IsRelativeIndentation && o2.IsRelativeIndentation)
        {
            // if they're at the same location, but indenting separate amounts, have the bigger delta go first.
            d = o2.IndentationDeltaOrPosition - o1.IndentationDeltaOrPosition;
            if (d != 0)
                return d;
        }
 
        return 0;
    };
 
    public static IEnumerable<(SyntaxToken, SyntaxToken)> ConvertToTokenPairs(this SyntaxNode root, IReadOnlyList<TextSpan> spans)
    {
        Contract.ThrowIfNull(root);
        Contract.ThrowIfFalse(spans.Count > 0);
 
        if (spans.Count == 1)
        {
            // special case, if there is only one span, return right away
            yield return root.ConvertToTokenPair(spans[0]);
            yield break;
        }
 
        var previousOne = root.ConvertToTokenPair(spans[0]);
 
        // iterate through each spans and make sure each one doesn't overlap each other
        for (var i = 1; i < spans.Count; i++)
        {
            var currentOne = root.ConvertToTokenPair(spans[i]);
            if (currentOne.Item1.SpanStart <= previousOne.Item2.Span.End)
            {
                // oops, looks like two spans are overlapping each other. merge them
                previousOne = ValueTuple.Create(previousOne.Item1, previousOne.Item2.Span.End < currentOne.Item2.Span.End ? currentOne.Item2 : previousOne.Item2);
                continue;
            }
 
            // okay, looks like things are in good shape
            yield return previousOne;
 
            // move to next one
            previousOne = currentOne;
        }
 
        // give out the last one
        yield return previousOne;
    }
 
    public static ValueTuple<SyntaxToken, SyntaxToken> ConvertToTokenPair(this SyntaxNode root, TextSpan textSpan)
    {
        Contract.ThrowIfNull(root);
        Contract.ThrowIfTrue(textSpan.IsEmpty);
 
        var startToken = root.FindToken(textSpan.Start);
 
        // empty token, get previous non-zero length token
        if (startToken.IsMissing)
        {
            // if there is no previous token, startToken will be set to SyntaxKind.None
            startToken = startToken.GetPreviousToken();
        }
 
        // span is on leading trivia
        if (textSpan.Start < startToken.SpanStart)
        {
            // if there is no previous token, startToken will be set to SyntaxKind.None
            startToken = startToken.GetPreviousToken();
        }
 
        // adjust position where we try to search end token
        var endToken = (root.FullSpan.End <= textSpan.End) ?
            root.GetLastToken(includeZeroWidth: true) : root.FindToken(textSpan.End);
 
        // empty token, get next token
        if (endToken.IsMissing)
        {
            endToken = endToken.GetNextToken();
        }
 
        // span is on trailing trivia
        if (endToken.Span.End < textSpan.End)
        {
            endToken = endToken.GetNextToken();
        }
 
        // make sure tokens are not SyntaxKind.None
        startToken = (startToken.RawKind != 0) ? startToken : root.GetFirstToken(includeZeroWidth: true);
        endToken = (endToken.RawKind != 0) ? endToken : root.GetLastToken(includeZeroWidth: true);
 
        // token is in right order
        Contract.ThrowIfFalse(startToken.Equals(endToken) || startToken.Span.End <= endToken.SpanStart);
        return ValueTuple.Create(startToken, endToken);
    }
 
    public static bool IsInvalidTokenRange(this SyntaxNode root, SyntaxToken startToken, SyntaxToken endToken)
    {
        // given token must be token exist excluding EndOfFile token.
        if (startToken.RawKind == 0 || endToken.RawKind == 0)
        {
            return true;
        }
 
        if (startToken.Equals(endToken))
        {
            return false;
        }
 
        // regular case. 
        // start token can't be end of file token and start token must be before end token if it's not the same token.
        return root.FullSpan.End == startToken.SpanStart || startToken.FullSpan.End > endToken.FullSpan.Start;
    }
 
    public static int GetTokenColumn(this SyntaxTree tree, SyntaxToken token, int tabSize)
    {
        Contract.ThrowIfNull(tree);
        Contract.ThrowIfTrue(token.RawKind == 0);
 
        var startPosition = token.SpanStart;
        var line = tree.GetText().Lines.GetLineFromPosition(startPosition);
 
        return line.GetColumnFromLineOffset(startPosition - line.Start, tabSize);
    }
 
    public static string GetText(this SourceText text, SyntaxToken token1, SyntaxToken token2)
        => (token1.RawKind == 0) ? text.ToString(TextSpan.FromBounds(0, token2.SpanStart)) : text.ToString(TextSpan.FromBounds(token1.Span.End, token2.SpanStart));
 
    public static string GetTextBetween(SyntaxToken token1, SyntaxToken token2)
    {
        var builder = new StringBuilder();
        AppendTextBetween(token1, token2, builder);
 
        return builder.ToString();
    }
 
    public static void AppendTextBetween(SyntaxToken token1, SyntaxToken token2, StringBuilder builder)
    {
        Contract.ThrowIfTrue(token1.RawKind == 0 && token2.RawKind == 0);
        Contract.ThrowIfTrue(token1.Equals(token2));
 
        if (token1.RawKind == 0)
        {
            AppendLeadingTriviaText(token2, builder);
            return;
        }
 
        if (token2.RawKind == 0)
        {
            AppendTrailingTriviaText(token1, builder);
            return;
        }
 
        if (token1.FullSpan.End == token2.FullSpan.Start)
        {
            AppendTextBetweenTwoAdjacentTokens(token1, token2, builder);
            return;
        }
 
        AppendTrailingTriviaText(token1, builder);
 
        for (var token = token1.GetNextToken(includeZeroWidth: true); token.FullSpan.End <= token2.FullSpan.Start; token = token.GetNextToken(includeZeroWidth: true))
        {
            builder.Append(token.ToFullString());
        }
 
        AppendPartialLeadingTriviaText(token2, builder, token1.TrailingTrivia.FullSpan.End);
    }
 
    private static void AppendTextBetweenTwoAdjacentTokens(SyntaxToken token1, SyntaxToken token2, StringBuilder builder)
    {
        AppendTrailingTriviaText(token1, builder);
        AppendLeadingTriviaText(token2, builder);
    }
 
    private static void AppendLeadingTriviaText(SyntaxToken token, StringBuilder builder)
    {
        if (!token.HasLeadingTrivia)
        {
            return;
        }
 
        foreach (var trivia in token.LeadingTrivia)
        {
            builder.Append(trivia.ToFullString());
        }
    }
 
    /// <summary>
    /// If the token1 is expected to be part of the leading trivia of the token2 then the trivia
    /// before the token1FullSpanEnd, which the fullspan end of the token1 should be ignored
    /// </summary>
    private static void AppendPartialLeadingTriviaText(SyntaxToken token, StringBuilder builder, int token1FullSpanEnd)
    {
        if (!token.HasLeadingTrivia)
        {
            return;
        }
 
        foreach (var trivia in token.LeadingTrivia)
        {
            if (trivia.FullSpan.End <= token1FullSpanEnd)
            {
                continue;
            }
 
            builder.Append(trivia.ToFullString());
        }
    }
 
    private static void AppendTrailingTriviaText(SyntaxToken token, StringBuilder builder)
    {
        if (!token.HasTrailingTrivia)
        {
            return;
        }
 
        foreach (var trivia in token.TrailingTrivia)
        {
            builder.Append(trivia.ToFullString());
        }
    }
 
    /// <summary>
    /// this will create a span that includes its trailing trivia of its previous token and leading trivia of its next token
    /// for example, for code such as "class A { int ...", if given tokens are "A" and "{", this will return span [] of "class[ A { ]int ..."
    /// which included trailing trivia of "class" which is previous token of "A", and leading trivia of "int" which is next token of "{"
    /// </summary>
    public static TextSpan GetSpanIncludingTrailingAndLeadingTriviaOfAdjacentTokens(SyntaxToken startToken, SyntaxToken endToken)
    {
        // most of cases we can just ask previous and next token to create the span, but in some corner cases such as omitted token case,
        // those navigation function doesn't work, so we have to explore the tree ourselves to create correct span
        var startPosition = GetStartPositionOfSpan(startToken);
        var endPosition = GetEndPositionOfSpan(endToken);
 
        return TextSpan.FromBounds(startPosition, endPosition);
    }
 
    private static int GetEndPositionOfSpan(SyntaxToken token)
    {
        var nextToken = token.GetNextToken();
        if (nextToken.RawKind != 0)
        {
            return nextToken.SpanStart;
        }
 
        var backwardPosition = token.FullSpan.End;
        var parentNode = GetParentThatContainsGivenSpan(token.Parent, backwardPosition, forward: false);
        if (parentNode == null)
        {
            // reached the end of tree
            return token.FullSpan.End;
        }
 
        Contract.ThrowIfFalse(backwardPosition < parentNode.FullSpan.End);
 
        nextToken = parentNode.FindToken(backwardPosition + 1);
 
        Contract.ThrowIfTrue(nextToken.RawKind == 0);
 
        return nextToken.SpanStart;
    }
 
    public static int GetStartPositionOfSpan(SyntaxToken token)
    {
        var previousToken = token.GetPreviousToken();
        if (previousToken.RawKind != 0)
        {
            return previousToken.Span.End;
        }
 
        // first token in the tree
        var forwardPosition = token.FullSpan.Start;
        if (forwardPosition <= 0)
        {
            return 0;
        }
 
        var parentNode = GetParentThatContainsGivenSpan(token.Parent, forwardPosition, forward: true);
        Contract.ThrowIfNull(parentNode);
        Contract.ThrowIfFalse(parentNode.FullSpan.Start < forwardPosition);
 
        previousToken = parentNode.FindToken(forwardPosition + 1);
 
        Contract.ThrowIfTrue(previousToken.RawKind == 0);
 
        return previousToken.Span.End;
    }
 
    private static SyntaxNode? GetParentThatContainsGivenSpan(SyntaxNode? node, int position, bool forward)
    {
        while (node != null)
        {
            var fullSpan = node.FullSpan;
            if (forward)
            {
                if (fullSpan.Start < position)
                {
                    return node;
                }
            }
            else
            {
                if (position > fullSpan.End)
                {
                    return node;
                }
            }
 
            node = node.Parent;
        }
 
        return null;
    }
 
    public static bool HasAnyWhitespaceElasticTrivia(SyntaxToken previousToken, SyntaxToken currentToken)
    {
        if (!previousToken.ContainsAnnotations && !currentToken.ContainsAnnotations)
            return false;
 
        if (!previousToken.HasTrailingTrivia && !currentToken.HasLeadingTrivia)
            return false;
 
        return previousToken.TrailingTrivia.HasAnyWhitespaceElasticTrivia() || currentToken.LeadingTrivia.HasAnyWhitespaceElasticTrivia();
    }
 
    public static TextSpan GetFormattingSpan(SyntaxNode root, TextSpan span)
    {
        Contract.ThrowIfNull(root);
 
        var startToken = root.FindToken(span.Start).GetPreviousToken();
        var endToken = root.FindTokenFromEnd(span.End).GetNextToken();
 
        var startPosition = startToken.SpanStart;
        var endPosition = endToken.RawKind == 0 ? root.Span.End : endToken.Span.End;
 
        return TextSpan.FromBounds(startPosition, endPosition);
    }
}