File: SplitOrMergeIfStatements\Consecutive\AbstractMergeConsecutiveIfStatementsCodeRefactoringProvider.cs
Web Access
Project: src\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.Generic;
using System.Diagnostics;
using System.Linq;
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;
using Microsoft.CodeAnalysis.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.SplitOrMergeIfStatements;
 
internal abstract class AbstractMergeConsecutiveIfStatementsCodeRefactoringProvider
    : AbstractMergeIfStatementsCodeRefactoringProvider
{
    // Converts:
    //    if (a)
    //        Console.WriteLine();
    //    else if (b)
    //        Console.WriteLine();
    //
    // To:
    //    if (a || b)
    //        Console.WriteLine();
 
    // Converts:
    //    if (a)
    //        return;
    //    if (b)
    //        return;
    //
    // To:
    //    if (a || b)
    //        return;
 
    // The body statements need to be equivalent. In the second case, control flow must quit from inside the body.
 
    protected sealed override CodeAction CreateCodeAction(Func<CancellationToken, Task<Document>> createChangedDocument, MergeDirection direction, string ifKeywordText)
    {
        var resourceText = direction == MergeDirection.Up ? FeaturesResources.Merge_with_previous_0_statement : FeaturesResources.Merge_with_next_0_statement;
        var title = string.Format(resourceText, ifKeywordText);
        return CodeAction.Create(title, createChangedDocument, title);
    }
 
    protected sealed override Task<bool> CanBeMergedUpAsync(
        Document document, SyntaxNode ifOrElseIf, CancellationToken cancellationToken, out SyntaxNode firstIfOrElseIf)
    {
        var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
        var blockFacts = document.GetLanguageService<IBlockFactsService>();
        var ifGenerator = document.GetLanguageService<IIfLikeStatementGenerator>();
 
        if (CanBeMergedWithParent(syntaxFacts, blockFacts, ifGenerator, ifOrElseIf, out firstIfOrElseIf))
            return SpecializedTasks.True;
 
        return CanBeMergedWithPreviousStatementAsync(document, syntaxFacts, blockFacts, ifGenerator, ifOrElseIf, cancellationToken, out firstIfOrElseIf);
    }
 
    protected sealed override Task<bool> CanBeMergedDownAsync(
        Document document, SyntaxNode ifOrElseIf, CancellationToken cancellationToken, out SyntaxNode secondIfOrElseIf)
    {
        var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
        var blockFacts = document.GetLanguageService<IBlockFactsService>();
        var ifGenerator = document.GetLanguageService<IIfLikeStatementGenerator>();
 
        if (CanBeMergedWithElseIf(syntaxFacts, blockFacts, ifGenerator, ifOrElseIf, out secondIfOrElseIf))
            return SpecializedTasks.True;
 
        return CanBeMergedWithNextStatementAsync(document, syntaxFacts, blockFacts, ifGenerator, ifOrElseIf, cancellationToken, out secondIfOrElseIf);
    }
 
    protected sealed override SyntaxNode GetChangedRoot(Document document, SyntaxNode root, SyntaxNode firstIfOrElseIf, SyntaxNode secondIfOrElseIf)
    {
        var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
        var ifGenerator = document.GetLanguageService<IIfLikeStatementGenerator>();
        var generator = document.GetLanguageService<SyntaxGenerator>();
 
        var newCondition = generator.LogicalOrExpression(
            ifGenerator.GetCondition(firstIfOrElseIf),
            ifGenerator.GetCondition(secondIfOrElseIf));
 
        newCondition = newCondition.WithAdditionalAnnotations(Formatter.Annotation);
 
        var editor = new SyntaxEditor(root, generator);
 
        editor.ReplaceNode(firstIfOrElseIf, (currentNode, _) => ifGenerator.WithCondition(currentNode, newCondition));
 
        if (ifGenerator.IsElseIfClause(secondIfOrElseIf, out _))
        {
            // We have:
            //    if (a)
            //        Console.WriteLine();
            //    else if (b)
            //        Console.WriteLine();
 
            // Remove the else-if clause and preserve any subsequent clauses.
 
            ifGenerator.RemoveElseIfClause(editor, secondIfOrElseIf);
        }
        else
        {
            // We have:
            //    if (a)
            //        return;
            //    if (b)
            //        return;
 
            // At this point, ifLikeStatement must be a standalone if statement, possibly with an else clause (there won't
            // be any on the first statement though). We'll move any else-if and else clauses to the first statement
            // and then remove the second one.
            // The opposite refactoring (SplitIntoConsecutiveIfStatements) never generates a separate statement
            // with an else clause but we support it anyway (in inserts an else-if instead).
            Debug.Assert(syntaxFacts.IsExecutableStatement(secondIfOrElseIf));
            Debug.Assert(syntaxFacts.IsExecutableStatement(firstIfOrElseIf));
            Debug.Assert(ifGenerator.GetElseIfAndElseClauses(firstIfOrElseIf).Length == 0);
 
            editor.ReplaceNode(
                firstIfOrElseIf,
                (currentNode, _) => ifGenerator.WithElseIfAndElseClausesOf(currentNode, secondIfOrElseIf));
 
            editor.RemoveNode(secondIfOrElseIf);
        }
 
        return editor.GetChangedRoot();
    }
 
    private static bool CanBeMergedWithParent(
        ISyntaxFactsService syntaxFacts,
        IBlockFactsService blockFacts,
        IIfLikeStatementGenerator ifGenerator,
        SyntaxNode ifOrElseIf,
        out SyntaxNode parentIfOrElseIf)
    {
        return ifGenerator.IsElseIfClause(ifOrElseIf, out parentIfOrElseIf) &&
               ContainEquivalentStatements(syntaxFacts, blockFacts, ifOrElseIf, parentIfOrElseIf, out _);
    }
 
    private static bool CanBeMergedWithElseIf(
        ISyntaxFactsService syntaxFacts,
        IBlockFactsService blockFacts,
        IIfLikeStatementGenerator ifGenerator,
        SyntaxNode ifOrElseIf,
        out SyntaxNode elseIfClause)
    {
        return ifGenerator.HasElseIfClause(ifOrElseIf, out elseIfClause) &&
               ContainEquivalentStatements(syntaxFacts, blockFacts, ifOrElseIf, elseIfClause, out _);
    }
 
    private static Task<bool> CanBeMergedWithPreviousStatementAsync(
        Document document,
        ISyntaxFactsService syntaxFacts,
        IBlockFactsService blockFacts,
        IIfLikeStatementGenerator ifGenerator,
        SyntaxNode ifOrElseIf,
        CancellationToken cancellationToken,
        out SyntaxNode previousStatement)
    {
        return TryGetSiblingStatement(syntaxFacts, blockFacts, ifOrElseIf, relativeIndex: -1, out previousStatement)
            ? CanStatementsBeMergedAsync(document, syntaxFacts, blockFacts, ifGenerator, previousStatement, ifOrElseIf, cancellationToken)
            : SpecializedTasks.False;
    }
 
    private static Task<bool> CanBeMergedWithNextStatementAsync(
        Document document,
        ISyntaxFactsService syntaxFacts,
        IBlockFactsService blockFacts,
        IIfLikeStatementGenerator ifGenerator,
        SyntaxNode ifOrElseIf,
        CancellationToken cancellationToken,
        out SyntaxNode nextStatement)
    {
        return TryGetSiblingStatement(syntaxFacts, blockFacts, ifOrElseIf, relativeIndex: 1, out nextStatement)
            ? CanStatementsBeMergedAsync(document, syntaxFacts, blockFacts, ifGenerator, ifOrElseIf, nextStatement, cancellationToken)
            : SpecializedTasks.False;
    }
 
    private static async Task<bool> CanStatementsBeMergedAsync(
        Document document,
        ISyntaxFactsService syntaxFacts,
        IBlockFactsService blockFacts,
        IIfLikeStatementGenerator ifGenerator,
        SyntaxNode firstStatement,
        SyntaxNode secondStatement,
        CancellationToken cancellationToken)
    {
        // We don't support cases where the previous if statement has any else-if or else clauses. In order for that
        // to be mergable, the control flow would have to quit from inside every branch, which is getting a little complex.
        if (!ifGenerator.IsIfOrElseIf(firstStatement) || ifGenerator.GetElseIfAndElseClauses(firstStatement).Length > 0)
            return false;
 
        if (!ifGenerator.IsIfOrElseIf(secondStatement))
            return false;
 
        if (!ContainEquivalentStatements(syntaxFacts, blockFacts, firstStatement, secondStatement, out var insideStatements))
            return false;
 
        if (insideStatements.Count == 0)
        {
            // Even though there are no statements inside, we still can't merge these into one statement
            // because it would change the semantics from always evaluating the second condition to short-circuiting.
            return false;
        }
        else
        {
            // There are statements inside. We can merge these into one statement if
            // control flow can't reach the end of these statements (otherwise, it would change from running
            // the second 'if' in the case that both conditions are true to only running the statements once).
            // 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;
        }
    }
 
    private static bool TryGetSiblingStatement(
        ISyntaxFactsService syntaxFacts,
        IBlockFactsService blockFacts,
        SyntaxNode ifOrElseIf,
        int relativeIndex,
        out SyntaxNode statement)
    {
        if (syntaxFacts.IsExecutableStatement(ifOrElseIf) &&
            blockFacts.IsExecutableBlock(ifOrElseIf.Parent))
        {
            var blockStatements = blockFacts.GetExecutableBlockStatements(ifOrElseIf.Parent);
 
            statement = blockStatements.ElementAtOrDefault(blockStatements.IndexOf(ifOrElseIf) + relativeIndex);
            return statement != null;
        }
 
        statement = null;
        return false;
    }
 
    private static bool ContainEquivalentStatements(
        ISyntaxFactsService syntaxFacts,
        IBlockFactsService blockFacts,
        SyntaxNode ifStatement1,
        SyntaxNode ifStatement2,
        out IReadOnlyList<SyntaxNode> statements)
    {
        var statements1 = WalkDownScopeBlocks(blockFacts, blockFacts.GetStatementContainerStatements(ifStatement1));
        var statements2 = WalkDownScopeBlocks(blockFacts, blockFacts.GetStatementContainerStatements(ifStatement2));
 
        statements = statements1;
        return statements1.SequenceEqual(statements2, syntaxFacts.AreEquivalent);
    }
}