From e44ef87f7b6934528ac05ca588e595147859bc4a Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Wed, 4 Nov 2026 12:29:13 +0000 Subject: [PATCH] Do not report undefined members of built-in classes when `PHP_VERSION_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 --- .../BuiltinClassMemberAvailabilityHelper.php | 67 +++++++++++++++++++ src/Rules/Classes/ClassConstantRule.php | 6 ++ src/Rules/Methods/MethodCallCheck.php | 6 ++ src/Rules/Methods/StaticMethodCallCheck.php | 6 ++ .../Properties/AccessPropertiesCheck.php | 6 ++ .../AccessStaticPropertiesCheck.php | 6 ++ .../Analyser/Bug9307CallMethodsRuleTest.php | 2 + .../Classes/ClassConstantPhp74RuleTest.php | 2 + .../Rules/Classes/ClassConstantRuleTest.php | 16 +++++ .../PHPStan/Rules/Classes/data/bug-15109.php | 18 +++++ .../Rules/Methods/CallMethodsRuleTest.php | 23 +++++++ .../Methods/CallStaticMethodsRuleTest.php | 21 ++++++ ...thPossiblyRenamedNamedArgumentRuleTest.php | 3 +- .../Rules/Methods/MethodCallableRuleTest.php | 2 + .../Methods/StaticMethodCallableRuleTest.php | 2 + .../PHPStan/Rules/Methods/data/bug-15109.php | 43 ++++++++++++ .../AccessPropertiesInAssignRuleTest.php | 2 + .../Properties/AccessPropertiesRuleTest.php | 21 ++++++ ...AccessStaticPropertiesInAssignRuleTest.php | 2 + .../AccessStaticPropertiesRuleTest.php | 16 +++++ .../Rules/Properties/data/bug-15109.php | 21 ++++++ 21 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 src/Rules/BuiltinClassMemberAvailabilityHelper.php create mode 100644 tests/PHPStan/Rules/Classes/data/bug-15109.php create mode 100644 tests/PHPStan/Rules/Methods/data/bug-15109.php create mode 100644 tests/PHPStan/Rules/Properties/data/bug-15109.php diff --git a/src/Rules/BuiltinClassMemberAvailabilityHelper.php b/src/Rules/BuiltinClassMemberAvailabilityHelper.php new file mode 100644 index 00000000000..c17d3970098 --- /dev/null +++ b/src/Rules/BuiltinClassMemberAvailabilityHelper.php @@ -0,0 +1,67 @@ +getObjectClassNames(); + if (count($classNames) === 0) { + return false; + } + + foreach ($classNames as $className) { + if (!$this->reflectionProvider->hasClass($className)) { + return false; + } + + if (!$this->hasBuiltinClassInHierarchy($this->reflectionProvider->getClass($className))) { + return false; + } + } + + return $scope->getPhpVersion()->getType()->isSuperTypeOf( + new ConstantIntegerType($this->phpVersion->getVersionId()), + )->no(); + } + + private function hasBuiltinClassInHierarchy(ClassReflection $classReflection): bool + { + if ($classReflection->isBuiltin()) { + return true; + } + + foreach ($classReflection->getParents() as $parentReflection) { + if ($parentReflection->isBuiltin()) { + return true; + } + } + + return false; + } + +} diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index ba307f16227..35c1e993f9e 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -11,6 +11,7 @@ use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\ClassNameUsageLocation; @@ -40,6 +41,7 @@ final class ClassConstantRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, + private BuiltinClassMemberAvailabilityHelper $builtinClassMemberAvailabilityHelper, private ClassNameCheck $classCheck, private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck, ) @@ -239,6 +241,10 @@ private function processSingleClassConstFetch(Scope $scope, ClassConstFetch $nod } if (!$classType->hasConstant($constantName)->yes()) { + if ($this->builtinClassMemberAvailabilityHelper->mightBeAvailableInScopePhpVersion($scope, $classType)) { + return $messages; + } + return array_merge($messages, [ RuleErrorBuilder::message(sprintf( 'Access to undefined constant %s::%s.', diff --git a/src/Rules/Methods/MethodCallCheck.php b/src/Rules/Methods/MethodCallCheck.php index 0a5c11b454e..bd400411b10 100644 --- a/src/Rules/Methods/MethodCallCheck.php +++ b/src/Rules/Methods/MethodCallCheck.php @@ -13,6 +13,7 @@ use PHPStan\Internal\SprintfHelper; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -31,6 +32,7 @@ final class MethodCallCheck public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, + private BuiltinClassMemberAvailabilityHelper $builtinClassMemberAvailabilityHelper, #[AutowiredParameter] private bool $checkFunctionNameCase, #[AutowiredParameter] @@ -133,6 +135,10 @@ public function check( } } + if ($this->builtinClassMemberAvailabilityHelper->mightBeAvailableInScopePhpVersion($scope, $type)) { + return [[], null]; + } + return [ [ RuleErrorBuilder::message(sprintf( diff --git a/src/Rules/Methods/StaticMethodCallCheck.php b/src/Rules/Methods/StaticMethodCallCheck.php index aaabe3d1e79..317715250fd 100644 --- a/src/Rules/Methods/StaticMethodCallCheck.php +++ b/src/Rules/Methods/StaticMethodCallCheck.php @@ -16,6 +16,7 @@ use PHPStan\Reflection\Native\NativeMethodReflection; use PHPStan\Reflection\Php\PhpMethodReflection; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\ClassNameUsageLocation; @@ -43,6 +44,7 @@ final class StaticMethodCallCheck public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, + private BuiltinClassMemberAvailabilityHelper $builtinClassMemberAvailabilityHelper, private ClassNameCheck $classCheck, #[AutowiredParameter] private bool $checkFunctionNameCase, @@ -240,6 +242,10 @@ public function check( } } + if ($this->builtinClassMemberAvailabilityHelper->mightBeAvailableInScopePhpVersion($scope, $classType)) { + return [$errors, null]; + } + return [ array_merge($errors, [ RuleErrorBuilder::message(sprintf( diff --git a/src/Rules/Properties/AccessPropertiesCheck.php b/src/Rules/Properties/AccessPropertiesCheck.php index c6bd1e3e5fa..2fa01f34aad 100644 --- a/src/Rules/Properties/AccessPropertiesCheck.php +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -17,6 +17,7 @@ use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\MissingPropertyFromReflectionException; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\RuleErrorBuilder; @@ -39,6 +40,7 @@ final class AccessPropertiesCheck public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, + private BuiltinClassMemberAvailabilityHelper $builtinClassMemberAvailabilityHelper, private PhpVersion $phpVersion, private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck, #[AutowiredParameter] @@ -212,6 +214,10 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string ]; } + if ($this->builtinClassMemberAvailabilityHelper->mightBeAvailableInScopePhpVersion($scope, $type)) { + return []; + } + $ruleErrorBuilder = RuleErrorBuilder::message(sprintf( 'Access to an undefined property %s::$%s.', $typeForDescribe->describe(VerbosityLevel::typeOnly()), diff --git a/src/Rules/Properties/AccessStaticPropertiesCheck.php b/src/Rules/Properties/AccessStaticPropertiesCheck.php index 92742621326..c6bd4f47b2d 100644 --- a/src/Rules/Properties/AccessStaticPropertiesCheck.php +++ b/src/Rules/Properties/AccessStaticPropertiesCheck.php @@ -18,6 +18,7 @@ use PHPStan\Internal\SprintfHelper; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\ClassNameUsageLocation; @@ -47,6 +48,7 @@ final class AccessStaticPropertiesCheck public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, + private BuiltinClassMemberAvailabilityHelper $builtinClassMemberAvailabilityHelper, private ClassNameCheck $classCheck, private PhpVersion $phpVersion, private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck, @@ -287,6 +289,10 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node, return []; } + if ($this->builtinClassMemberAvailabilityHelper->mightBeAvailableInScopePhpVersion($scope, $classType)) { + return $messages; + } + return array_merge($messages, [ RuleErrorBuilder::message(sprintf( 'Access to an undefined static property %s::$%s.', diff --git a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php index 5b50b8c9676..bb29e0fd6a1 100644 --- a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php +++ b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\Methods\CallMethodsRule; use PHPStan\Rules\Methods\MethodCallCheck; @@ -37,6 +38,7 @@ protected function getRule(): Rule new MethodCallCheck( $reflectionProvider, $ruleLevelHelper, + self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class), checkFunctionNameCase: true, reportMagicMethods: true, ), diff --git a/tests/PHPStan/Rules/Classes/ClassConstantPhp74RuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantPhp74RuleTest.php index 0f3a9421f46..82b3c6564e3 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantPhp74RuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantPhp74RuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Classes; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -35,6 +36,7 @@ protected function getRule(): Rule return new ClassConstantRule( $reflectionProvider, $ruleLevelHelper, + self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: true, checkImportedClassNameCase: true), new ClassForbiddenNameCheck($container->getExtensionsCollection(ForbiddenClassNameExtension::class)), diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index e6e6d1837bf..aa5e1f850de 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Classes; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -40,6 +41,7 @@ protected function getRule(): Rule return new ClassConstantRule( $reflectionProvider, $ruleLevelHelper, + self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: true, checkImportedClassNameCase: $this->checkImportedClassNameCase), new ClassForbiddenNameCheck($container->getExtensionsCollection(ForbiddenClassNameExtension::class)), @@ -636,4 +638,18 @@ public function testClosureAttributeArguments(): void ]); } + public function testBug15109(): void + { + $this->analyse([__DIR__ . '/data/bug-15109.php'], [ + [ + 'Access to undefined constant ReflectionProperty::FUTURE_CONSTANT.', + 12, + ], + [ + 'Access to undefined constant Bug15109ClassConstant\\Foo::DOES_NOT_EXIST.', + 16, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/bug-15109.php b/tests/PHPStan/Rules/Classes/data/bug-15109.php new file mode 100644 index 00000000000..73b36aa013a --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/bug-15109.php @@ -0,0 +1,18 @@ += 90000) { + echo \ReflectionProperty::FUTURE_CONSTANT; + echo Foo::DOES_NOT_EXIST; + } +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 62761326049..7e85fcfd483 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Methods; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\NullsafeCheck; @@ -48,6 +49,7 @@ protected function getRule(): Rule new MethodCallCheck( $reflectionProvider, $ruleLevelHelper, + self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class), checkFunctionNameCase: true, reportMagicMethods: true, ), @@ -4650,4 +4652,25 @@ public function testDeadCatch(): void ]); } + public function testBug15109(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-15109.php'], [ + [ + 'Call to an undefined method ReflectionMethod::futureMethod().', + 26, + ], + [ + 'Call to an undefined method Bug15109\\Foo::doesNotExist().', + 34, + ], + [ + 'Call to an undefined method Bug15109\\Foo::doesNotExist().', + 41, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index f499119a6f0..950c59516bf 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Methods; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -53,6 +54,7 @@ protected function getRule(): Rule new StaticMethodCallCheck( $reflectionProvider, $ruleLevelHelper, + self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class), new ClassNameCheck( new ClassCaseSensitivityCheck( $reflectionProvider, @@ -1158,4 +1160,23 @@ public function testBug11421(): void ]); } + public function testBug15109(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-15109.php'], [ + [ + 'Call to an undefined static method ReflectionMethod::futureStaticMethod().', + 25, + ], + [ + 'Call to an undefined static method Bug15109\\Foo::doesNotExist().', + 33, + ], + [ + 'Call to an undefined static method Bug15109\\Foo::doesNotExist().', + 40, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php b/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php index d69008e793c..f1e87576dac 100644 --- a/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Reflection\Php\PhpClassReflectionExtension; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\NullsafeCheck; @@ -33,7 +34,7 @@ protected function getRule(): Rule // @phpstan-ignore argument.type return new CompositeRule([ new CallMethodsRule( - new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), + new MethodCallCheck($reflectionProvider, $ruleLevelHelper, self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class), true, true), new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), $reflectionProvider, self::getContainer()->getByType(ParameterClosureTypeHelper::class), true, true, true, true), new NonStringableDynamicAccessCheck($ruleLevelHelper, true), ), diff --git a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php index 1eae1fccc44..4fdb477d970 100644 --- a/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodCallableRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Methods; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -32,6 +33,7 @@ protected function getRule(): Rule new MethodCallCheck( $reflectionProvider, $ruleLevelHelper, + self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class), checkFunctionNameCase: true, reportMagicMethods: true, ), diff --git a/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php index 6576eecfdd2..bfb7f85f05a 100644 --- a/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php +++ b/tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Methods; use PHPStan\Classes\ForbiddenClassNameExtension; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -38,6 +39,7 @@ protected function getRule(): Rule new StaticMethodCallCheck( $reflectionProvider, $ruleLevelHelper, + self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class), new ClassNameCheck( new ClassCaseSensitivityCheck( $reflectionProvider, diff --git a/tests/PHPStan/Rules/Methods/data/bug-15109.php b/tests/PHPStan/Rules/Methods/data/bug-15109.php new file mode 100644 index 00000000000..047259e7848 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-15109.php @@ -0,0 +1,43 @@ += 80400) { + $ref = \ReflectionMethod::createFromMethodName($func); + } else { + $ref = new \ReflectionMethod($func); + } +} + +class Foo +{ + +} + +class MyReflectionMethod extends \ReflectionMethod +{ + +} + +function doBar(\ReflectionMethod $m, Foo $foo, MyReflectionMethod $myM): void +{ + \ReflectionMethod::futureStaticMethod(); + $m->futureMethod(); + + if (PHP_VERSION_ID >= 90000) { + \ReflectionMethod::futureStaticMethod(); + MyReflectionMethod::futureStaticMethod(); + $m->futureMethod(); + $myM->futureMethod(); + Foo::doesNotExist(); + $foo->doesNotExist(); + } + + if (PHP_VERSION_ID < 50000) { + \ReflectionMethod::pastStaticMethod(); + $m->pastMethod(); + Foo::doesNotExist(); + $foo->doesNotExist(); + } +} diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php index 280ecfd4406..0687107b417 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Properties; use PHPStan\Php\PhpVersion; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; @@ -33,6 +34,7 @@ protected function getRule(): Rule new AccessPropertiesCheck( $reflectionProvider, $ruleLevelHelper, + self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class), new PhpVersion(PHP_VERSION_ID), new NonStringableDynamicAccessCheck($ruleLevelHelper, true), reportMagicProperties: true, diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index 11329e9f8b5..e6d1df62461 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Properties; use PHPStan\Php\PhpVersion; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; @@ -41,6 +42,7 @@ protected function getRule(): Rule new AccessPropertiesCheck( $reflectionProvider, $ruleLevelHelper, + self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class), new PhpVersion(PHP_VERSION_ID), new NonStringableDynamicAccessCheck($ruleLevelHelper, true), reportMagicProperties: true, @@ -1321,4 +1323,23 @@ public function testBug15002(): void $this->analyse([__DIR__ . '/data/bug-15002.php'], []); } + public function testBug15109(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-15109.php'], [ + [ + 'Access to an undefined property Exception::$futureProperty.', + 12, + 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', + ], + [ + 'Access to an undefined property Bug15109Properties\\Foo::$doesNotExist.', + 18, + 'Learn more: https://phpstan.org/blog/solving-phpstan-access-to-undefined-property', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php index e9e8421ef6c..b64980602d4 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Classes\ForbiddenClassNameExtension; use PHPStan\Php\PhpVersion; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -38,6 +39,7 @@ protected function getRule(): Rule new AccessStaticPropertiesCheck( $reflectionProvider, $ruleLevelHelper, + self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true, true), new ClassForbiddenNameCheck(self::getContainer()->getExtensionsCollection(ForbiddenClassNameExtension::class)), diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index b94650dcb48..c9b600b50dc 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Classes\ForbiddenClassNameExtension; use PHPStan\Php\PhpVersion; +use PHPStan\Rules\BuiltinClassMemberAvailabilityHelper; use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; @@ -38,6 +39,7 @@ protected function getRule(): Rule new AccessStaticPropertiesCheck( $reflectionProvider, $ruleLevelHelper, + self::getContainer()->getByType(BuiltinClassMemberAvailabilityHelper::class), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true, true), new ClassForbiddenNameCheck(self::getContainer()->getExtensionsCollection(ForbiddenClassNameExtension::class)), @@ -396,4 +398,18 @@ public function testBug15002(): void $this->analyse([__DIR__ . '/../Methods/data/bug-15002.php'], []); } + public function testBug15109(): void + { + $this->analyse([__DIR__ . '/data/bug-15109.php'], [ + [ + 'Access to an undefined static property Exception::$futureStaticProperty.', + 13, + ], + [ + 'Access to an undefined static property Bug15109Properties\\Foo::$doesNotExist.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-15109.php b/tests/PHPStan/Rules/Properties/data/bug-15109.php new file mode 100644 index 00000000000..ea7431f2e74 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-15109.php @@ -0,0 +1,21 @@ +futureProperty; + echo \Exception::$futureStaticProperty; + + if (PHP_VERSION_ID >= 90000) { + echo $exception->futureProperty; + echo \Exception::$futureStaticProperty; + echo $foo->doesNotExist; + echo Foo::$doesNotExist; + } +}