Skip to content

Suppress undefined static property error when property_exists() guard is present#5544

Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-7mzisbk
Open

Suppress undefined static property error when property_exists() guard is present#5544
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-7mzisbk

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When property_exists() is called with a class-string first argument (e.g. property_exists(static::class, 'default')), the subsequent static property access static::$default was reported as "Access to an undefined static property" even though the existence check guarded it. This fix teaches PHPStan to recognize that guard.

Changes

  • src/Type/Php/PropertyExistsTypeSpecifyingExtension.php: When the first argument to property_exists() is a string type (class-string or constant class name), mark the property_exists() FuncCall expression itself as ConstantBooleanType(true) in scope. Previously, class-string arguments caused the extension to return empty SpecifiedTypes, providing no narrowing. The approach of intersecting HasPropertyType with class-string was not viable because HasPropertyType uses ObjectTypeTrait, making it incompatible with string types in TypeCombinator::intersect() (producing never). This matches the approach already used for non-constant property names.

  • src/Rules/Properties/AccessStaticPropertiesCheck.php: Before reporting "Access to an undefined static property", construct a virtual property_exists(ClassName::class, 'propName') FuncCall and check if the scope evaluates it as true. For Name-based class references (static, self, Foo), constructs ClassConstFetch(Name, 'class') as the first argument. For expression-based references ($className::$prop), uses the expression directly.

  • phpstan-baseline.neon: Updated expected count for instanceof ConstantStringType in PropertyExistsTypeSpecifyingExtension.php from 2 to 1 (removed the old $objectType instanceof ConstantStringType check).

Analogous cases probed

  • Static property assignment (AccessStaticPropertiesInAssignRule): Uses the same AccessStaticPropertiesCheck, so automatically covered. Added regression test.
  • Expression-based class access ($className::$prop after property_exists($className, 'prop')): Works because the expression key matches. Added test case.
  • Instance properties ($this->prop after property_exists($this, 'prop')): Already handled by existing code in AccessPropertiesCheck (lines 201-209 for dynamic names, and HasPropertyType narrowing for static names).
  • HasMethodType vs HasPropertyType with class-string: Investigated why method_exists() correctly narrows class-string types while property_exists() didn't — HasMethodType::isSuperTypeOf checks hasMethod() which class-strings support (for static method calls), but HasPropertyType::isSuperTypeOf checks hasInstanceProperty()/hasStaticProperty() which class-strings don't support. This is why the FuncCall-marking approach was chosen over type intersection.

Root cause

PropertyExistsTypeSpecifyingExtension did not handle class-string first arguments. When the first argument was a ConstantStringType (which includes class-string types), the extension returned empty SpecifiedTypes, so no type narrowing occurred. Meanwhile, AccessStaticPropertiesCheck had no mechanism to detect that a property_exists() guard was present before reporting undefined static properties — unlike AccessPropertiesCheck which already had a virtual property_exists() check for dynamic instance property names.

Test

  • tests/PHPStan/Rules/Properties/data/bug-2861.php — Regression test with trait using property_exists(static::class, 'default') and property_exists(self::class, 'default') guards, plus expression-based $className::$prop access. Expects no errors.
  • tests/PHPStan/Rules/Properties/data/bug-2861-assign.php — Assignment context regression test with static::$default = 'value' after property_exists() guard. Expects no errors.

Fixes phpstan/phpstan#2861

…rd is present

- Handle class-string first argument in `PropertyExistsTypeSpecifyingExtension`
  by marking the `property_exists()` FuncCall expression as `true` in scope,
  instead of trying to intersect `HasPropertyType` with the class-string type
  (which produces `never` due to ObjectTypeTrait incompatibility)
- Add a virtual `property_exists()` check in `AccessStaticPropertiesCheck`
  before reporting "Access to an undefined static property" — constructs
  a `property_exists(ClassName::class, 'propName')` FuncCall and checks
  if the scope evaluates it as `true`
- Covers `static::$prop`, `self::$prop`, `ClassName::$prop`, and
  expression-based `$className::$prop` access patterns
- Also works for the assign context via `AccessStaticPropertiesInAssignRule`
  which shares the same `AccessStaticPropertiesCheck`
- Updated phpstan-baseline.neon to reflect one fewer `instanceof ConstantStringType`
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.

2 participants