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