Skip to content

Conversation

@thomhurst
Copy link
Owner

Summary

  • Add support for Has.Count.EqualTo(n)Count().IsEqualTo(n) conversion
  • Add support for Has.Exactly(n).ItemsCount().IsEqualTo(n) conversion
  • Improve Is.Not.Zero to use IsNotZero() instead of IsNotEqualTo(0)
  • Add support for Assert.Multiple(() => { ... })using (Assert.Multiple()) { ... } conversion

Fixes #4191

Test plan

  • Added test NUnit_HasCount_Converted - verifies Has.Count.EqualTo pattern
  • Added test NUnit_HasExactlyItems_Converted - verifies Has.Exactly(n).Items pattern
  • Added test NUnit_IsNotZero_Converted - verifies Is.Not.Zero pattern
  • Added test NUnit_AssertMultiple_Lambda_Converted - verifies Assert.Multiple lambda conversion
  • All 65 NUnit migration tests pass

🤖 Generated with Claude Code

Add support for:
- Has.Count.EqualTo(n) -> Count().IsEqualTo(n)
- Has.Exactly(n).Items -> Count().IsEqualTo(n)
- Is.Not.Zero -> IsNotZero() (improved from IsNotEqualTo(0))
- Assert.Multiple(() => { ... }) -> using (Assert.Multiple()) { ... }

Fixes #4191

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings January 8, 2026 19:20
@thomhurst
Copy link
Owner Author

Summary

Enhances the NUnit-to-TUnit analyzer with four new conversion patterns.

Critical Issues

None found - all changes look good.

Suggestions

  1. Consider null checks in ConvertAssertMultipleLambda when Visit returns null
  2. Review trivia handling in CreateUsingMultipleStatement for closing brace indentation

Verdict

APPROVE - No blocking issues. Great work on test coverage!

@thomhurst thomhurst enabled auto-merge (squash) January 8, 2026 19:26
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request enhances the NUnit to TUnit migration analyzer with four new conversion patterns:

  • Has.Count.EqualTo(n)Count().IsEqualTo(n) for collection count assertions
  • Has.Exactly(n).ItemsCount().IsEqualTo(n) for exact item count checks
  • Is.Not.ZeroIsNotZero() for improved zero-check assertions
  • Assert.Multiple(() => { ... })using (Assert.Multiple()) { ... } for batched assertions

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

File Description
TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs Adds 4 comprehensive test cases validating each new conversion pattern with expected input/output code
TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs Implements the conversion logic for all 4 patterns including VisitExpressionStatement override for Assert.Multiple, constraint conversion methods, and CreateCountAssertion helper

SyntaxList<StatementSyntax> statements;
if (lambda.Body is BlockSyntax block)
{
var convertedStatements = block.Statements.Select(s => (StatementSyntax)Visit(s)!).ToArray();
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

This Visit call uses a null-forgiving operator but could potentially return null. Consider adding null checks before casting to avoid potential NullReferenceExceptions.

Copilot uses AI. Check for mistakes.
}
else if (lambda.Body is ExpressionSyntax expr)
{
var visitedExpr = (ExpressionSyntax)Visit(expr)!;
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

This Visit call uses a null-forgiving operator but could potentially return null. Consider adding null checks before casting to avoid potential NullReferenceExceptions.

Copilot uses AI. Check for mistakes.
Comment on lines +284 to +304
private SyntaxNode ConvertAssertMultipleSimpleLambda(ExpressionStatementSyntax originalStatement, SimpleLambdaExpressionSyntax lambda)
{
SyntaxList<StatementSyntax> statements;
if (lambda.Body is BlockSyntax block)
{
var convertedStatements = block.Statements.Select(s => (StatementSyntax)Visit(s)!).ToArray();
statements = SyntaxFactory.List(convertedStatements);
}
else if (lambda.Body is ExpressionSyntax expr)
{
var visitedExpr = (ExpressionSyntax)Visit(expr)!;
statements = SyntaxFactory.SingletonList<StatementSyntax>(
SyntaxFactory.ExpressionStatement(visitedExpr));
}
else
{
return originalStatement;
}

return CreateUsingMultipleStatement(originalStatement, statements);
}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The ConvertAssertMultipleSimpleLambda and ConvertAssertMultipleLambda methods contain identical implementation logic. Consider extracting the common code into a single helper method that both can call, passing the lambda body as a parameter. This would reduce code duplication and improve maintainability.

Copilot uses AI. Check for mistakes.
Comment on lines +544 to +556
// Handle Has.Exactly(n).Items -> Count().IsEqualTo(n)
// Pattern: constraint.Name is "Items", constraint.Expression is Has.Exactly(n) invocation
if (memberName == "Items" &&
constraint.Expression is InvocationExpressionSyntax exactlyInvocation &&
exactlyInvocation.Expression is MemberAccessExpressionSyntax exactlyMemberAccess &&
exactlyMemberAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Has" } &&
exactlyMemberAccess.Name.Identifier.Text == "Exactly" &&
exactlyInvocation.ArgumentList.Arguments.Count > 0)
{
// Extract the count argument from Has.Exactly(n)
var countArg = exactlyInvocation.ArgumentList.Arguments[0];
return CreateCountAssertion(actualValue, "IsEqualTo", message, countArg);
}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

This code block for handling Has.Exactly(n).Items is duplicated in the ConvertConstraintMemberToTUnit method (lines 729-741). Consider extracting this pattern matching logic into a shared helper method to reduce code duplication and improve maintainability.

Copilot uses AI. Check for mistakes.
Comment on lines +480 to +495
// Handle Has.Count.EqualTo(n) -> Count().IsEqualTo(n)
// Pattern: Has.Count is a MemberAccess, then .EqualTo(n) is invoked on it
if (memberAccess.Expression is MemberAccessExpressionSyntax hasCountAccess &&
hasCountAccess.Expression is IdentifierNameSyntax { Identifier.Text: "Has" } &&
hasCountAccess.Name.Identifier.Text == "Count")
{
return methodName switch
{
"EqualTo" => CreateCountAssertion(actualValue, "IsEqualTo", message, constraint.ArgumentList.Arguments.ToArray()),
"GreaterThan" => CreateCountAssertion(actualValue, "IsGreaterThan", message, constraint.ArgumentList.Arguments.ToArray()),
"LessThan" => CreateCountAssertion(actualValue, "IsLessThan", message, constraint.ArgumentList.Arguments.ToArray()),
"GreaterThanOrEqualTo" => CreateCountAssertion(actualValue, "IsGreaterThanOrEqualTo", message, constraint.ArgumentList.Arguments.ToArray()),
"LessThanOrEqualTo" => CreateCountAssertion(actualValue, "IsLessThanOrEqualTo", message, constraint.ArgumentList.Arguments.ToArray()),
_ => CreateCountAssertion(actualValue, "IsEqualTo", message, constraint.ArgumentList.Arguments.ToArray())
};
}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The Has.Count.EqualTo(n) pattern handling is implemented in ConvertConstraintToTUnitWithMessage (lines 480-495) but is missing in the ConvertConstraintToTUnit method (line 589+). While ConvertConstraintToTUnit is primarily used for handling chained constraints like .Within(), it should still handle Has.Count patterns consistently for completeness and to avoid potential bugs if the code is refactored in the future.

Copilot uses AI. Check for mistakes.
Comment on lines +266 to +272
var convertedStatements = block.Statements.Select(s => (StatementSyntax)Visit(s)!).ToArray();
statements = SyntaxFactory.List(convertedStatements);
}
else if (lambda.Body is ExpressionSyntax expr)
{
// Single expression lambda - convert it
var visitedExpr = (ExpressionSyntax)Visit(expr)!;
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The Visit method is being called with null-forgiving operators (!) on lines 266, 272, 289, and 294, but CSharpSyntaxRewriter.Visit can return null for certain node types. While in this specific case the Visit calls are likely safe (they're visiting statements/expressions that will be converted), it would be more defensive to add null checks before casting to avoid potential NullReferenceExceptions if the Visit method returns null in unexpected cases.

Suggested change
var convertedStatements = block.Statements.Select(s => (StatementSyntax)Visit(s)!).ToArray();
statements = SyntaxFactory.List(convertedStatements);
}
else if (lambda.Body is ExpressionSyntax expr)
{
// Single expression lambda - convert it
var visitedExpr = (ExpressionSyntax)Visit(expr)!;
var convertedStatements = block.Statements
.Select(s => (StatementSyntax)(Visit(s) ?? s))
.ToArray();
statements = SyntaxFactory.List(convertedStatements);
}
else if (lambda.Body is ExpressionSyntax expr)
{
// Single expression lambda - convert it
var visitedExpr = (ExpressionSyntax)(Visit(expr) ?? expr);

Copilot uses AI. Check for mistakes.
else if (lambda.Body is ExpressionSyntax expr)
{
// Single expression lambda - convert it
var visitedExpr = (ExpressionSyntax)Visit(expr)!;
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

Similar to line 266, this Visit call uses a null-forgiving operator but could potentially return null. Consider adding null checks before casting.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Enhance NUnit analyzer converters

2 participants