diff --git a/rules-tests/CodeQuality/Rector/MethodCall/WithCallbackIdenticalToStandaloneAssertsRector/Fixture/use_variable.php.inc b/rules-tests/CodeQuality/Rector/MethodCall/WithCallbackIdenticalToStandaloneAssertsRector/Fixture/use_variable.php.inc new file mode 100644 index 00000000..f093fef2 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/MethodCall/WithCallbackIdenticalToStandaloneAssertsRector/Fixture/use_variable.php.inc @@ -0,0 +1,48 @@ +getMockBuilder('AnyType')->getMock(); + + $someMock->expects($this->any()) + ->method('trans') + ->with( + $this->callback(fn ($args): bool => count($args) === 5 && $args[0] === $expectedValue) + ); + } +} + +?> +----- +getMockBuilder('AnyType')->getMock(); + + $someMock->expects($this->any()) + ->method('trans') + ->with( + $this->callback(function ($args) use ($expectedValue): void { + $this->assertCount(5, $args); + $this->assertSame($expectedValue, $args[0]); + }) + ); + } +} + +?> diff --git a/rules-tests/CodeQuality/Rector/MethodCall/WithCallbackIdenticalToStandaloneAssertsRector/Fixture/use_variable_require_once.php.inc b/rules-tests/CodeQuality/Rector/MethodCall/WithCallbackIdenticalToStandaloneAssertsRector/Fixture/use_variable_require_once.php.inc new file mode 100644 index 00000000..50959f19 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/MethodCall/WithCallbackIdenticalToStandaloneAssertsRector/Fixture/use_variable_require_once.php.inc @@ -0,0 +1,49 @@ +getMockBuilder('AnyType')->getMock(); + + $someMock->expects($this->any()) + ->method('trans') + ->with( + $this->callback(fn ($args): bool => count($args) === 5 && $args[0] === $expectedValue && $args[2] === $expectedValue) + ); + } +} + +?> +----- +getMockBuilder('AnyType')->getMock(); + + $someMock->expects($this->any()) + ->method('trans') + ->with( + $this->callback(function ($args) use ($expectedValue): void { + $this->assertCount(5, $args); + $this->assertSame($expectedValue, $args[0]); + $this->assertSame($expectedValue, $args[2]); + }) + ); + } +} + +?> diff --git a/rules/CodeQuality/Rector/MethodCall/WithCallbackIdenticalToStandaloneAssertsRector.php b/rules/CodeQuality/Rector/MethodCall/WithCallbackIdenticalToStandaloneAssertsRector.php index cfec21b0..5e6bd83c 100644 --- a/rules/CodeQuality/Rector/MethodCall/WithCallbackIdenticalToStandaloneAssertsRector.php +++ b/rules/CodeQuality/Rector/MethodCall/WithCallbackIdenticalToStandaloneAssertsRector.php @@ -5,14 +5,17 @@ namespace Rector\PHPUnit\CodeQuality\Rector\MethodCall; use PhpParser\Node; +use PhpParser\Node\ClosureUse; use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\Node\Expr\BinaryOp\BooleanAnd; use PhpParser\Node\Expr\BinaryOp\BooleanOr; 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\PHPUnit\CodeQuality\NodeFactory\FromBinaryAndAssertExpressionsFactory; use Rector\PHPUnit\CodeQuality\ValueObject\ArgAndFunctionLike; use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer; @@ -28,6 +31,7 @@ final class WithCallbackIdenticalToStandaloneAssertsRector extends AbstractRecto public function __construct( private readonly TestsNodeAnalyzer $testsNodeAnalyzer, private readonly FromBinaryAndAssertExpressionsFactory $fromBinaryAndAssertExpressionsFactory, + private readonly BetterNodeFinder $betterNodeFinder, ) { } @@ -130,12 +134,16 @@ public function refactor(Node $node): MethodCall|null // arrow function -> flip to closure $functionLikeInArg = $argAndFunctionLike->getArg(); + $externalVariables = $this->resolveExternalClosureUses($innerFunctionLike); + $closure = new Closure([ 'params' => $argAndFunctionLike->getFunctionLike() ->params, 'stmts' => $assertExpressions, 'returnType' => new Identifier('void'), + 'uses' => $externalVariables, ]); + $functionLikeInArg->value = $closure; } @@ -210,4 +218,45 @@ private function matchInnerSoleExpr(Closure|ArrowFunction $functionLike): ?Expr return $functionLike->expr; } + + /** + * @return ClosureUse[] + */ + private function resolveExternalClosureUses(ArrowFunction $arrowFunction): array + { + // fill needed uses from arrow function to closure + $arrowFunctionVariables = $this->betterNodeFinder->findInstancesOfScoped( + $arrowFunction->getStmts(), + Variable::class + ); + $paramNames = []; + foreach ($arrowFunction->getParams() as $param) { + $paramNames[] = $this->getName($param); + } + + $externalVariableNames = []; + + foreach ($arrowFunctionVariables as $arrowFunctionVariable) { + // skip those defined in params + if ($this->isNames($arrowFunctionVariable, $paramNames)) { + continue; + } + + $variableName = $this->getName($arrowFunctionVariable); + if (! is_string($variableName)) { + continue; + } + + $externalVariableNames[] = $variableName; + } + + $externalVariableNames = array_unique($externalVariableNames); + + $closureUses = []; + foreach ($externalVariableNames as $externalVariableName) { + $closureUses[] = new ClosureUse(new Variable($externalVariableName)); + } + + return $closureUses; + } }