File: SplitOrMergeIfStatements\AbstractSplitIfStatementCodeRefactoringProvider.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.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);
    }
}