File: SplitOrMergeIfStatements\Consecutive\AbstractSplitIntoConsecutiveIfStatementsCodeRefactoringProvider.cs
Web Access
Project: ..\..\..\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.
 
#nullable disable
 
using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.SplitOrMergeIfStatements
{
    internal abstract class AbstractSplitIntoConsecutiveIfStatementsCodeRefactoringProvider
        : AbstractSplitIfStatementCodeRefactoringProvider
    {
        // Converts:
        //    if (a || b)
        //        Console.WriteLine();
        //
        // To:
        //    if (a)
        //        Console.WriteLine();
        //    else if (b)
        //        Console.WriteLine();
 
        // Converts:
        //    if (a || b)
        //        return;
        //
        // To:
        //    if (a)
        //        return;
        //    if (b)
        //        return;
 
        // The second case is applied if control flow quits from inside the body.
 
        protected sealed override int GetLogicalExpressionKind(ISyntaxKindsService syntaxKinds)
            => syntaxKinds.LogicalOrExpression;
 
        protected sealed override CodeAction CreateCodeAction(Func<CancellationToken, Task<Document>> createChangedDocument, string ifKeywordText)
            => CodeAction.Create(
                string.Format(FeaturesResources.Split_into_consecutive_0_statements, ifKeywordText),
                createChangedDocument,
                nameof(FeaturesResources.Split_into_consecutive_0_statements) + "_" + ifKeywordText);
 
        protected sealed override async Task<SyntaxNode> GetChangedRootAsync(
            Document document,
            SyntaxNode root,
            SyntaxNode ifOrElseIf,
            SyntaxNode leftCondition,
            SyntaxNode rightCondition,
            CancellationToken cancellationToken)
        {
            var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
            var blockFacts = document.GetLanguageService<IBlockFactsService>();
            var ifGenerator = document.GetLanguageService<IIfLikeStatementGenerator>();
            var generator = document.GetLanguageService<SyntaxGenerator>();
 
            leftCondition = leftCondition.WithAdditionalAnnotations(Formatter.Annotation);
            rightCondition = rightCondition.WithAdditionalAnnotations(Formatter.Annotation);
 
            var editor = new SyntaxEditor(root, generator);
 
            editor.ReplaceNode(ifOrElseIf, (currentNode, _) => ifGenerator.WithCondition(currentNode, leftCondition));
 
            if (await CanBeSeparateStatementsAsync(document, blockFacts, ifGenerator, ifOrElseIf, cancellationToken).ConfigureAwait(false))
            {
                // Generate:
                // if (a)
                //     return;
                // if (b)
                //     return;
 
                // At this point, ifLikeStatement must be a standalone if statement with no else clause.
                Debug.Assert(syntaxFacts.IsExecutableStatement(ifOrElseIf));
                Debug.Assert(ifGenerator.GetElseIfAndElseClauses(ifOrElseIf).Length == 0);
 
                var secondIfStatement = ifGenerator.WithCondition(ifOrElseIf, rightCondition)
                    .WithPrependedLeadingTrivia(generator.ElasticCarriageReturnLineFeed);
 
                if (!blockFacts.IsExecutableBlock(ifOrElseIf.Parent))
                {
                    // In order to insert a new statement, we have to be inside a block.
                    editor.ReplaceNode(ifOrElseIf, (currentNode, _) => generator.ScopeBlock(ImmutableArray.Create(currentNode)));
                }
 
                editor.InsertAfter(ifOrElseIf, secondIfStatement);
            }
            else
            {
                // Generate:
                // if (a)
                //     Console.WriteLine();
                // else if (b)
                //     Console.WriteLine();
 
                // If the if statement is not an else-if clause, we convert it to an else-if clause first (for VB).
                // Then we insert it right after our current if statement or else-if clause.
 
                var elseIfClause = ifGenerator.WithCondition(ifGenerator.ToElseIfClause(ifOrElseIf), rightCondition);
 
                ifGenerator.InsertElseIfClause(editor, ifOrElseIf, elseIfClause);
            }
 
            return editor.GetChangedRoot();
        }
 
        private static async Task<bool> CanBeSeparateStatementsAsync(
            Document document,
            IBlockFactsService blockFacts,
            IIfLikeStatementGenerator ifGenerator,
            SyntaxNode ifOrElseIf,
            CancellationToken cancellationToken)
        {
            // In order to make separate statements, ifOrElseIf must be an if statement, not an else-if clause.
            if (ifGenerator.IsElseIfClause(ifOrElseIf, out _))
            {
                return false;
            }
 
            // If there is an else clause, we *could* in theory separate these and move the current else clause to the second
            // statement, but we won't. It would break the else-if chain in an odd way. We'll insert an else-if instead.
            if (ifGenerator.GetElseIfAndElseClauses(ifOrElseIf).Length > 0)
            {
                return false;
            }
 
            var insideStatements = blockFacts.GetStatementContainerStatements(ifOrElseIf);
            if (insideStatements.Count == 0)
            {
                // Even though there are no statements inside, we still can't split this into separate statements
                // because it would change the semantics from short-circuiting to always evaluating the second condition,
                // breaking code like 'if (a == null || a.InstanceMethod())'.
                return false;
            }
            else
            {
                // There are statements inside. We can split this into separate statements and leave out the 'else' if
                // control flow can't reach the end of these statements (otherwise, it would continue to the second 'if'
                // and in the case that both conditions are true, run the same statements twice).
                // This will typically look like a single return, break, continue or a throw statement.
 
                var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
                var controlFlow = semanticModel.AnalyzeControlFlow(insideStatements[0], insideStatements[insideStatements.Count - 1]);
 
                return !controlFlow.EndPointIsReachable;
            }
        }
    }
}