@@ -64,7 +64,7 @@ private static async Task<Document> ConvertAssertionAsync(CodeFixContext context
6464
6565 var genericArgs = GetGenericArguments ( memberAccessExpressionSyntax . Name ) ;
6666
67- var newExpression = await GetNewExpression ( context , memberAccessExpressionSyntax , methodName , actual , expected , genericArgs , expressionSyntax . ArgumentList . Arguments ) ;
67+ var newExpression = await GetNewExpression ( context , expressionSyntax , memberAccessExpressionSyntax , methodName , actual , expected , genericArgs , expressionSyntax . ArgumentList . Arguments ) ;
6868
6969 if ( newExpression != null )
7070 {
@@ -75,12 +75,16 @@ private static async Task<Document> ConvertAssertionAsync(CodeFixContext context
7575 }
7676
7777 private static async Task < ExpressionSyntax ? > GetNewExpression ( CodeFixContext context ,
78+ InvocationExpressionSyntax expressionSyntax ,
7879 MemberAccessExpressionSyntax memberAccessExpressionSyntax , string method ,
7980 ArgumentSyntax ? actual , ArgumentSyntax ? expected , string genericArgs ,
8081 SeparatedSyntaxList < ArgumentSyntax > argumentListArguments )
8182 {
8283 var isGeneric = ! string . IsNullOrEmpty ( genericArgs ) ;
8384
85+ // Check if we're inside a .Satisfy() or .Satisfies() lambda
86+ var ( isInSatisfy , parameterName ) = IsInsideSatisfyLambda ( expressionSyntax ) ;
87+
8488 return method switch
8589 {
8690 "Equal" => await IsEqualTo ( context , argumentListArguments , actual , expected ) ,
@@ -95,13 +99,21 @@ private static async Task<Document> ConvertAssertionAsync(CodeFixContext context
9599
96100 "EndsWith" => SyntaxFactory . ParseExpression ( $ "Assert.That({ actual } ).EndsWith({ expected } )") ,
97101
98- "NotNull" => SyntaxFactory . ParseExpression ( $ "Assert.That({ actual } ).IsNotNull()") ,
102+ "NotNull" => isInSatisfy && parameterName != null
103+ ? SyntaxFactory . ParseExpression ( $ "{ actual } .IsNotNull()")
104+ : SyntaxFactory . ParseExpression ( $ "Assert.That({ actual } ).IsNotNull()") ,
99105
100- "Null" => SyntaxFactory . ParseExpression ( $ "Assert.That({ actual } ).IsNull()") ,
106+ "Null" => isInSatisfy && parameterName != null
107+ ? SyntaxFactory . ParseExpression ( $ "{ actual } .IsNull()")
108+ : SyntaxFactory . ParseExpression ( $ "Assert.That({ actual } ).IsNull()") ,
101109
102- "True" => SyntaxFactory . ParseExpression ( $ "Assert.That({ actual } ).IsTrue()") ,
110+ "True" => isInSatisfy && parameterName != null
111+ ? SyntaxFactory . ParseExpression ( $ "{ actual } .IsTrue()")
112+ : SyntaxFactory . ParseExpression ( $ "Assert.That({ actual } ).IsTrue()") ,
103113
104- "False" => SyntaxFactory . ParseExpression ( $ "Assert.That({ actual } ).IsFalse()") ,
114+ "False" => isInSatisfy && parameterName != null
115+ ? SyntaxFactory . ParseExpression ( $ "{ actual } .IsFalse()")
116+ : SyntaxFactory . ParseExpression ( $ "Assert.That({ actual } ).IsFalse()") ,
105117
106118 "Same" => SyntaxFactory . ParseExpression ( $ "Assert.That({ actual } ).IsSameReferenceAs({ expected } )") ,
107119
@@ -123,7 +135,7 @@ private static async Task<Document> ConvertAssertionAsync(CodeFixContext context
123135 ? SyntaxFactory . ParseExpression ( $ "Assert.That({ actual } ).IsNotAssignableFrom<{ genericArgs } >()")
124136 : SyntaxFactory . ParseExpression ( $ "Assert.That({ actual } ).IsNotAssignableFrom({ expected } )") ,
125137
126- "All" => SyntaxFactory . ParseExpression ( $ "Assert.That( { expected } ).All().Satisfy( { actual } )" ) ,
138+ "All" => ConvertAll ( expected , actual ) ,
127139
128140 "Single" => SyntaxFactory . ParseExpression ( $ "Assert.That({ actual } ).HasSingleItem()") ,
129141
@@ -278,4 +290,147 @@ public static string GetGenericArguments(ExpressionSyntax expressionSyntax)
278290
279291 return string . Empty ;
280292 }
293+
294+ private static ( bool isInSatisfy , string ? parameterName ) IsInsideSatisfyLambda ( SyntaxNode node )
295+ {
296+ var current = node . Parent ;
297+
298+ while ( current != null )
299+ {
300+ // Check if we're in a lambda expression
301+ if ( current is SimpleLambdaExpressionSyntax simpleLambda )
302+ {
303+ // Check if the lambda is an argument to a .Satisfy() or .Satisfies() call
304+ if ( current . Parent is ArgumentSyntax argument &&
305+ argument . Parent is ArgumentListSyntax argumentList &&
306+ argumentList . Parent is InvocationExpressionSyntax invocation &&
307+ invocation . Expression is MemberAccessExpressionSyntax memberAccess )
308+ {
309+ var methodName = memberAccess . Name . Identifier . ValueText ;
310+ if ( methodName is "Satisfy" or "Satisfies" )
311+ {
312+ return ( true , simpleLambda . Parameter . Identifier . ValueText ) ;
313+ }
314+ }
315+ }
316+ else if ( current is ParenthesizedLambdaExpressionSyntax parenLambda )
317+ {
318+ // Check if the lambda is an argument to a .Satisfy() or .Satisfies() call
319+ if ( current . Parent is ArgumentSyntax argument &&
320+ argument . Parent is ArgumentListSyntax argumentList &&
321+ argumentList . Parent is InvocationExpressionSyntax invocation &&
322+ invocation . Expression is MemberAccessExpressionSyntax memberAccess )
323+ {
324+ var methodName = memberAccess . Name . Identifier . ValueText ;
325+ if ( methodName is "Satisfy" or "Satisfies" )
326+ {
327+ // For parenthesized lambda, get the first parameter
328+ var firstParam = parenLambda . ParameterList . Parameters . FirstOrDefault ( ) ;
329+ return ( true , firstParam ? . Identifier . ValueText ) ;
330+ }
331+ }
332+ }
333+
334+ current = current . Parent ;
335+ }
336+
337+ return ( false , null ) ;
338+ }
339+
340+ private static ExpressionSyntax ConvertAll ( ArgumentSyntax ? collection , ArgumentSyntax ? lambda )
341+ {
342+ if ( lambda ? . Expression is not LambdaExpressionSyntax lambdaExpression )
343+ {
344+ // Fallback to simple conversion
345+ return SyntaxFactory . ParseExpression ( $ "Assert.That({ collection } ).All().Satisfy({ lambda } )") ;
346+ }
347+
348+ // Extract lambda parameter name
349+ string ? paramName = lambdaExpression switch
350+ {
351+ SimpleLambdaExpressionSyntax simple => simple . Parameter . Identifier . ValueText ,
352+ ParenthesizedLambdaExpressionSyntax paren => paren . ParameterList . Parameters . FirstOrDefault ( ) ? . Identifier . ValueText ,
353+ _ => null
354+ } ;
355+
356+ if ( paramName == null )
357+ {
358+ return SyntaxFactory . ParseExpression ( $ "Assert.That({ collection } ).All().Satisfy({ lambda } )") ;
359+ }
360+
361+ // Find xUnit assertions in lambda body
362+ var assertions = FindXUnitAssertions ( lambdaExpression ) ;
363+
364+ // If no nested assertions or more than one, use simple conversion
365+ if ( assertions . Count != 1 )
366+ {
367+ return SyntaxFactory . ParseExpression ( $ "Assert.That({ collection } ).All().Satisfy({ lambda } )") ;
368+ }
369+
370+ // Try to convert to two-parameter Satisfy for single assertion
371+ var ( invocation , methodName ) = assertions [ 0 ] ;
372+ var convertedSatisfy = ConvertToTwoParameterSatisfy ( invocation , methodName , paramName , collection ) ;
373+
374+ if ( convertedSatisfy != null )
375+ {
376+ return SyntaxFactory . ParseExpression ( convertedSatisfy ) ;
377+ }
378+
379+ // Fallback to simple conversion
380+ return SyntaxFactory . ParseExpression ( $ "Assert.That({ collection } ).All().Satisfy({ lambda } )") ;
381+ }
382+
383+ private static List < ( InvocationExpressionSyntax invocation , string methodName ) > FindXUnitAssertions ( LambdaExpressionSyntax lambda )
384+ {
385+ var assertions = new List < ( InvocationExpressionSyntax , string ) > ( ) ;
386+ var body = lambda switch
387+ {
388+ SimpleLambdaExpressionSyntax simple => simple . Body ,
389+ ParenthesizedLambdaExpressionSyntax paren => paren . Body ,
390+ _ => null
391+ } ;
392+
393+ if ( body == null ) return assertions ;
394+
395+ // Walk the syntax tree to find Xunit.Assert.* calls
396+ var invocations = body . DescendantNodes ( ) . OfType < InvocationExpressionSyntax > ( ) ;
397+ foreach ( var invocation in invocations )
398+ {
399+ if ( invocation . Expression is MemberAccessExpressionSyntax memberAccess &&
400+ memberAccess . Expression . ToString ( ) . Contains ( "Xunit.Assert" ) )
401+ {
402+ assertions . Add ( ( invocation , memberAccess . Name . Identifier . ValueText ) ) ;
403+ }
404+ }
405+
406+ return assertions ;
407+ }
408+
409+ private static string ? ConvertToTwoParameterSatisfy (
410+ InvocationExpressionSyntax invocation ,
411+ string methodName ,
412+ string paramName ,
413+ ArgumentSyntax ? collection )
414+ {
415+ var args = invocation . ArgumentList . Arguments ;
416+ if ( args . Count == 0 ) return null ;
417+
418+ // Extract the expression being tested
419+ var testedExpression = args [ 0 ] . Expression ;
420+
421+ // Determine the TUnit assertion method
422+ var tunitMethod = methodName switch
423+ {
424+ "NotNull" => "IsNotNull" ,
425+ "Null" => "IsNull" ,
426+ "True" => "IsTrue" ,
427+ "False" => "IsFalse" ,
428+ _ => null
429+ } ;
430+
431+ if ( tunitMethod == null ) return null ;
432+
433+ // Generate the two-parameter Satisfy call
434+ return $ "Assert.That({ collection } ).All().Satisfy({ paramName } => { testedExpression } , result => result.{ tunitMethod } ())";
435+ }
281436}
0 commit comments