Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

xUnit1051 code fix chooses incorrect overload for CancellationToken #3068

Closed
SapiensAnatis opened this issue Nov 29, 2024 · 7 comments · Fixed by xunit/xunit.analyzers#192
Closed

Comments

@SapiensAnatis
Copy link

I ran into this while upgrading to v3 and with EF Core's FindAsync method inside my tests, which exposes a params object[] keyValues overload and an object[] keyValues, CancellationToken cancellationToken overload.

Given the following code:

public class UnitTest1
{
    [Fact]
    public async Task Test1()
    {
        await M(1);
    }
    
    private async Task M(params int[] numbers)
    {
        
    }

    private async Task M(int[] numbers, CancellationToken cancellationToken = default)
    {
        
    }
}

I get a diagnostic for xUnit1051 on the invocation of M in UnitTest1. But if I apply the code fix, I get

[Fact]
public async Task Test1()
{
    await M(1, TestContext.Current.CancellationToken);
}

which is a compile error. The correct code is:

[Fact]
public async Task Test1()
{
    await M([1], TestContext.Current.CancellationToken);
}

There's an argument here that the EF Core API is not the best (ReSharper does warn me about the second method being hidden). In fact, it's a bit worse in that case since params object[] accepts the CancellationToken but just leads to a runtime failure by trying to treat it as a primary key.

That being said, I didn't see any existing issues with this so I thought I'd raise it all the same - maybe it's an easy fix.

@bradwilson
Copy link
Member

I'm tempted, given the params object[] problem, to say that we should just say that those two methods are not compatible and don't trigger xUnit1051 at all.

@SapiensAnatis
Copy link
Author

That sounds reasonable to me. Although, I wonder what CA2016 - Forward the CancellationToken parameter to methods that take one does in this case?

I might be able to work on this by the way, I have a little bit of experience with analyzers.

@bradwilson
Copy link
Member

That'd be awesome! Thanks!

@SapiensAnatis
Copy link
Author

The code fix for CA2016 in this instance will create a new array for you when it switches to the other overload:

public class C
{
    public async Task M(CancellationToken cancellationToken = default)
    {
-      await M2(1, 2, 3);
+      await M2(new int[] { 1, 2, 3 }, cancellationToken: cancellationToken);
    }

    public async Task M2(params int[] values)
    {
    }

    public async Task M2(int[] values, CancellationToken cancellationToken = default)
    {
    }
}

So it's perhaps not unreasonable to attempt making xUnit1051 do the same. However, the complexity of this may have increased a bit with the new support in C# 13 for non-array params, which even CA2016 doesn't appear to handle properly.

SapiensAnatis added a commit to SapiensAnatis/xunit.analyzers that referenced this issue Nov 29, 2024
If the analyzer has fired on a `params` method, it means that the new
overload is actually array + `CancellationToken`, since C# forbids
placing any argument after a params argument. To avoid generating
invalid code, we need to replace the params expression with an array
creation expression.

Does not currently handle the expanded range of allowed `params` types
implemented in C# 13. Considering you can use _any_ type that implements
`IEnumerable<T>`, I think it would be quite difficult to handle all the
different cases there.
Being a new feature, it's also less likely to be used, though this will
of course change over time...

Closes [#3068](xunit/xunit#3068)
@SapiensAnatis
Copy link
Author

I've gone with copying CA2016 and attempting to create an array, since it wasn't that hard to do in the end (assuming my solution works). Let me know if you'd prefer to go the route of not firing this diagnostic in the first place, though.

@bradwilson
Copy link
Member

This sounds good, I'll take a look at the PR.

@bradwilson
Copy link
Member

Available in 1.18.0-pre.6 https://xunit.net/docs/using-ci-builds

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants