|
// 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.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.SplitOrMergeIfStatements;
internal abstract class AbstractSplitIfStatementCodeRefactoringProvider : CodeRefactoringProvider
{
protected abstract int GetLogicalExpressionKind(ISyntaxKindsService syntaxKinds);
protected abstract CodeAction CreateCodeAction(Func<CancellationToken, Task<Document>> createChangedDocument, string ifKeywordText);
protected abstract Task<SyntaxNode> GetChangedRootAsync(
Document document,
SyntaxNode root,
SyntaxNode ifOrElseIf,
SyntaxNode leftCondition,
SyntaxNode rightCondition,
CancellationToken cancellationToken);
public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var (document, textSpan, cancellationToken) = context;
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var token = root.FindToken(textSpan.Start);
if (textSpan.Length > 0 &&
textSpan != token.Span)
{
return;
}
var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
var syntaxKinds = document.GetLanguageService<ISyntaxKindsService>();
var ifGenerator = document.GetLanguageService<IIfLikeStatementGenerator>();
if (IsPartOfBinaryExpressionChain(token, GetLogicalExpressionKind(syntaxKinds), out var rootExpression) &&
ifGenerator.IsCondition(rootExpression, out var ifOrElseIf))
{
context.RegisterRefactoring(
CreateCodeAction(
c => RefactorAsync(document, token.Span, ifOrElseIf.Span, c),
syntaxFacts.GetText(syntaxKinds.IfKeyword)),
ifOrElseIf.Span);
}
}
private async Task<Document> RefactorAsync(Document document, TextSpan tokenSpan, TextSpan ifOrElseIfSpan, CancellationToken cancellationToken)
{
var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
var ifGenerator = document.GetLanguageService<IIfLikeStatementGenerator>();
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var token = root.FindToken(tokenSpan.Start);
var ifOrElseIf = root.FindNode(ifOrElseIfSpan);
Debug.Assert(ifGenerator.IsIfOrElseIf(ifOrElseIf));
var (left, right) = SplitBinaryExpressionChain(token, ifGenerator.GetCondition(ifOrElseIf), syntaxFacts);
var newRoot = await GetChangedRootAsync(document, root, ifOrElseIf, left, right, cancellationToken).ConfigureAwait(false);
return document.WithSyntaxRoot(newRoot);
}
private static bool IsPartOfBinaryExpressionChain(SyntaxToken token, int syntaxKind, out SyntaxNode rootExpression)
{
// Check whether the token is part of a binary expression, and if so,
// return the topmost binary expression in the chain (e.g. `a && b && c`).
SyntaxNodeOrToken current = token;
while (current.Parent?.RawKind == syntaxKind)
{
current = current.Parent;
}
rootExpression = current.AsNode();
return current.IsNode;
}
private static (SyntaxNode left, SyntaxNode right) SplitBinaryExpressionChain(
SyntaxToken token, SyntaxNode rootExpression, ISyntaxFactsService syntaxFacts)
{
// We have a left-associative binary expression chain, e.g. `a && b && c && d`.
// Let's say our token is the second `&&` token, between b and c. We'd like to split the chain at this point
// and build new expressions for the left side and the right side of this token. This will
// effectively change the associativity from `((a && b) && c) && d` to `(a && b) && (c && d)`.
// The left side is in the proper shape already and we can build the right side by getting the
// topmost expression and replacing our parent with our right side. In the example: `(a && b) && c` to `c`.
syntaxFacts.GetPartsOfBinaryExpression(token.Parent, out var parentLeft, out _, out var parentRight);
var left = parentLeft;
var right = rootExpression.ReplaceNode(token.Parent, parentRight);
return (left, right);
}
}
|