Skip to content

Do not report undefined members of built-in classes when PHP_VERSION_ID narrowing excludes the analysed PHP version - #6945

Closed
phpstan-bot wants to merge 1 commit into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-c52nkeq
Closed

phpstan-bot wants to merge 1 commit into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-c52nkeq

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

if (PHP_VERSION_ID >= 80400) { ReflectionMethod::createFromMethodName($func); } reported Call to an undefined static method when the analysed PHP version is older than 8.3. Built-in class stubs are filtered for the single analysed PHP version, but the scope inside the guard is narrowed to PHP versions where the method does exist. With this change, PHPStan stops reporting a missing member of a built-in class when the scope's PHP version range excludes the analysed version.

Changes

  • New src/Rules/BuiltinClassMemberAvailabilityHelper.php. It returns true when every class in the type is built-in (or extends a built-in class) and Scope::getPhpVersion() does not include the analysed PhpVersion.
  • The helper is checked before the "undefined" error in:
    • src/Rules/Methods/StaticMethodCallCheck.php (the reported case)
    • src/Rules/Methods/MethodCallCheck.php (instance methods, e.g. ReflectionProperty::isVirtual())
    • src/Rules/Classes/ClassConstantRule.php (e.g. ReflectionProperty::IS_READONLY)
    • src/Rules/Properties/AccessPropertiesCheck.php (e.g. DOMNode::$isConnected)
    • src/Rules/Properties/AccessStaticPropertiesCheck.php
  • Updated the test constructors of these checks.
  • Probed but not changed: unknown built-in classes (new Random\Randomizer() behind a guard), functions and global constants. Functions already work because the functionMap is version-independent. Classes and constants are reported from ~20 rules, and handling them would need a version-independent stub lookup, so they are left for a separate change.

Root cause

Reflection for built-in classes is built once from phpstorm-stubs filtered by the configured PhpVersion. The rules then trusted that reflection even in branches where PHP_VERSION_ID narrowing has ruled the analysed version out. In those branches the reflection says nothing about which members really exist.

Test

  • tests/PHPStan/Rules/Methods/data/bug-15109.php (used by CallStaticMethodsRuleTest and CallMethodsRuleTest). It contains the issue's snippet verbatim, plus calls guarded by PHP_VERSION_ID >= 90000 and PHP_VERSION_ID < 50000 on ReflectionMethod and on a subclass. These are no longer reported. The same calls without a guard, and calls on user classes inside the guards, are still reported.
  • tests/PHPStan/Rules/Classes/data/bug-15109.php (ClassConstantRuleTest) covers class constants.
  • tests/PHPStan/Rules/Properties/data/bug-15109.php (AccessPropertiesRuleTest, AccessStaticPropertiesRuleTest) covers instance and static properties.
  • All new tests fail without the fix.

Fixes phpstan/phpstan#15109

🤖 Generated with Claude Code

…_ID` narrowing excludes the analysed PHP version

- Add `BuiltinClassMemberAvailabilityHelper`: the stubs of built-in classes are filtered for the
  single analysed `PhpVersion`, so when `Scope::getPhpVersion()` (narrowed by a `PHP_VERSION_ID`
  check) excludes that version, a member missing from the stubs may exist in the guarded branch.
  The helper answers this for built-in classes and classes extending them.
- Use it before reporting `staticMethod.notFound` (StaticMethodCallCheck), `method.notFound`
  (MethodCallCheck), `classConstant.notFound` (ClassConstantRule), `property.notFound`
  (AccessPropertiesCheck) and `staticProperty.notFound` (AccessStaticPropertiesCheck).
- Members of user classes are still reported in such branches.
- Not covered: unknown built-in classes, functions and global constants. These are reported
  from many different rules and would need a version-independent stub lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@VincentLanglet VincentLanglet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This fix just ignore the error but loose also the type inference. Shouldn't we load the stubs for those PHP_VERSION_ID conditions ? Is it too complex ?

@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

I didn't change any code or push commits for this review. The reviewer asked a design question, and the answer depends on a decision that should be made first.

Does this PR lose type inference? No, there was none to lose. I analysed the issue snippet with phpVersion: 80200 and dumped $ref inside the PHP_VERSION_ID >= 80400 branch. On the commit before this PR, PHPStan reports the "undefined static method" error and $ref is *ERROR*. With this PR, the error is gone and $ref is still *ERROR*. The PR only removes the false positive. Making the stubs give a real type would be a new feature.

Could we load the stubs for the narrowed versions? Possibly, but it is a much bigger change than this fix:

  • One version-filtered reflection stack. Built-in class reflection comes from phpstorm-stubs filtered for one PHP version, and the result is cached and shared. The PHP 8 signature map is probably filtered for that version too. Supporting this would mean a second stubber and reflector (and maybe a second signature map) for each extra PHP version, plus a way to build class reflections from them.
  • Types don't know the scope. ObjectType::hasMethod() and getMethod() read the global ReflectionProvider. The new lookup would have to go in the scope layer: MutatingScope::getMethodReflection(), the property and constant lookups, and the handlers such as StaticCallHandler, MethodCallHandler and the property and constant fetch handlers. The rules would have to use that path too, because they currently call $type->hasMethod() directly.
  • C++ mirrors. MutatingScope, ObjectType, StaticCallHandler and MethodCallHandler are all #[ShadowedByTurboExtension], so every change there also has to be ported to turbo-ext/.
  • Which version to use. The scope holds a range of versions (e.g. >= 80400), not one version. We would need a rule, for example "use the lower bound", and members removed in later versions would still be wrong.
  • User subclasses. For a user class extending a built-in one (MyReflectionMethod), the class hierarchy would have to be rebuilt against the other stubs.

My suggestion is to merge this PR as the narrow false-positive fix and do version-specific stub loading as a separate feature. If the reviewer would rather have the full approach in this PR, I can start on it, but it will be a large change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Call to an undefined static method ReflectionMethod::createFromMethodName().

2 participants