Conversation
…iltering Instead of requiring `.Where(predicate).HasSingleItem()`, users can now write `.HasSingleItem(predicate)` directly. Updates xUnit code fixers to emit the new overload when converting `Assert.Single(collection, predicate)`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
|
||
| return (AssertionConversionKind.Single, $"await Assert.That({collection}).HasSingleItem()", true, null); | ||
| } | ||
|
|
There was a problem hiding this comment.
Issue: Code fixer doesn't distinguish between xUnit's predicate and expected-value overloads
Both code fixers assume any second argument to Assert.Single is a predicate, but xUnit has a non-generic overload Assert.Single(IEnumerable collection, object? expected) where the second argument is an expected value, not a predicate.
When users migrate code like:
Assert.Single(myCollection, expectedObject);The fixer converts it to:
Assert.That(myCollection).HasSingleItem(expectedObject);This fails to compile because HasSingleItem expects Func<TItem, bool>, not an object value.
Why this matters:
xUnit provides four Assert.Single overloads (see xUnit source):
Assert.Single(IEnumerable)Assert.Single(IEnumerable, object?)← problematic overloadAssert.Single<T>(IEnumerable<T>)Assert.Single<T>(IEnumerable<T>, Predicate<T>)
Suggested approach:
Use semantic analysis to distinguish between these overloads, similar to how ConvertContains handles parameter type checking (XUnitTwoPhaseAnalyzer.cs:369-377):
var symbol = SemanticModel.GetSymbolInfo(memberAccess).Symbol;
if (symbol is IMethodSymbol { Parameters.Length: 2 } methodSymbol)
{
// Check if second parameter is Predicate<T> vs object
if (methodSymbol.Parameters[1].Type is INamedTypeSymbol { DelegateInvokeMethod: not null })
{
// This is the predicate overload - safe to convert
}
else
{
// This is the object expected overload - skip or handle differently
}
}Why semantic analysis is better:
- Accurately distinguishes overloads based on actual types, not syntax
- Prevents compilation errors in migrated code
- Consistent with the defensive pattern already used elsewhere in this codebase
See
TUnit/TUnit.Analyzers.CodeFixers/TwoPhase/XUnitTwoPhaseAnalyzer.cs
Lines 352 to 357 in bdad834
There was a problem hiding this comment.
Fixed. Both code fixers now use semantic analysis to check if the second parameter is a delegate type (DelegateInvokeMethod: not null) before converting to HasSingleItem(predicate). For the object? expected-value overload, the two-phase analyzer falls back to HasSingleItem() with a TODO comment, and the code fix provider falls back to plain HasSingleItem().
|
|
||
| "Single" => SyntaxFactory.ParseExpression($"Assert.That({actual}).HasSingleItem()"), | ||
| "Single" => argumentListArguments.Count >= 2 | ||
| ? SyntaxFactory.ParseExpression($"Assert.That({argumentListArguments[0]}).HasSingleItem({argumentListArguments[1]})") |
There was a problem hiding this comment.
Issue: Same overload ambiguity exists here
This code fixer has the same issue as XUnitTwoPhaseAnalyzer.cs - it assumes any second argument to Assert.Single is a predicate.
xUnit's Assert.Single(IEnumerable, object?) overload would be incorrectly converted, causing compilation errors because HasSingleItem expects Func<TItem, bool>, not an object value.
Suggested fix:
Add semantic analysis before line 191 to check the parameter type:
"Single" => argumentListArguments.Count >= 2 && IsPredicateOverload(invocation, 1)
? SyntaxFactory.ParseExpression($"Assert.That({argumentListArguments[0]}).HasSingleItem({argumentListArguments[1]})")
: SyntaxFactory.ParseExpression($"Assert.That({argumentListArguments[0]}).HasSingleItem()"),Where IsPredicateOverload uses SemanticModel to verify the second parameter is actually a delegate type, not object?.
See the detailed explanation and suggested implementation approach in the comment on XUnitTwoPhaseAnalyzer.cs.
See
TUnit/TUnit.Assertions.Analyzers.CodeFixers/XUnitAssertionCodeFixProvider.cs
Lines 190 to 193 in bdad834
There was a problem hiding this comment.
Fixed in the same commit - extracted to an async Single method matching the pattern used by Contains.
…loads Use semantic analysis to check if the second parameter is a delegate type before converting to HasSingleItem(predicate). The object? expected-value overload now falls back to HasSingleItem() instead of misusing the predicate. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
HasSingleItem(Func<TItem, bool> predicate)overload so users can writeAssert.That(collection).HasSingleItem(x => x.Prop == "value")instead ofAssert.That(collection.Where(x => x.Prop == "value")).HasSingleItem()XUnitAssertionCodeFixProviderandXUnitTwoPhaseAnalyzer) to emit the new overload when convertingAssert.Single(collection, predicate)HasSingleItemPredicateAssertionclass,CheckHasSingleItemPredicateinCollectionChecks, and corresponding testsTest plan
ListAssertionTests- 4 HasSingleItem tests pass (1 existing + 3 new: predicate match, no-match failure, multiple-match failure)XUnitAssertionCodeFixProviderTests- 13 tests pass including updatedXunit_Single_With_Predicate_Preserves_LambdaXUnitMigrationAnalyzerTests- 67 tests pass🤖 Generated with Claude Code