File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\CSharp\Extensions\BlockSyntaxExtensions.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.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.LanguageService;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Extensions;
 
internal static class BlockSyntaxExtensions
{
    public static bool TryConvertToExpressionBody(
        this BlockSyntax? block,
        LanguageVersion languageVersion,
        ExpressionBodyPreference preference,
        CancellationToken cancellationToken,
        [NotNullWhen(true)] out ExpressionSyntax? expression,
        out SyntaxToken semicolonToken)
    {
        if (preference != ExpressionBodyPreference.Never &&
            block is { Statements: [var statement] } &&
            TryGetExpression(statement, languageVersion, out expression, out semicolonToken) &&
            MatchesPreference(expression, preference) &&
            HasAcceptableDirectiveShape(statement, block.CloseBraceToken))
        {
            // The close brace of the block may have important trivia on it (like 
            // comments or directives).  Preserve them on the semicolon when we
            // convert to an expression body.
            semicolonToken = semicolonToken.WithAppendedTrailingTrivia(
                block.CloseBraceToken.LeadingTrivia.Where(t => !t.IsWhitespaceOrEndOfLine()));
            return true;
        }
 
        expression = null;
        semicolonToken = default;
        return false;
 
        static bool IsAnyCodeDirective(SyntaxTrivia trivia)
            => trivia.GetStructure() is ConditionalDirectiveTriviaSyntax;
 
        // We can have an ifdef'ed section around the statement, as long as each segment of the ifdef
        // contains an expression-statement or throw-statement.
        bool HasAcceptableDirectiveShape(StatementSyntax statement, SyntaxToken closeBrace)
        {
            var leadingDirectives = statement.GetLeadingTrivia().Where(IsAnyCodeDirective).ToImmutableArray();
            var closeBraceLeadingDirectives = block.CloseBraceToken.LeadingTrivia.Where(IsAnyCodeDirective).ToImmutableArray();
 
            if (leadingDirectives.Length == 0)
            {
                // If we don't have any leading directives, our close brace token better not have any as well.
                return closeBraceLeadingDirectives.Length == 0;
            }
 
            // Ok, we have some if/elif/else/endif pp directives above us.  If we're one of hte branches, and all
            // the rest of the branches are ok as well, we can convert this.
 
            if (leadingDirectives.Any(t => t.Kind() == SyntaxKind.EndIfDirectiveTrivia))
                return false;
 
            var firstDirective = (DirectiveTriviaSyntax)leadingDirectives.First().GetStructure()!;
            var conditionalDirectives = firstDirective.GetMatchingConditionalDirectives(cancellationToken);
 
            // The sequence of conditionals have to all be within the method body.
            if (conditionalDirectives.First().SpanStart <= block.OpenBraceToken.SpanStart ||
                conditionalDirectives.Last().Span.End >= block.CloseBraceToken.Span.End)
            {
                return false;
            }
 
            // Last directive has to come after our statement.
            if (conditionalDirectives.Last().Span.End <= statement.Span.Start)
                return false;
 
            // Now, check each part of the conditional chain
            foreach (var conditionalDirective in conditionalDirectives)
            {
                var parentTrivia = conditionalDirective.ParentTrivia;
                var parentToken = parentTrivia.Token;
                var triviaIndex = parentToken.LeadingTrivia.IndexOf(parentTrivia);
                if (triviaIndex + 1 < parentToken.LeadingTrivia.Count)
                {
                    var nextTrivia = parentToken.LeadingTrivia[triviaIndex + 1];
                    if (nextTrivia.Kind() == SyntaxKind.DisabledTextTrivia)
                    {
                        // This was a conditional before a disabled section.  Parse out the disabled section and make
                        // sure it can legally become the body of a expression-bodied member.
                        var parsed = SyntaxFactory.ParseStatement(nextTrivia.ToFullString());
                        if (parsed.GetDiagnostics().Any(static d => d.Severity == DiagnosticSeverity.Error))
                            return false;
                    }
                }
            }
 
            // Make sure there aren't any *new* pp directives before the close brace.
            foreach (var closeBraceDirective in closeBraceLeadingDirectives)
            {
                if (!conditionalDirectives.Contains((DirectiveTriviaSyntax)closeBraceDirective.GetStructure()!))
                    return false;
            }
 
            return true;
        }
    }
 
    public static bool TryConvertToArrowExpressionBody(
        this BlockSyntax block,
        SyntaxKind declarationKind,
        LanguageVersion languageVersion,
        ExpressionBodyPreference preference,
        CancellationToken cancellationToken,
        [NotNullWhen(true)] out ArrowExpressionClauseSyntax? arrowExpression,
        out SyntaxToken semicolonToken)
    {
        // We can always use arrow-expression bodies in C# 7 or above.
        // We can also use them in C# 6, but only a select set of member kinds.
        var acceptableVersion =
            languageVersion >= LanguageVersion.CSharp7 ||
            (languageVersion >= LanguageVersion.CSharp6 && IsSupportedInCSharp6(declarationKind));
 
        if (acceptableVersion &&
            block.TryConvertToExpressionBody(languageVersion, preference, cancellationToken, out var expression, out semicolonToken))
        {
            arrowExpression = SyntaxFactory.ArrowExpressionClause(expression);
 
            var parent = block.GetRequiredParent();
 
            if (parent.Kind() == SyntaxKind.GetAccessorDeclaration)
            {
                var comments = parent.GetLeadingTrivia().Where(t => !t.IsWhitespaceOrEndOfLine());
                if (!comments.IsEmpty())
                {
                    arrowExpression = arrowExpression.WithLeadingTrivia(
                        parent.GetLeadingTrivia());
                }
            }
 
            return true;
        }
 
        arrowExpression = null;
        semicolonToken = default;
        return false;
    }
 
    private static bool IsSupportedInCSharp6(SyntaxKind declarationKind)
    {
        switch (declarationKind)
        {
            case SyntaxKind.ConstructorDeclaration:
            case SyntaxKind.DestructorDeclaration:
            case SyntaxKind.AddAccessorDeclaration:
            case SyntaxKind.RemoveAccessorDeclaration:
            case SyntaxKind.GetAccessorDeclaration:
            case SyntaxKind.SetAccessorDeclaration:
                return false;
        }
 
        return true;
    }
 
    public static bool MatchesPreference(
        ExpressionSyntax expression, ExpressionBodyPreference preference)
    {
        if (preference == ExpressionBodyPreference.WhenPossible)
        {
            return true;
        }
 
        Contract.ThrowIfFalse(preference == ExpressionBodyPreference.WhenOnSingleLine);
        return CSharpSyntaxFacts.Instance.IsOnSingleLine(expression, fullSpan: false);
    }
 
    private static bool TryGetExpression(StatementSyntax firstStatement, LanguageVersion languageVersion, [NotNullWhen(true)] out ExpressionSyntax? expression, out SyntaxToken semicolonToken)
    {
        if (firstStatement is ExpressionStatementSyntax exprStatement)
        {
            expression = exprStatement.Expression;
            semicolonToken = exprStatement.SemicolonToken;
            return true;
        }
        else if (firstStatement is ReturnStatementSyntax returnStatement)
        {
            if (returnStatement.Expression != null)
            {
                // If there are any comments or directives on the return keyword, move them to
                // the expression.
                expression = firstStatement.GetLeadingTrivia().Any(t => t.IsDirective || t.IsSingleOrMultiLineComment())
                    ? returnStatement.Expression.WithLeadingTrivia(returnStatement.GetLeadingTrivia())
                    : returnStatement.Expression;
                semicolonToken = returnStatement.SemicolonToken;
                return true;
            }
        }
        else if (firstStatement is ThrowStatementSyntax throwStatement)
        {
            if (languageVersion >= LanguageVersion.CSharp7 && throwStatement.Expression != null)
            {
                expression = SyntaxFactory.ThrowExpression(throwStatement.ThrowKeyword, throwStatement.Expression);
                semicolonToken = throwStatement.SemicolonToken;
                return true;
            }
        }
 
        expression = null;
        semicolonToken = default;
        return false;
    }
}