// 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.Formatting; using Microsoft.CodeAnalysis.Shared.Extensions; namespace Microsoft.CodeAnalysis.CSharp.Formatting; internal sealed 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[^1])) { 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; } } } |