|
// 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);
}
}
|