From dca4d54eba0908763ede34c5f15a20ec73045a62 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 8 Sep 2026 17:06:34 +0400 Subject: [PATCH 1/2] [DowngradePhp82] Add DowngradeReflectionMethodHasPrototypeRector ReflectionMethod::hasPrototype() was added in PHP 8.2 and cannot be polyfilled, since the method simply does not exist below 8.2. Emulate it by calling getPrototype() inside a try/catch: it returns the prototype on 8.2- when one exists and throws ReflectionException otherwise. Because the replacement needs statements, wrap it in an immediately invoked closure so the call can be downgraded in any expression context; the receiver is passed as an argument rather than captured, so complex receiver expressions keep working. Assisted-By: Claude Opus 4.8 (1M context) --- config/set/downgrade-php82.php | 4 +- ...ReflectionMethodHasPrototypeRectorTest.php | 28 +++++ .../Fixture/if_condition.php.inc | 40 ++++++ .../Fixture/skip_first_class_callable.php.inc | 10 ++ .../Fixture/skip_other_type.php.inc | 16 +++ .../Fixture/some_class.php.inc | 32 +++++ .../config/configured_rule.php | 10 ++ ...radeReflectionMethodHasPrototypeRector.php | 118 ++++++++++++++++++ 8 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/DowngradeReflectionMethodHasPrototypeRectorTest.php create mode 100644 rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/if_condition.php.inc create mode 100644 rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/skip_first_class_callable.php.inc create mode 100644 rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/skip_other_type.php.inc create mode 100644 rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/some_class.php.inc create mode 100644 rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/config/configured_rule.php create mode 100644 rules/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector.php diff --git a/config/set/downgrade-php82.php b/config/set/downgrade-php82.php index e7e81dbe..18138f6e 100644 --- a/config/set/downgrade-php82.php +++ b/config/set/downgrade-php82.php @@ -3,11 +3,12 @@ declare(strict_types=1); use Rector\Config\RectorConfig; -use Rector\ValueObject\PhpVersion; use Rector\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector; use Rector\DowngradePhp82\Rector\Class_\DowngradeUnionIntersectionRector; use Rector\DowngradePhp82\Rector\FuncCall\DowngradeIteratorCountToArrayRector; use Rector\DowngradePhp82\Rector\FunctionLike\DowngradeStandaloneNullTrueFalseReturnTypeRector; +use Rector\DowngradePhp82\Rector\MethodCall\DowngradeReflectionMethodHasPrototypeRector; +use Rector\ValueObject\PhpVersion; return static function (RectorConfig $rectorConfig): void { $rectorConfig->phpVersion(PhpVersion::PHP_81); @@ -16,5 +17,6 @@ DowngradeStandaloneNullTrueFalseReturnTypeRector::class, DowngradeIteratorCountToArrayRector::class, DowngradeUnionIntersectionRector::class, + DowngradeReflectionMethodHasPrototypeRector::class, ]); }; diff --git a/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/DowngradeReflectionMethodHasPrototypeRectorTest.php b/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/DowngradeReflectionMethodHasPrototypeRectorTest.php new file mode 100644 index 00000000..5792323f --- /dev/null +++ b/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/DowngradeReflectionMethodHasPrototypeRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/if_condition.php.inc b/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/if_condition.php.inc new file mode 100644 index 00000000..a95990e2 --- /dev/null +++ b/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/if_condition.php.inc @@ -0,0 +1,40 @@ +hasPrototype()) { + $reflectionMethod = $reflectionMethod->getPrototype(); + } + + return $reflectionMethod; +} + +?> +----- +getPrototype(); + return true; + } catch (\ReflectionException) { + return false; + } + })($reflectionMethod)) { + $reflectionMethod = $reflectionMethod->getPrototype(); + } + + return $reflectionMethod; +} + +?> diff --git a/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/skip_first_class_callable.php.inc b/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/skip_first_class_callable.php.inc new file mode 100644 index 00000000..fd2e7c28 --- /dev/null +++ b/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/skip_first_class_callable.php.inc @@ -0,0 +1,10 @@ +hasPrototype(...); +} diff --git a/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/skip_other_type.php.inc b/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/skip_other_type.php.inc new file mode 100644 index 00000000..eb9ea655 --- /dev/null +++ b/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/skip_other_type.php.inc @@ -0,0 +1,16 @@ +hasPrototype(); +} diff --git a/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/some_class.php.inc b/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/some_class.php.inc new file mode 100644 index 00000000..d87cac7a --- /dev/null +++ b/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/some_class.php.inc @@ -0,0 +1,32 @@ +hasPrototype(); +} + +?> +----- +getPrototype(); + return true; + } catch (\ReflectionException) { + return false; + } + })($reflectionMethod); +} + +?> diff --git a/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/config/configured_rule.php b/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/config/configured_rule.php new file mode 100644 index 00000000..8b62fc70 --- /dev/null +++ b/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(DowngradeReflectionMethodHasPrototypeRector::class); +}; diff --git a/rules/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector.php b/rules/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector.php new file mode 100644 index 00000000..f3d29781 --- /dev/null +++ b/rules/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector.php @@ -0,0 +1,118 @@ +hasPrototype(); + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +class SomeClass +{ + public function run(ReflectionMethod $reflectionMethod): bool + { + return (function (\ReflectionMethod $reflectionMethod): bool { + try { + $reflectionMethod->getPrototype(); + return true; + } catch (\ReflectionException) { + return false; + } + })($reflectionMethod); + } +} +CODE_SAMPLE + ), + ]); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [MethodCall::class]; + } + + /** + * @param MethodCall $node + */ + public function refactor(Node $node): ?Node + { + if ($node->isFirstClassCallable()) { + return null; + } + + if (! $this->isName($node->name, 'hasPrototype')) { + return null; + } + + if (! $this->isObjectType($node->var, new ObjectType('ReflectionMethod'))) { + return null; + } + + return new FuncCall($this->createClosure(), [new Arg($node->var)]); + } + + private function createClosure(): Closure + { + $reflectionMethodVariable = new Variable('reflectionMethod'); + + $tryCatch = new TryCatch( + [ + new Expression(new MethodCall($reflectionMethodVariable, 'getPrototype')), + new Return_(new ConstFetch(new Name('true'))), + ], + [ + new Catch_( + [new FullyQualified('ReflectionException')], + null, + [new Return_(new ConstFetch(new Name('false')))] + ), + ] + ); + + return new Closure([ + 'params' => [new Param($reflectionMethodVariable, null, new FullyQualified('ReflectionMethod'))], + 'returnType' => new Identifier('bool'), + 'stmts' => [$tryCatch], + ]); + } +} From 00b7eb7239ae914e8ab1a41a064aac9d9f3112c3 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Thu, 10 Sep 2026 22:27:48 +0400 Subject: [PATCH 2/2] [DowngradePhp82] Use VariableNaming for the closure parameter in DowngradeReflectionMethodHasPrototypeRector Requested in review: pick the closure parameter name via VariableNaming so it never shadows a variable already present in the enclosing scope, matching the other downgrade rules. Assisted-By: Claude Fable 5.1 --- .../Fixture/if_condition.php.inc | 4 ++-- .../Fixture/some_class.php.inc | 4 ++-- ...radeReflectionMethodHasPrototypeRector.php | 20 ++++++++++++++----- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/if_condition.php.inc b/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/if_condition.php.inc index a95990e2..9ee1beee 100644 --- a/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/if_condition.php.inc +++ b/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/if_condition.php.inc @@ -23,9 +23,9 @@ use ReflectionMethod; function resolvePrototype(ReflectionMethod $reflectionMethod): ReflectionMethod { - if ((function (\ReflectionMethod $reflectionMethod): bool { + if ((function (\ReflectionMethod $reflectionMethod2): bool { try { - $reflectionMethod->getPrototype(); + $reflectionMethod2->getPrototype(); return true; } catch (\ReflectionException) { return false; diff --git a/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/some_class.php.inc b/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/some_class.php.inc index d87cac7a..c4f543ad 100644 --- a/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/some_class.php.inc +++ b/rules-tests/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector/Fixture/some_class.php.inc @@ -19,9 +19,9 @@ use ReflectionMethod; function checkPrototype(ReflectionMethod $reflectionMethod): bool { - return (function (\ReflectionMethod $reflectionMethod): bool { + return (function (\ReflectionMethod $reflectionMethod2): bool { try { - $reflectionMethod->getPrototype(); + $reflectionMethod2->getPrototype(); return true; } catch (\ReflectionException) { return false; diff --git a/rules/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector.php b/rules/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector.php index f3d29781..19e6cb54 100644 --- a/rules/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector.php +++ b/rules/DowngradePhp82/Rector/MethodCall/DowngradeReflectionMethodHasPrototypeRector.php @@ -20,6 +20,8 @@ use PhpParser\Node\Stmt\Return_; use PhpParser\Node\Stmt\TryCatch; use PHPStan\Type\ObjectType; +use Rector\Naming\Naming\VariableNaming; +use Rector\PHPStan\ScopeFetcher; use Rector\Rector\AbstractRector; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -29,6 +31,11 @@ */ final class DowngradeReflectionMethodHasPrototypeRector extends AbstractRector { + public function __construct( + private readonly VariableNaming $variableNaming + ) { + } + public function getRuleDefinition(): RuleDefinition { return new RuleDefinition('Downgrade ReflectionMethod::hasPrototype() by emulating it with getPrototype()', [ @@ -48,9 +55,9 @@ class SomeClass { public function run(ReflectionMethod $reflectionMethod): bool { - return (function (\ReflectionMethod $reflectionMethod): bool { + return (function (\ReflectionMethod $reflectionMethod2): bool { try { - $reflectionMethod->getPrototype(); + $reflectionMethod2->getPrototype(); return true; } catch (\ReflectionException) { return false; @@ -88,12 +95,15 @@ public function refactor(Node $node): ?Node return null; } - return new FuncCall($this->createClosure(), [new Arg($node->var)]); + $scope = ScopeFetcher::fetch($node); + $parameterName = $this->variableNaming->createCountedValueName('reflectionMethod', $scope); + + return new FuncCall($this->createClosure($parameterName), [new Arg($node->var)]); } - private function createClosure(): Closure + private function createClosure(string $parameterName): Closure { - $reflectionMethodVariable = new Variable('reflectionMethod'); + $reflectionMethodVariable = new Variable($parameterName); $tryCatch = new TryCatch( [