diff --git a/ChangeLog.md b/ChangeLog.md index 66f4b98e3f..2fc70123ad 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Improve inversion of logical expressions to handling additional cases ([#1086](https://github.com/josefpihrt/roslynator/pull/1086)). + ## [4.3.0] - 2023-04-24 ### Changed diff --git a/src/CSharp.Workspaces/CSharp/SyntaxLogicalInverter.cs b/src/CSharp.Workspaces/CSharp/SyntaxLogicalInverter.cs index 3c3a6914d9..515508274e 100644 --- a/src/CSharp.Workspaces/CSharp/SyntaxLogicalInverter.cs +++ b/src/CSharp.Workspaces/CSharp/SyntaxLogicalInverter.cs @@ -67,7 +67,7 @@ public ExpressionSyntax LogicallyInvert( return newExpression.WithTriviaFrom(expression); } - private ParenthesizedExpressionSyntax LogicallyInvertAndParenthesize( + private ExpressionSyntax LogicallyInvertAndParenthesize( ExpressionSyntax expression, SemanticModel semanticModel, CancellationToken cancellationToken) @@ -75,7 +75,8 @@ private ParenthesizedExpressionSyntax LogicallyInvertAndParenthesize( if (expression is null) return null; - return LogicallyInvertImpl(expression, semanticModel, cancellationToken).Parenthesize(); + var inverted = LogicallyInvertImpl(expression, semanticModel, cancellationToken); + return inverted.IsKind(SyntaxKind.LogicalNotExpression, SyntaxKind.ParenthesizedExpression) ? inverted : inverted.Parenthesize(); } private ExpressionSyntax LogicallyInvertImpl( @@ -88,18 +89,22 @@ private ExpressionSyntax LogicallyInvertImpl( switch (expression.Kind()) { + case SyntaxKind.IdentifierName: case SyntaxKind.SimpleMemberAccessExpression: case SyntaxKind.InvocationExpression: case SyntaxKind.ElementAccessExpression: + case SyntaxKind.CheckedExpression: + case SyntaxKind.UncheckedExpression: + case SyntaxKind.DefaultExpression: + case SyntaxKind.ConditionalAccessExpression: + { + return DefaultInvert(expression, false); + } case SyntaxKind.PostIncrementExpression: case SyntaxKind.PostDecrementExpression: case SyntaxKind.ObjectCreationExpression: case SyntaxKind.AnonymousObjectCreationExpression: case SyntaxKind.TypeOfExpression: - case SyntaxKind.DefaultExpression: - case SyntaxKind.CheckedExpression: - case SyntaxKind.UncheckedExpression: - case SyntaxKind.IdentifierName: { return DefaultInvert(expression); } @@ -500,7 +505,7 @@ private ExpressionSyntax InvertIsPattern(IsPatternExpressionSyntax isPattern) return isPattern.WithPattern(newConstantPattern); } - else if (constantExpression.IsKind(SyntaxKind.NullLiteralExpression)) + else if (constantExpression.IsKind(SyntaxKind.NullLiteralExpression, SyntaxKind.NumericLiteralExpression, SyntaxKind.StringLiteralExpression)) { UnaryPatternSyntax notPattern = NotPattern(constantPattern.WithoutTrivia()).WithTriviaFrom(constantPattern); @@ -515,14 +520,17 @@ private ExpressionSyntax InvertIsPattern(IsPatternExpressionSyntax isPattern) return DefaultInvert(isPattern); } - private static PrefixUnaryExpressionSyntax DefaultInvert(ExpressionSyntax expression) + private static PrefixUnaryExpressionSyntax DefaultInvert(ExpressionSyntax expression, bool needsParenthesize = true) { SyntaxDebug.Assert(expression.Kind() != SyntaxKind.ParenthesizedExpression, expression); SyntaxTriviaList leadingTrivia = expression.GetLeadingTrivia(); + expression = expression.WithoutLeadingTrivia(); + if (needsParenthesize) + expression = expression.Parenthesize(); return LogicalNotExpression( - expression.WithoutLeadingTrivia().Parenthesize(), + expression, Token(leadingTrivia, SyntaxKind.ExclamationToken, SyntaxTriviaList.Empty)); } } diff --git a/src/Tests/CSharp.Workspaces.Tests/SyntaxLogicalInverterTests.cs b/src/Tests/CSharp.Workspaces.Tests/SyntaxLogicalInverterTests.cs new file mode 100644 index 0000000000..90640c8612 --- /dev/null +++ b/src/Tests/CSharp.Workspaces.Tests/SyntaxLogicalInverterTests.cs @@ -0,0 +1,70 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Xunit; + +namespace Roslynator.CSharp.Workspaces.Tests; + +public class SyntaxLogicallyInvertTests +{ + private SyntaxLogicalInverter _inverter; + + public SyntaxLogicallyInvertTests() + { + _inverter = SyntaxLogicalInverter.Default; + } + + [Theory] + [InlineData(@"x", @"!x")] + [InlineData(@"!x", @"x")] + [InlineData(@"x is ""abc""", @"x is not ""abc""")] + [InlineData(@"x is 1", @"x is not 1")] + [InlineData(@"x is null", @"x is not null")] + [InlineData(@"x is true", @"x is false")] + [InlineData(@"true", @"false")] + [InlineData(@"false", @"true")] + [InlineData(@"x >= 3", @"x < 3")] + [InlineData(@"x > 3", @"x <= 3")] + [InlineData(@"x <= 3", @"x > 3")] + [InlineData(@"x < 3", @"x >= 3")] + [InlineData(@"x == y", @"x != y")] + [InlineData(@"x != y", @"x == y")] + [InlineData(@"(bool)x || (bool)y", @"!((bool)x) && !((bool)y)")] + [InlineData(@"(bool)x && (bool)y", @"!((bool)x) || !((bool)y)")] + [InlineData(@"x ?? true", @"x == false")] + [InlineData(@"x ?? false", @"x != true")] + [InlineData(@"(bool)x ? y : z", @"(bool)x ? !y : !z")] + [InlineData(@"x[0]", @"!x[0]")] + [InlineData(@"default(bool)", @"!default(bool)")] + [InlineData(@"checked(x + y)", @"!checked(x + y)")] + [InlineData(@"unchecked(x + y)", @"!unchecked(x + y)")] + [InlineData(@"(bool)x", @"!((bool)x)")] + [InlineData(@"x & y", @"!x | !y")] + [InlineData(@"x ^ y", @"!(x ^ y)")] + [InlineData(@"x | y", @"!x & !y")] + [InlineData(@"x = y", @"!(x = y)")] + [InlineData(@"await x", @"!(await x)")] + [InlineData(@"x ?? y", @"!(x ?? y)")] + [InlineData(@"x.a", @"!x.a")] + [InlineData(@"x.a()", @"!x.a()")] + [InlineData(@"x?.a", @"!x?.a")] + public async Task LogicallyInvert(string source, string expected) + { + var sourceCode = $"class C {{ void M(dynamic x, dynamic y, dynamic z){{ if({source})return;}} }}"; + var workspace = new AdhocWorkspace(); + var newProject = workspace.AddProject("TestProject", LanguageNames.CSharp); + var newDocument = workspace.AddDocument(newProject.Id, "TestDocument.cs", SourceText.From(sourceCode)); + var syntaxTree = await newDocument.GetSyntaxTreeAsync(); + var compilation = await newDocument.Project.GetCompilationAsync(); + var semanticModel = compilation.GetSemanticModel(syntaxTree); + + var expression = syntaxTree.GetRoot().DescendantNodes().OfType().Single().Condition; + + var result = _inverter.LogicallyInvert(expression, semanticModel, CancellationToken.None); + Assert.Equal(expected, result.NormalizeWhitespace().ToFullString()); + } + +}