Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
131 changes: 109 additions & 22 deletions TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -601,35 +601,122 @@ private ExpressionSyntax ConvertInstanceOf(SeparatedSyntaxList<ArgumentSyntax> a

private ExpressionSyntax ConvertNUnitThrows(InvocationExpressionSyntax invocation)
{
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
memberAccess.Name is GenericNameSyntax genericName)
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
{
var exceptionType = genericName.TypeArgumentList.Arguments[0];
var action = invocation.ArgumentList.Arguments[0].Expression;

var throwsAsyncInvocation = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName("Assert"),
SyntaxFactory.GenericName("ThrowsAsync")
.WithTypeArgumentList(
SyntaxFactory.TypeArgumentList(
SyntaxFactory.SingletonSeparatedList(exceptionType)
// Handle generic form: Assert.Throws<T>(() => ...) or Assert.ThrowsAsync<T>(() => ...)
if (memberAccess.Name is GenericNameSyntax genericName)
{
var exceptionType = genericName.TypeArgumentList.Arguments[0];
var action = invocation.ArgumentList.Arguments[0].Expression;

var throwsAsyncInvocation = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName("Assert"),
SyntaxFactory.GenericName("ThrowsAsync")
.WithTypeArgumentList(
SyntaxFactory.TypeArgumentList(
SyntaxFactory.SingletonSeparatedList(exceptionType)
)
)
),
SyntaxFactory.ArgumentList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.Argument(action)
)
),
SyntaxFactory.ArgumentList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.Argument(action)
)
)
);
);

return SyntaxFactory.AwaitExpression(throwsAsyncInvocation);
}

// Handle non-generic constraint-based form: Assert.Throws(constraint, () => ...) or Assert.ThrowsAsync(constraint, () => ...)
// where constraint is typically Is.TypeOf(typeof(T))
if (invocation.ArgumentList.Arguments.Count >= 2)
{
var constraint = invocation.ArgumentList.Arguments[0].Expression;
var action = invocation.ArgumentList.Arguments[1].Expression;

// Try to extract the exception type from the constraint
var exceptionType = TryExtractTypeFromConstraint(constraint);

if (exceptionType != null)
{
// Convert to generic ThrowsAsync form: Assert.ThrowsAsync<T>(() => ...)
var throwsAsyncInvocation = SyntaxFactory.InvocationExpression(
SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.IdentifierName("Assert"),
SyntaxFactory.GenericName("ThrowsAsync")
.WithTypeArgumentList(
SyntaxFactory.TypeArgumentList(
SyntaxFactory.SingletonSeparatedList(exceptionType)
)
)
),
SyntaxFactory.ArgumentList(
SyntaxFactory.SingletonSeparatedList(
SyntaxFactory.Argument(action)
)
)
);

return SyntaxFactory.AwaitExpression(throwsAsyncInvocation);
return SyntaxFactory.AwaitExpression(throwsAsyncInvocation);
}
}
}

// Fallback for non-generic Throws
return CreateTUnitAssertion("Throws", invocation.ArgumentList.Arguments[0].Expression);
// Fallback for unsupported Throws patterns
// If we have 2+ arguments, it's a constraint-based form where arg[1] is the action
// Otherwise, it's a single-argument form where arg[0] is the action
var fallbackArg = invocation.ArgumentList.Arguments.Count >= 2
? invocation.ArgumentList.Arguments[1].Expression
: invocation.ArgumentList.Arguments[0].Expression;
return CreateTUnitAssertion("Throws", fallbackArg);
}

/// <summary>
/// Attempts to extract the exception type from NUnit constraint expressions like Is.TypeOf(typeof(T)).
/// Returns null if the type cannot be extracted.
/// </summary>
private TypeSyntax? TryExtractTypeFromConstraint(ExpressionSyntax constraint)
{
// Handle Is.TypeOf(typeof(T)) pattern
if (constraint is InvocationExpressionSyntax invocation)
{
// Check if it's a method call like Is.TypeOf(...) or TypeOf(...)
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
memberAccess.Name.Identifier.Text == "TypeOf" &&
invocation.ArgumentList.Arguments.Count > 0)
{
// Extract the argument to TypeOf - should be typeof(T)
var typeofArg = invocation.ArgumentList.Arguments[0].Expression;
return ExtractTypeFromTypeof(typeofArg);
}

// Handle standalone TypeOf(typeof(T)) calls
if (invocation.Expression is IdentifierNameSyntax { Identifier.Text: "TypeOf" } &&
invocation.ArgumentList.Arguments.Count > 0)
{
var typeofArg = invocation.ArgumentList.Arguments[0].Expression;
return ExtractTypeFromTypeof(typeofArg);
}
}

Comment on lines +679 to +705
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider extending TryExtractTypeFromConstraint to also support Is.InstanceOf constraint patterns. While the current implementation correctly handles Is.TypeOf(typeof(T)), NUnit also supports Is.InstanceOf<T>() (generic form) and Is.InstanceOf(typeof(T)) (typeof form), which behave similarly to TypeOf but allow subclasses.

To extract the type from the generic form Is.InstanceOf<T>(), you would need to check if the invocation expression is a generic method and extract the type argument from the GenericNameSyntax. This would allow more NUnit assertions to be migrated to the optimized generic TUnit form instead of falling back to Assert.That(...).Throws().

Suggested change
/// Attempts to extract the exception type from NUnit constraint expressions like Is.TypeOf(typeof(T)).
/// Returns null if the type cannot be extracted.
/// </summary>
private TypeSyntax? TryExtractTypeFromConstraint(ExpressionSyntax constraint)
{
// Handle Is.TypeOf(typeof(T)) pattern
if (constraint is InvocationExpressionSyntax invocation)
{
// Check if it's a method call like Is.TypeOf(...) or TypeOf(...)
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
memberAccess.Name.Identifier.Text == "TypeOf" &&
invocation.ArgumentList.Arguments.Count > 0)
{
// Extract the argument to TypeOf - should be typeof(T)
var typeofArg = invocation.ArgumentList.Arguments[0].Expression;
return ExtractTypeFromTypeof(typeofArg);
}
// Handle standalone TypeOf(typeof(T)) calls
if (invocation.Expression is IdentifierNameSyntax { Identifier.Text: "TypeOf" } &&
invocation.ArgumentList.Arguments.Count > 0)
{
var typeofArg = invocation.ArgumentList.Arguments[0].Expression;
return ExtractTypeFromTypeof(typeofArg);
}
}
/// Attempts to extract the exception type from NUnit constraint expressions like
/// Is.TypeOf(typeof(T)), Is.InstanceOf(typeof(T)) or Is.InstanceOf&lt;T&gt;().
/// Returns null if the type cannot be extracted.
/// </summary>
private TypeSyntax? TryExtractTypeFromConstraint(ExpressionSyntax constraint)
{
if (constraint is InvocationExpressionSyntax invocation)
{
// Handle qualified method calls like Is.TypeOf(...), Is.InstanceOf(...), Is.InstanceOf<T>()
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess)
{
// Generic form: Is.InstanceOf<T>()
if (memberAccess.Name is GenericNameSyntax genericName &&
genericName.Identifier.Text == "InstanceOf" &&
genericName.TypeArgumentList.Arguments.Count > 0)
{
return genericName.TypeArgumentList.Arguments[0];
}
// typeof form: Is.TypeOf(typeof(T)) or Is.InstanceOf(typeof(T))
if ((memberAccess.Name.Identifier.Text == "TypeOf" ||
memberAccess.Name.Identifier.Text == "InstanceOf") &&
invocation.ArgumentList.Arguments.Count > 0)
{
var typeofArg = invocation.ArgumentList.Arguments[0].Expression;
return ExtractTypeFromTypeof(typeofArg);
}
}
// Handle standalone generic calls: InstanceOf<T>()
if (invocation.Expression is GenericNameSyntax genericIdentifier &&
genericIdentifier.Identifier.Text == "InstanceOf" &&
genericIdentifier.TypeArgumentList.Arguments.Count > 0)
{
return genericIdentifier.TypeArgumentList.Arguments[0];
}
// Handle standalone typeof form calls: TypeOf(typeof(T)) or InstanceOf(typeof(T))
if (invocation.Expression is IdentifierNameSyntax { Identifier.Text: var name } &&
(name == "TypeOf" || name == "InstanceOf") &&
invocation.ArgumentList.Arguments.Count > 0)
{
var typeofArg = invocation.ArgumentList.Arguments[0].Expression;
return ExtractTypeFromTypeof(typeofArg);
}
}

Copilot uses AI. Check for mistakes.
return null;
}

/// <summary>
/// Extracts the type from a typeof(T) expression.
/// </summary>
private TypeSyntax? ExtractTypeFromTypeof(ExpressionSyntax expression)
{
if (expression is TypeOfExpressionSyntax typeofExpression)
{
return typeofExpression.Type;
}

return null;
}

private ExpressionSyntax CreatePassAssertion(SeparatedSyntaxList<ArgumentSyntax> arguments)
Expand Down
Loading
Loading