Skip to content

NUnit Migration: Interface implementation methods incorrectly converted to async #4342

@thomhurst

Description

@thomhurst

Description

When running the TUnit NUnit migration code fixer, methods that implement interface members with void return types are incorrectly converted to async Task methods. This breaks the interface implementation because the interface still expects void.

Steps to Reproduce

  1. Create a project with classes that implement interfaces (like IInterceptor, IProxyGenerationHook) and use NUnit assertions
  2. Add TUnit packages
  3. Run dotnet format analyzers --severity info --diagnostics TUNU0001
  4. Build the project

Expected Behavior

Methods that implement interface members should NOT have their return type changed. The assertions should either:

  1. Use synchronous assertion patterns
  2. Be left unchanged for manual migration

Actual Behavior

Interface implementation methods are converted to async, causing build errors:

error CS0738: 'ChangeProxyTargetInterceptor' does not implement interface member 
'IInterceptor.Intercept(IInvocation)'. 'ChangeProxyTargetInterceptor.Intercept(IInvocation)' 
cannot implement 'IInterceptor.Intercept(IInvocation)' because it does not have the 
matching return type of 'void'.

Example

Interface definition (not modified by migration):

public interface IInterceptor
{
    void Intercept(IInvocation invocation);
}

Before migration:

public class ChangeProxyTargetInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        var targetAccessor = invocation.Proxy as IProxyTargetAccessor;
        Assert.IsNotNull(targetAccessor);
        targetAccessor.DynProxySetTarget(target);
        invocation.Proceed();
    }
}

After migration (current - broken):

public class ChangeProxyTargetInterceptor : IInterceptor
{
    public async Task Intercept(IInvocation invocation)  // <-- Breaks interface implementation
    {
        var targetAccessor = invocation.Proxy as IProxyTargetAccessor;
        await Assert.That(targetAccessor).IsNotNull();
        targetAccessor.DynProxySetTarget(target);
        invocation.Proceed();
    }
}

Expected after migration:

Option 1 - Use synchronous pattern:

public class ChangeProxyTargetInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        var targetAccessor = invocation.Proxy as IProxyTargetAccessor;
        Assert.That(targetAccessor).IsNotNull().Wait();  // or synchronous alternative
        targetAccessor.DynProxySetTarget(target);
        invocation.Proceed();
    }
}

Option 2 - Leave unchanged for manual migration:

public class ChangeProxyTargetInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        var targetAccessor = invocation.Proxy as IProxyTargetAccessor;
        Assert.IsNotNull(targetAccessor);  // Left for manual migration
        targetAccessor.DynProxySetTarget(target);
        invocation.Proceed();
    }
}

Detection Suggestion

The code fixer could detect interface implementations by checking:

  1. If the containing class implements any interfaces
  2. If the method signature matches an interface member
  3. If so, don't convert to async

Impact

This is a critical issue because it generates code that cannot compile.

Environment

  • TUnit version: 1.9.91
  • .NET version: .NET 8.0
  • OS: Windows

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions