Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
fix: handle async methods in ExpectedException conversion
When converting [ExpectedException] on async methods, the generated
lambda now correctly uses the async modifier if the original method
body contains await expressions.

This addresses the code review feedback on PR #4353 about the critical
async lambda issue.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
  • Loading branch information
thomhurst and claude committed Jan 13, 2026
commit c43747662af3674608850f05430ab60273e748f8
33 changes: 30 additions & 3 deletions TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1055,11 +1055,38 @@ private static List<AttributeListSyntax> RemoveExpectedExceptionAttribute(Syntax
return null;
}

// Check if the original statements contain any await expressions
var hasAwait = originalStatements.Any(s => s.DescendantNodes().OfType<AwaitExpressionSyntax>().Any());

// Create: await Assert.ThrowsAsync<T>(() => { original statements });
var lambda = SyntaxFactory.ParenthesizedLambdaExpression(
// or: await Assert.ThrowsAsync<T>(async () => { original statements }); if async
// Add extra indentation for statements inside the lambda block (4 more spaces)
var indentedStatements = originalStatements.Select(s =>
{
var existingTrivia = s.GetLeadingTrivia();
var newTrivia = existingTrivia.Add(SyntaxFactory.Whitespace(" "));
return s.WithLeadingTrivia(newTrivia);
}).ToArray();
var lambdaBody = SyntaxFactory.Block(indentedStatements)
.WithOpenBraceToken(SyntaxFactory.Token(SyntaxKind.OpenBraceToken)
.WithLeadingTrivia(SyntaxFactory.Whitespace(" "))
.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed))
.WithCloseBraceToken(SyntaxFactory.Token(SyntaxKind.CloseBraceToken)
.WithLeadingTrivia(SyntaxFactory.Whitespace(" ")));
ParenthesizedLambdaExpressionSyntax lambda;

lambda = SyntaxFactory.ParenthesizedLambdaExpression(
SyntaxFactory.ParameterList(),
SyntaxFactory.Block(originalStatements)
);
lambdaBody
).WithArrowToken(SyntaxFactory.Token(SyntaxKind.EqualsGreaterThanToken)
.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed));

if (hasAwait)
{
// Need async lambda for await expressions
lambda = lambda.WithAsyncKeyword(SyntaxFactory.Token(SyntaxKind.AsyncKeyword)
.WithTrailingTrivia(SyntaxFactory.Space));
}

var throwsAsyncCall = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
Expand Down
46 changes: 46 additions & 0 deletions TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,52 @@ await Assert.ThrowsAsync<ArgumentException>(() =>
);
}

[Test]
public async Task MSTest_ExpectedException_Attribute_On_Async_Method_Converted_To_ThrowsAsync()
{
await CodeFixer.VerifyCodeFixAsync(
"""
using System;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;

public class MyClass
{
{|#0:[TestMethod]|}
[ExpectedException(typeof(ArgumentException))]
public async Task TestMethodAsync()
{
await Task.Delay(1);
throw new ArgumentException("test");
}
}
""",
Verifier.Diagnostic(Rules.MSTestMigration).WithLocation(0),
"""
using System;
using System.Threading.Tasks;
using TUnit.Core;
using TUnit.Assertions;
using static TUnit.Assertions.Assert;
using TUnit.Assertions.Extensions;

public class MyClass
{
[Test]
public async Task TestMethodAsync()
{
await Assert.ThrowsAsync<ArgumentException>(async () =>
{
await Task.Delay(1);
throw new ArgumentException("test");
});
}
}
""",
ConfigureMSTestTest
);
}

[Test]
public async Task MSTest_DirectoryAssert_Exists_Converted()
{
Expand Down
Loading