diff --git a/rules-tests/CodeQuality/Rector/MethodCall/MergeWithCallableAndWillReturnRector/Fixture/return_array_with_variable_use.php.inc b/rules-tests/CodeQuality/Rector/MethodCall/MergeWithCallableAndWillReturnRector/Fixture/return_array_with_variable_use.php.inc new file mode 100644 index 00000000..53a08a2e --- /dev/null +++ b/rules-tests/CodeQuality/Rector/MethodCall/MergeWithCallableAndWillReturnRector/Fixture/return_array_with_variable_use.php.inc @@ -0,0 +1,44 @@ +createMock('SomeClass') + ->method('someMethod') + ->with($this->callback(function ($arg): bool { + return true; + })) + ->willReturn([$initialStatements, []]); + } +} + +?> +----- +createMock('SomeClass') + ->method('someMethod') + ->willReturnCallback(function ($arg) use ($initialStatements): array { + return [$initialStatements, []]; + }); + } +} + +?> diff --git a/rules/CodeQuality/Rector/MethodCall/MergeWithCallableAndWillReturnRector.php b/rules/CodeQuality/Rector/MethodCall/MergeWithCallableAndWillReturnRector.php index 77011f06..0d7b8141 100644 --- a/rules/CodeQuality/Rector/MethodCall/MergeWithCallableAndWillReturnRector.php +++ b/rules/CodeQuality/Rector/MethodCall/MergeWithCallableAndWillReturnRector.php @@ -7,11 +7,13 @@ use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\ClosureUse; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Identifier; use PhpParser\Node\Stmt\Return_; +use Rector\PhpParser\Node\BetterNodeFinder; use Rector\PhpParser\Node\Value\ValueResolver; use Rector\PHPStanStaticTypeMapper\Enum\TypeKind; use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer; @@ -29,6 +31,7 @@ public function __construct( private readonly TestsNodeAnalyzer $testsNodeAnalyzer, private readonly ValueResolver $valueResolver, private readonly StaticTypeMapper $staticTypeMapper, + private readonly BetterNodeFinder $betterNodeFinder, ) { } @@ -143,8 +146,8 @@ public function refactor(Node $node): MethodCall|null $parentCaller->name = new Identifier('willReturnCallback'); $parentCaller->args = [new Arg($innerClosure)]; - if ($returnedExpr instanceof Variable) { - $innerClosure->uses[] = new ClosureUse($returnedExpr); + foreach ($this->resolveExternalUses($innerClosure, $returnedExpr) as $closureUse) { + $innerClosure->uses[] = $closureUse; } $returnedExprType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($returnedExpr); @@ -171,6 +174,55 @@ private function matchFirstArgCallbackMethodCall(MethodCall $withMethodCall): ?M return $firstArgValue; } + /** + * @return ClosureUse[] + */ + private function resolveExternalUses(Closure $innerClosure, Expr $returnedExpr): array + { + $paramNames = []; + foreach ($innerClosure->getParams() as $param) { + $paramNames[] = $this->getName($param); + } + + $existingUseNames = []; + foreach ($innerClosure->uses as $existingUse) { + $existingUseNames[] = $this->getName($existingUse->var); + } + + $closureUses = []; + $seenNames = []; + + /** @var Variable[] $variables */ + $variables = $this->betterNodeFinder->findInstancesOf($returnedExpr, [Variable::class]); + foreach ($variables as $variable) { + $variableName = $this->getName($variable); + if (! is_string($variableName)) { + continue; + } + + if ($variableName === 'this') { + continue; + } + + if (in_array($variableName, $paramNames, true)) { + continue; + } + + if (in_array($variableName, $existingUseNames, true)) { + continue; + } + + if (in_array($variableName, $seenNames, true)) { + continue; + } + + $seenNames[] = $variableName; + $closureUses[] = new ClosureUse(new Variable($variableName)); + } + + return $closureUses; + } + private function isLastStmtReturnTrue(Closure $closure): bool { $lastStmt = $closure->stmts[count($closure->stmts) - 1];