File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\CSharp\Formatting\Engine\Trivia\TriviaDataFactory.CodeShapeAnalyzer.cs
Web Access
Project: src\src\Workspaces\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Workspaces.csproj (Microsoft.CodeAnalysis.CSharp.Workspaces)
// 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.Diagnostics;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Formatting;
 
internal partial class TriviaDataFactory
{
    private struct CodeShapeAnalyzer
    {
        private readonly FormattingContext _context;
        private readonly SyntaxFormattingOptions _options;
        private readonly TriviaList _triviaList;
 
        private int _indentation;
        private bool _hasTrailingSpace;
        private int _lastLineBreakIndex;
        private bool _touchedNoisyCharacterOnCurrentLine;
 
        public static bool ShouldFormatMultiLine(FormattingContext context, bool firstTriviaInTree, TriviaList triviaList)
        {
            var analyzer = new CodeShapeAnalyzer(context, firstTriviaInTree, triviaList);
            return analyzer.ShouldFormat();
        }
 
        public static bool ShouldFormatSingleLine(TriviaList list)
        {
            foreach (var trivia in list)
            {
                Contract.ThrowIfTrue(trivia.Kind() == SyntaxKind.EndOfLineTrivia);
                Contract.ThrowIfTrue(trivia.Kind() == SyntaxKind.SkippedTokensTrivia);
                Contract.ThrowIfTrue(trivia.Kind() == SyntaxKind.PreprocessingMessageTrivia);
 
                // if it contains elastic trivia, always format
                if (trivia.IsElastic())
                {
                    return true;
                }
 
                if (trivia.Kind() == SyntaxKind.WhitespaceTrivia)
                {
                    Debug.Assert(trivia.ToString() == trivia.ToFullString());
                    var text = trivia.ToString();
                    if (text.IndexOf('\t') >= 0)
                    {
                        return true;
                    }
                }
 
                // we don't touch space between two tokens on a single line that contains
                // multiline comments between them
                if (trivia.IsRegularOrDocComment())
                {
                    return false;
                }
 
                if (trivia.Kind() == SyntaxKind.RegionDirectiveTrivia ||
                    trivia.Kind() == SyntaxKind.EndRegionDirectiveTrivia ||
                    SyntaxFacts.IsPreprocessorDirective(trivia.Kind()))
                {
                    return false;
                }
            }
 
            return true;
        }
 
        public static bool ContainsSkippedTokensOrText(TriviaList list)
        {
            foreach (var trivia in list)
            {
                if (trivia.Kind() is SyntaxKind.SkippedTokensTrivia or
                    SyntaxKind.PreprocessingMessageTrivia)
                {
                    return true;
                }
            }
 
            return false;
        }
 
        private CodeShapeAnalyzer(FormattingContext context, bool firstTriviaInTree, TriviaList triviaList)
        {
            _context = context;
            _options = context.Options;
            _triviaList = triviaList;
 
            _indentation = 0;
            _hasTrailingSpace = false;
            _lastLineBreakIndex = firstTriviaInTree ? 0 : -1;
            _touchedNoisyCharacterOnCurrentLine = false;
        }
 
        private readonly bool UseIndentation
        {
            get { return _lastLineBreakIndex >= 0; }
        }
 
        private static bool OnElastic(SyntaxTrivia trivia)
        {
            // if this is structured trivia then we need to check for elastic trivia in any descendant
            if (trivia.GetStructure() is { ContainsAnnotations: true } structure)
            {
                foreach (var t in structure.DescendantTrivia())
                {
                    if (t.IsElastic())
                    {
                        return true;
                    }
                }
            }
 
            // if it contains elastic trivia, always format
            return trivia.IsElastic();
        }
 
        private bool OnWhitespace(SyntaxTrivia trivia)
        {
            if (trivia.Kind() != SyntaxKind.WhitespaceTrivia)
            {
                return false;
            }
 
            // there was noisy char after end of line trivia
            if (!this.UseIndentation || _touchedNoisyCharacterOnCurrentLine)
            {
                _hasTrailingSpace = true;
                return false;
            }
 
            // right after end of line trivia. calculate indentation for current line
            Debug.Assert(trivia.ToString() == trivia.ToFullString());
            var text = trivia.ToString();
 
            // if text contains tab, we will give up perf optimization and use more expensive one to see whether we need to replace this trivia
            if (text.IndexOf('\t') >= 0)
            {
                return true;
            }
 
            _indentation += text.ConvertTabToSpace(_options.TabSize, _indentation, text.Length);
 
            return false;
        }
 
        private bool OnEndOfLine(SyntaxTrivia trivia, int currentIndex)
        {
            if (trivia.Kind() != SyntaxKind.EndOfLineTrivia)
            {
                return false;
            }
 
            // end of line trivia right after whitespace trivia
            if (_hasTrailingSpace)
            {
                // has trailing whitespace
                return true;
            }
 
            // empty line with spaces. remove it.
            if (_indentation > 0 && !_touchedNoisyCharacterOnCurrentLine)
            {
                return true;
            }
 
            ResetStateAfterNewLine(currentIndex);
            return false;
        }
 
        private void ResetStateAfterNewLine(int currentIndex)
        {
            // reset states for current line
            _indentation = 0;
            _touchedNoisyCharacterOnCurrentLine = false;
            _hasTrailingSpace = false;
 
            // remember last line break index
            _lastLineBreakIndex = currentIndex;
        }
 
        private readonly bool OnComment(SyntaxTrivia trivia)
        {
            if (!trivia.IsRegularOrDocComment())
            {
                return false;
            }
 
            // check whether indentation are right
            if (this.UseIndentation && _indentation != _context.GetBaseIndentation(trivia.SpanStart))
            {
                // comment has wrong indentation
                return true;
            }
 
            // go deep down for single line documentation comment
            if (trivia.IsSingleLineDocComment() &&
                ShouldFormatSingleLineDocumentationComment(_indentation, _options.TabSize, trivia))
            {
                return true;
            }
 
            return false;
        }
 
        private static bool OnSkippedTokensOrText(SyntaxTrivia trivia)
        {
            if (trivia.Kind() is not SyntaxKind.SkippedTokensTrivia and
                not SyntaxKind.PreprocessingMessageTrivia)
            {
                return false;
            }
 
            throw ExceptionUtilities.Unreachable();
        }
 
        private bool OnRegion(SyntaxTrivia trivia, int currentIndex)
        {
            if (trivia.Kind() is not SyntaxKind.RegionDirectiveTrivia and
                not SyntaxKind.EndRegionDirectiveTrivia)
            {
                return false;
            }
 
            if (!this.UseIndentation)
            {
                return true;
            }
 
            if (_indentation != _context.GetBaseIndentation(trivia.SpanStart))
            {
                return true;
            }
 
            ResetStateAfterNewLine(currentIndex);
            return false;
        }
 
        private bool OnPreprocessor(SyntaxTrivia trivia, int currentIndex)
        {
            if (!SyntaxFacts.IsPreprocessorDirective(trivia.Kind()))
            {
                return false;
            }
 
            if (!this.UseIndentation)
            {
                return true;
            }
 
            // preprocessor must be at from column 0
            if (_indentation != 0)
            {
                return true;
            }
 
            ResetStateAfterNewLine(currentIndex);
            return false;
        }
 
        private bool OnTouchedNoisyCharacter(SyntaxTrivia trivia)
        {
            if (trivia.IsElastic() ||
                trivia.Kind() == SyntaxKind.WhitespaceTrivia ||
                trivia.Kind() == SyntaxKind.EndOfLineTrivia)
            {
                return false;
            }
 
            _touchedNoisyCharacterOnCurrentLine = true;
            _hasTrailingSpace = false;
 
            return false;
        }
 
        private bool ShouldFormat()
        {
            var index = -1;
            foreach (var trivia in _triviaList)
            {
                index++;
 
                // order in which these methods run has a side effect. don't change the order
                // each method run
                if (OnElastic(trivia) ||
                    OnWhitespace(trivia) ||
                    OnEndOfLine(trivia, index) ||
                    OnTouchedNoisyCharacter(trivia) ||
                    OnComment(trivia) ||
                    OnSkippedTokensOrText(trivia) ||
                    OnRegion(trivia, index) ||
                    OnPreprocessor(trivia, index) ||
                    OnDisabledTextTrivia(trivia, index))
                {
                    return true;
                }
            }
 
            return false;
        }
 
        private bool OnDisabledTextTrivia(SyntaxTrivia trivia, int index)
        {
            if (trivia.IsKind(SyntaxKind.DisabledTextTrivia))
            {
                var triviaString = trivia.ToString();
                if (!string.IsNullOrEmpty(triviaString) && SyntaxFacts.IsNewLine(triviaString.Last()))
                {
                    ResetStateAfterNewLine(index);
                }
            }
 
            return false;
        }
 
        private static bool ShouldFormatSingleLineDocumentationComment(int indentation, int tabSize, SyntaxTrivia trivia)
        {
            Debug.Assert(trivia.HasStructure);
 
            var xmlComment = (DocumentationCommentTriviaSyntax)trivia.GetStructure()!;
 
            var sawFirstOne = false;
            foreach (var token in xmlComment.DescendantTokens())
            {
                foreach (var xmlTrivia in token.LeadingTrivia)
                {
                    if (xmlTrivia.Kind() == SyntaxKind.DocumentationCommentExteriorTrivia)
                    {
                        // skip first one since its leading whitespace will belong to syntax tree's syntax token
                        // not xml doc comment's token
                        if (!sawFirstOne)
                        {
                            sawFirstOne = true;
                            break;
                        }
 
                        var xmlCommentText = xmlTrivia.ToString();
 
                        // "///" == 3.
                        if (xmlCommentText.GetColumnFromLineOffset(xmlCommentText.Length - 3, tabSize) != indentation)
                        {
                            return true;
                        }
 
                        break;
                    }
                }
            }
 
            return false;
        }
    }
}