File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\CSharp\Formatting\Rules\ElasticTriviaFormattingRule.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;
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)));
}