Suppress undefined static property error when property_exists() guard is present#5544
Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
Open
Suppress undefined static property error when property_exists() guard is present#5544phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
property_exists() guard is present#5544phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
Conversation
…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`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When
property_exists()is called with a class-string first argument (e.g.property_exists(static::class, 'default')), the subsequent static property accessstatic::$defaultwas 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 toproperty_exists()is a string type (class-string or constant class name), mark theproperty_exists()FuncCall expression itself asConstantBooleanType(true)in scope. Previously, class-string arguments caused the extension to return emptySpecifiedTypes, providing no narrowing. The approach of intersectingHasPropertyTypewith class-string was not viable becauseHasPropertyTypeusesObjectTypeTrait, making it incompatible with string types inTypeCombinator::intersect()(producingnever). 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 virtualproperty_exists(ClassName::class, 'propName')FuncCall and check if the scope evaluates it astrue. ForName-based class references (static,self,Foo), constructsClassConstFetch(Name, 'class')as the first argument. For expression-based references ($className::$prop), uses the expression directly.phpstan-baseline.neon: Updated expected count forinstanceof ConstantStringTypeinPropertyExistsTypeSpecifyingExtension.phpfrom 2 to 1 (removed the old$objectType instanceof ConstantStringTypecheck).Analogous cases probed
AccessStaticPropertiesInAssignRule): Uses the sameAccessStaticPropertiesCheck, so automatically covered. Added regression test.$className::$propafterproperty_exists($className, 'prop')): Works because the expression key matches. Added test case.$this->propafterproperty_exists($this, 'prop')): Already handled by existing code inAccessPropertiesCheck(lines 201-209 for dynamic names, andHasPropertyTypenarrowing for static names).HasMethodTypevsHasPropertyTypewith class-string: Investigated whymethod_exists()correctly narrows class-string types whileproperty_exists()didn't —HasMethodType::isSuperTypeOfcheckshasMethod()which class-strings support (for static method calls), butHasPropertyType::isSuperTypeOfcheckshasInstanceProperty()/hasStaticProperty()which class-strings don't support. This is why the FuncCall-marking approach was chosen over type intersection.Root cause
PropertyExistsTypeSpecifyingExtensiondid not handle class-string first arguments. When the first argument was aConstantStringType(which includes class-string types), the extension returned emptySpecifiedTypes, so no type narrowing occurred. Meanwhile,AccessStaticPropertiesCheckhad no mechanism to detect that aproperty_exists()guard was present before reporting undefined static properties — unlikeAccessPropertiesCheckwhich already had a virtualproperty_exists()check for dynamic instance property names.Test
tests/PHPStan/Rules/Properties/data/bug-2861.php— Regression test with trait usingproperty_exists(static::class, 'default')andproperty_exists(self::class, 'default')guards, plus expression-based$className::$propaccess. Expects no errors.tests/PHPStan/Rules/Properties/data/bug-2861-assign.php— Assignment context regression test withstatic::$default = 'value'afterproperty_exists()guard. Expects no errors.Fixes phpstan/phpstan#2861