Skip to content

Commit

Permalink
xunit/xunit#1435: Flip Assert.True/False when passed a negated expres…
Browse files Browse the repository at this point in the history
…sion
  • Loading branch information
bradwilson committed Nov 9, 2023
1 parent 51d5c17 commit 4a9c2c3
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Composition;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace Xunit.Analyzers.Fixes;

[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public class BooleanAssertsShouldNotBeNegatedFixer : BatchedCodeFixProvider
{
public const string Key_UseSuggestedAssert = "xUnit2022_UseSuggestedAssert";

public BooleanAssertsShouldNotBeNegatedFixer() :
base(Descriptors.X2022_BooleanAssertionsShouldNotBeNegated.Id)
{ }

public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null)
return;

var invocation = root.FindNode(context.Span).FirstAncestorOrSelf<InvocationExpressionSyntax>();
if (invocation is null)
return;

var diagnostic = context.Diagnostics.FirstOrDefault();
if (diagnostic is null)
return;
if (!diagnostic.Properties.TryGetValue(Constants.Properties.Replacement, out var replacement))
return;
if (replacement is null)
return;

context.RegisterCodeFix(
CodeAction.Create(
string.Format(CultureInfo.CurrentCulture, "Use Assert.{0}", replacement),
ct => UseSuggestedAssert(context.Document, invocation, replacement, ct),
Key_UseSuggestedAssert
),
context.Diagnostics
);
}

static async Task<Document> UseSuggestedAssert(
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)
editor.ReplaceNode(
invocation,
invocation
.WithArgumentList(ArgumentList(SeparatedList(new[] { Argument(prefixUnaryExpression.Operand) })))
.WithExpression(memberAccess.WithName(IdentifierName(replacement)))
);

return editor.GetChangedDocument();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Xunit;
using Verify = CSharpVerifier<Xunit.Analyzers.BooleanAssertsShouldNotBeNegated>;

public class BooleanAssertsShouldNotBeNegatedTests
{
[Theory]
[InlineData("False", "True")]
[InlineData("True", "False")]
public async void NegatedBooleanAssertionTriggers(
string assertion,
string replacement)
{
var code = $@"
using Xunit;
public class TestClass {{
[Fact]
public void TestMethod() {{
bool condition = true;
Assert.{assertion}(!condition);
}}
}}";
var expected =
Verify
.Diagnostic()
.WithSpan(9, 9, 9, 28 + assertion.Length)
.WithArguments(assertion, replacement);

await Verify.VerifyAnalyzer(code, expected);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Xunit;
using Xunit.Analyzers.Fixes;
using Verify = CSharpVerifier<Xunit.Analyzers.BooleanAssertsShouldNotBeNegated>;

public class BooleanAssertsShouldNotBeNegatedFixerTests
{
const string template = @"
using Xunit;
public class TestClass {{
[Fact]
public void TestMethod() {{
bool condition = true;
{0};
}}
}}";

[Theory]
[InlineData("False", "True")]
[InlineData("True", "False")]
public async void ReplacesBooleanAssert(
string assertion,
string replacement)
{
var before = string.Format(template, $"[|Assert.{assertion}(!condition)|]");
var after = string.Format(template, $"Assert.{replacement}(condition)");

await Verify.VerifyCodeFix(before, after, BooleanAssertsShouldNotBeNegatedFixer.Key_UseSuggestedAssert);
}
}
9 changes: 8 additions & 1 deletion src/xunit.analyzers/Utility/Descriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -615,7 +615,14 @@ public static DiagnosticDescriptor X2019_AssertThrowsShouldNotBeUsedForAsyncThro
"Assert.{0} is async. The resulting task should be awaited (or stored for later awaiting)."
);

// Placeholder for rule X2022
public static DiagnosticDescriptor X2022_BooleanAssertionsShouldNotBeNegated { get; } =
Rule(
"xUnit2022",
"Boolean assertions should not be negated",
Assertions,
Info,
"Do not negate your value when calling Assert.{0}. Call Assert.{1} without the negation instead."
);

// Placeholder for rule X2023

Expand Down
55 changes: 55 additions & 0 deletions src/xunit.analyzers/X2000/BooleanAssertsShouldNotBeNegated.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Xunit.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class BooleanAssertsShouldNotBeNegated : AssertUsageAnalyzerBase
{
static readonly string[] targetMethods =
{
Constants.Asserts.False,
Constants.Asserts.True,
};

public BooleanAssertsShouldNotBeNegated() :
base(Descriptors.X2022_BooleanAssertionsShouldNotBeNegated, targetMethods)
{ }

protected override void AnalyzeInvocation(
OperationAnalysisContext context,
XunitContext xunitContext,
IInvocationOperation invocationOperation,
IMethodSymbol method)
{
if (invocationOperation.Arguments.Length < 1)
return;

if (invocationOperation.Arguments[0].Value is not IUnaryOperation unaryOperation)
return;

if (!unaryOperation.Syntax.IsKind(SyntaxKind.LogicalNotExpression))
return;

var suggestedAssertion =
method.Name == Constants.Asserts.False
? Constants.Asserts.True
: Constants.Asserts.False;

var builder = ImmutableDictionary.CreateBuilder<string, string?>();
builder[Constants.Properties.Replacement] = suggestedAssertion;

context.ReportDiagnostic(
Diagnostic.Create(
Descriptors.X2022_BooleanAssertionsShouldNotBeNegated,
invocationOperation.Syntax.GetLocation(),
builder.ToImmutable(),
method.Name,
suggestedAssertion
)
);
}
}

0 comments on commit 4a9c2c3

Please sign in to comment.