Skip to content

Commit

Permalink
Update xUnit2018 fixer to use exactMatch (when supported) instead of …
Browse files Browse the repository at this point in the history
…swapping asserts
  • Loading branch information
bradwilson committed Nov 3, 2024
1 parent 6b8347c commit cf7f210
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
Expand All @@ -25,26 +26,69 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
if (root is null)
return;

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

var simpleNameSyntax = invocation.GetSimpleName();
if (simpleNameSyntax is null)
if (!diagnostic.Properties.TryGetValue(Constants.Properties.UseExactMatch, out var useExactMatch))
return;

var methodName = simpleNameSyntax.Identifier.Text;
if (!AssertIsTypeShouldNotBeUsedForAbstractType.ReplacementMethods.TryGetValue(methodName, out var replacementName))
var invocation = root.FindNode(context.Span).FirstAncestorOrSelf<InvocationExpressionSyntax>();
if (invocation is null)
return;

context.RegisterCodeFix(
XunitCodeAction.Create(
ct => UseIsAssignableFrom(context.Document, simpleNameSyntax, replacementName, ct),
Key_UseAlternateAssert,
"Use Assert.{0}", replacementName
),
context.Diagnostics
);
if (useExactMatch == bool.TrueString)
{
context.RegisterCodeFix(
XunitCodeAction.Create(
ct => UseExactMatchFalse(context.Document, invocation, ct),
Key_UseAlternateAssert,
"Use 'exactMatch: false'"
),
context.Diagnostics
);
}
else
{
var simpleNameSyntax = invocation.GetSimpleName();
if (simpleNameSyntax is null)
return;

var methodName = simpleNameSyntax.Identifier.Text;
if (!AssertIsTypeShouldNotBeUsedForAbstractType.ReplacementMethods.TryGetValue(methodName, out var replacementName))
return;

context.RegisterCodeFix(
XunitCodeAction.Create(
ct => UseIsAssignableFrom(context.Document, simpleNameSyntax, replacementName, ct),
Key_UseAlternateAssert,
"Use Assert.{0}", replacementName
),
context.Diagnostics
);
}
}

static async Task<Document> UseExactMatchFalse(
Document document,
InvocationExpressionSyntax invocation,
CancellationToken cancellationToken)
{
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);

var falseArgument =
ParseArgumentList("false")
.Arguments[0]
.WithNameColon(NameColon("exactMatch"));

var argumentList = invocation.ArgumentList;
if (argumentList.Arguments.Count == 2)
argumentList = argumentList.ReplaceNode(argumentList.Arguments[1], falseArgument);
else
argumentList = argumentList.AddArguments(falseArgument);

editor.ReplaceNode(invocation.ArgumentList, argumentList);
return editor.GetChangedDocument();
}

static async Task<Document> UseIsAssignableFrom(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,119 @@
using System;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Xunit;
using Xunit.Analyzers;
using Xunit.Analyzers.Fixes;
using Verify = CSharpVerifier<Xunit.Analyzers.AssertIsTypeShouldNotBeUsedForAbstractType>;
using Verify_v2_Pre2_9_3 = CSharpVerifier<AssertIsTypeShouldNotBeUsedForAbstractTypeFixerTests.Analyzer_v2_Pre2_9_3>;
using Verify_v3_Pre0_6_0 = CSharpVerifier<AssertIsTypeShouldNotBeUsedForAbstractTypeFixerTests.Analyzer_v3_Pre0_6_0>;

public class AssertIsTypeShouldNotBeUsedForAbstractTypeFixerTests
{
const string template = /* lang=c#-test */ """
using System;
using Xunit;
public abstract class TestClass {{
[Fact]
public void TestMethod() {{
var data = new object();
{0};
}}
}}
""";

[Theory]
[InlineData(
/* lang=c#-test */ "[|Assert.IsType<IDisposable>(data)|]",
/* lang=c#-test */ "Assert.IsAssignableFrom<IDisposable>(data)")]
[InlineData(
/* lang=c#-test */ "[|Assert.IsType<TestClass>(data)|]",
/* lang=c#-test */ "Assert.IsAssignableFrom<TestClass>(data)")]
[InlineData(
/* lang=c#-test */ "[|Assert.IsNotType<IDisposable>(data)|]",
/* lang=c#-test */ "Assert.IsNotAssignableFrom<IDisposable>(data)")]
[InlineData(
/* lang=c#-test */ "[|Assert.IsNotType<TestClass>(data)|]",
/* lang=c#-test */ "Assert.IsNotAssignableFrom<TestClass>(data)")]
public async Task Conversions(
string beforeAssert,
string afterAssert)
[Fact]
public async Task Conversions_WithoutExactMatch()
{
var before = string.Format(template, beforeAssert);
var after = string.Format(template, afterAssert);
var before = /* lang=c#-test */ """
using System;
using Xunit;
public abstract class TestClass {
[Fact]
public void TestMethod() {
var data = new object();
[|Assert.IsType<IDisposable>(data)|];
[|Assert.IsType<TestClass>(data)|];
[|Assert.IsNotType<IDisposable>(data)|];
[|Assert.IsNotType<TestClass>(data)|];
}
}
""";
var after = /* lang=c#-test */ """
using System;
using Xunit;
public abstract class TestClass {
[Fact]
public void TestMethod() {
var data = new object();
Assert.IsAssignableFrom<IDisposable>(data);
Assert.IsAssignableFrom<TestClass>(data);
Assert.IsNotAssignableFrom<IDisposable>(data);
Assert.IsNotAssignableFrom<TestClass>(data);
}
}
""";

await Verify_v2_Pre2_9_3.VerifyCodeFix(before, after, AssertIsTypeShouldNotBeUsedForAbstractTypeFixer.Key_UseAlternateAssert);
await Verify_v3_Pre0_6_0.VerifyCodeFix(before, after, AssertIsTypeShouldNotBeUsedForAbstractTypeFixer.Key_UseAlternateAssert);
}

[Fact]
public async Task Conversions_WithExactMatch()
{
var before = /* lang=c#-test */ """
using System;
using Xunit;
public abstract class TestClass {
[Fact]
public void TestMethod() {
var data = new object();
[|Assert.IsType<IDisposable>(data)|];
[|Assert.IsType<IDisposable>(data, true)|];
[|Assert.IsType<IDisposable>(data, exactMatch: true)|];
[|Assert.IsType<TestClass>(data)|];
[|Assert.IsType<TestClass>(data, true)|];
[|Assert.IsType<TestClass>(data, exactMatch: true)|];
[|Assert.IsNotType<IDisposable>(data)|];
[|Assert.IsNotType<IDisposable>(data, true)|];
[|Assert.IsNotType<IDisposable>(data, exactMatch: true)|];
[|Assert.IsNotType<TestClass>(data)|];
[|Assert.IsNotType<TestClass>(data, true)|];
[|Assert.IsNotType<TestClass>(data, exactMatch: true)|];
}
}
""";
var after = /* lang=c#-test */ """
using System;
using Xunit;
public abstract class TestClass {
[Fact]
public void TestMethod() {
var data = new object();
Assert.IsType<IDisposable>(data, exactMatch: false);
Assert.IsType<IDisposable>(data, exactMatch: false);
Assert.IsType<IDisposable>(data, exactMatch: false);
Assert.IsType<TestClass>(data, exactMatch: false);
Assert.IsType<TestClass>(data, exactMatch: false);
Assert.IsType<TestClass>(data, exactMatch: false);
Assert.IsNotType<IDisposable>(data, exactMatch: false);
Assert.IsNotType<IDisposable>(data, exactMatch: false);
Assert.IsNotType<IDisposable>(data, exactMatch: false);
Assert.IsNotType<TestClass>(data, exactMatch: false);
Assert.IsNotType<TestClass>(data, exactMatch: false);
Assert.IsNotType<TestClass>(data, exactMatch: false);
}
}
""";

await Verify.VerifyCodeFix(before, after, AssertIsTypeShouldNotBeUsedForAbstractTypeFixer.Key_UseAlternateAssert);
}

internal class Analyzer_v2_Pre2_9_3 : AssertIsTypeShouldNotBeUsedForAbstractType
{
protected override XunitContext CreateXunitContext(Compilation compilation) =>
XunitContext.ForV2(compilation, new Version(2, 9, 2));
}

internal class Analyzer_v3_Pre0_6_0 : AssertIsTypeShouldNotBeUsedForAbstractType
{
protected override XunitContext CreateXunitContext(Compilation compilation) =>
XunitContext.ForV3(compilation, new Version(0, 5, 999));
}
}
1 change: 1 addition & 0 deletions src/xunit.analyzers/Utility/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ public static class Properties
public const string TFixtureDisplayName = nameof(TFixtureDisplayName);
public const string TFixtureName = nameof(TFixtureName);
public const string TypeName = nameof(TypeName);
public const string UseExactMatch = nameof(UseExactMatch);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
Expand Down Expand Up @@ -58,22 +59,28 @@ protected override void AnalyzeInvocation(
}

var typeName = SymbolDisplay.ToDisplayString(type);

var builder = ImmutableDictionary.CreateBuilder<string, string?>();
string? replacement;

if (xunitContext.Assert.SupportsInexactTypeAssertions)
{
replacement = "exactMatch: false";
builder[Constants.Properties.UseExactMatch] = bool.TrueString;
}
else
{
if (!ReplacementMethods.TryGetValue(invocationOperation.TargetMethod.Name, out replacement))
return;

builder[Constants.Properties.UseExactMatch] = bool.FalseString;
replacement = "Assert." + replacement;
}

context.ReportDiagnostic(
Diagnostic.Create(
Descriptors.X2018_AssertIsTypeShouldNotBeUsedForAbstractType,
invocationOperation.Syntax.GetLocation(),
builder.ToImmutable(),
typeKind,
typeName,
replacement
Expand Down

0 comments on commit cf7f210

Please sign in to comment.