55namespace Rector \PHPUnit \CodeQuality \Rector \MethodCall ;
66
77use PhpParser \Node ;
8+ use PhpParser \Node \ClosureUse ;
89use PhpParser \Node \Expr ;
910use PhpParser \Node \Expr \ArrowFunction ;
1011use PhpParser \Node \Expr \BinaryOp \BooleanAnd ;
1112use PhpParser \Node \Expr \BinaryOp \BooleanOr ;
1213use PhpParser \Node \Expr \Closure ;
1314use PhpParser \Node \Expr \MethodCall ;
15+ use PhpParser \Node \Expr \Variable ;
1416use PhpParser \Node \Identifier ;
1517use PhpParser \Node \Stmt \Return_ ;
18+ use Rector \PhpParser \Node \BetterNodeFinder ;
1619use Rector \PHPUnit \CodeQuality \NodeFactory \FromBinaryAndAssertExpressionsFactory ;
1720use Rector \PHPUnit \CodeQuality \ValueObject \ArgAndFunctionLike ;
1821use Rector \PHPUnit \NodeAnalyzer \TestsNodeAnalyzer ;
@@ -28,6 +31,7 @@ final class WithCallbackIdenticalToStandaloneAssertsRector extends AbstractRecto
2831 public function __construct (
2932 private readonly TestsNodeAnalyzer $ testsNodeAnalyzer ,
3033 private readonly FromBinaryAndAssertExpressionsFactory $ fromBinaryAndAssertExpressionsFactory ,
34+ private readonly BetterNodeFinder $ betterNodeFinder ,
3135 ) {
3236 }
3337
@@ -130,12 +134,16 @@ public function refactor(Node $node): MethodCall|null
130134 // arrow function -> flip to closure
131135 $ functionLikeInArg = $ argAndFunctionLike ->getArg ();
132136
137+ $ externalVariables = $ this ->resolveExternalClosureUses ($ innerFunctionLike );
138+
133139 $ closure = new Closure ([
134140 'params ' => $ argAndFunctionLike ->getFunctionLike ()
135141 ->params ,
136142 'stmts ' => $ assertExpressions ,
137143 'returnType ' => new Identifier ('void ' ),
144+ 'uses ' => $ externalVariables ,
138145 ]);
146+
139147 $ functionLikeInArg ->value = $ closure ;
140148 }
141149
@@ -210,4 +218,45 @@ private function matchInnerSoleExpr(Closure|ArrowFunction $functionLike): ?Expr
210218
211219 return $ functionLike ->expr ;
212220 }
221+
222+ /**
223+ * @return ClosureUse[]
224+ */
225+ private function resolveExternalClosureUses (ArrowFunction $ arrowFunction ): array
226+ {
227+ // fill needed uses from arrow function to closure
228+ $ arrowFunctionVariables = $ this ->betterNodeFinder ->findInstancesOfScoped (
229+ $ arrowFunction ->getStmts (),
230+ Variable::class
231+ );
232+ $ paramNames = [];
233+ foreach ($ arrowFunction ->getParams () as $ param ) {
234+ $ paramNames [] = $ this ->getName ($ param );
235+ }
236+
237+ $ externalVariableNames = [];
238+
239+ foreach ($ arrowFunctionVariables as $ arrowFunctionVariable ) {
240+ // skip those defined in params
241+ if ($ this ->isNames ($ arrowFunctionVariable , $ paramNames )) {
242+ continue ;
243+ }
244+
245+ $ variableName = $ this ->getName ($ arrowFunctionVariable );
246+ if (! is_string ($ variableName )) {
247+ continue ;
248+ }
249+
250+ $ externalVariableNames [] = $ variableName ;
251+ }
252+
253+ $ externalVariableNames = array_unique ($ externalVariableNames );
254+
255+ $ closureUses = [];
256+ foreach ($ externalVariableNames as $ externalVariableName ) {
257+ $ closureUses [] = new ClosureUse (new Variable ($ externalVariableName ));
258+ }
259+
260+ return $ closureUses ;
261+ }
213262}
0 commit comments