Skip to content

Commit

Permalink
Add xUnit2032 to soft-deprecate Assert.IsAssignableFrom
Browse files Browse the repository at this point in the history
  • Loading branch information
bradwilson committed Nov 3, 2024
1 parent c707020 commit 350ab93
Show file tree
Hide file tree
Showing 14 changed files with 286 additions and 16 deletions.
4 changes: 2 additions & 2 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="xunit.v3.assert.source" Version="0.5.0-pre.35" />
<PackageReference Include="xunit.v3.core" Version="0.5.0-pre.35" />
<PackageReference Include="xunit.v3.assert.source" Version="0.6.0-pre.1" />
<PackageReference Include="xunit.v3.core" Version="0.6.0-pre.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.0-pre.48" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Composition;
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 AssignableFromAssertionIsConfusinglyNamedFixer : BatchedCodeFixProvider
{
public const string Key_UseIsType = "xUnit2032_UseIsType";

public AssignableFromAssertionIsConfusinglyNamedFixer() :
base(Descriptors.X2032_AssignableFromAssertionIsConfusinglyNamed.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 simpleNameSyntax = invocation.GetSimpleName();
if (simpleNameSyntax is null)
return;

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

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

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

editor.ReplaceNode(
invocation,
invocation
.ReplaceNode(
simpleName,
simpleName.WithIdentifier(Identifier(replacementName))
)
.WithArgumentList(
invocation
.ArgumentList
.AddArguments(
ParseArgumentList("false")
.Arguments[0]
.WithNameColon(NameColon("exactMatch"))
)
)
);

return editor.GetChangedDocument();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Xunit;
using Xunit.Analyzers;
using Verify = CSharpVerifier<Xunit.Analyzers.AssignableFromAssertionIsConfusinglyNamed>;
using Verify_v2_Pre2_9_3 = CSharpVerifier<AssignableFromAssertionIsConfusinglyNamedTests.Analyzer_v2_Pre2_9_3>;
using Verify_v3_Pre0_6_0 = CSharpVerifier<AssignableFromAssertionIsConfusinglyNamedTests.Analyzer_v3_Pre0_6_0>;

public class AssignableFromAssertionIsConfusinglyNamedTests
{
public static TheoryData<string, string> Methods = new()
{
{ "IsAssignableFrom", "IsType" },
{ "IsNotAssignableFrom", "IsNotType"},
};

[Theory]
[MemberData(nameof(Methods))]
public async Task WhenReplacementAvailable_Triggers(
string method,
string replacement)
{
var source = string.Format(/* lang=c#-test */ """
using System;
using Xunit;
class TestClass {{
void TestMethod() {{
{{|#0:Assert.{0}<object>(new object())|}};
{{|#1:Assert.{0}(typeof(object), new object())|}};
}}
}}
""", method);
var expected = new[] {
Verify.Diagnostic().WithLocation(0).WithArguments(method, replacement),
Verify.Diagnostic().WithLocation(1).WithArguments(method, replacement),
};

await Verify.VerifyAnalyzer(source, expected);
}

[Theory]
[MemberData(nameof(Methods))]
public async Task WhenReplacementNotAvailable_DoesNotTriggers(
string method,
string _)
{
var source = string.Format(/* lang=c#-test */ """
using System;
using Xunit;
class TestClass {{
void TestMethod() {{
Assert.{0}<object>(new object());
Assert.{0}(typeof(object), new object());
}}
}}
""", method);

await Verify_v2_Pre2_9_3.VerifyAnalyzer(source);
await Verify_v3_Pre0_6_0.VerifyAnalyzer(source);
}

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

internal class Analyzer_v3_Pre0_6_0 : AssignableFromAssertionIsConfusinglyNamed
{
protected override XunitContext CreateXunitContext(Compilation compilation) =>
XunitContext.ForV3(compilation, new Version(0, 5, 999));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Threading.Tasks;
using Xunit;
using Xunit.Analyzers.Fixes;
using Verify = CSharpVerifier<Xunit.Analyzers.AssignableFromAssertionIsConfusinglyNamed>;

public class AssignableFromAssertionIsConfusinglyNamedFixerTests
{
[Fact]
public async Task Conversions()
{
var before = /* lang=c#-test */ """
using System;
using Xunit;
public class TestClass {
[Fact]
public void TestMethod() {
var data = "Hello world";
[|Assert.IsAssignableFrom(typeof(object), data)|];
[|Assert.IsAssignableFrom<object>(data)|];
}
}
""";
var after = /* lang=c#-test */ """
using System;
using Xunit;
public class TestClass {
[Fact]
public void TestMethod() {
var data = "Hello world";
Assert.IsType(typeof(object), data, exactMatch: false);
Assert.IsType<object>(data, exactMatch: false);
}
}
""";

await Verify.VerifyCodeFix(before, after, AssignableFromAssertionIsConfusinglyNamedFixer.Key_UseIsType);
}
}
12 changes: 6 additions & 6 deletions src/xunit.analyzers.tests/Utility/CodeAnalyzerHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,10 @@ static CodeAnalyzerHelper()
new PackageIdentity("Microsoft.Extensions.Primitives", "8.0.0"),
new PackageIdentity("System.Threading.Tasks.Extensions", "4.5.4"),
new PackageIdentity("System.Text.Json", "8.0.0"),
new PackageIdentity("xunit.v3.assert", "0.5.0-pre.35"),
new PackageIdentity("xunit.v3.common", "0.5.0-pre.35"),
new PackageIdentity("xunit.v3.extensibility.core", "0.5.0-pre.35"),
new PackageIdentity("xunit.v3.runner.common", "0.5.0-pre.35")
new PackageIdentity("xunit.v3.assert", "0.6.0-pre.1"),
new PackageIdentity("xunit.v3.common", "0.6.0-pre.1"),
new PackageIdentity("xunit.v3.extensibility.core", "0.6.0-pre.1"),
new PackageIdentity("xunit.v3.runner.common", "0.6.0-pre.1")
)
);

Expand All @@ -74,8 +74,8 @@ static CodeAnalyzerHelper()
new PackageIdentity("Microsoft.Extensions.Primitives", "8.0.0"),
new PackageIdentity("System.Threading.Tasks.Extensions", "4.5.4"),
new PackageIdentity("System.Text.Json", "8.0.0"),
new PackageIdentity("xunit.v3.common", "0.5.0-pre.35"),
new PackageIdentity("xunit.v3.runner.utility", "0.5.0-pre.35")
new PackageIdentity("xunit.v3.common", "0.6.0-pre.1"),
new PackageIdentity("xunit.v3.runner.utility", "0.6.0-pre.1")
)
);
}
Expand Down
10 changes: 5 additions & 5 deletions src/xunit.analyzers.tests/xunit.analyzers.tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
<PackageDownload Include="xunit.extensibility.execution" Version="[2.9.3-pre.4]" />
<PackageDownload Include="xunit.runner.utility" Version="[2.9.3-pre.4]" />

<PackageDownload Include="xunit.v3.assert" Version="[0.5.0-pre.35]" />
<PackageDownload Include="xunit.v3.common" Version="[0.5.0-pre.35]" />
<PackageDownload Include="xunit.v3.extensibility.core" Version="[0.5.0-pre.35]" />
<PackageDownload Include="xunit.v3.runner.common" Version="[0.5.0-pre.35]" />
<PackageDownload Include="xunit.v3.runner.utility" Version="[0.5.0-pre.35]" />
<PackageDownload Include="xunit.v3.assert" Version="[0.6.0-pre.1]" />
<PackageDownload Include="xunit.v3.common" Version="[0.6.0-pre.1]" />
<PackageDownload Include="xunit.v3.extensibility.core" Version="[0.6.0-pre.1]" />
<PackageDownload Include="xunit.v3.runner.common" Version="[0.6.0-pre.1]" />
<PackageDownload Include="xunit.v3.runner.utility" Version="[0.6.0-pre.1]" />

<!-- Download packages referenced by CodeAnalysisNetAnalyzers -->
<PackageDownload Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="[9.0.0-preview.24454.1]" />
Expand Down
9 changes: 8 additions & 1 deletion src/xunit.analyzers/Utility/Descriptors.xUnit2xxx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,14 @@ public static partial class Descriptors
"Do not use a Where clause to filter before calling Assert.Single. Use the overload of Assert.Single that accepts a filtering function."
);

// Placeholder for rule X2032
public static DiagnosticDescriptor X2032_AssignableFromAssertionIsConfusinglyNamed { get; } =
Diagnostic(
"xUnit2032",
"Type assertions based on 'assignable from' are confusingly named",
Assertions,
Info,
"The naming of Assert.{0} can be confusing. An overload of Assert.{0} is available with an exact match flag which can be set to false to perform the same operation."
);

// Placeholder for rule X2033

Expand Down
2 changes: 2 additions & 0 deletions src/xunit.analyzers/Utility/EmptyAssertContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ public class EmptyAssertContext : IAssertContext

public bool SupportsAssertFail => false;

public bool SupportsInexactTypeAssertions => false;

public Version Version { get; } = new();
}
7 changes: 7 additions & 0 deletions src/xunit.analyzers/Utility/IAssertContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ public interface IAssertContext
/// </summary>
bool SupportsAssertFail { get; }

/// <summary>
/// Gets a flag indicating whether <c>Assert.IsType</c> and <c>Assert.IsNotType</c>
/// support inexact matches (soft-deprecating <c>Assert.IsAssignableFrom</c>
/// and <c>Assert.IsNotAssignableFrom</c>).
/// </summary>
bool SupportsInexactTypeAssertions { get; }

/// <summary>
/// Gets the version number of the assertion assembly.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/xunit.analyzers/Utility/V2AssertContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Xunit.Analyzers;
public class V2AssertContext : IAssertContext
{
internal static readonly Version Version_2_5_0 = new("2.5.0");
internal static readonly Version Version_2_9_3 = new("2.9.3");

readonly Lazy<INamedTypeSymbol?> lazyAssertType;

Expand All @@ -27,6 +28,10 @@ public class V2AssertContext : IAssertContext
public bool SupportsAssertFail =>
Version >= Version_2_5_0;

/// <inheritdoc/>
public bool SupportsInexactTypeAssertions =>
Version >= Version_2_9_3;

/// <inheritdoc/>
public Version Version { get; }

Expand Down
6 changes: 6 additions & 0 deletions src/xunit.analyzers/Utility/V3AssertContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ namespace Xunit.Analyzers;

public class V3AssertContext : IAssertContext
{
internal static readonly Version Version_0_6_0 = new("0.6.0");

readonly Lazy<INamedTypeSymbol?> lazyAssertType;

V3AssertContext(
Expand All @@ -24,6 +26,10 @@ public class V3AssertContext : IAssertContext
/// <inheritdoc/>
public bool SupportsAssertFail => true;

/// <inheritdoc/>
public bool SupportsInexactTypeAssertions =>
Version >= Version_0_6_0;

/// <inheritdoc/>
public Version Version { get; }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Xunit.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AssignableFromAssertionIsConfusinglyNamed : AssertUsageAnalyzerBase
{
public static readonly Dictionary<string, string> ReplacementMethods = new()
{
{ Constants.Asserts.IsAssignableFrom, Constants.Asserts.IsType },
{ Constants.Asserts.IsNotAssignableFrom, Constants.Asserts.IsNotType },
};

public AssignableFromAssertionIsConfusinglyNamed() :
base(Descriptors.X2032_AssignableFromAssertionIsConfusinglyNamed, ReplacementMethods.Keys)
{ }

protected override void AnalyzeInvocation(
OperationAnalysisContext context,
XunitContext xunitContext,
IInvocationOperation invocationOperation,
IMethodSymbol method)
{
Guard.ArgumentNotNull(xunitContext);
Guard.ArgumentNotNull(invocationOperation);
Guard.ArgumentNotNull(method);

if (!xunitContext.Assert.SupportsInexactTypeAssertions)
return;

if (!ReplacementMethods.TryGetValue(invocationOperation.TargetMethod.Name, out var replacement))
return;

context.ReportDiagnostic(
Diagnostic.Create(
Descriptors.X2032_AssignableFromAssertionIsConfusinglyNamed,
invocationOperation.Syntax.GetLocation(),
invocationOperation.TargetMethod.Name,
replacement
)
);
}
}
2 changes: 1 addition & 1 deletion tools/builder/build.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
</ItemGroup>

<ItemGroup>
<PackageDownload Include="xunit.v3.runner.console" Version="[0.5.0-pre.33]" />
<PackageDownload Include="xunit.v3.runner.console" Version="[0.6.0-pre.1]" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion tools/builder/models/BuildContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public partial IReadOnlyList<string> GetSkippedAnalysisFolders() =>

partial void Initialize()
{
consoleRunner = Path.Combine(NuGetPackageCachePath, "xunit.v3.runner.console", "0.5.0-pre.33", "tools", "net472", "xunit.v3.runner.console.exe");
consoleRunner = Path.Combine(NuGetPackageCachePath, "xunit.v3.runner.console", "0.6.0-pre.1", "tools", "net472", "xunit.v3.runner.console.exe");
if (!File.Exists(consoleRunner))
throw new InvalidOperationException($"Cannot find console runner at '{consoleRunner}'");
}
Expand Down

0 comments on commit 350ab93

Please sign in to comment.