diff --git a/Directory.Packages.props b/Directory.Packages.props
index 32dc5dccf4e..12a639919b8 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -99,4 +99,4 @@
-
+
\ No newline at end of file
diff --git a/TUnit.Analyzers.CodeFixers/Base/AssertionRewriter.cs b/TUnit.Analyzers.CodeFixers/Base/AssertionRewriter.cs
index ea964fc8f2e..2e666c1feb2 100644
--- a/TUnit.Analyzers.CodeFixers/Base/AssertionRewriter.cs
+++ b/TUnit.Analyzers.CodeFixers/Base/AssertionRewriter.cs
@@ -19,9 +19,35 @@ protected AssertionRewriter(SemanticModel semanticModel)
var convertedAssertion = ConvertAssertionIfNeeded(node);
if (convertedAssertion != null)
{
- // Preserve the original trivia (whitespace, comments, etc.)
+ var conversionTrivia = convertedAssertion.GetLeadingTrivia();
+ var originalTrivia = node.GetLeadingTrivia();
+
+ SyntaxTriviaList finalTrivia;
+ // Only do special handling when there's actually a TODO comment
+ var hasComment = conversionTrivia.Any(t => t.IsKind(SyntaxKind.SingleLineCommentTrivia));
+ if (hasComment)
+ {
+ // Conversion added trivia (TODO comments). Structure should be:
+ // [original whitespace] [TODO comment] [newline] [original whitespace] [await expression]
+ var whitespaceTrivia = originalTrivia.Where(t => t.IsKind(SyntaxKind.WhitespaceTrivia)).ToList();
+ var nonWhitespaceTrivia = originalTrivia.Where(t => !t.IsKind(SyntaxKind.WhitespaceTrivia)).ToList();
+
+ var builder = new List();
+ builder.AddRange(nonWhitespaceTrivia); // Add any non-whitespace (e.g., leading newlines)
+ builder.AddRange(whitespaceTrivia); // Add indentation
+ builder.AddRange(conversionTrivia); // Add TODO comment + newline
+ builder.AddRange(whitespaceTrivia); // Add indentation again for the await
+
+ finalTrivia = new SyntaxTriviaList(builder);
+ }
+ else
+ {
+ // No TODO comment, just use original trivia
+ finalTrivia = originalTrivia;
+ }
+
return convertedAssertion
- .WithLeadingTrivia(node.GetLeadingTrivia())
+ .WithLeadingTrivia(finalTrivia)
.WithTrailingTrivia(node.GetTrailingTrivia());
}
@@ -91,7 +117,10 @@ protected ExpressionSyntax CreateTUnitAssertionWithMessage(
}
// Now wrap the entire thing in await: await Assert.That(actualValue).MethodName(args).Because(message)
- return SyntaxFactory.AwaitExpression(fullInvocation);
+ // Need to add a trailing space after 'await' keyword
+ var awaitKeyword = SyntaxFactory.Token(SyntaxKind.AwaitKeyword)
+ .WithTrailingTrivia(SyntaxFactory.Space);
+ return SyntaxFactory.AwaitExpression(awaitKeyword, fullInvocation);
}
private static bool IsEmptyOrNullMessage(ExpressionSyntax message)
@@ -171,26 +200,47 @@ protected static ExpressionSyntax CreateMessageExpression(
///
/// Checks if the argument at the given index appears to be a comparer (IComparer, IEqualityComparer).
+ /// Returns null if the type cannot be determined.
///
- protected bool IsLikelyComparerArgument(ArgumentSyntax argument)
+ protected bool? IsLikelyComparerArgument(ArgumentSyntax argument)
{
var typeInfo = SemanticModel.GetTypeInfo(argument.Expression);
- if (typeInfo.Type == null) return false;
+ if (typeInfo.Type == null || typeInfo.Type.TypeKind == TypeKind.Error)
+ {
+ // Type couldn't be resolved - return null to indicate unknown
+ return null;
+ }
var typeName = typeInfo.Type.ToDisplayString();
+ // If it's a string type, it's definitely a message, not a comparer
+ if (typeInfo.Type.SpecialType == SpecialType.System_String ||
+ typeName == "string" || typeName == "System.String")
+ {
+ return false;
+ }
+
// Check for IComparer, IComparer, IEqualityComparer, IEqualityComparer
if (typeName.Contains("IComparer") || typeName.Contains("IEqualityComparer"))
{
return true;
}
- // Check interfaces
+ // Check interfaces - also check for generic interface names like IComparer`1
if (typeInfo.Type is INamedTypeSymbol namedType)
{
- return namedType.AllInterfaces.Any(i =>
- i.Name == "IComparer" ||
- i.Name == "IEqualityComparer");
+ if (namedType.AllInterfaces.Any(i =>
+ i.Name.StartsWith("IComparer") ||
+ i.Name.StartsWith("IEqualityComparer")))
+ {
+ return true;
+ }
+ }
+
+ // Also check if the type name itself contains Comparer (for StringComparer, etc.)
+ if (typeName.Contains("Comparer"))
+ {
+ return true;
}
return false;
@@ -206,12 +256,14 @@ protected static SyntaxTrivia CreateTodoComment(string message)
protected bool IsFrameworkAssertion(InvocationExpressionSyntax invocation)
{
- var symbol = SemanticModel.GetSymbolInfo(invocation).Symbol;
+ var symbolInfo = SemanticModel.GetSymbolInfo(invocation);
+ var symbol = symbolInfo.Symbol;
+
if (symbol is not IMethodSymbol methodSymbol)
{
return false;
}
-
+
var namespaceName = methodSymbol.ContainingNamespace?.ToDisplayString() ?? "";
return IsFrameworkAssertionNamespace(namespaceName);
}
diff --git a/TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs
index 508048417d5..52ddd1f2c62 100644
--- a/TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs
@@ -186,31 +186,50 @@ protected override bool IsFrameworkAssertionNamespace(string namespaceName)
protected override ExpressionSyntax? ConvertAssertionIfNeeded(InvocationExpressionSyntax invocation)
{
- if (!IsFrameworkAssertion(invocation))
+ // First try semantic analysis
+ var isFrameworkAssertionViaSemantic = false;
+ try
+ {
+ isFrameworkAssertionViaSemantic = IsFrameworkAssertion(invocation);
+ }
+ catch (InvalidOperationException)
+ {
+ // Semantic analysis failed due to invalid compilation state, fall back to syntax-based detection
+ }
+ catch (ArgumentException)
+ {
+ // Semantic analysis failed due to invalid arguments, fall back to syntax-based detection
+ }
+
+ // Check if it looks like an MSTest assertion syntactically
+ var isMsTestAssertionSyntax = invocation.Expression is MemberAccessExpressionSyntax ma &&
+ ma.Expression is IdentifierNameSyntax { Identifier.Text: "Assert" or "CollectionAssert" or "StringAssert" };
+
+ if (!isFrameworkAssertionViaSemantic && !isMsTestAssertionSyntax)
{
return null;
}
-
+
if (invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
memberAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Assert" })
{
return ConvertMSTestAssertion(invocation, memberAccess.Name.Identifier.Text);
}
-
+
// Handle CollectionAssert
if (invocation.Expression is MemberAccessExpressionSyntax collectionAccess &&
collectionAccess.Expression is IdentifierNameSyntax { Identifier.Text: "CollectionAssert" })
{
return ConvertCollectionAssertion(invocation, collectionAccess.Name.Identifier.Text);
}
-
+
// Handle StringAssert
if (invocation.Expression is MemberAccessExpressionSyntax stringAccess &&
stringAccess.Expression is IdentifierNameSyntax { Identifier.Text: "StringAssert" })
{
return ConvertStringAssertion(invocation, stringAccess.Name.Identifier.Text);
}
-
+
return null;
}
@@ -224,15 +243,9 @@ protected override bool IsFrameworkAssertionNamespace(string namespaceName)
return methodName switch
{
- // 2-arg assertions with message as 3rd param
- "AreEqual" when arguments.Count >= 3 =>
- CreateTUnitAssertionWithMessage("IsEqualTo", arguments[1].Expression, arguments[2].Expression, arguments[0]),
- "AreEqual" when arguments.Count >= 2 =>
- CreateTUnitAssertion("IsEqualTo", arguments[1].Expression, arguments[0]),
- "AreNotEqual" when arguments.Count >= 3 =>
- CreateTUnitAssertionWithMessage("IsNotEqualTo", arguments[1].Expression, arguments[2].Expression, arguments[0]),
- "AreNotEqual" when arguments.Count >= 2 =>
- CreateTUnitAssertion("IsNotEqualTo", arguments[1].Expression, arguments[0]),
+ // Equality assertions - check for comparer overloads and format strings
+ "AreEqual" => ConvertAreEqual(arguments),
+ "AreNotEqual" => ConvertAreNotEqual(arguments),
"AreSame" when arguments.Count >= 3 =>
CreateTUnitAssertionWithMessage("IsSameReference", arguments[1].Expression, arguments[2].Expression, arguments[0]),
"AreSame" when arguments.Count >= 2 =>
@@ -281,6 +294,254 @@ protected override bool IsFrameworkAssertionNamespace(string namespaceName)
};
}
+ ///
+ /// Converts Assert.AreEqual with support for comparer overloads and format string messages.
+ /// MSTest overloads:
+ /// - Assert.AreEqual(expected, actual)
+ /// - Assert.AreEqual(expected, actual, message)
+ /// - Assert.AreEqual(expected, actual, message, params object[] parameters)
+ /// - Assert.AreEqual(expected, actual, comparer)
+ /// - Assert.AreEqual(expected, actual, comparer, message)
+ ///
+ private ExpressionSyntax? ConvertAreEqual(SeparatedSyntaxList arguments)
+ {
+ if (arguments.Count < 2)
+ {
+ return null;
+ }
+
+ var expected = arguments[0];
+ var actual = arguments[1];
+
+ // 2 args: AreEqual(expected, actual)
+ if (arguments.Count == 2)
+ {
+ return CreateTUnitAssertion("IsEqualTo", actual.Expression, expected);
+ }
+
+ // 3+ args: Determine if 3rd arg is a message (string) or comparer
+ // Check for named arguments first (most reliable)
+ var thirdArg = arguments[2];
+ var namedArg = thirdArg.NameColon?.Name.Identifier.Text;
+ if (namedArg == "message")
+ {
+ var (msg, fmtArgs) = ExtractMessageWithFormatArgs(arguments, 2);
+ if (msg != null)
+ {
+ var msgExpr = CreateMessageExpression(msg, fmtArgs);
+ return CreateTUnitAssertionWithMessage("IsEqualTo", actual.Expression, msgExpr, expected);
+ }
+ return CreateTUnitAssertion("IsEqualTo", actual.Expression, expected);
+ }
+ if (namedArg == "comparer")
+ {
+ var result = CreateTUnitAssertion("IsEqualTo", actual.Expression, expected);
+ if (arguments.Count >= 4)
+ {
+ var (message, formatArgs) = ExtractMessageWithFormatArgs(arguments, 3);
+ if (message != null)
+ {
+ var messageExpr = CreateMessageExpression(message, formatArgs);
+ result = CreateTUnitAssertionWithMessage("IsEqualTo", actual.Expression, messageExpr, expected);
+ }
+ }
+ return result.WithLeadingTrivia(
+ SyntaxFactory.Comment("// TODO: TUnit migration - IEqualityComparer was used. TUnit uses .IsEqualTo() which may have different comparison semantics."),
+ SyntaxFactory.EndOfLine("\n"));
+ }
+
+ // Check syntactically if it looks like a string (message)
+ var isLikelyMessage = thirdArg.Expression is LiteralExpressionSyntax literal &&
+ literal.IsKind(SyntaxKind.StringLiteralExpression);
+ // Also check for interpolated strings
+ isLikelyMessage = isLikelyMessage || thirdArg.Expression is InterpolatedStringExpressionSyntax;
+
+ // If it's a string expression, treat as message
+ if (isLikelyMessage)
+ {
+ var (msg, fmtArgs) = ExtractMessageWithFormatArgs(arguments, 2);
+ if (msg != null)
+ {
+ var msgExpr = CreateMessageExpression(msg, fmtArgs);
+ return CreateTUnitAssertionWithMessage("IsEqualTo", actual.Expression, msgExpr, expected);
+ }
+ return CreateTUnitAssertion("IsEqualTo", actual.Expression, expected);
+ }
+
+ // If not a string literal, try semantic analysis to check for comparer
+ var isComparer = IsLikelyComparerArgumentSafe(arguments[2]);
+
+ if (isComparer == true)
+ {
+ // AreEqual(expected, actual, comparer) or AreEqual(expected, actual, comparer, message)
+ var result = CreateTUnitAssertion("IsEqualTo", actual.Expression, expected);
+ if (arguments.Count >= 4)
+ {
+ // Has message after comparer
+ var (message, formatArgs) = ExtractMessageWithFormatArgs(arguments, 3);
+ if (message != null)
+ {
+ var messageExpr = CreateMessageExpression(message, formatArgs);
+ result = CreateTUnitAssertionWithMessage("IsEqualTo", actual.Expression, messageExpr, expected);
+ }
+ }
+ // Add TODO for comparer
+ return result.WithLeadingTrivia(
+ SyntaxFactory.Comment("// TODO: TUnit migration - IEqualityComparer was used. TUnit uses .IsEqualTo() which may have different comparison semantics."),
+ SyntaxFactory.EndOfLine("\n"));
+ }
+
+ if (isComparer == null)
+ {
+ // Type couldn't be determined - add TODO for manual review
+ return CreateTUnitAssertion("IsEqualTo", actual.Expression, expected).WithLeadingTrivia(
+ SyntaxFactory.Comment("// TODO: TUnit migration - third argument could not be identified as comparer or message. Manual verification required."),
+ SyntaxFactory.EndOfLine("\n"));
+ }
+
+ // isComparer == false: Not a comparer, treat remaining args as message with format args
+ var (msg2, fmtArgs2) = ExtractMessageWithFormatArgs(arguments, 2);
+ if (msg2 != null)
+ {
+ var msgExpr = CreateMessageExpression(msg2, fmtArgs2);
+ return CreateTUnitAssertionWithMessage("IsEqualTo", actual.Expression, msgExpr, expected);
+ }
+
+ return CreateTUnitAssertion("IsEqualTo", actual.Expression, expected);
+ }
+
+ ///
+ /// Safely checks if an argument is a comparer, catching any exceptions from semantic analysis.
+ /// Returns null if the type cannot be determined.
+ ///
+ private bool? IsLikelyComparerArgumentSafe(ArgumentSyntax argument)
+ {
+ try
+ {
+ return IsLikelyComparerArgument(argument);
+ }
+ catch (InvalidOperationException)
+ {
+ // Semantic analysis failed due to invalid compilation state
+ return null;
+ }
+ catch (ArgumentException)
+ {
+ // Semantic analysis failed due to invalid arguments
+ return null;
+ }
+ }
+
+ ///
+ /// Converts Assert.AreNotEqual with support for comparer overloads and format string messages.
+ ///
+ private ExpressionSyntax? ConvertAreNotEqual(SeparatedSyntaxList arguments)
+ {
+ if (arguments.Count < 2)
+ {
+ return null;
+ }
+
+ var expected = arguments[0];
+ var actual = arguments[1];
+
+ // 2 args: AreNotEqual(expected, actual)
+ if (arguments.Count == 2)
+ {
+ return CreateTUnitAssertion("IsNotEqualTo", actual.Expression, expected);
+ }
+
+ // 3+ args: Determine if 3rd arg is a message (string) or comparer
+ // Check for named arguments first (most reliable)
+ var thirdArg = arguments[2];
+ var namedArg = thirdArg.NameColon?.Name.Identifier.Text;
+ if (namedArg == "message")
+ {
+ var (msg, fmtArgs) = ExtractMessageWithFormatArgs(arguments, 2);
+ if (msg != null)
+ {
+ var msgExpr = CreateMessageExpression(msg, fmtArgs);
+ return CreateTUnitAssertionWithMessage("IsNotEqualTo", actual.Expression, msgExpr, expected);
+ }
+ return CreateTUnitAssertion("IsNotEqualTo", actual.Expression, expected);
+ }
+ if (namedArg == "comparer")
+ {
+ var result = CreateTUnitAssertion("IsNotEqualTo", actual.Expression, expected);
+ if (arguments.Count >= 4)
+ {
+ var (message, formatArgs) = ExtractMessageWithFormatArgs(arguments, 3);
+ if (message != null)
+ {
+ var messageExpr = CreateMessageExpression(message, formatArgs);
+ result = CreateTUnitAssertionWithMessage("IsNotEqualTo", actual.Expression, messageExpr, expected);
+ }
+ }
+ return result.WithLeadingTrivia(
+ SyntaxFactory.Comment("// TODO: TUnit migration - IEqualityComparer was used. TUnit uses .IsNotEqualTo() which may have different comparison semantics."),
+ SyntaxFactory.EndOfLine("\n"));
+ }
+
+ // Check syntactically if it looks like a string (message)
+ var isLikelyMessage = thirdArg.Expression is LiteralExpressionSyntax literal &&
+ literal.IsKind(SyntaxKind.StringLiteralExpression);
+ // Also check for interpolated strings
+ isLikelyMessage = isLikelyMessage || thirdArg.Expression is InterpolatedStringExpressionSyntax;
+
+ // If it's a string expression, treat as message
+ if (isLikelyMessage)
+ {
+ var (msg, fmtArgs) = ExtractMessageWithFormatArgs(arguments, 2);
+ if (msg != null)
+ {
+ var msgExpr = CreateMessageExpression(msg, fmtArgs);
+ return CreateTUnitAssertionWithMessage("IsNotEqualTo", actual.Expression, msgExpr, expected);
+ }
+ return CreateTUnitAssertion("IsNotEqualTo", actual.Expression, expected);
+ }
+
+ // If not a string literal, try semantic analysis to check for comparer
+ var isComparer = IsLikelyComparerArgumentSafe(arguments[2]);
+
+ if (isComparer == true)
+ {
+ // AreNotEqual(expected, actual, comparer) or AreNotEqual(expected, actual, comparer, message)
+ var result = CreateTUnitAssertion("IsNotEqualTo", actual.Expression, expected);
+ if (arguments.Count >= 4)
+ {
+ // Has message after comparer
+ var (message, formatArgs) = ExtractMessageWithFormatArgs(arguments, 3);
+ if (message != null)
+ {
+ var messageExpr = CreateMessageExpression(message, formatArgs);
+ result = CreateTUnitAssertionWithMessage("IsNotEqualTo", actual.Expression, messageExpr, expected);
+ }
+ }
+ // Add TODO for comparer
+ return result.WithLeadingTrivia(
+ SyntaxFactory.Comment("// TODO: TUnit migration - IEqualityComparer was used. TUnit uses .IsNotEqualTo() which may have different comparison semantics."),
+ SyntaxFactory.EndOfLine("\n"));
+ }
+
+ if (isComparer == null)
+ {
+ // Type couldn't be determined - add TODO for manual review
+ return CreateTUnitAssertion("IsNotEqualTo", actual.Expression, expected).WithLeadingTrivia(
+ SyntaxFactory.Comment("// TODO: TUnit migration - third argument could not be identified as comparer or message. Manual verification required."),
+ SyntaxFactory.EndOfLine("\n"));
+ }
+
+ // isComparer == false: Not a comparer, treat remaining args as message with format args
+ var (msg2, fmtArgs2) = ExtractMessageWithFormatArgs(arguments, 2);
+ if (msg2 != null)
+ {
+ var msgExpr = CreateMessageExpression(msg2, fmtArgs2);
+ return CreateTUnitAssertionWithMessage("IsNotEqualTo", actual.Expression, msgExpr, expected);
+ }
+
+ return CreateTUnitAssertion("IsNotEqualTo", actual.Expression, expected);
+ }
+
private ExpressionSyntax CreateInconclusiveAssertion(SeparatedSyntaxList arguments)
{
// Convert Assert.Inconclusive(message) to await Assert.Skip(message)
diff --git a/TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs
index 191a51476f6..479dd6e1529 100644
--- a/TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs
@@ -525,7 +525,7 @@ private ExpressionSyntax ConvertAreEqualWithComparer(SeparatedSyntaxList= 3 && IsLikelyComparerArgument(arguments[2]))
+ if (arguments.Count >= 3 && IsLikelyComparerArgument(arguments[2]) == true)
{
// Add TODO comment and skip the comparer
var result = CreateTUnitAssertion("IsEqualTo", actual, expected);
@@ -546,7 +546,7 @@ private ExpressionSyntax ConvertAreNotEqualWithMessage(SeparatedSyntaxList= 3 && IsLikelyComparerArgument(arguments[2]))
+ if (arguments.Count >= 3 && IsLikelyComparerArgument(arguments[2]) == true)
{
var result = CreateTUnitAssertion("IsNotEqualTo", actual, expected);
return result.WithLeadingTrivia(
diff --git a/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs
index 02b2be8bdbd..f566d0a81f6 100644
--- a/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs
@@ -44,7 +44,9 @@ private class PassThroughRewriter : CSharpSyntaxRewriter
protected override CompilationUnitSyntax ApplyFrameworkSpecificConversions(CompilationUnitSyntax compilationUnit, SemanticModel semanticModel, Compilation compilation)
{
- var syntaxTree = compilationUnit.SyntaxTree;
+ // Use the original syntax tree from the semantic model, not from the (potentially modified) compilation unit
+ // After assertion rewriting, compilationUnit.SyntaxTree is a new tree not in the compilation
+ var syntaxTree = semanticModel.SyntaxTree;
SyntaxNode updatedRoot = compilationUnit;
updatedRoot = UpdateInitializeDispose(compilation, updatedRoot);
@@ -540,11 +542,11 @@ protected override bool IsFrameworkAssertionNamespace(string namespaceName)
return methodName switch
{
// Equality assertions - check for comparer overloads
- "Equal" when arguments.Count >= 3 && IsLikelyComparerArgument(arguments[2]) =>
+ "Equal" when arguments.Count >= 3 && IsLikelyComparerArgument(arguments[2]) == true =>
CreateEqualWithComparerComment(arguments),
"Equal" when arguments.Count >= 2 =>
CreateTUnitAssertion("IsEqualTo", arguments[1].Expression, arguments[0]),
- "NotEqual" when arguments.Count >= 3 && IsLikelyComparerArgument(arguments[2]) =>
+ "NotEqual" when arguments.Count >= 3 && IsLikelyComparerArgument(arguments[2]) == true =>
CreateNotEqualWithComparerComment(arguments),
"NotEqual" when arguments.Count >= 2 =>
CreateTUnitAssertion("IsNotEqualTo", arguments[1].Expression, arguments[0]),
@@ -616,9 +618,9 @@ protected override bool IsFrameworkAssertionNamespace(string namespaceName)
"Superset" when arguments.Count >= 2 =>
CreateTUnitAssertion("IsSupersetOf", arguments[0].Expression, arguments[1]),
"ProperSubset" when arguments.Count >= 2 =>
- CreateTUnitAssertion("IsSubsetOf", arguments[0].Expression, arguments[1]),
+ CreateProperSubsetWithTodo(arguments),
"ProperSuperset" when arguments.Count >= 2 =>
- CreateTUnitAssertion("IsSupersetOf", arguments[0].Expression, arguments[1]),
+ CreateProperSupersetWithTodo(arguments),
// Unique items
"Distinct" when arguments.Count >= 1 =>
@@ -628,6 +630,28 @@ protected override bool IsFrameworkAssertionNamespace(string namespaceName)
"Equivalent" when arguments.Count >= 2 =>
CreateTUnitAssertion("IsEquivalentTo", arguments[1].Expression, arguments[0]),
+ // Regex assertions
+ "Matches" when arguments.Count >= 2 =>
+ CreateTUnitAssertion("Matches", arguments[1].Expression, arguments[0]),
+ "DoesNotMatch" when arguments.Count >= 2 =>
+ CreateTUnitAssertion("DoesNotMatch", arguments[1].Expression, arguments[0]),
+
+ // Collection with inspectors - complex, needs TODO
+ "Collection" when arguments.Count >= 2 =>
+ CreateCollectionWithTodo(arguments),
+
+ // PropertyChanged - not supported in TUnit
+ "PropertyChanged" when arguments.Count >= 3 =>
+ CreatePropertyChangedTodo(arguments),
+ "PropertyChangedAsync" when arguments.Count >= 3 =>
+ CreatePropertyChangedTodo(arguments),
+
+ // Raises events - not supported in TUnit
+ "Raises" => CreateRaisesTodo(arguments),
+ "RaisesAsync" => CreateRaisesTodo(arguments),
+ "RaisesAny" => CreateRaisesTodo(arguments),
+ "RaisesAnyAsync" => CreateRaisesTodo(arguments),
+
_ => null
};
}
@@ -637,8 +661,7 @@ private ExpressionSyntax CreateEqualWithComparerComment(SeparatedSyntaxList arguments)
@@ -646,8 +669,79 @@ private ExpressionSyntax CreateNotEqualWithComparerComment(SeparatedSyntaxList arguments)
+ {
+ // Assert.Collection(collection, inspector1, inspector2, ...) has no direct TUnit equivalent
+ // Convert to HasCount check and add TODO for manual inspector conversion
+ var collection = arguments[0].Expression;
+ var inspectorCount = arguments.Count - 1;
+
+ var result = CreateTUnitAssertion("HasCount", collection,
+ SyntaxFactory.Argument(
+ SyntaxFactory.LiteralExpression(
+ SyntaxKind.NumericLiteralExpression,
+ SyntaxFactory.Literal(inspectorCount))));
+
+ // Just add TODO comment and newline - indentation will be handled by VisitInvocationExpression
+ return result.WithLeadingTrivia(
+ SyntaxFactory.Comment("// TODO: TUnit migration - Assert.Collection had element inspectors. Manually add assertions for each element."),
+ SyntaxFactory.EndOfLine("\n"));
+ }
+
+ private ExpressionSyntax CreatePropertyChangedTodo(SeparatedSyntaxList arguments)
+ {
+ // Assert.PropertyChanged(object, propertyName, action) - TUnit doesn't have this
+ // Create a placeholder that executes the action and add TODO
+ var action = arguments.Count > 2 ? arguments[2].Expression : arguments[0].Expression;
+
+ // Create: action() with TODO comment
+ var invocation = action is LambdaExpressionSyntax
+ ? (ExpressionSyntax)SyntaxFactory.InvocationExpression(
+ SyntaxFactory.ParenthesizedExpression(action))
+ : SyntaxFactory.InvocationExpression(action);
+
+ return invocation.WithLeadingTrivia(
+ SyntaxFactory.Comment("// TODO: TUnit migration - PropertyChanged assertion not supported. Implement INotifyPropertyChanged testing manually."),
+ SyntaxFactory.EndOfLine("\n"));
+ }
+
+ private ExpressionSyntax CreateRaisesTodo(SeparatedSyntaxList arguments)
+ {
+ // Assert.Raises(attach, detach, action) - TUnit doesn't have this
+ // Create placeholder with TODO
+ var action = arguments.Count > 2 ? arguments[2].Expression : arguments[0].Expression;
+
+ var invocation = action is LambdaExpressionSyntax
+ ? (ExpressionSyntax)SyntaxFactory.InvocationExpression(
+ SyntaxFactory.ParenthesizedExpression(action))
+ : SyntaxFactory.InvocationExpression(action);
+
+ return invocation.WithLeadingTrivia(
+ SyntaxFactory.Comment("// TODO: TUnit migration - Raises assertion not supported. Implement event testing manually."),
+ SyntaxFactory.EndOfLine("\n"));
+ }
+
+ private ExpressionSyntax CreateProperSubsetWithTodo(SeparatedSyntaxList arguments)
+ {
+ // ProperSubset means strict subset (not equal to superset)
+ // TUnit's IsSubsetOf doesn't distinguish between proper/improper
+ var result = CreateTUnitAssertion("IsSubsetOf", arguments[0].Expression, arguments[1]);
+ return result.WithLeadingTrivia(
+ SyntaxFactory.Comment("// TODO: TUnit migration - ProperSubset requires strict subset (not equal). Add additional assertion if needed."),
+ SyntaxFactory.EndOfLine("\n"));
+ }
+
+ private ExpressionSyntax CreateProperSupersetWithTodo(SeparatedSyntaxList arguments)
+ {
+ // ProperSuperset means strict superset (not equal to subset)
+ // TUnit's IsSupersetOf doesn't distinguish between proper/improper
+ var result = CreateTUnitAssertion("IsSupersetOf", arguments[0].Expression, arguments[1]);
+ return result.WithLeadingTrivia(
+ SyntaxFactory.Comment("// TODO: TUnit migration - ProperSuperset requires strict superset (not equal). Add additional assertion if needed."),
+ SyntaxFactory.EndOfLine("\n"));
}
private ExpressionSyntax ConvertThrowsAny(InvocationExpressionSyntax invocation, SimpleNameSyntax nameNode)
diff --git a/TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs b/TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs
index 14593d1c106..4e508ad4b7a 100644
--- a/TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs
+++ b/TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs
@@ -738,6 +738,91 @@ public async Task TestWithMessages()
);
}
+ [Test]
+ public async Task MSTest_Assertions_With_FormatStrings_Converted()
+ {
+ await CodeFixer.VerifyCodeFixAsync(
+ """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+ {|#0:public class MyClass|}
+ {
+ [TestMethod]
+ public void TestWithFormatStrings()
+ {
+ int x = 5;
+ Assert.AreEqual(5, x, "Expected {0} but got {1}", 5, x);
+ Assert.AreNotEqual(3, x, "Values should differ: {0}", x);
+ }
+ }
+ """,
+ Verifier.Diagnostic(Rules.MSTestMigration).WithLocation(0),
+ """
+ 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 TestWithFormatStrings()
+ {
+ int x = 5;
+ await Assert.That(x).IsEqualTo(5).Because(string.Format("Expected {0} but got {1}", 5, x));
+ await Assert.That(x).IsNotEqualTo(3).Because(string.Format("Values should differ: {0}", x));
+ }
+ }
+ """,
+ ConfigureMSTestTest
+ );
+ }
+
+ [Test]
+ public async Task MSTest_Assertions_With_Comparer_AddsTodoComment()
+ {
+ // When the comparer type cannot be determined via semantic analysis (e.g., in test context),
+ // a TODO comment is added for manual review instead of passing invalid arguments to .Because().
+ await CodeFixer.VerifyCodeFixAsync(
+ """
+ using Microsoft.VisualStudio.TestTools.UnitTesting;
+ using System.Collections.Generic;
+
+ {|#0:public class MyClass|}
+ {
+ [TestMethod]
+ public void TestWithComparer()
+ {
+ var comparer = StringComparer.OrdinalIgnoreCase;
+ Assert.AreEqual("hello", "HELLO", comparer);
+ }
+ }
+ """,
+ Verifier.Diagnostic(Rules.MSTestMigration).WithLocation(0),
+ """
+ using System.Collections.Generic;
+ 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 TestWithComparer()
+ {
+ var comparer = StringComparer.OrdinalIgnoreCase;
+ // TODO: TUnit migration - third argument could not be identified as comparer or message. Manual verification required.
+ await Assert.That("HELLO").IsEqualTo("hello");
+ }
+ }
+ """,
+ ConfigureMSTestTest
+ );
+ }
+
private static void ConfigureMSTestTest(Verifier.Test test)
{
test.TestState.AdditionalReferences.Add(typeof(TestMethodAttribute).Assembly);
diff --git a/TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj b/TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj
index dabf679d094..0f0db63bd6a 100644
--- a/TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj
+++ b/TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj
@@ -10,6 +10,8 @@
+
+
diff --git a/TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs b/TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs
index 63dc60ddcbd..265731dd9b5 100644
--- a/TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs
+++ b/TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs
@@ -631,15 +631,243 @@ public void Test1()
);
}
+ [Test]
+ public async Task Assert_Equal_Can_Be_Converted()
+ {
+ await CodeFixer
+ .VerifyCodeFixAsync(
+ """
+ {|#0:using TUnit.Core;
+
+ public class MyClass
+ {
+ [Fact]
+ public void MyTest()
+ {
+ Assert.Equal(5, 2 + 3);
+ }
+ }|}
+ """,
+ Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0),
+ """
+ using TUnit.Core;
+
+ public class MyClass
+ {
+ [Test]
+ public async Task MyTest()
+ {
+ await Assert.That(2 + 3).IsEqualTo(5);
+ }
+ }
+ """,
+ ConfigureXUnitTest
+ );
+ }
+
+ [Test]
+ public async Task Assert_Matches_Can_Be_Converted()
+ {
+ await CodeFixer
+ .VerifyCodeFixAsync(
+ """
+ {|#0:using TUnit.Core;
+
+ public class MyClass
+ {
+ [Fact]
+ public void MyTest()
+ {
+ Assert.Matches(@"\d+", "abc123");
+ }
+ }|}
+ """,
+ Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0),
+ """
+ using TUnit.Core;
+
+ public class MyClass
+ {
+ [Test]
+ public async Task MyTest()
+ {
+ await Assert.That("abc123").Matches(@"\d+");
+ }
+ }
+ """,
+ ConfigureXUnitTest
+ );
+ }
+
+ [Test]
+ public async Task Assert_DoesNotMatch_Can_Be_Converted()
+ {
+ await CodeFixer
+ .VerifyCodeFixAsync(
+ """
+ {|#0:using TUnit.Core;
+
+ public class MyClass
+ {
+ [Fact]
+ public void MyTest()
+ {
+ Assert.DoesNotMatch(@"^\d+$", "abc123");
+ }
+ }|}
+ """,
+ Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0),
+ """
+ using TUnit.Core;
+
+ public class MyClass
+ {
+ [Test]
+ public async Task MyTest()
+ {
+ await Assert.That("abc123").DoesNotMatch(@"^\d+$");
+ }
+ }
+ """,
+ ConfigureXUnitTest
+ );
+ }
+
+ [Test]
+ public async Task Assert_Collection_Adds_Todo_Comment()
+ {
+ await CodeFixer
+ .VerifyCodeFixAsync(
+ """
+ {|#0:using System;
+ using TUnit.Core;
+
+ public class MyClass
+ {
+ [Fact]
+ public void MyTest()
+ {
+ var items = new[] { 1, 2, 3 };
+ Assert.Collection(items,
+ x => Assert.Equal(1, x),
+ x => Assert.Equal(2, x),
+ x => Assert.Equal(3, x));
+ }
+ }|}
+ """,
+ Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0),
+ """
+ using System;
+ using TUnit.Core;
+
+ public class MyClass
+ {
+ [Test]
+ public async Task MyTest()
+ {
+ var items = new[] { 1, 2, 3 };
+ // TODO: TUnit migration - Assert.Collection had element inspectors. Manually add assertions for each element.
+ await Assert.That(items).HasCount(3);
+ }
+ }
+ """,
+ ConfigureXUnitTest
+ );
+ }
+
+ [Test]
+ public async Task Assert_ProperSubset_Adds_Todo_Comment()
+ {
+ await CodeFixer
+ .VerifyCodeFixAsync(
+ """
+ {|#0:using System;
+ using System.Collections.Generic;
+ using TUnit.Core;
+
+ public class MyClass
+ {
+ [Fact]
+ public void MyTest()
+ {
+ var subset = new HashSet { 1, 2 };
+ var superset = new HashSet { 1, 2, 3 };
+ Assert.ProperSubset(superset, subset);
+ }
+ }|}
+ """,
+ Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0),
+ """
+ using System;
+ using System.Collections.Generic;
+ using TUnit.Core;
+
+ public class MyClass
+ {
+ [Test]
+ public async Task MyTest()
+ {
+ var subset = new HashSet { 1, 2 };
+ var superset = new HashSet { 1, 2, 3 };
+ // TODO: TUnit migration - ProperSubset requires strict subset (not equal). Add additional assertion if needed.
+ await Assert.That(superset).IsSubsetOf(subset);
+ }
+ }
+ """,
+ ConfigureXUnitTest
+ );
+ }
+
+ [Test]
+ public async Task Assert_ProperSuperset_Adds_Todo_Comment()
+ {
+ await CodeFixer
+ .VerifyCodeFixAsync(
+ """
+ {|#0:using System;
+ using System.Collections.Generic;
+ using TUnit.Core;
+
+ public class MyClass
+ {
+ [Fact]
+ public void MyTest()
+ {
+ var subset = new HashSet { 1, 2 };
+ var superset = new HashSet { 1, 2, 3 };
+ Assert.ProperSuperset(subset, superset);
+ }
+ }|}
+ """,
+ Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0),
+ """
+ using System;
+ using System.Collections.Generic;
+ using TUnit.Core;
+
+ public class MyClass
+ {
+ [Test]
+ public async Task MyTest()
+ {
+ var subset = new HashSet { 1, 2 };
+ var superset = new HashSet { 1, 2, 3 };
+ // TODO: TUnit migration - ProperSuperset requires strict superset (not equal). Add additional assertion if needed.
+ await Assert.That(subset).IsSupersetOf(superset);
+ }
+ }
+ """,
+ ConfigureXUnitTest
+ );
+ }
+
private static void ConfigureXUnitTest(Verifier.Test test)
{
var globalUsings = ("GlobalUsings.cs", SourceText.From("global using Xunit;"));
test.TestState.Sources.Add(globalUsings);
-
- test.ReferenceAssemblies = test.ReferenceAssemblies.AddPackages([
- new PackageIdentity("xunit.v3.extensibility.core", "3.0.1")
- ]);
+ test.TestState.AdditionalReferences.Add(typeof(Xunit.FactAttribute).Assembly);
+ test.TestState.AdditionalReferences.Add(typeof(Xunit.Assert).Assembly);
}
private static void ConfigureXUnitTest(CodeFixer.Test test)
@@ -649,9 +877,13 @@ private static void ConfigureXUnitTest(CodeFixer.Test test)
test.TestState.Sources.Add(globalUsings);
test.FixedState.Sources.Add(globalUsings);
- test.ReferenceAssemblies = test.ReferenceAssemblies.AddPackages([
- new PackageIdentity("xunit.v3.extensibility.core", "3.0.1")
- ]);
+ // Add xUnit assemblies to TestState
+ test.TestState.AdditionalReferences.Add(typeof(Xunit.FactAttribute).Assembly);
+ test.TestState.AdditionalReferences.Add(typeof(Xunit.Assert).Assembly);
+
+ // Add TUnit assemblies to FixedState for the converted assertions
+ test.FixedState.AdditionalReferences.Add(typeof(TUnit.Core.TestAttribute).Assembly);
+ test.FixedState.AdditionalReferences.Add(typeof(TUnit.Assertions.Assert).Assembly);
}
}