Skip to content

[DowngradePhp82] Add DowngradeReflectionMethodHasPrototypeRector - #398

Merged
samsonasik merged 2 commits into
rectorphp:mainfrom
roxblnfk:feature/downgrade-reflection-has-prototype
Sep 10, 2026
Merged

[DowngradePhp82] Add DowngradeReflectionMethodHasPrototypeRector#398
samsonasik merged 2 commits into
rectorphp:mainfrom
roxblnfk:feature/downgrade-reflection-has-prototype

Conversation

@roxblnfk

@roxblnfk roxblnfk commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🔍 What was changed

New DowngradeReflectionMethodHasPrototypeRector (PHP 8.2 → 8.1) that rewrites $reflectionMethod->hasPrototype() into an emulation based on getPrototype().

$reflectionMethod->hasPrototype() becomes:

(function (\ReflectionMethod $reflectionMethod): bool {
    try {
        $reflectionMethod->getPrototype();
        return true;
    } catch (\ReflectionException) {
        return false;
    }
})($reflectionMethod)

The rule bails out on first-class callables, on non-ReflectionMethod receivers, and on any other method name.

Why?

ReflectionMethod::hasPrototype() was added in PHP 8.2 and does not exist below it, so the usual method_exists() ternary idiom used by other reflection downgrade rules would always evaluate to false — semantically wrong. getPrototype() (available long before) throws ReflectionException exactly when there is no prototype, which is precisely what hasPrototype() reports, so it is a faithful substitute.

The result must be usable in any expression context (if (...), assignment, argument), but try/catch is a statement — hence the immediately invoked closure. The receiver is passed as an argument instead of captured via use, so a complex receiver expression keeps working without a separate variable.

Chained downgrades below 8.0 are already covered: the emitted non-capturing catch (\ReflectionException) is lowered by the existing DowngradeNonCapturingCatchesRector.

Checklist

  • How was this tested:
    • Unit tests added

Fixtures cover the plain return case, the if ($m->hasPrototype()) { $m = $m->getPrototype(); } pattern, and two skip cases (non-ReflectionMethod receiver, first-class callable). Locally green: PHPUnit, PHPStan, ECS, rector process --dry-run, class-leak.

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) <noreply@anthropic.com>
…gradeReflectionMethodHasPrototypeRector

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 <noreply@anthropic.com>

@samsonasik samsonasik left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me 👍

@samsonasik

Copy link
Copy Markdown
Member

Thank you @roxblnfk , @TomasVotruba let's give it a try 👍

@samsonasik
samsonasik merged commit f755087 into rectorphp:main Sep 10, 2026
9 checks passed
@roxblnfk
roxblnfk deleted the feature/downgrade-reflection-has-prototype branch September 10, 2026 18:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants