|
// 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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Utilities;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.Formatting;
internal class ElasticTriviaFormattingRule : BaseFormattingRule
{
internal const string Name = "CSharp Elastic trivia Formatting Rule";
public override void AddSuppressOperations(ArrayBuilder<SuppressOperation> list, SyntaxNode node, in NextSuppressOperationAction nextOperation)
{
nextOperation.Invoke();
if (!node.ContainsAnnotations)
{
return;
}
AddPropertyDeclarationSuppressOperations(list, node);
AddInitializerSuppressOperations(list, node);
AddCollectionExpressionSuppressOperations(list, node);
}
private static void AddPropertyDeclarationSuppressOperations(ArrayBuilder<SuppressOperation> list, SyntaxNode node)
{
if (node is BasePropertyDeclarationSyntax basePropertyDeclaration && basePropertyDeclaration.AccessorList != null &&
basePropertyDeclaration.AccessorList.Accessors.All(a => a.Body == null) &&
basePropertyDeclaration.GetAnnotatedTrivia(SyntaxAnnotation.ElasticAnnotation).Any())
{
var (firstToken, lastToken) = basePropertyDeclaration.GetFirstAndLastMemberDeclarationTokensAfterAttributes();
list.Add(FormattingOperations.CreateSuppressOperation(firstToken, lastToken, SuppressOption.NoWrapping | SuppressOption.IgnoreElasticWrapping));
}
}
private static void AddInitializerSuppressOperations(ArrayBuilder<SuppressOperation> list, SyntaxNode node)
{
var initializer = GetInitializerNode(node);
var lastTokenOfType = GetLastTokenOfType(node);
if (initializer != null && lastTokenOfType != null)
{
AddSuppressWrappingIfOnSingleLineOperation(list, lastTokenOfType.Value, initializer.CloseBraceToken, SuppressOption.IgnoreElasticWrapping);
return;
}
if (node is AnonymousObjectCreationExpressionSyntax anonymousCreationNode)
{
AddSuppressWrappingIfOnSingleLineOperation(list, anonymousCreationNode.NewKeyword, anonymousCreationNode.CloseBraceToken, SuppressOption.IgnoreElasticWrapping);
return;
}
}
private static void AddCollectionExpressionSuppressOperations(ArrayBuilder<SuppressOperation> list, SyntaxNode node)
{
if (node is CollectionExpressionSyntax { OpenBracketToken.IsMissing: false, CloseBracketToken.IsMissing: false } collectionExpression)
{
AddSuppressWrappingIfOnSingleLineOperation(list, collectionExpression.OpenBracketToken, collectionExpression.CloseBracketToken, SuppressOption.IgnoreElasticWrapping);
return;
}
}
private static InitializerExpressionSyntax? GetInitializerNode(SyntaxNode node)
=> node switch
{
ObjectCreationExpressionSyntax objectCreationNode => objectCreationNode.Initializer,
ArrayCreationExpressionSyntax arrayCreationNode => arrayCreationNode.Initializer,
ImplicitArrayCreationExpressionSyntax implicitArrayNode => implicitArrayNode.Initializer,
_ => null,
};
private static SyntaxToken? GetLastTokenOfType(SyntaxNode node)
{
if (node is ObjectCreationExpressionSyntax objectCreationNode)
{
return objectCreationNode.Type.GetLastToken();
}
if (node is ArrayCreationExpressionSyntax arrayCreationNode)
{
return arrayCreationNode.Type.GetLastToken();
}
if (node is ImplicitArrayCreationExpressionSyntax implicitArrayNode)
{
return implicitArrayNode.CloseBracketToken;
}
return null;
}
public override AdjustNewLinesOperation? GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation)
{
var operation = nextOperation.Invoke(in previousToken, in currentToken);
if (operation == null)
{
// If there are more than one Type Parameter Constraint Clause then each go in separate line
if (CommonFormattingHelpers.HasAnyWhitespaceElasticTrivia(previousToken, currentToken) &&
currentToken.IsKind(SyntaxKind.WhereKeyword) &&
currentToken.Parent.IsKind(SyntaxKind.TypeParameterConstraintClause))
{
RoslynDebug.AssertNotNull(previousToken.Parent);
// Check if there is another TypeParameterConstraintClause before
if (previousToken.Parent.Ancestors().OfType<TypeParameterConstraintClauseSyntax>().Any())
{
return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
}
// Check if there is another TypeParameterConstraintClause after
var firstTokenAfterTypeConstraint = currentToken.Parent.GetLastToken().GetNextToken();
var lastTokenForTypeConstraint = currentToken.Parent.GetLastToken().GetNextToken();
if (CommonFormattingHelpers.HasAnyWhitespaceElasticTrivia(lastTokenForTypeConstraint, firstTokenAfterTypeConstraint) &&
firstTokenAfterTypeConstraint.IsKind(SyntaxKind.WhereKeyword) &&
firstTokenAfterTypeConstraint.Parent.IsKind(SyntaxKind.TypeParameterConstraintClause))
{
return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
}
}
return null;
}
// Special case for formatting if-statements blocks on new lines
if (CommonFormattingHelpers.HasAnyWhitespaceElasticTrivia(previousToken, currentToken) &&
currentToken.IsKind(SyntaxKind.OpenBraceToken) &&
currentToken.Parent.IsParentKind(SyntaxKind.IfStatement))
{
var num = LineBreaksAfter(previousToken, currentToken);
return CreateAdjustNewLinesOperation(num, AdjustNewLinesOption.ForceLinesIfOnSingleLine);
}
// if operation is already forced, return as it is.
if (operation.Option == AdjustNewLinesOption.ForceLines)
return operation;
if (!CommonFormattingHelpers.HasAnyWhitespaceElasticTrivia(previousToken, currentToken))
return operation;
var afterFileScopedNamespaceOperation = GetAdjustNewLinesOperationAfterFileScopedNamespace(previousToken, currentToken);
if (afterFileScopedNamespaceOperation != null)
return afterFileScopedNamespaceOperation;
var betweenMemberOperation = GetAdjustNewLinesOperationBetweenMembers(previousToken, currentToken);
if (betweenMemberOperation != null)
return betweenMemberOperation;
var line = Math.Max(LineBreaksAfter(previousToken, currentToken), operation.Line);
if (line == 0)
{
return CreateAdjustNewLinesOperation(0, AdjustNewLinesOption.PreserveLines);
}
return CreateAdjustNewLinesOperation(line, AdjustNewLinesOption.ForceLines);
}
private static AdjustNewLinesOperation? GetAdjustNewLinesOperationAfterFileScopedNamespace(SyntaxToken previousToken, SyntaxToken currentToken)
{
if (previousToken.Kind() != SyntaxKind.SemicolonToken)
return null;
if (currentToken.Kind() == SyntaxKind.EndOfFileToken)
return null;
if (currentToken.Kind() == SyntaxKind.CloseBraceToken)
return null;
if (previousToken.Parent is not FileScopedNamespaceDeclarationSyntax)
return null;
if (TryGetOperationBeforeDocComment(currentToken, out var operation))
return operation;
return FormattingOperations.CreateAdjustNewLinesOperation(2, AdjustNewLinesOption.ForceLines);
}
private static AdjustNewLinesOperation? GetAdjustNewLinesOperationBetweenMembers(SyntaxToken previousToken, SyntaxToken currentToken)
{
if (!FormattingRangeHelper.InBetweenTwoMembers(previousToken, currentToken))
{
return null;
}
var previousMember = FormattingRangeHelper.GetEnclosingMember(previousToken);
var nextMember = FormattingRangeHelper.GetEnclosingMember(currentToken);
if (previousMember == null || nextMember == null)
{
return null;
}
if (TryGetOperationBeforeDocComment(currentToken, out var operation))
return operation;
// If we have two members of the same kind, we won't insert a blank line if both members
// have any content (e.g. accessors bodies, non-empty method bodies, etc.).
if (previousMember.Kind() == nextMember.Kind())
{
// Easy cases:
if (previousMember.Kind() is SyntaxKind.FieldDeclaration or
SyntaxKind.EventFieldDeclaration)
{
// Ensure that fields and events are each declared on a separate line.
return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.ForceLines);
}
// Don't insert a blank line between properties, indexers or events with no accessors
if (previousMember is BasePropertyDeclarationSyntax previousProperty)
{
var nextProperty = (BasePropertyDeclarationSyntax)nextMember;
if (previousProperty?.AccessorList?.Accessors.All(a => a.Body == null) == true &&
nextProperty?.AccessorList?.Accessors.All(a => a.Body == null) == true)
{
return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
}
}
// Don't insert a blank line between methods with no bodies
if (previousMember is BaseMethodDeclarationSyntax previousMethod)
{
var nextMethod = (BaseMethodDeclarationSyntax)nextMember;
if (previousMethod.Body == null &&
nextMethod.Body == null)
{
return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
}
}
}
return FormattingOperations.CreateAdjustNewLinesOperation(2 /* +1 for member itself and +1 for a blank line*/, AdjustNewLinesOption.ForceLines);
}
private static bool TryGetOperationBeforeDocComment(SyntaxToken currentToken, [NotNullWhen(true)] out AdjustNewLinesOperation? operation)
{
// see whether first non whitespace trivia after before the current member is a comment or not
var triviaList = currentToken.LeadingTrivia;
var firstNonWhitespaceTrivia = triviaList.FirstOrDefault(trivia => !IsWhitespace(trivia));
if (!firstNonWhitespaceTrivia.IsRegularOrDocComment())
{
operation = null;
return false;
}
// the first one is a comment, add two more lines than existing number of lines
var numberOfLines = GetNumberOfLines(triviaList);
var numberOfLinesBeforeComment = GetNumberOfLines(triviaList.Take(triviaList.IndexOf(firstNonWhitespaceTrivia)));
var addedLines = numberOfLinesBeforeComment < 1 ? 2 : 1;
operation = CreateAdjustNewLinesOperation(numberOfLines + addedLines, AdjustNewLinesOption.ForceLines);
return true;
}
public override AdjustSpacesOperation? GetAdjustSpacesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustSpacesOperation nextOperation)
{
var operation = nextOperation.Invoke(in previousToken, in currentToken);
if (operation == null)
{
return null;
}
// if operation is already forced, return as it is.
if (operation.Option == AdjustSpacesOption.ForceSpaces)
{
return operation;
}
if (CommonFormattingHelpers.HasAnyWhitespaceElasticTrivia(previousToken, currentToken))
{
// current implementation of engine gives higher priority on new line operations over space operations if
// two are conflicting.
// ex) new line operation says add 1 line between tokens, and
// space operation says give 1 space between two tokens (basically means remove new lines)
// then, engine will pick new line operation and ignore space operation
// make attributes have a space following
if (previousToken.IsKind(SyntaxKind.CloseBracketToken) &&
previousToken.Parent is AttributeListSyntax &&
currentToken.Parent is not AttributeListSyntax)
{
return CreateAdjustSpacesOperation(1, AdjustSpacesOption.ForceSpaces);
}
// make every operation forced
return CreateAdjustSpacesOperation(Math.Max(0, operation.Space), AdjustSpacesOption.ForceSpaces);
}
return operation;
}
// copied from compiler formatter to have same base forced format
private static int LineBreaksAfter(SyntaxToken previousToken, SyntaxToken currentToken)
{
if (currentToken.Kind() == SyntaxKind.None)
{
return 0;
}
switch (previousToken.Kind())
{
case SyntaxKind.None:
return 0;
case SyntaxKind.OpenBraceToken:
case SyntaxKind.FinallyKeyword:
return 1;
case SyntaxKind.CloseBraceToken:
return LineBreaksAfterCloseBrace(currentToken);
case SyntaxKind.CloseParenToken:
return (((previousToken.Parent is StatementSyntax) && currentToken.Parent != previousToken.Parent)
|| currentToken.Kind() == SyntaxKind.OpenBraceToken) ? 1 : 0;
case SyntaxKind.CloseBracketToken:
// Assembly and module-level attributes followed by non-attributes should have a blank line after
// them, unless it's the end of the file which will already have a blank line.
if (previousToken.Parent is AttributeListSyntax parent)
{
if (parent.Target != null &&
(parent.Target.Identifier.IsKindOrHasMatchingText(SyntaxKind.AssemblyKeyword) ||
parent.Target.Identifier.IsKindOrHasMatchingText(SyntaxKind.ModuleKeyword)))
{
if (!currentToken.IsKind(SyntaxKind.EndOfFileToken) && !(currentToken.Parent is AttributeListSyntax))
{
return 2;
}
}
if (previousToken.GetAncestor<ParameterSyntax>() == null
&& previousToken.GetAncestor<TypeParameterSyntax>() == null)
{
return 1;
}
}
break;
case SyntaxKind.SemicolonToken:
return LineBreaksAfterSemicolon(previousToken, currentToken);
case SyntaxKind.CommaToken:
return previousToken.Parent is EnumDeclarationSyntax ? 1 : 0;
case SyntaxKind.ElseKeyword:
return currentToken.Kind() != SyntaxKind.IfKeyword ? 1 : 0;
case SyntaxKind.ColonToken:
if (previousToken.Parent is LabeledStatementSyntax or SwitchLabelSyntax)
{
return 1;
}
break;
}
if ((currentToken.Kind() == SyntaxKind.FromKeyword && currentToken.Parent.IsKind(SyntaxKind.FromClause)) ||
(currentToken.Kind() == SyntaxKind.LetKeyword && currentToken.Parent.IsKind(SyntaxKind.LetClause)) ||
(currentToken.Kind() == SyntaxKind.WhereKeyword && currentToken.Parent.IsKind(SyntaxKind.WhereClause)) ||
(currentToken.Kind() == SyntaxKind.JoinKeyword && currentToken.Parent.IsKind(SyntaxKind.JoinClause)) ||
(currentToken.Kind() == SyntaxKind.JoinKeyword && currentToken.Parent.IsKind(SyntaxKind.JoinIntoClause)) ||
(currentToken.Kind() == SyntaxKind.OrderByKeyword && currentToken.Parent.IsKind(SyntaxKind.OrderByClause)) ||
(currentToken.Kind() == SyntaxKind.SelectKeyword && currentToken.Parent.IsKind(SyntaxKind.SelectClause)) ||
(currentToken.Kind() == SyntaxKind.GroupKeyword && currentToken.Parent.IsKind(SyntaxKind.GroupClause)))
{
return 1;
}
switch (currentToken.Kind())
{
case SyntaxKind.OpenBraceToken:
case SyntaxKind.CloseBraceToken:
case SyntaxKind.ElseKeyword:
case SyntaxKind.FinallyKeyword:
return 1;
case SyntaxKind.OpenBracketToken:
// Assembly and module-level attributes preceded by non-attributes should have
// a blank line separating them.
if (currentToken.Parent is AttributeListSyntax parent)
{
if (parent.Target != null &&
parent.Target.Identifier.Kind() is SyntaxKind.AssemblyKeyword or SyntaxKind.ModuleKeyword &&
previousToken.Parent is not AttributeListSyntax)
{
return 2;
}
// Attributes on parameters should have no lines between them.
if (parent.Parent is ParameterSyntax)
{
return 0;
}
return 1;
}
break;
case SyntaxKind.WhereKeyword:
return previousToken.Parent is TypeParameterListSyntax ? 1 : 0;
}
return 0;
}
private static int LineBreaksAfterCloseBrace(SyntaxToken nextToken)
{
if (nextToken.Kind() == SyntaxKind.CloseBraceToken)
{
return 1;
}
else if (
nextToken.Kind() is SyntaxKind.CatchKeyword or
SyntaxKind.FinallyKeyword or
SyntaxKind.ElseKeyword)
{
return 1;
}
else if (
nextToken.Kind() == SyntaxKind.WhileKeyword &&
nextToken.Parent.IsKind(SyntaxKind.DoStatement))
{
return 1;
}
else if (nextToken.Kind() == SyntaxKind.EndOfFileToken)
{
return 0;
}
else
{
return 2;
}
}
private static int LineBreaksAfterSemicolon(SyntaxToken previousToken, SyntaxToken currentToken)
{
if (previousToken.Parent is ForStatementSyntax)
{
return 0;
}
else if (currentToken.Kind() == SyntaxKind.CloseBraceToken)
{
return 1;
}
else if (previousToken.Parent is UsingDirectiveSyntax)
{
return currentToken.Parent is UsingDirectiveSyntax ? 1 : 2;
}
else if (previousToken.Parent is ExternAliasDirectiveSyntax)
{
return currentToken.Parent is ExternAliasDirectiveSyntax ? 1 : 2;
}
else if (currentToken.Parent is LocalFunctionStatementSyntax)
{
return 2;
}
else
{
return 1;
}
}
private static bool IsWhitespace(SyntaxTrivia trivia)
{
return trivia.Kind() is SyntaxKind.WhitespaceTrivia
or SyntaxKind.EndOfLineTrivia;
}
private static int GetNumberOfLines(IEnumerable<SyntaxTrivia> triviaList)
=> triviaList.Sum(t => t.ToFullString().Replace("\r\n", "\r").Cast<char>().Count(c => SyntaxFacts.IsNewLine(c)));
}
|