File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\CSharp\Formatting\Engine\Trivia\TriviaRewriter.cs
Web Access
Project: src\src\CodeStyle\CSharp\Analyzers\Microsoft.CodeAnalysis.CSharp.CodeStyle.csproj (Microsoft.CodeAnalysis.CSharp.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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Formatting;
 
internal class TriviaRewriter : CSharpSyntaxRewriter
{
    private readonly SyntaxNode _node;
    private readonly TextSpanMutableIntervalTree _spans;
    private readonly CancellationToken _cancellationToken;
 
    private readonly Dictionary<SyntaxToken, SyntaxTriviaList> _trailingTriviaMap = [];
    private readonly Dictionary<SyntaxToken, SyntaxTriviaList> _leadingTriviaMap = [];
 
    public TriviaRewriter(
        SyntaxNode node,
        TextSpanMutableIntervalTree spanToFormat,
        Dictionary<ValueTuple<SyntaxToken, SyntaxToken>, TriviaData> map,
        CancellationToken cancellationToken)
    {
        Contract.ThrowIfNull(node);
        Contract.ThrowIfNull(map);
 
        _node = node;
        _spans = spanToFormat;
        _cancellationToken = cancellationToken;
 
        PreprocessTriviaListMap(map, cancellationToken);
    }
 
    public SyntaxNode Transform()
        => Visit(_node);
 
    private void PreprocessTriviaListMap(
        Dictionary<ValueTuple<SyntaxToken, SyntaxToken>, TriviaData> map,
        CancellationToken cancellationToken)
    {
        foreach (var pair in map)
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            var (trailingTrivia, leadingTrivia) = GetTrailingAndLeadingTrivia(pair, cancellationToken);
 
            if (pair.Key.Item1.RawKind != 0)
            {
                _trailingTriviaMap.Add(pair.Key.Item1, trailingTrivia);
            }
 
            if (pair.Key.Item2.RawKind != 0)
            {
                _leadingTriviaMap.Add(pair.Key.Item2, leadingTrivia);
            }
        }
    }
 
    private (SyntaxTriviaList trailingTrivia, SyntaxTriviaList leadingTrivia) GetTrailingAndLeadingTrivia(
        KeyValuePair<ValueTuple<SyntaxToken, SyntaxToken>,
        TriviaData> pair,
        CancellationToken cancellationToken)
    {
        if (pair.Key.Item1.RawKind == 0)
        {
            return (default(SyntaxTriviaList), GetLeadingTriviaAtBeginningOfTree(pair.Key, pair.Value, cancellationToken));
        }
 
        if (pair.Value is TriviaDataWithList csharpTriviaData)
        {
            var triviaList = csharpTriviaData.GetTriviaList(cancellationToken);
            var index = GetFirstEndOfLineIndexOrRightBeforeComment(triviaList);
 
            return (TriviaHelpers.CreateTriviaListFromTo(triviaList, 0, index),
                    TriviaHelpers.CreateTriviaListFromTo(triviaList, index + 1, triviaList.Count - 1));
        }
 
        // whitespace trivia case such as spaces/tabs/new lines
        // these will always have a single text change
        var text = pair.Value.GetTextChanges(GetTextSpan(pair.Key)).Single().NewText ?? "";
        var trailingTrivia = SyntaxFactory.ParseTrailingTrivia(text);
 
        var width = trailingTrivia.GetFullWidth();
        var leadingTrivia = SyntaxFactory.ParseLeadingTrivia(text[width..]);
 
        return (trailingTrivia, leadingTrivia);
    }
 
    private TextSpan GetTextSpan(ValueTuple<SyntaxToken, SyntaxToken> pair)
    {
        if (pair.Item1.RawKind == 0)
        {
            return TextSpan.FromBounds(_node.FullSpan.Start, pair.Item2.SpanStart);
        }
 
        if (pair.Item2.RawKind == 0)
        {
            return TextSpan.FromBounds(pair.Item1.Span.End, _node.FullSpan.End);
        }
 
        return TextSpan.FromBounds(pair.Item1.Span.End, pair.Item2.SpanStart);
    }
 
    private static int GetFirstEndOfLineIndexOrRightBeforeComment(SyntaxTriviaList triviaList)
    {
        for (var i = 0; i < triviaList.Count; i++)
        {
            var trivia = triviaList[i];
 
            if (trivia.Kind() == SyntaxKind.EndOfLineTrivia)
            {
                return i;
            }
 
            if (trivia.IsDocComment())
            {
                return i - 1;
            }
        }
 
        return triviaList.Count - 1;
    }
 
    private SyntaxTriviaList GetLeadingTriviaAtBeginningOfTree(
        ValueTuple<SyntaxToken, SyntaxToken> pair,
        TriviaData triviaData,
        CancellationToken cancellationToken)
    {
        if (triviaData is TriviaDataWithList csharpTriviaData)
        {
            return csharpTriviaData.GetTriviaList(cancellationToken);
        }
 
        // whitespace trivia case such as spaces/tabs/new lines
        // these will always have single text changes
        var text = triviaData.GetTextChanges(GetTextSpan(pair)).Single().NewText ?? "";
        return SyntaxFactory.ParseLeadingTrivia(text);
    }
 
    [return: NotNullIfNotNull(nameof(node))]
    public override SyntaxNode? Visit(SyntaxNode? node)
    {
        _cancellationToken.ThrowIfCancellationRequested();
 
        if (node == null || !_spans.HasIntervalThatIntersectsWith(node.FullSpan))
        {
            return node;
        }
 
        return base.Visit(node);
    }
 
    public override SyntaxToken VisitToken(SyntaxToken token)
    {
        _cancellationToken.ThrowIfCancellationRequested();
 
        if (!_spans.HasIntervalThatIntersectsWith(token.FullSpan))
        {
            return token;
        }
 
        var hasChanges = false;
 
        // get token span
 
        // check whether we have trivia info belongs to this token
        if (_trailingTriviaMap.TryGetValue(token, out var trailingTrivia))
        {
            // okay, we have this situation
            // token|trivia
            hasChanges = true;
        }
        else
        {
            trailingTrivia = token.TrailingTrivia;
        }
 
        if (_leadingTriviaMap.TryGetValue(token, out var leadingTrivia))
        {
            // okay, we have this situation
            // trivia|token
            hasChanges = true;
        }
        else
        {
            leadingTrivia = token.LeadingTrivia;
        }
 
        if (hasChanges)
        {
            return CreateNewToken(leadingTrivia, token, trailingTrivia);
        }
 
        // we have no trivia belongs to this one
        return token;
    }
 
    private static SyntaxToken CreateNewToken(SyntaxTriviaList leadingTrivia, SyntaxToken token, SyntaxTriviaList trailingTrivia)
        => token.With(leadingTrivia, trailingTrivia);
}