From db0902a8ca9b83c17910e350352ce4cecc69a53a Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Thu, 25 Nov 2021 13:08:58 +0100 Subject: [PATCH 1/4] [Downgrade PHP 7.0] Add DowngradeClosureCallRector --- config/set/downgrade-php70.php | 2 + .../TypeAnalyzer/MethodTypeAnalyzer.php | 75 ++++++++++++++ .../DowngradeClosureCallRectorTest.php | 33 +++++++ .../Fixture/closure_call_expr.php.inc | 35 +++++++ .../Fixture/closure_call_variable.php.inc | 37 +++++++ .../Fixture/skip_not_closure.php.inc | 16 +++ .../config/configured_rule.php | 11 +++ .../MethodCall/DowngradeClosureCallRector.php | 97 +++++++++++++++++++ 8 files changed, 306 insertions(+) create mode 100644 packages/NodeTypeResolver/TypeAnalyzer/MethodTypeAnalyzer.php create mode 100644 rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/DowngradeClosureCallRectorTest.php create mode 100644 rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/Fixture/closure_call_expr.php.inc create mode 100644 rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/Fixture/closure_call_variable.php.inc create mode 100644 rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/Fixture/skip_not_closure.php.inc create mode 100644 rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/config/configured_rule.php create mode 100644 rules/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector.php diff --git a/config/set/downgrade-php70.php b/config/set/downgrade-php70.php index 00d272c149f..b9de2dfd696 100644 --- a/config/set/downgrade-php70.php +++ b/config/set/downgrade-php70.php @@ -13,6 +13,7 @@ use Rector\DowngradePhp70\Rector\FuncCall\DowngradeSessionStartArrayOptionsRector; use Rector\DowngradePhp70\Rector\FunctionLike\DowngradeScalarTypeDeclarationRector; use Rector\DowngradePhp70\Rector\GroupUse\SplitGroupedUseImportsRector; +use Rector\DowngradePhp70\Rector\MethodCall\DowngradeClosureCallRector; use Rector\DowngradePhp70\Rector\New_\DowngradeAnonymousClassRector; use Rector\DowngradePhp70\Rector\Spaceship\DowngradeSpaceshipRector; use Rector\DowngradePhp70\Rector\String_\DowngradeGeneratedScalarTypesRector; @@ -33,6 +34,7 @@ $services->set(DowngradeDirnameLevelsRector::class); $services->set(DowngradeSessionStartArrayOptionsRector::class); $services->set(SplitGroupedUseImportsRector::class); + $services->set(DowngradeClosureCallRector::class); $services->set(DowngradeGeneratedScalarTypesRector::class); $services->set(DowngradeParentTypeDeclarationRector::class); }; diff --git a/packages/NodeTypeResolver/TypeAnalyzer/MethodTypeAnalyzer.php b/packages/NodeTypeResolver/TypeAnalyzer/MethodTypeAnalyzer.php new file mode 100644 index 00000000000..18f3049f907 --- /dev/null +++ b/packages/NodeTypeResolver/TypeAnalyzer/MethodTypeAnalyzer.php @@ -0,0 +1,75 @@ +isMethodName($methodCall, $expectedMethod)) { + return false; + } + + return $this->isInstanceOf($methodCall->var, $expectedClass); + } + + /** + * @param non-empty-string $expectedName + */ + private function isMethodName(MethodCall $methodCall, string $expectedName): bool + { + if ($methodCall->name instanceof Identifier) { + $comparison = strcasecmp($methodCall->name->toString(), $expectedName); + if ($comparison === 0) { + return true; + } + } + + $type = $this->nodeTypeResolver->getType($methodCall->name); + + if ($type instanceof ConstantStringType) { + $comparison = strcasecmp($type->getValue(), $expectedName); + if ($comparison === 0) { + return true; + } + } + + return false; + } + + /** + * @param class-string $expectedClass + */ + private function isInstanceOf(Expr $expr, string $expectedClass): bool + { + $type = $this->nodeTypeResolver->getType($expr); + if (! $type instanceof TypeWithClassName) { + return false; + } + + $comparison = strcasecmp($expectedClass, $type->getClassName()); + if ($comparison === 0) { + return true; + } + + return $type->getAncestorWithClassName($expectedClass) !== null; + } +} diff --git a/rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/DowngradeClosureCallRectorTest.php b/rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/DowngradeClosureCallRectorTest.php new file mode 100644 index 00000000000..273af510a03 --- /dev/null +++ b/rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/DowngradeClosureCallRectorTest.php @@ -0,0 +1,33 @@ +doTestFileInfo($fileInfo); + } + + /** + * @return Iterator + */ + public function provideData(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/Fixture/closure_call_expr.php.inc b/rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/Fixture/closure_call_expr.php.inc new file mode 100644 index 00000000000..4d3c99e64c5 --- /dev/null +++ b/rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/Fixture/closure_call_expr.php.inc @@ -0,0 +1,35 @@ +value, PHP_EOL; +}; +$args = []; + +$closure->call(new Foo(), ...$args); + +?> +----- +value, PHP_EOL; +}; +$args = []; + +call_user_func($closure->bindTo(...array_fill(0, 2, new Foo())), ...$args); + +?> diff --git a/rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/Fixture/closure_call_variable.php.inc b/rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/Fixture/closure_call_variable.php.inc new file mode 100644 index 00000000000..adea472a693 --- /dev/null +++ b/rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/Fixture/closure_call_variable.php.inc @@ -0,0 +1,37 @@ +value, PHP_EOL; +}; +$args = []; + +$closure->call($foo, ...$args); + +?> +----- +value, PHP_EOL; +}; +$args = []; + +call_user_func($closure->bindTo($foo, $foo), ...$args); + +?> diff --git a/rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/Fixture/skip_not_closure.php.inc b/rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/Fixture/skip_not_closure.php.inc new file mode 100644 index 00000000000..c83f628f913 --- /dev/null +++ b/rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/Fixture/skip_not_closure.php.inc @@ -0,0 +1,16 @@ +call($bar); + +?> diff --git a/rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/config/configured_rule.php b/rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/config/configured_rule.php new file mode 100644 index 00000000000..d2dcdc5bfc0 --- /dev/null +++ b/rules-tests/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector/config/configured_rule.php @@ -0,0 +1,11 @@ +services(); + $services->set(DowngradeClosureCallRector::class); +}; diff --git a/rules/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector.php b/rules/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector.php new file mode 100644 index 00000000000..f609e191b58 --- /dev/null +++ b/rules/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector.php @@ -0,0 +1,97 @@ +call($newObj, ...$args); +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +call_user_func($closure->bindTo($newObj, $newObj), ...$args); +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [MethodCall::class]; + } + + /** + * @param MethodCall $node + */ + public function refactor(Node $node): ?FuncCall + { + if ($this->shouldSkip($node)) { + return null; + } + + $methodCall = $this->createBindToCall($node); + $args = [new Arg($methodCall), ...array_slice($node->args, 1)]; + + return new FuncCall(new Name('call_user_func'), $args); + } + + private function shouldSkip(MethodCall $methodCall): bool + { + if ($methodCall->args === []) { + return true; + } + + return ! $this->methodTypeAnalyzer->isCallTo($methodCall, Closure::class, 'call'); + } + + private function createBindToCall(MethodCall $methodCall): MethodCall + { + $newObj = $methodCall->args[0]; + if ($newObj->value instanceof Variable) { + $args = [$newObj, $newObj]; + } else { + // we dont' want the expression to be executed twice so we use array_fill() as a trick + $args = [new Arg(new LNumber(0)), new Arg(new LNumber(2)), $newObj]; + $funcCall = new FuncCall(new Name('array_fill'), $args); + $args = [new Arg($funcCall, false, true)]; + } + + return new MethodCall($methodCall->var, 'bindTo', $args); + } +} From 3e8646a3945992c5b00dae2c7441c561093118f5 Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Thu, 25 Nov 2021 13:23:24 +0100 Subject: [PATCH 2/4] Fix rule definition --- .../Rector/MethodCall/DowngradeClosureCallRector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector.php b/rules/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector.php index f609e191b58..3934f9c3a71 100644 --- a/rules/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector.php +++ b/rules/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector.php @@ -33,7 +33,7 @@ public function __construct( public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( - 'Replace the 2nd argument of dirname()', + 'Replace Closure::call() by Closure::bindTo()', [ new CodeSample( <<<'CODE_SAMPLE' From ecba97655ed276ac9c63f1d11c87a69110e79338 Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Thu, 25 Nov 2021 13:26:32 +0100 Subject: [PATCH 3/4] Fix typo in comment --- .../Rector/MethodCall/DowngradeClosureCallRector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector.php b/rules/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector.php index 3934f9c3a71..c6cc2809815 100644 --- a/rules/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector.php +++ b/rules/DowngradePhp70/Rector/MethodCall/DowngradeClosureCallRector.php @@ -86,7 +86,7 @@ private function createBindToCall(MethodCall $methodCall): MethodCall if ($newObj->value instanceof Variable) { $args = [$newObj, $newObj]; } else { - // we dont' want the expression to be executed twice so we use array_fill() as a trick + // we don't want the expression to be executed twice so we use array_fill() as a trick $args = [new Arg(new LNumber(0)), new Arg(new LNumber(2)), $newObj]; $funcCall = new FuncCall(new Name('array_fill'), $args); $args = [new Arg($funcCall, false, true)]; From aed120c79a466f80daf485e09a38e2c8d309d899 Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Thu, 25 Nov 2021 14:27:16 +0100 Subject: [PATCH 4/4] Split methods --- .../TypeAnalyzer/MethodTypeAnalyzer.php | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/NodeTypeResolver/TypeAnalyzer/MethodTypeAnalyzer.php b/packages/NodeTypeResolver/TypeAnalyzer/MethodTypeAnalyzer.php index 18f3049f907..fa388e64902 100644 --- a/packages/NodeTypeResolver/TypeAnalyzer/MethodTypeAnalyzer.php +++ b/packages/NodeTypeResolver/TypeAnalyzer/MethodTypeAnalyzer.php @@ -36,23 +36,23 @@ public function isCallTo(MethodCall $methodCall, string $expectedClass, string $ */ private function isMethodName(MethodCall $methodCall, string $expectedName): bool { - if ($methodCall->name instanceof Identifier) { - $comparison = strcasecmp($methodCall->name->toString(), $expectedName); - if ($comparison === 0) { - return true; - } + if ( + $methodCall->name instanceof Identifier + && $this->areMethodNamesEqual($methodCall->name->toString(), $expectedName) + ) { + return true; } $type = $this->nodeTypeResolver->getType($methodCall->name); - if ($type instanceof ConstantStringType) { - $comparison = strcasecmp($type->getValue(), $expectedName); - if ($comparison === 0) { - return true; - } - } + return $type instanceof ConstantStringType && $this->areMethodNamesEqual($type->getValue(), $expectedName); + } + + private function areMethodNamesEqual(string $left, string $right): bool + { + $comparison = strcasecmp($left, $right); - return false; + return $comparison === 0; } /** @@ -65,11 +65,17 @@ private function isInstanceOf(Expr $expr, string $expectedClass): bool return false; } - $comparison = strcasecmp($expectedClass, $type->getClassName()); - if ($comparison === 0) { + if ($this->areClassNamesEqual($expectedClass, $type->getClassName())) { return true; } return $type->getAncestorWithClassName($expectedClass) !== null; } + + private function areClassNamesEqual(string $left, string $right): bool + { + $comparison = strcasecmp($left, $right); + + return $comparison === 0; + } }