Skip to content

Suppress property.nonObject and method.nonObject errors when property_exists()/method_exists() guard is present#5729

Open
phpstan-bot wants to merge 1 commit into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-jmikw0u
Open

Suppress property.nonObject and method.nonObject errors when property_exists()/method_exists() guard is present#5729
phpstan-bot wants to merge 1 commit into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-jmikw0u

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When property_exists($mixed, 'prop') is used as a guard and then $mixed->prop is accessed, PHPStan incorrectly reported "Cannot access property $prop on class-string|object." This happened because property_exists() narrows mixed to class-string|(object&hasProperty(prop)), and the class-string member of the union causes canAccessProperties() to return maybe(), triggering the property.nonObject error. The same issue existed for method_exists() with canCallMethods().

PR #5544 added property_exists() guard handling in AccessStaticPropertiesCheck but missed the instance property access case in AccessPropertiesCheck, and also missed the analogous method_exists() case in MethodCallCheck.

Changes

  • src/Type/Php/PropertyExistsTypeSpecifyingExtension.php: In the !isObject()->yes() branch, also store the property_exists() call result as true (via createFuncCallSpec) in addition to narrowing the argument type. This enables synthetic call checks to find the stored specification.
  • src/Type/Php/MethodExistsTypeSpecifyingExtension.php: Same fix for the generic (non-string) branch.
  • src/Rules/Properties/AccessPropertiesCheck.php: Add a synthetic property_exists() call check before the canAccessProperties() error (line 122). Also extend the existing property_exists() guard check for undefined properties (previously only handled Expr names) to also handle Identifier names by constructing a String_ node.
  • src/Rules/Methods/MethodCallCheck.php: Add a synthetic method_exists() call check before the canCallMethods() error. Also extend the existing method_exists() guard check for undefined methods to handle Identifier names.
  • tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php: Remove the assertion for the now-suppressed false positive "Cannot call method foo() on class-string|object."

Analogous cases probed

  • Static property access (AccessStaticPropertiesCheck): Already fixed by PR Suppress undefined static property error when property_exists() guard is present #5544. Verified no regression.
  • Property access in assignment (AccessPropertiesInAssignRule): Uses AccessPropertiesCheck, automatically covered. Added dedicated test.
  • Object-type branch in type specifiers: When the first argument IS already an object, the narrowed type is object&hasProperty(prop) (no class-string union), so canAccessProperties() returns yes() and no false positive occurs. No fix needed.
  • Static method calls (CallStaticMethodsRule): Not affected — static method calls on class-strings are valid PHP and handled differently.

Root cause

Two independent issues combined to produce the false positive:

  1. PropertyExistsTypeSpecifyingExtension (and MethodExistsTypeSpecifyingExtension) narrowed the argument type for mixed to class-string|(object&hasProperty(…)) but did NOT store the function call result as true. This meant synthetic property_exists()/method_exists() call checks in rule code could not detect the guard.

  2. AccessPropertiesCheck (and MethodCallCheck) did not check for property_exists()/method_exists() guards before reporting the canAccessProperties()/canCallMethods() error. The existing guard checks only applied to the "undefined property/method" errors and only for dynamic (Expr) names.

Test

  • tests/PHPStan/Rules/Properties/data/bug-14667.php — Tests instance property access after property_exists() with mixed, explicit mixed, object|string, object, $this, self, and chained conditions.
  • tests/PHPStan/Rules/Properties/data/bug-14667-assign.php — Tests property assignment after property_exists() with mixed, explicit mixed, and object|string.
  • tests/PHPStan/Analyser/nsrt/bug-14667.php — Verifies type narrowing produces class-string|(object&hasProperty(prop)) for mixed.
  • tests/PHPStan/Rules/Methods/data/bug-14667.php — Tests method call after method_exists() with mixed, explicit mixed, object|string, object, and chained conditions.

Fixes phpstan/phpstan#14667

…perty_exists()`/`method_exists()` guard is present

- Add synthetic `property_exists()` call check in `AccessPropertiesCheck` before
  the `canAccessProperties()` error, so `mixed` narrowed to
  `class-string|(object&hasProperty(…))` no longer triggers a false positive
- Extend the existing `property_exists()` guard check for undefined properties
  to also handle `Identifier` property names (previously only handled `Expr`)
- Apply the same fix to `MethodCallCheck` for `method_exists()` guards
- Update `PropertyExistsTypeSpecifyingExtension` and
  `MethodExistsTypeSpecifyingExtension` to also store the function call result
  as `true` in the `!isObject()->yes()` branch, so synthetic call checks
  can find the stored specification
- Remove the now-incorrect assertion in `CallMethodsRuleTest` that expected
  the false positive "Cannot call method foo() on class-string|object"
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.

1 participant