|
// 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;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.Formatting;
/// <summary>
/// trivia factory.
///
/// it will cache some commonly used trivia to reduce memory footprint and heap allocation
/// </summary>
internal partial class TriviaDataFactory : AbstractTriviaDataFactory
{
public TriviaDataFactory(TreeData treeInfo, LineFormattingOptions options)
: base(treeInfo, options)
{
}
private static bool IsCSharpWhitespace(char c)
=> SyntaxFacts.IsWhitespace(c) || SyntaxFacts.IsNewLine(c);
public override TriviaData CreateLeadingTrivia(SyntaxToken token)
{
// no trivia
if (!token.HasLeadingTrivia)
{
Debug.Assert(this.TreeInfo.GetTextBetween(default, token).All(IsCSharpWhitespace));
return GetSpaceTriviaData(space: 0);
}
var result = Analyzer.Leading(token);
var info = GetWhitespaceOnlyTriviaInfo(default, token, result);
if (info != null)
{
Debug.Assert(this.TreeInfo.GetTextBetween(default, token).All(IsCSharpWhitespace));
return info;
}
return new ComplexTrivia(this.Options, this.TreeInfo, default, token);
}
public override TriviaData CreateTrailingTrivia(SyntaxToken token)
{
// no trivia
if (!token.HasTrailingTrivia)
{
Debug.Assert(this.TreeInfo.GetTextBetween(token, default).All(IsCSharpWhitespace));
return GetSpaceTriviaData(space: 0);
}
var result = Analyzer.Trailing(token);
var info = GetWhitespaceOnlyTriviaInfo(token, default, result);
if (info != null)
{
Debug.Assert(this.TreeInfo.GetTextBetween(token, default).All(IsCSharpWhitespace));
return info;
}
return new ComplexTrivia(this.Options, this.TreeInfo, token, default);
}
public override TriviaData Create(SyntaxToken token1, SyntaxToken token2)
{
// no trivia in between
if (!token1.HasTrailingTrivia && !token2.HasLeadingTrivia)
{
Debug.Assert(string.IsNullOrWhiteSpace(this.TreeInfo.GetTextBetween(token1, token2)));
return GetSpaceTriviaData(space: 0);
}
var result = Analyzer.Between(token1, token2);
var info = GetWhitespaceOnlyTriviaInfo(token1, token2, result);
if (info != null)
{
Debug.Assert(string.IsNullOrWhiteSpace(this.TreeInfo.GetTextBetween(token1, token2)));
return info;
}
return new ComplexTrivia(this.Options, this.TreeInfo, token1, token2);
}
private static bool ContainsOnlyWhitespace(Analyzer.AnalysisResult result)
{
return
!result.HasComments &&
!result.HasPreprocessor &&
!result.HasSkippedTokens &&
!result.HasSkippedOrDisabledText &&
!result.HasConflictMarker;
}
private TriviaData? GetWhitespaceOnlyTriviaInfo(SyntaxToken token1, SyntaxToken token2, Analyzer.AnalysisResult result)
{
if (!ContainsOnlyWhitespace(result))
{
return null;
}
// only whitespace in between
var space = GetSpaceOnSingleLine(result);
Contract.ThrowIfFalse(space >= -1);
if (space >= 0)
{
// check whether we can use cache
return GetSpaceTriviaData(space, result.TreatAsElastic);
}
// tab is used in a place where it is not an indentation
if (result.LineBreaks == 0 && result.Tab > 0)
{
// calculate actual space size from tab
var spaces = CalculateSpaces(token1, token2);
return new ModifiedWhitespace(this.Options, result.LineBreaks, indentation: spaces, elastic: result.TreatAsElastic);
}
// check whether we can cache trivia info for current indentation
var lineCountAndIndentation = GetLineBreaksAndIndentation(result);
return GetWhitespaceTriviaData(lineCountAndIndentation.lineBreaks, lineCountAndIndentation.indentation, lineCountAndIndentation.canUseTriviaAsItIs, result.TreatAsElastic);
}
private int CalculateSpaces(SyntaxToken token1, SyntaxToken token2)
{
var initialColumn = (token1.RawKind == 0) ? 0 : this.TreeInfo.GetOriginalColumn(Options.TabSize, token1) + token1.Span.Length;
var textSnippet = this.TreeInfo.GetTextBetween(token1, token2);
return textSnippet.ConvertTabToSpace(Options.TabSize, initialColumn, textSnippet.Length);
}
private (bool canUseTriviaAsItIs, int lineBreaks, int indentation) GetLineBreaksAndIndentation(Analyzer.AnalysisResult result)
{
Debug.Assert(result.Tab >= 0);
Debug.Assert(result.LineBreaks >= 0);
var indentation = result.Tab * Options.TabSize + result.Space;
if (result.HasTrailingSpace || result.HasUnknownWhitespace)
{
if (result.HasUnknownWhitespace && result.LineBreaks == 0 && indentation == 0)
{
// make sure we don't remove all whitespace
indentation = 1;
}
return (canUseTriviaAsItIs: false, result.LineBreaks, indentation);
}
if (!Options.UseTabs)
{
if (result.Tab > 0)
{
return (canUseTriviaAsItIs: false, result.LineBreaks, indentation);
}
return (canUseTriviaAsItIs: true, result.LineBreaks, indentation);
}
Debug.Assert(Options.UseTabs);
// tab can only appear before space to be a valid tab for indentation
if (result.HasTabAfterSpace)
{
return (canUseTriviaAsItIs: false, result.LineBreaks, indentation);
}
if (result.Space >= Options.TabSize)
{
return (canUseTriviaAsItIs: false, result.LineBreaks, indentation);
}
Debug.Assert((indentation / Options.TabSize) == result.Tab);
Debug.Assert((indentation % Options.TabSize) == result.Space);
return (canUseTriviaAsItIs: true, result.LineBreaks, indentation);
}
private static int GetSpaceOnSingleLine(Analyzer.AnalysisResult result)
{
if (result.HasTrailingSpace || result.HasUnknownWhitespace || result.LineBreaks > 0 || result.Tab > 0)
{
return -1;
}
return result.Space;
}
}
|