File: InvertLogical\AbstractInvertLogicalCodeRefactoringProvider.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.
 
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.InvertLogical;
 
/// <summary>
/// Code refactoring to help convert code like `!a || !b` to `!(a &amp;&amp; b)`
/// </summary>
internal abstract class AbstractInvertLogicalCodeRefactoringProvider<
    TSyntaxKind,
    TExpressionSyntax,
    TBinaryExpressionSyntax>
    : CodeRefactoringProvider
    where TSyntaxKind : struct
    where TExpressionSyntax : SyntaxNode
    where TBinaryExpressionSyntax : TExpressionSyntax
{
    /// <summary>
    /// See comment in <see cref="InvertLogicalAsync"/> to understand the need for this annotation.
    /// </summary>
    private static readonly SyntaxAnnotation s_annotation = new();
 
    protected abstract string GetOperatorText(TSyntaxKind binaryExprKind);
 
    public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
    {
        var (document, span, cancellationToken) = context;
 
        var expression = (SyntaxNode?)await context.TryGetRelevantNodeAsync<TBinaryExpressionSyntax>().ConfigureAwait(false);
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        var syntaxKinds = document.GetRequiredLanguageService<ISyntaxKindsService>();
 
        if (expression == null ||
            (!syntaxFacts.IsLogicalAndExpression(expression) &&
            !syntaxFacts.IsLogicalOrExpression(expression)))
        {
            return;
        }
 
        if (span.IsEmpty)
        {
            // Walk up to the topmost binary of the same type.  When converting || to && (or vice versa)
            // we want to grab the entire set.  i.e.  `!a && !b && !c` should become `!(a || b || c)` not
            // `!(a || b) && !c`
            while (expression.Parent?.RawKind == expression.RawKind)
            {
                expression = expression.Parent;
            }
        }
        else
        {
            // When selection is non-empty -> allow only top-level full selections.
            // Currently the refactoring can't handle invert of arbitrary nodes but only whole subtrees
            // and allowing it just for selection of those nodes that - by chance - form a full subtree
            // would produce only confusion.
            if (CodeRefactoringHelpers.IsNodeUnderselected(expression, span) ||
                syntaxFacts.IsLogicalAndExpression(expression.Parent) || syntaxFacts.IsLogicalOrExpression(expression.Parent))
            {
                return;
            }
        }
 
        var title = GetTitle(syntaxKinds, expression.RawKind);
        context.RegisterRefactoring(
            CodeAction.Create(
                title,
                c => InvertLogicalAsync(document, expression, c),
                title),
            expression.Span);
    }
 
    private static async Task<Document> InvertLogicalAsync(
        Document document1, SyntaxNode binaryExpression, CancellationToken cancellationToken)
    {
        // We invert in two steps.  To invert `a op b` we are effectively generating two negations:
        // `!(!(a op b)`.  The inner `!` will distribute on the inside to make `!a op' !b` leaving
        // us with `!(!a op' !b)`.
 
        // Because we need to do two negations, we actually perform the inner one, marking the
        // result with an annotation, then we do the outer one (making sure we don't descend in
        // and undo the work we just did).  Because our negation helper needs semantics, we generate
        // a new document at each step so that we'll be able to properly analyze things as we go
        // along.
        var document2 = await InvertInnerExpressionAsync(document1, binaryExpression, cancellationToken).ConfigureAwait(false);
        var document3 = await InvertOuterExpressionAsync(document2, cancellationToken).ConfigureAwait(false);
        return document3;
    }
 
    private static async Task<Document> InvertInnerExpressionAsync(
        Document document, SyntaxNode binaryExpression, CancellationToken cancellationToken)
    {
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
        var generator = SyntaxGenerator.GetGenerator(document);
        var newBinary = generator.Negate(generator.SyntaxGeneratorInternal, binaryExpression, semanticModel, cancellationToken);
 
        return document.WithSyntaxRoot(root.ReplaceNode(
            binaryExpression,
            newBinary.WithAdditionalAnnotations(s_annotation)));
    }
 
    private static async Task<Document> InvertOuterExpressionAsync(
        Document document, CancellationToken cancellationToken)
    {
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
 
        var expression = root.GetAnnotatedNodes(s_annotation).Single()!;
 
        // Walk up parens and !'s.  That way we don't end up with something like !!.
        // It also ensures that this refactoring reverses itself when invoked twice.
        while (syntaxFacts.IsParenthesizedExpression(expression.Parent) ||
               syntaxFacts.IsLogicalNotExpression(expression.Parent))
        {
            expression = expression.Parent;
        }
 
        var generator = SyntaxGenerator.GetGenerator(document);
 
        // Negate the containing binary expr.  Pass the 'negateBinary:false' flag so we don't
        // just negate the work we're actually doing right now.
        return document.WithSyntaxRoot(root.ReplaceNode(
            expression,
            generator.Negate(generator.SyntaxGeneratorInternal, expression, semanticModel, negateBinary: false, cancellationToken)));
    }
 
    private string GetTitle(ISyntaxKindsService syntaxKinds, int binaryExprKind)
        => string.Format(FeaturesResources.Replace_0_with_1,
                GetOperatorText(syntaxKinds.Convert<TSyntaxKind>(binaryExprKind)),
                GetOperatorText(syntaxKinds.Convert<TSyntaxKind>(InvertedKind(syntaxKinds, binaryExprKind))));
 
    private static int InvertedKind(ISyntaxKindsService syntaxKinds, int binaryExprKind)
        => binaryExprKind == syntaxKinds.LogicalAndExpression
            ? syntaxKinds.LogicalOrExpression
            : syntaxKinds.LogicalAndExpression;
}