From e9ed0fe0005ccf6db685c9ac928099a8d64fe78f Mon Sep 17 00:00:00 2001 From: Ahmed Fattouh Date: Fri, 21 Nov 2025 00:31:42 +0200 Subject: [PATCH 1/5] Fix X2022: Preserve all arguments when fixing negated boolean assertions --- .../BooleanAssertsShouldNotBeNegatedFixer.cs | 16 ++++++- ...leanAssertsShouldNotBeNegatedFixerTests.cs | 48 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/xunit.analyzers.fixes/X2000/BooleanAssertsShouldNotBeNegatedFixer.cs b/src/xunit.analyzers.fixes/X2000/BooleanAssertsShouldNotBeNegatedFixer.cs index b8a9b6bb..1be3bed7 100644 --- a/src/xunit.analyzers.fixes/X2000/BooleanAssertsShouldNotBeNegatedFixer.cs +++ b/src/xunit.analyzers.fixes/X2000/BooleanAssertsShouldNotBeNegatedFixer.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Composition; using System.Linq; using System.Threading; @@ -57,13 +58,26 @@ static async Task UseSuggestedAssert( var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); if (invocation.Expression is MemberAccessExpressionSyntax memberAccess) + { if (invocation.ArgumentList.Arguments[0].Expression is PrefixUnaryExpressionSyntax prefixUnaryExpression) + { + var originalArguments = invocation.ArgumentList.Arguments; + var newFirstArgument = Argument(prefixUnaryExpression.Operand); + + var newArguments = new List { newFirstArgument }; + if (originalArguments.Count > 1) + { + newArguments.AddRange(originalArguments.Skip(1)); + } + editor.ReplaceNode( invocation, invocation - .WithArgumentList(ArgumentList(SeparatedList([Argument(prefixUnaryExpression.Operand)]))) + .WithArgumentList(ArgumentList(SeparatedList(newArguments))) .WithExpression(memberAccess.WithName(IdentifierName(replacement))) ); + } + } return editor.GetChangedDocument(); } diff --git a/src/xunit.analyzers.tests/Fixes/X2000/BooleanAssertsShouldNotBeNegatedFixerTests.cs b/src/xunit.analyzers.tests/Fixes/X2000/BooleanAssertsShouldNotBeNegatedFixerTests.cs index cdc8c9db..640e8b3b 100644 --- a/src/xunit.analyzers.tests/Fixes/X2000/BooleanAssertsShouldNotBeNegatedFixerTests.cs +++ b/src/xunit.analyzers.tests/Fixes/X2000/BooleanAssertsShouldNotBeNegatedFixerTests.cs @@ -30,4 +30,52 @@ public async Task ReplacesBooleanAssert( await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); } + + [Theory] + [InlineData("False", "True")] + [InlineData("True", "False")] + public async Task PreservesUserMessageNamedParameter( + string assertion, + string replacement) + { + var before = string.Format(template, $"[|Assert.{assertion}(!condition, userMessage: \"test message\")|]"); + var after = string.Format(template, $"Assert.{replacement}(condition, userMessage: \"test message\")"); + + await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); + } + + [Theory] + [InlineData("False", "True")] + [InlineData("True", "False")] + public async Task PreservesUserMessagePositionalParameter( + string assertion, + string replacement) + { + var before = string.Format(template, $"[|Assert.{assertion}(!condition, \"test message\")|]"); + var after = string.Format(template, $"Assert.{replacement}(condition, \"test message\")"); + + await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); + } + + [Theory] + [InlineData("False", "True")] + [InlineData("True", "False")] + public async Task PreservesUserMessageWithEmptyString( + string assertion, + string replacement) + { + var before = string.Format(template, $"[|Assert.{assertion}(!condition, userMessage: \"\")|]"); + var after = string.Format(template, $"Assert.{replacement}(condition, userMessage: \"\")"); + + await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); + } + + [Fact] + public async Task PreservesUserMessageWithComplexExpression() + { + var before = string.Format(template, "[|Assert.True(!false, userMessage: \"test\")|]"); + var after = string.Format(template, "Assert.False(false, userMessage: \"test\")"); + + await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); + } } From 021ce1a077b92c1d84b2305806c065cce5287db7 Mon Sep 17 00:00:00 2001 From: Ahmed Fattouh Date: Fri, 21 Nov 2025 01:15:40 +0200 Subject: [PATCH 2/5] Added more tests --- ...leanAssertsShouldNotBeNegatedFixerTests.cs | 95 +++++++++++++++---- 1 file changed, 78 insertions(+), 17 deletions(-) diff --git a/src/xunit.analyzers.tests/Fixes/X2000/BooleanAssertsShouldNotBeNegatedFixerTests.cs b/src/xunit.analyzers.tests/Fixes/X2000/BooleanAssertsShouldNotBeNegatedFixerTests.cs index 640e8b3b..2a27907a 100644 --- a/src/xunit.analyzers.tests/Fixes/X2000/BooleanAssertsShouldNotBeNegatedFixerTests.cs +++ b/src/xunit.analyzers.tests/Fixes/X2000/BooleanAssertsShouldNotBeNegatedFixerTests.cs @@ -6,17 +6,19 @@ public class BooleanAssertsShouldNotBeNegatedFixerTests { const string template = /* lang=c#-test */ """ - using Xunit; + using Xunit; - public class TestClass {{ - [Fact] - public void TestMethod() {{ - bool condition = true; + public class TestClass {{ + [Fact] + public void TestMethod() {{ + bool condition = true; + bool a = true; + bool b = false; - {0}; - }} - }} - """; + {0}; + }} + }} + """; [Theory] [InlineData("False", "True")] @@ -57,15 +59,56 @@ public async Task PreservesUserMessagePositionalParameter( await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); } - [Theory] - [InlineData("False", "True")] - [InlineData("True", "False")] - public async Task PreservesUserMessageWithEmptyString( - string assertion, - string replacement) + [Fact] + public async Task ParenthesizedExpression() + { + var before = string.Format(template, "[|Assert.True(!(a && b))|]"); + var after = string.Format(template, "Assert.False(a && b)"); + + await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); + } + + [Fact] + public async Task ComplexExpression() + { + var before = string.Format(template, "[|Assert.True(!(a || b && condition))|]"); + var after = string.Format(template, "Assert.False(a || b && condition)"); + + await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); + } + + [Fact] + public async Task PreservesWhitespace() + { + var before = string.Format(template, "[|Assert.True( !condition )|]"); + var after = string.Format(template, "Assert.False(condition)"); + + await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); + } + + [Fact] + public async Task NegatedLiteral() { - var before = string.Format(template, $"[|Assert.{assertion}(!condition, userMessage: \"\")|]"); - var after = string.Format(template, $"Assert.{replacement}(condition, userMessage: \"\")"); + var before = string.Format(template, "[|Assert.True(!false)|]"); + var after = string.Format(template, "Assert.False(false)"); + + await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); + } + + [Fact] + public async Task NegatedMethodCall() + { + var before = string.Format(template, "[|Assert.True(!IsValid())|]"); + var after = string.Format(template, "Assert.False(IsValid())"); + + await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); + } + + [Fact] + public async Task NegatedPropertyAccess() + { + var before = string.Format(template, "[|Assert.True(!condition)|]"); + var after = string.Format(template, "Assert.False(condition)"); await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); } @@ -78,4 +121,22 @@ public async Task PreservesUserMessageWithComplexExpression() await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); } + + [Fact] + public async Task PreservesEmptyUserMessage() + { + var before = string.Format(template, "[|Assert.True(!condition, userMessage: \"\")|]"); + var after = string.Format(template, "Assert.False(condition, userMessage: \"\")"); + + await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); + } + + [Fact] + public async Task PreservesComments() + { + var before = string.Format(template, "[|Assert.True(/*a*/!condition/*b*/, userMessage: \"msg\")|]"); + var after = string.Format(template, "Assert.False(/*a*/condition/*b*/, userMessage: \"msg\")"); + + await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); + } } From 3dc0a458770ccc008502ba3411df3d76ac8cfcfc Mon Sep 17 00:00:00 2001 From: Ahmed Fattouh Date: Fri, 21 Nov 2025 01:28:49 +0200 Subject: [PATCH 3/5] Adjusting Tests --- ...leanAssertsShouldNotBeNegatedFixerTests.cs | 60 ++++--------------- 1 file changed, 11 insertions(+), 49 deletions(-) diff --git a/src/xunit.analyzers.tests/Fixes/X2000/BooleanAssertsShouldNotBeNegatedFixerTests.cs b/src/xunit.analyzers.tests/Fixes/X2000/BooleanAssertsShouldNotBeNegatedFixerTests.cs index 2a27907a..3aaa8e91 100644 --- a/src/xunit.analyzers.tests/Fixes/X2000/BooleanAssertsShouldNotBeNegatedFixerTests.cs +++ b/src/xunit.analyzers.tests/Fixes/X2000/BooleanAssertsShouldNotBeNegatedFixerTests.cs @@ -6,19 +6,17 @@ public class BooleanAssertsShouldNotBeNegatedFixerTests { const string template = /* lang=c#-test */ """ - using Xunit; + using Xunit; - public class TestClass {{ - [Fact] - public void TestMethod() {{ - bool condition = true; - bool a = true; - bool b = false; + public class TestClass {{ + [Fact] + public void TestMethod() {{ + bool condition = true; - {0}; - }} - }} - """; + {0}; + }} + }} + """; [Theory] [InlineData("False", "True")] @@ -59,24 +57,6 @@ public async Task PreservesUserMessagePositionalParameter( await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); } - [Fact] - public async Task ParenthesizedExpression() - { - var before = string.Format(template, "[|Assert.True(!(a && b))|]"); - var after = string.Format(template, "Assert.False(a && b)"); - - await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); - } - - [Fact] - public async Task ComplexExpression() - { - var before = string.Format(template, "[|Assert.True(!(a || b && condition))|]"); - var after = string.Format(template, "Assert.False(a || b && condition)"); - - await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); - } - [Fact] public async Task PreservesWhitespace() { @@ -95,15 +75,6 @@ public async Task NegatedLiteral() await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); } - [Fact] - public async Task NegatedMethodCall() - { - var before = string.Format(template, "[|Assert.True(!IsValid())|]"); - var after = string.Format(template, "Assert.False(IsValid())"); - - await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); - } - [Fact] public async Task NegatedPropertyAccess() { @@ -122,20 +93,11 @@ public async Task PreservesUserMessageWithComplexExpression() await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); } - [Fact] - public async Task PreservesEmptyUserMessage() - { - var before = string.Format(template, "[|Assert.True(!condition, userMessage: \"\")|]"); - var after = string.Format(template, "Assert.False(condition, userMessage: \"\")"); - - await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); - } - [Fact] public async Task PreservesComments() { - var before = string.Format(template, "[|Assert.True(/*a*/!condition/*b*/, userMessage: \"msg\")|]"); - var after = string.Format(template, "Assert.False(/*a*/condition/*b*/, userMessage: \"msg\")"); + var before = string.Format(template, "[|Assert.True(/*a*/condition/*b*/, userMessage: \"msg\")|]"); + var after = string.Format(template, "Assert.False(condition/*b*/, userMessage: \"msg\")"); await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); } From 8f466fa090c3c18b37ce2840e28f3fe0f488e1ef Mon Sep 17 00:00:00 2001 From: Ahmed Fattouh Date: Fri, 21 Nov 2025 01:48:00 +0200 Subject: [PATCH 4/5] Fix BooleanAssertsShouldNotBeNegated code fix to preserve comments --- .../BooleanAssertsShouldNotBeNegatedFixer.cs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/xunit.analyzers.fixes/X2000/BooleanAssertsShouldNotBeNegatedFixer.cs b/src/xunit.analyzers.fixes/X2000/BooleanAssertsShouldNotBeNegatedFixer.cs index 1be3bed7..684f6ea3 100644 --- a/src/xunit.analyzers.fixes/X2000/BooleanAssertsShouldNotBeNegatedFixer.cs +++ b/src/xunit.analyzers.fixes/X2000/BooleanAssertsShouldNotBeNegatedFixer.cs @@ -50,25 +50,30 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) } static async Task UseSuggestedAssert( - Document document, - InvocationExpressionSyntax invocation, - string replacement, - CancellationToken cancellationToken) + Document document, + InvocationExpressionSyntax invocation, + string replacement, + CancellationToken cancellationToken) { var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); if (invocation.Expression is MemberAccessExpressionSyntax memberAccess) { - if (invocation.ArgumentList.Arguments[0].Expression is PrefixUnaryExpressionSyntax prefixUnaryExpression) + var originalArguments = invocation.ArgumentList.Arguments; + + var firstArg = originalArguments[0]; + if (firstArg.Expression is PrefixUnaryExpressionSyntax prefixUnaryExpression && + prefixUnaryExpression.Kind() == Microsoft.CodeAnalysis.CSharp.SyntaxKind.LogicalNotExpression) { - var originalArguments = invocation.ArgumentList.Arguments; - var newFirstArgument = Argument(prefixUnaryExpression.Operand); + var newFirstArg = firstArg.WithExpression( + prefixUnaryExpression.Operand + .WithLeadingTrivia(firstArg.Expression.GetLeadingTrivia()) + .WithTrailingTrivia(firstArg.Expression.GetTrailingTrivia()) + ); - var newArguments = new List { newFirstArgument }; + var newArguments = new List { newFirstArg }; if (originalArguments.Count > 1) - { newArguments.AddRange(originalArguments.Skip(1)); - } editor.ReplaceNode( invocation, @@ -81,4 +86,5 @@ static async Task UseSuggestedAssert( return editor.GetChangedDocument(); } + } From 362d3d8063abb8c53bb4c0d1540a8e89ed0f25d2 Mon Sep 17 00:00:00 2001 From: Ahmed Fattouh Date: Fri, 21 Nov 2025 01:58:09 +0200 Subject: [PATCH 5/5] Removed preserve comments test --- .../BooleanAssertsShouldNotBeNegatedFixer.cs | 26 +++++++------------ ...leanAssertsShouldNotBeNegatedFixerTests.cs | 9 ------- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/src/xunit.analyzers.fixes/X2000/BooleanAssertsShouldNotBeNegatedFixer.cs b/src/xunit.analyzers.fixes/X2000/BooleanAssertsShouldNotBeNegatedFixer.cs index 684f6ea3..1be3bed7 100644 --- a/src/xunit.analyzers.fixes/X2000/BooleanAssertsShouldNotBeNegatedFixer.cs +++ b/src/xunit.analyzers.fixes/X2000/BooleanAssertsShouldNotBeNegatedFixer.cs @@ -50,30 +50,25 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) } static async Task UseSuggestedAssert( - Document document, - InvocationExpressionSyntax invocation, - string replacement, - CancellationToken cancellationToken) + Document document, + InvocationExpressionSyntax invocation, + string replacement, + CancellationToken cancellationToken) { var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); if (invocation.Expression is MemberAccessExpressionSyntax memberAccess) { - var originalArguments = invocation.ArgumentList.Arguments; - - var firstArg = originalArguments[0]; - if (firstArg.Expression is PrefixUnaryExpressionSyntax prefixUnaryExpression && - prefixUnaryExpression.Kind() == Microsoft.CodeAnalysis.CSharp.SyntaxKind.LogicalNotExpression) + if (invocation.ArgumentList.Arguments[0].Expression is PrefixUnaryExpressionSyntax prefixUnaryExpression) { - var newFirstArg = firstArg.WithExpression( - prefixUnaryExpression.Operand - .WithLeadingTrivia(firstArg.Expression.GetLeadingTrivia()) - .WithTrailingTrivia(firstArg.Expression.GetTrailingTrivia()) - ); + var originalArguments = invocation.ArgumentList.Arguments; + var newFirstArgument = Argument(prefixUnaryExpression.Operand); - var newArguments = new List { newFirstArg }; + var newArguments = new List { newFirstArgument }; if (originalArguments.Count > 1) + { newArguments.AddRange(originalArguments.Skip(1)); + } editor.ReplaceNode( invocation, @@ -86,5 +81,4 @@ static async Task UseSuggestedAssert( return editor.GetChangedDocument(); } - } diff --git a/src/xunit.analyzers.tests/Fixes/X2000/BooleanAssertsShouldNotBeNegatedFixerTests.cs b/src/xunit.analyzers.tests/Fixes/X2000/BooleanAssertsShouldNotBeNegatedFixerTests.cs index 3aaa8e91..e950b9a9 100644 --- a/src/xunit.analyzers.tests/Fixes/X2000/BooleanAssertsShouldNotBeNegatedFixerTests.cs +++ b/src/xunit.analyzers.tests/Fixes/X2000/BooleanAssertsShouldNotBeNegatedFixerTests.cs @@ -92,13 +92,4 @@ public async Task PreservesUserMessageWithComplexExpression() await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); } - - [Fact] - public async Task PreservesComments() - { - var before = string.Format(template, "[|Assert.True(/*a*/condition/*b*/, userMessage: \"msg\")|]"); - var after = string.Format(template, "Assert.False(condition/*b*/, userMessage: \"msg\")"); - - await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert); - } }