File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\CSharp\Formatting\Rules\TokenBasedFormattingRule.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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Utilities;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Formatting;
 
internal sealed class TokenBasedFormattingRule : BaseFormattingRule
{
    internal const string Name = "CSharp Token Based Formatting Rule";
 
    private readonly CSharpSyntaxFormattingOptions _options;
 
    public TokenBasedFormattingRule()
        : this(CSharpSyntaxFormattingOptions.Default)
    {
    }
 
    private TokenBasedFormattingRule(CSharpSyntaxFormattingOptions options)
    {
        _options = options;
    }
 
    public override AbstractFormattingRule WithOptions(SyntaxFormattingOptions options)
    {
        var newOptions = options as CSharpSyntaxFormattingOptions ?? CSharpSyntaxFormattingOptions.Default;
 
        if (_options.SeparateImportDirectiveGroups == newOptions.SeparateImportDirectiveGroups)
        {
            return this;
        }
 
        return new TokenBasedFormattingRule(newOptions);
    }
 
    public override AdjustNewLinesOperation? GetAdjustNewLinesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustNewLinesOperation nextOperation)
    {
        ////////////////////////////////////////////////////
        // brace related operations
        // * { or * }
        switch (currentToken.Kind())
        {
            case SyntaxKind.OpenBraceToken:
                if (currentToken.IsInterpolation())
                {
                    return null;
                }
 
                if (!previousToken.IsParenInParenthesizedExpression())
                {
                    return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
                }
 
                break;
 
            case SyntaxKind.CloseBraceToken:
                if (currentToken.IsInterpolation())
                {
                    return null;
                }
 
                return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
        }
 
        // do { } while case
        if (previousToken.Kind() == SyntaxKind.CloseBraceToken && currentToken.Kind() == SyntaxKind.WhileKeyword)
        {
            return CreateAdjustNewLinesOperation(0, AdjustNewLinesOption.PreserveLines);
        }
 
        // { * or } *
        switch (previousToken.Kind())
        {
            case SyntaxKind.CloseBraceToken:
                if (previousToken.IsInterpolation())
                {
                    return null;
                }
 
                if (!previousToken.IsCloseBraceOfExpression())
                {
                    if (!currentToken.IsKind(SyntaxKind.SemicolonToken) &&
                        !currentToken.IsParenInParenthesizedExpression() &&
                        !currentToken.IsCommaInInitializerExpression() &&
                        !currentToken.IsCommaInAnyArgumentsList() &&
                        !currentToken.IsCommaInTupleExpression() &&
                        !currentToken.IsCommaInCollectionExpression() &&
                        !currentToken.IsParenInArgumentList() &&
                        !currentToken.IsDotInMemberAccess() &&
                        !currentToken.IsCloseParenInStatement() &&
                        !currentToken.IsEqualsTokenInAutoPropertyInitializers() &&
                        !currentToken.IsColonInCasePatternSwitchLabel() && // no newline required before colon in pattern-switch-label (ex: `case {<pattern>}:`)
                        !currentToken.IsColonInSwitchExpressionArm())  // no newline required before colon in switch-expression-arm (ex: `{<pattern>}: expression`)
                    {
                        return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
                    }
                }
 
                break;
 
            case SyntaxKind.OpenBraceToken:
                if (previousToken.IsInterpolation())
                {
                    return null;
                }
 
                return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
        }
 
        ///////////////////////////////////////////////////
        // statement related operations
        // object and anonymous initializer "," case
        if (previousToken.IsCommaInInitializerExpression())
            return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
 
        if (previousToken.IsCommaInCollectionExpression())
            return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
 
        // , * in switch expression arm
        // ```
        // e switch
        // {
        //     pattern1: expression1, // newline with minimum of 1 line (each arm must be on its own line)
        //     pattern2: expression2 ...
        // ```
        if (previousToken.IsCommaInSwitchExpression())
        {
            return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
        }
 
        // , * in property sub-pattern
        // ```
        // e is
        // {
        //     property1: pattern1, // newline so the next line should be indented same as this one
        //     property2: pattern2, property3: pattern3, ... // but with minimum 0 lines so each property isn't forced to its own line
        // ```
        if (previousToken.IsCommaInPropertyPatternClause())
        {
            return CreateAdjustNewLinesOperation(0, AdjustNewLinesOption.PreserveLines);
        }
 
        // else * except else if case
        if (previousToken.Kind() == SyntaxKind.ElseKeyword && currentToken.Kind() != SyntaxKind.IfKeyword)
        {
            return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
        }
 
        // , * in enum declarations
        if (previousToken.IsCommaInEnumDeclaration())
        {
            return CreateAdjustNewLinesOperation(0, AdjustNewLinesOption.PreserveLines);
        }
 
        // : cases
        if (previousToken.IsColonInSwitchLabel() ||
            previousToken.IsColonInLabeledStatement())
        {
            return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
        }
 
        // embedded statement 
        if (previousToken.Kind() == SyntaxKind.CloseParenToken && previousToken.Parent.IsEmbeddedStatementOwnerWithCloseParen())
        {
            return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
        }
 
        if (previousToken.Kind() == SyntaxKind.DoKeyword && previousToken.Parent.IsKind(SyntaxKind.DoStatement))
        {
            return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
        }
 
        // for (int i = 10; i < 10; i++) case
        if (previousToken.IsSemicolonInForStatement())
        {
            return nextOperation.Invoke(in previousToken, in currentToken);
        }
 
        // ; case in the switch case statement and else condition
        if (previousToken.Kind() == SyntaxKind.SemicolonToken &&
            (currentToken.Kind() == SyntaxKind.CaseKeyword || currentToken.Kind() == SyntaxKind.DefaultKeyword || currentToken.Kind() == SyntaxKind.ElseKeyword))
        {
            return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
        }
 
        // ; * or ; * for using directive
        if (previousToken.Kind() == SyntaxKind.SemicolonToken)
        {
            return AdjustNewLinesAfterSemicolonToken(previousToken, currentToken);
        }
 
        // attribute case ] *
        // force to next line for top level attributes
        if (previousToken.Kind() == SyntaxKind.CloseBracketToken && previousToken.Parent is AttributeListSyntax)
        {
            var attributeOwner = previousToken.Parent?.Parent;
 
            if (attributeOwner is CompilationUnitSyntax or
                MemberDeclarationSyntax or
                AccessorDeclarationSyntax)
            {
                return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
            }
 
            return CreateAdjustNewLinesOperation(0, AdjustNewLinesOption.PreserveLines);
        }
 
        return nextOperation.Invoke(in previousToken, in currentToken);
    }
 
    private AdjustNewLinesOperation AdjustNewLinesAfterSemicolonToken(
        SyntaxToken previousToken, SyntaxToken currentToken)
    {
        // between anything that isn't a using directive, we don't touch newlines after a semicolon
        if (previousToken.Parent is not UsingDirectiveSyntax previousUsing)
            return CreateAdjustNewLinesOperation(0, AdjustNewLinesOption.PreserveLines);
 
        // if the user is separating using-groups, and we're between two usings, and these
        // usings *should* be separated, then do so (if the usings were already properly
        // sorted).
        if (_options.SeparateImportDirectiveGroups &&
            currentToken.Parent is UsingDirectiveSyntax currentUsing &&
            UsingsAndExternAliasesOrganizer.NeedsGrouping(previousUsing, currentUsing))
        {
            RoslynDebug.AssertNotNull(currentUsing.Parent);
 
            var usings = GetUsings(currentUsing.Parent);
            if (usings.IsSorted(UsingsAndExternAliasesDirectiveComparer.SystemFirstInstance) ||
                usings.IsSorted(UsingsAndExternAliasesDirectiveComparer.NormalInstance))
            {
                // Force at least one blank line here.
                return CreateAdjustNewLinesOperation(2, AdjustNewLinesOption.PreserveLines);
            }
        }
 
        // For all other cases where we have a using-directive, just make sure it's followed by
        // a new-line.
        return CreateAdjustNewLinesOperation(1, AdjustNewLinesOption.PreserveLines);
    }
 
    private static SyntaxList<UsingDirectiveSyntax> GetUsings(SyntaxNode node)
        => node switch
        {
            CompilationUnitSyntax compilationUnit => compilationUnit.Usings,
            BaseNamespaceDeclarationSyntax namespaceDecl => namespaceDecl.Usings,
            _ => throw ExceptionUtilities.UnexpectedValue(node.Kind()),
        };
 
    public override AdjustSpacesOperation? GetAdjustSpacesOperation(in SyntaxToken previousToken, in SyntaxToken currentToken, in NextGetAdjustSpacesOperation nextOperation)
    {
        //////////////////////////////////////////////////////
        // ";" related operations
        if (currentToken.Kind() == SyntaxKind.SemicolonToken)
        {
            // ; ;
            if (previousToken.Kind() == SyntaxKind.SemicolonToken)
            {
                return CreateAdjustSpacesOperation(1, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
            }
 
            // ) ; with embedded statement case
            if (previousToken.Kind() == SyntaxKind.CloseParenToken && previousToken.Parent.IsEmbeddedStatementOwnerWithCloseParen())
            {
                return CreateAdjustSpacesOperation(1, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
            }
 
            // * ;
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // omitted tokens case
        if (previousToken.Kind() == SyntaxKind.OmittedArraySizeExpressionToken ||
            previousToken.Kind() == SyntaxKind.OmittedTypeArgumentToken ||
            currentToken.Kind() == SyntaxKind.OmittedArraySizeExpressionToken ||
            currentToken.Kind() == SyntaxKind.OmittedTypeArgumentToken)
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        if (previousToken.IsKind(SyntaxKind.CloseBracketToken) &&
            previousToken.Parent.IsKind(SyntaxKind.AttributeList) &&
            previousToken.Parent.IsParentKind(SyntaxKind.Parameter))
        {
            if (currentToken.IsKind(SyntaxKind.OpenBracketToken))
            {
                // multiple attribute on parameter stick together
                // void M([...][...]
                return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
            }
            else
            {
                // attribute is spaced from parameter type
                // void M([...] int
                return CreateAdjustSpacesOperation(1, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
            }
        }
 
        // extension method on tuple type
        // M(this (
        if (currentToken.Kind() == SyntaxKind.OpenParenToken &&
            previousToken.Kind() == SyntaxKind.ThisKeyword)
        {
            return CreateAdjustSpacesOperation(1, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        if (previousToken.Kind() == SyntaxKind.NewKeyword)
        {
            // After a 'new' we almost always want a space.  only exceptions are `new()` as an implicit object 
            // creation, or `new()` as a constructor constraint or `new[] {}` for an implicit array creation.
            var spaces = previousToken.Parent is (kind:
                SyntaxKind.ConstructorConstraint or
                SyntaxKind.ImplicitObjectCreationExpression or
                SyntaxKind.ImplicitArrayCreationExpression) ? 0 : 1;
            return CreateAdjustSpacesOperation(spaces, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // some * "(" cases
        if (currentToken.Kind() == SyntaxKind.OpenParenToken)
        {
            if (previousToken.Kind() == SyntaxKind.IdentifierToken ||
                previousToken.Kind() == SyntaxKind.DefaultKeyword ||
                previousToken.Kind() == SyntaxKind.BaseKeyword ||
                previousToken.Kind() == SyntaxKind.ThisKeyword ||
                previousToken.IsGenericGreaterThanToken() ||
                currentToken.IsParenInArgumentList())
            {
                return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
            }
        }
 
        // empty () or []
        if (previousToken.ParenOrBracketContainsNothing(currentToken))
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // attribute case
        // , [
        if (previousToken.Kind() == SyntaxKind.CommaToken && currentToken.Kind() == SyntaxKind.OpenBracketToken && currentToken.Parent is AttributeListSyntax)
        {
            return CreateAdjustSpacesOperation(1, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // ] *
        if (previousToken.Kind() == SyntaxKind.CloseBracketToken && previousToken.Parent is AttributeListSyntax)
        {
            // preserving dev10 behavior, in dev10 we didn't touch space after attribute
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.PreserveSpaces);
        }
 
        // * )
        // * ]
        // * ,
        // * .
        // * ->
        switch (currentToken.Kind())
        {
            case SyntaxKind.CloseParenToken:
            case SyntaxKind.CloseBracketToken:
            case SyntaxKind.CommaToken:
            case SyntaxKind.DotToken:
            case SyntaxKind.MinusGreaterThanToken:
                return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // * [
        if (currentToken.IsKind(SyntaxKind.OpenBracketToken) &&
            currentToken.Parent?.Kind() is not SyntaxKind.CollectionExpression and not SyntaxKind.AttributeList &&
            !previousToken.IsOpenBraceOrCommaOfObjectInitializer())
        {
            if (previousToken.IsOpenBraceOfAccessorList() ||
                previousToken.IsLastTokenOfNode<AccessorDeclarationSyntax>())
            {
                return CreateAdjustSpacesOperation(1, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
            }
            else
            {
                return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
            }
        }
 
        // case * :
        // default:
        // <label> :
        // { Property1.Property2: ... }
        if (currentToken.IsKind(SyntaxKind.ColonToken))
        {
            if (currentToken.Parent is (kind:
                    SyntaxKind.CaseSwitchLabel or
                    SyntaxKind.CasePatternSwitchLabel or
                    SyntaxKind.DefaultSwitchLabel or
                    SyntaxKind.LabeledStatement or
                    SyntaxKind.AttributeTargetSpecifier or
                    SyntaxKind.NameColon or
                    SyntaxKind.ExpressionColon or SyntaxKind.SwitchExpressionArm))
            {
                return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
            }
        }
 
        // [cast expression] * case
        if (previousToken.Parent is CastExpressionSyntax &&
            previousToken.Kind() == SyntaxKind.CloseParenToken)
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // generic name
        if (previousToken.Parent is (kind: SyntaxKind.TypeArgumentList or SyntaxKind.TypeParameterList or SyntaxKind.FunctionPointerType))
        {
            // generic name < * 
            if (previousToken.Kind() == SyntaxKind.LessThanToken)
            {
                return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
            }
 
            // generic name > *
            if (previousToken.Kind() == SyntaxKind.GreaterThanToken && currentToken.Kind() == SyntaxKind.GreaterThanToken)
            {
                return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
            }
        }
 
        // generic name * < or * >
        if ((currentToken.Kind() == SyntaxKind.LessThanToken || currentToken.Kind() == SyntaxKind.GreaterThanToken) &&
            currentToken.Parent is (kind: SyntaxKind.TypeArgumentList or SyntaxKind.TypeParameterList))
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // ++ * or -- *
        if ((previousToken.Kind() == SyntaxKind.PlusPlusToken || previousToken.Kind() == SyntaxKind.MinusMinusToken) &&
             previousToken.Parent is PrefixUnaryExpressionSyntax)
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // * ++ or * --
        if ((currentToken.Kind() == SyntaxKind.PlusPlusToken || currentToken.Kind() == SyntaxKind.MinusMinusToken) &&
             currentToken.Parent is PostfixUnaryExpressionSyntax)
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // For spacing between the identifier and the conditional operator 
        if (currentToken.IsKind(SyntaxKind.QuestionToken) && currentToken.Parent.IsKind(SyntaxKind.ConditionalAccessExpression))
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // nullable
        if (currentToken.Kind() == SyntaxKind.QuestionToken &&
            currentToken.Parent is (kind: SyntaxKind.NullableType or SyntaxKind.ClassConstraint))
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // No space between an array type and ?
        if (currentToken.IsKind(SyntaxKind.QuestionToken) &&
            previousToken.Parent?.IsParentKind(SyntaxKind.ArrayType) == true)
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpaces);
        }
 
        // suppress warning operator: null! or x! or x++! or x[i]! or (x)! or ...
        if (currentToken.Kind() == SyntaxKind.ExclamationToken &&
            currentToken.Parent.IsKind(SyntaxKind.SuppressNullableWarningExpression))
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // pointer case for regular pointers
        if (currentToken.Kind() == SyntaxKind.AsteriskToken && currentToken.Parent is PointerTypeSyntax)
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // unary asterisk operator (PointerIndirectionExpression)
        if (previousToken.Kind() == SyntaxKind.AsteriskToken && previousToken.Parent is PrefixUnaryExpressionSyntax)
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // ( * or ) * or [ * or ] * or . * or -> *
        switch (previousToken.Kind())
        {
            case SyntaxKind.OpenParenToken:
            case SyntaxKind.OpenBracketToken:
            case SyntaxKind.DotToken:
            case SyntaxKind.MinusGreaterThanToken:
                return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
 
            case SyntaxKind.CloseParenToken:
            case SyntaxKind.CloseBracketToken:
                var space = (previousToken.Kind() == currentToken.Kind()) ? 0 : 1;
                return CreateAdjustSpacesOperation(space, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // +1 or -1
        if (previousToken.IsPlusOrMinusExpression() && !currentToken.IsPlusOrMinusExpression())
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // +- or -+ 
        if (previousToken.IsPlusOrMinusExpression() && currentToken.IsPlusOrMinusExpression() &&
            previousToken.Kind() != currentToken.Kind())
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // ! *, except where ! is the suppress nullable warning operator
        if (previousToken.Kind() == SyntaxKind.ExclamationToken
            && !previousToken.Parent.IsKind(SyntaxKind.SuppressNullableWarningExpression))
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // ~ * case
        if (previousToken.Kind() == SyntaxKind.TildeToken && (previousToken.Parent is PrefixUnaryExpressionSyntax || previousToken.Parent is DestructorDeclarationSyntax))
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // & * case
        if (previousToken.Kind() == SyntaxKind.AmpersandToken &&
            previousToken.Parent is PrefixUnaryExpressionSyntax)
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        // * :: or :: * case
        if (previousToken.Kind() == SyntaxKind.ColonColonToken || currentToken.Kind() == SyntaxKind.ColonColonToken)
        {
            return CreateAdjustSpacesOperation(0, AdjustSpacesOption.ForceSpacesIfOnSingleLine);
        }
 
        return nextOperation.Invoke(in previousToken, in currentToken);
    }
}