Skip to content

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

Merged
VincentLanglet merged 3 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-7mzisbk
May 21, 2026
Merged

Suppress undefined static property error when property_exists() guard is present#5544
VincentLanglet merged 3 commits into
phpstan: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

@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-7mzisbk branch from 096ab28 to 6b1a093 Compare May 20, 2026 15:56
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

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

Does the same issue exists for method_exists and/or for non-static property/method ?

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Both make tests (12129 tests, all passing) and make phpstan (no errors) pass.

Summary: The reviewer asked whether the same issue exists for method_exists() and/or non-static property/method access. After investigation, the answer is no — all those cases are already handled:

  • method_exists() + static methods: Works because HasMethodType can intersect with class-strings (class-strings support hasMethod() for static method calls), and StaticMethodCallCheck already checks $classStringType->hasMethod() at line 72.
  • Non-static property_exists(): HasPropertyType narrowing works on object types, and AccessPropertiesCheck has a dynamic property name guard (lines 205-213).
  • Non-static method_exists(): HasMethodType narrowing works on object types, and MethodCallCheck has a dynamic method name guard (lines 125-134).

The bug was unique to property_exists() + static properties because HasPropertyType checks instance properties (which class-strings don't support), unlike HasMethodType which checks hasMethod() (which class-strings do support). No code changes needed.

@VincentLanglet VincentLanglet requested a review from staabm May 20, 2026 16:31
@VincentLanglet VincentLanglet self-assigned this May 20, 2026

class Foo {
public static function test(): void {
if (property_exists(static::class, 'default')) {
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.

we should also test non-static property fetches, like in https://phpstan.org/r/20ab67ef-ef8b-4f24-8783-1ff18cfe7168

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done. Added non-static (instance) property fetch test cases as requested by staabm:

  • bug-2861.php: Added InstancePropertyAccess class with $this->default and $obj->default reads guarded by property_exists()
  • bug-2861-assign.php: Added testInstance() and testInstanceObj() methods with $this->default = 'value' and $obj->default = 'value' writes guarded by property_exists()
  • AccessPropertiesRuleTest: Added testBug2861() to verify instance property reads don't trigger errors
  • AccessPropertiesInAssignRuleTest: Added testBug2861() to verify instance property writes don't trigger errors

All 12131 tests pass and make phpstan reports no errors.

);
}

if (!$objectType->isObject()->yes()) {
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 if-statement is not covered by a test

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Pushed successfully. Here's a summary of what was done:

Addressing staabm's review comments:

  1. Non-static property fetch tests — The remote already had concurrent changes adding InstancePropertyAccess class tests (with $this and $obj patterns) to both bug-2861.php and bug-2861-assign.php. I resolved the rebase conflicts keeping those additions.

  2. Uncovered !$objectType->isObject()->yes() branch — Added tests/PHPStan/Analyser/nsrt/bug-2861.php with assertType tests that exercise the object|string narrowing path in PropertyExistsTypeSpecifyingExtension. This covers the branch where the first argument to property_exists() is neither purely string nor purely object, verifying it narrows to class-string|(object&hasProperty(foo)).

VincentLanglet and others added 3 commits May 21, 2026 07:14
…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`
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…owing

- Add instance property access tests (property_exists($this, 'default'))
  to bug-2861.php and bug-2861-assign.php test data files
- Add corresponding test methods in AccessPropertiesRuleTest and
  AccessPropertiesInAssignRuleTest
- Add nsrt test covering the !$objectType->isObject()->yes() branch in
  PropertyExistsTypeSpecifyingExtension for object|string first argument

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@staabm staabm force-pushed the create-pull-request/patch-7mzisbk branch from 7155dbd to 42ffe26 Compare May 21, 2026 05:14
@VincentLanglet VincentLanglet merged commit 2f46b52 into phpstan:2.1.x May 21, 2026
656 of 661 checks passed
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-7mzisbk branch May 21, 2026 06:48
@staabm
Copy link
Copy Markdown
Contributor

staabm commented May 21, 2026

hmm somehow the error

Cannot access property $prop on mixed.

did not got fixed with this PR

https://phpstan.org/r/521529b7-8d98-41ad-8499-105fc5c36795

@VincentLanglet
Copy link
Copy Markdown
Contributor

hmm somehow the error

Cannot access property $prop on mixed.

did not got fixed with this PR

phpstan.org/r/521529b7-8d98-41ad-8499-105fc5c36795

Do you mind opening an issue.

I think it's because we have a isString check
https://github.com/phpstan/phpstan-src/pull/5544/changes#diff-3aeefa37f58b006f6b8f9482b60728f31b093737e51249fe08dbdab566d110f0R71

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.

3 participants