From f407edc084556064934d66ea03cc5c9e2f119685 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Thu, 21 May 2026 08:44:35 +0000 Subject: [PATCH 01/12] Suppress `property.nonObject` and `method.nonObject` errors when `property_exists()`/`method_exists()` guard is present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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" --- src/Rules/Methods/MethodCallCheck.php | 26 ++++++-- .../Properties/AccessPropertiesCheck.php | 23 ++++++-- .../MethodExistsTypeSpecifyingExtension.php | 2 +- .../PropertyExistsTypeSpecifyingExtension.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-14667.php | 20 +++++++ .../Rules/Methods/CallMethodsRuleTest.php | 12 ++-- .../PHPStan/Rules/Methods/data/bug-14667.php | 41 +++++++++++++ .../AccessPropertiesInAssignRuleTest.php | 5 ++ .../Properties/AccessPropertiesRuleTest.php | 8 +++ .../Properties/data/bug-14667-assign.php | 26 ++++++++ .../Rules/Properties/data/bug-14667.php | 59 +++++++++++++++++++ 11 files changed, 209 insertions(+), 15 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14667.php create mode 100644 tests/PHPStan/Rules/Methods/data/bug-14667.php create mode 100644 tests/PHPStan/Rules/Properties/data/bug-14667-assign.php create mode 100644 tests/PHPStan/Rules/Properties/data/bug-14667.php diff --git a/src/Rules/Methods/MethodCallCheck.php b/src/Rules/Methods/MethodCallCheck.php index 0a5c11b454e..cc1a1b79cff 100644 --- a/src/Rules/Methods/MethodCallCheck.php +++ b/src/Rules/Methods/MethodCallCheck.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr; use PhpParser\Node\Identifier; use PhpParser\Node\Name\FullyQualified; +use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; @@ -65,6 +66,18 @@ public function check( if ($type instanceof StaticType) { $typeForDescribe = $type->getStaticObjectType(); } + $methodExistsCall = new Expr\FuncCall(new FullyQualified('method_exists'), [ + new Arg($var), + new Arg(new String_($methodName)), + ]); + if ($scope->getType($methodExistsCall)->isTrue()->yes()) { + if ($type->hasMethod($methodName)->yes()) { + return [[], $type->getMethod($methodName, $scope)]; + } + + return [[], null]; + } + if (!$type->canCallMethods()->yes() || $type->isClassString()->yes()) { return [ [ @@ -122,15 +135,20 @@ public function check( } } - if ($astName instanceof Expr) { + if ($astName instanceof Identifier) { + $methodExistsExpr = new Expr\FuncCall(new FullyQualified('method_exists'), [ + new Arg($var), + new Arg(new String_($methodName)), + ]); + } else { $methodExistsExpr = new Expr\FuncCall(new FullyQualified('method_exists'), [ new Arg($var), new Arg($astName), ]); + } - if ($scope->getType($methodExistsExpr)->isTrue()->yes()) { - return [[], null]; - } + if ($scope->getType($methodExistsExpr)->isTrue()->yes()) { + return [[], null]; } return [ diff --git a/src/Rules/Properties/AccessPropertiesCheck.php b/src/Rules/Properties/AccessPropertiesCheck.php index ce6717addfc..857bf521c43 100644 --- a/src/Rules/Properties/AccessPropertiesCheck.php +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -3,11 +3,11 @@ namespace PHPStan\Rules\Properties; use PhpParser\Node\Arg; -use PhpParser\Node\Expr; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Identifier; use PhpParser\Node\Name\FullyQualified; +use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredParameter; @@ -119,6 +119,14 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string $typeForDescribe = $type->getStaticObjectType(); } + $propertyExistsCall = new FuncCall(new FullyQualified('property_exists'), [ + new Arg($node->var), + new Arg(new String_($name)), + ]); + if ($scope->getType($propertyExistsCall)->isTrue()->yes()) { + return []; + } + if ($type->canAccessProperties()->no() || $type->canAccessProperties()->maybe() && !$scope->isUndefinedExpressionAllowed($node)) { return [ RuleErrorBuilder::message(sprintf( @@ -202,15 +210,20 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string } } - if ($node->name instanceof Expr) { + if ($node->name instanceof Identifier) { + $propertyExistsExpr = new FuncCall(new FullyQualified('property_exists'), [ + new Arg($node->var), + new Arg(new String_($name)), + ]); + } else { $propertyExistsExpr = new FuncCall(new FullyQualified('property_exists'), [ new Arg($node->var), new Arg($node->name), ]); + } - if ($scope->getType($propertyExistsExpr)->isTrue()->yes()) { - return []; - } + if ($scope->getType($propertyExistsExpr)->isTrue()->yes()) { + return []; } if ($hasStatic->yes()) { diff --git a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php index 0422497f373..15db9aad87c 100644 --- a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php @@ -96,7 +96,7 @@ public function specifyTypes( ]), $context, $scope, - ); + )->unionWith($this->createFuncCallSpec($node, $context, $scope)); } private function createFuncCallSpec(FuncCall $node, TypeSpecifierContext $context, Scope $scope): SpecifiedTypes diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index 26807506622..0f574056eec 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -89,7 +89,7 @@ public function specifyTypes( ]), $context, $scope, - ); + )->unionWith($this->createFuncCallSpec($node, $context, $scope)); } $propertyNode = new PropertyFetch( diff --git a/tests/PHPStan/Analyser/nsrt/bug-14667.php b/tests/PHPStan/Analyser/nsrt/bug-14667.php new file mode 100644 index 00000000000..fada67f9775 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14667.php @@ -0,0 +1,20 @@ +checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/bug-14667.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-14667.php b/tests/PHPStan/Rules/Methods/data/bug-14667.php new file mode 100644 index 00000000000..40645b2443c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-14667.php @@ -0,0 +1,41 @@ +foo(); + } +} + +function testExplicitMixed(mixed $row): void +{ + if (method_exists($row, 'foo')) { + $row->foo(); + } +} + +/** @param object|string $row */ +function testObjectOrString($row): void +{ + if (method_exists($row, 'foo')) { + $row->foo(); + } +} + +function testObject(object $row): void +{ + if (method_exists($row, 'bar')) { + $row->bar(); + } +} + +/** @param mixed $x */ +function testMixedChained($x): void +{ + if (method_exists($x, 'getName') && $x->getName() !== null) { + echo $x->getName(); + } +} diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php index b07e632d170..ee87a59bb9d 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php @@ -258,4 +258,9 @@ public function testBug2861(): void $this->analyse([__DIR__ . '/data/bug-2861-assign.php'], []); } + public function testBug14667(): void + { + $this->analyse([__DIR__ . '/data/bug-14667-assign.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index 0a6c1240901..ed6218bd71c 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -1302,4 +1302,12 @@ public function testBug2861(): void $this->analyse([__DIR__ . '/data/bug-2861.php'], []); } + public function testBug14667(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/bug-14667.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-14667-assign.php b/tests/PHPStan/Rules/Properties/data/bug-14667-assign.php new file mode 100644 index 00000000000..d039d6ca222 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-14667-assign.php @@ -0,0 +1,26 @@ +prop = 'value'; + } +} + +function testExplicitMixed(mixed $row): void +{ + if (property_exists($row, 'prop')) { + $row->prop = 'value'; + } +} + +/** @param object|string $row */ +function testObjectOrString($row): void +{ + if (property_exists($row, 'prop')) { + $row->prop = 'value'; + } +} diff --git a/tests/PHPStan/Rules/Properties/data/bug-14667.php b/tests/PHPStan/Rules/Properties/data/bug-14667.php new file mode 100644 index 00000000000..2cf16eae4b3 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-14667.php @@ -0,0 +1,59 @@ +prop; + } +} + +function testExplicitMixed(mixed $row): void +{ + if (property_exists($row, 'prop')) { + echo $row->prop; + } +} + +/** @param object|string $row */ +function testObjectOrString($row): void +{ + if (property_exists($row, 'prop')) { + echo $row->prop; + } +} + +function testObject(object $row): void +{ + if (property_exists($row, 'prop')) { + echo $row->prop; + } +} + +final class Foo +{ + public function testThis(): void + { + if (property_exists($this, 'default')) { + echo $this->default; + } + } + + /** @param self $obj */ + public function testSelf(self $obj): void + { + if (property_exists($obj, 'default')) { + echo $obj->default; + } + } + + /** @param mixed $x */ + public function testMixedChained($x): void + { + if (property_exists($x, 'name') && $x->name !== null) { + echo $x->name; + } + } +} From 92fedf04ab3b4bb6826cf3285a0fe374ac86388e Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 23 May 2026 06:21:12 +0000 Subject: [PATCH 02/12] Add `// lint >= 8.0` comment to bug-14667 test files using `mixed` type Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-14667.php | 2 +- tests/PHPStan/Rules/Methods/data/bug-14667.php | 2 +- tests/PHPStan/Rules/Properties/data/bug-14667-assign.php | 2 +- tests/PHPStan/Rules/Properties/data/bug-14667.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-14667.php b/tests/PHPStan/Analyser/nsrt/bug-14667.php index fada67f9775..bccc3170fc3 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14667.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14667.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug14667Nsrt; diff --git a/tests/PHPStan/Rules/Methods/data/bug-14667.php b/tests/PHPStan/Rules/Methods/data/bug-14667.php index 40645b2443c..a49e4776add 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-14667.php +++ b/tests/PHPStan/Rules/Methods/data/bug-14667.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug14667Methods; diff --git a/tests/PHPStan/Rules/Properties/data/bug-14667-assign.php b/tests/PHPStan/Rules/Properties/data/bug-14667-assign.php index d039d6ca222..e0455534186 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-14667-assign.php +++ b/tests/PHPStan/Rules/Properties/data/bug-14667-assign.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug14667Assign; diff --git a/tests/PHPStan/Rules/Properties/data/bug-14667.php b/tests/PHPStan/Rules/Properties/data/bug-14667.php index 2cf16eae4b3..7f13b16d37c 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-14667.php +++ b/tests/PHPStan/Rules/Properties/data/bug-14667.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug14667; From 83e8923e93e44d7e8ab0349689da742e8cde78d8 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 23 May 2026 06:42:59 +0000 Subject: [PATCH 03/12] Do not suppress property.nonObject error when type cannot access properties The synthetic `property_exists()` guard check must also verify that instance property access is valid for the type. For class-strings, `property_exists()` checks if the CLASS has the property, but instance property access (`$x->prop`) on a string is invalid PHP regardless of the guard. Added class-string test cases for both property and method access to document that `method.nonObject` and `property.nonObject` errors are correctly reported when the type is a class-string, even inside a `method_exists()`/`property_exists()` guard. Co-Authored-By: Claude Opus 4.6 --- src/Rules/Properties/AccessPropertiesCheck.php | 2 +- tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php | 7 ++++++- tests/PHPStan/Rules/Methods/data/bug-14667.php | 8 ++++++++ .../PHPStan/Rules/Properties/AccessPropertiesRuleTest.php | 7 ++++++- tests/PHPStan/Rules/Properties/data/bug-14667.php | 8 ++++++++ 5 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/Rules/Properties/AccessPropertiesCheck.php b/src/Rules/Properties/AccessPropertiesCheck.php index 857bf521c43..a334cc2ef77 100644 --- a/src/Rules/Properties/AccessPropertiesCheck.php +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -123,7 +123,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string new Arg($node->var), new Arg(new String_($name)), ]); - if ($scope->getType($propertyExistsCall)->isTrue()->yes()) { + if ($scope->getType($propertyExistsCall)->isTrue()->yes() && !$type->canAccessProperties()->no()) { return []; } diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 191745210e1..c046962890d 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -4107,7 +4107,12 @@ public function testBug14667(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/data/bug-14667.php'], []); + $this->analyse([__DIR__ . '/data/bug-14667.php'], [ + [ + 'Cannot call method foo() on class-string.', + 47, + ], + ]); } } diff --git a/tests/PHPStan/Rules/Methods/data/bug-14667.php b/tests/PHPStan/Rules/Methods/data/bug-14667.php index a49e4776add..ea02bd2c7dd 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-14667.php +++ b/tests/PHPStan/Rules/Methods/data/bug-14667.php @@ -39,3 +39,11 @@ function testMixedChained($x): void echo $x->getName(); } } + +/** @param class-string $row */ +function testClassString(string $row): void +{ + if (method_exists($row, 'foo')) { + $row->foo(); // error: Cannot call method foo() on class-string. + } +} diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index ed6218bd71c..3f7183a066a 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -1307,7 +1307,12 @@ public function testBug14667(): void $this->checkThisOnly = false; $this->checkUnionTypes = true; $this->checkDynamicProperties = true; - $this->analyse([__DIR__ . '/data/bug-14667.php'], []); + $this->analyse([__DIR__ . '/data/bug-14667.php'], [ + [ + 'Cannot access property $prop on class-string.', + 65, + ], + ]); } } diff --git a/tests/PHPStan/Rules/Properties/data/bug-14667.php b/tests/PHPStan/Rules/Properties/data/bug-14667.php index 7f13b16d37c..eb2d61d2291 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-14667.php +++ b/tests/PHPStan/Rules/Properties/data/bug-14667.php @@ -57,3 +57,11 @@ public function testMixedChained($x): void } } } + +/** @param class-string $row */ +function testClassString(string $row): void +{ + if (property_exists($row, 'prop')) { + echo $row->prop; // error: Cannot access property $prop on class-string. + } +} From 43d6936f7da20d291ace25982e6bead43a6f8272 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 23 May 2026 08:32:51 +0000 Subject: [PATCH 04/12] Merge assign test into main bug-14667 test file and add method_exists() nsrt tests Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-14667.php | 15 +++++++++++ .../AccessPropertiesInAssignRuleTest.php | 2 +- .../Properties/AccessPropertiesRuleTest.php | 2 +- .../Properties/data/bug-14667-assign.php | 26 ------------------- .../Rules/Properties/data/bug-14667.php | 23 ++++++++++++++++ 5 files changed, 40 insertions(+), 28 deletions(-) delete mode 100644 tests/PHPStan/Rules/Properties/data/bug-14667-assign.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-14667.php b/tests/PHPStan/Analyser/nsrt/bug-14667.php index bccc3170fc3..2852b201236 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14667.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14667.php @@ -18,3 +18,18 @@ function testExplicitMixed(mixed $row): void assertType('class-string|(object&hasProperty(prop))', $row); } } + +/** @param mixed $row */ +function testMethodExistsMixed($row): void +{ + if (method_exists($row, 'foo')) { + assertType('class-string|(object&hasMethod(foo))', $row); + } +} + +function testMethodExistsExplicitMixed(mixed $row): void +{ + if (method_exists($row, 'foo')) { + assertType('class-string|(object&hasMethod(foo))', $row); + } +} diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php index ee87a59bb9d..01aea7cc36d 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php @@ -260,7 +260,7 @@ public function testBug2861(): void public function testBug14667(): void { - $this->analyse([__DIR__ . '/data/bug-14667-assign.php'], []); + $this->analyse([__DIR__ . '/data/bug-14667.php'], []); } } diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index 3f7183a066a..17ec5b12dd1 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -1310,7 +1310,7 @@ public function testBug14667(): void $this->analyse([__DIR__ . '/data/bug-14667.php'], [ [ 'Cannot access property $prop on class-string.', - 65, + 88, ], ]); } diff --git a/tests/PHPStan/Rules/Properties/data/bug-14667-assign.php b/tests/PHPStan/Rules/Properties/data/bug-14667-assign.php deleted file mode 100644 index e0455534186..00000000000 --- a/tests/PHPStan/Rules/Properties/data/bug-14667-assign.php +++ /dev/null @@ -1,26 +0,0 @@ -= 8.0 - -namespace Bug14667Assign; - -/** @param mixed $row */ -function testImplicitMixed($row): void -{ - if (property_exists($row, 'prop')) { - $row->prop = 'value'; - } -} - -function testExplicitMixed(mixed $row): void -{ - if (property_exists($row, 'prop')) { - $row->prop = 'value'; - } -} - -/** @param object|string $row */ -function testObjectOrString($row): void -{ - if (property_exists($row, 'prop')) { - $row->prop = 'value'; - } -} diff --git a/tests/PHPStan/Rules/Properties/data/bug-14667.php b/tests/PHPStan/Rules/Properties/data/bug-14667.php index eb2d61d2291..4129d141fb0 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-14667.php +++ b/tests/PHPStan/Rules/Properties/data/bug-14667.php @@ -58,6 +58,29 @@ public function testMixedChained($x): void } } +/** @param mixed $row */ +function testImplicitMixedAssign($row): void +{ + if (property_exists($row, 'prop')) { + $row->prop = 'value'; + } +} + +function testExplicitMixedAssign(mixed $row): void +{ + if (property_exists($row, 'prop')) { + $row->prop = 'value'; + } +} + +/** @param object|string $row */ +function testObjectOrStringAssign($row): void +{ + if (property_exists($row, 'prop')) { + $row->prop = 'value'; + } +} + /** @param class-string $row */ function testClassString(string $row): void { From 701ce15e7888e3b376489bb782298f2e40c47e3d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 23 May 2026 10:46:14 +0200 Subject: [PATCH 05/12] remove now dead code --- src/Type/Php/PropertyExistsTypeSpecifyingExtension.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index 0f574056eec..09043b17d56 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -68,15 +68,6 @@ public function specifyTypes( } $objectType = $scope->getType($args[0]->value); - if ($objectType->isString()->yes()) { - return $this->typeSpecifier->create( - new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()), - new ConstantBooleanType(true), - $context, - $scope, - ); - } - if (!$objectType->isObject()->yes()) { return $this->typeSpecifier->create( $args[0]->value, From e9be37e5a29b8a2a05cfe78ed40007708340fa76 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 23 May 2026 08:57:35 +0000 Subject: [PATCH 06/12] Add explicit canAccessProperties/canCallMethods maybe tests for class-string|object Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php | 2 +- tests/PHPStan/Rules/Methods/data/bug-14667.php | 8 ++++++++ .../PHPStan/Rules/Properties/AccessPropertiesRuleTest.php | 2 +- tests/PHPStan/Rules/Properties/data/bug-14667.php | 8 ++++++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index c046962890d..02cc7422ad3 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -4110,7 +4110,7 @@ public function testBug14667(): void $this->analyse([__DIR__ . '/data/bug-14667.php'], [ [ 'Cannot call method foo() on class-string.', - 47, + 55, ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/bug-14667.php b/tests/PHPStan/Rules/Methods/data/bug-14667.php index ea02bd2c7dd..da374611c5f 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-14667.php +++ b/tests/PHPStan/Rules/Methods/data/bug-14667.php @@ -40,6 +40,14 @@ function testMixedChained($x): void } } +/** @param class-string|object $row */ +function testClassStringOrObject($row): void +{ + if (method_exists($row, 'foo')) { + $row->foo(); + } +} + /** @param class-string $row */ function testClassString(string $row): void { diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index 17ec5b12dd1..c9c1323d200 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -1310,7 +1310,7 @@ public function testBug14667(): void $this->analyse([__DIR__ . '/data/bug-14667.php'], [ [ 'Cannot access property $prop on class-string.', - 88, + 96, ], ]); } diff --git a/tests/PHPStan/Rules/Properties/data/bug-14667.php b/tests/PHPStan/Rules/Properties/data/bug-14667.php index 4129d141fb0..556a157a439 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-14667.php +++ b/tests/PHPStan/Rules/Properties/data/bug-14667.php @@ -81,6 +81,14 @@ function testObjectOrStringAssign($row): void } } +/** @param class-string|object $row */ +function testClassStringOrObject($row): void +{ + if (property_exists($row, 'prop')) { + echo $row->prop; + } +} + /** @param class-string $row */ function testClassString(string $row): void { From 0f7c35ef2ab2f62bef7436a34e5640cd4ec2f099 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 23 May 2026 09:11:36 +0000 Subject: [PATCH 07/12] Narrow class-string to class-string&hasMethod() in method_exists() guard When method_exists() narrows mixed to class-string|(object&hasMethod(foo)), the class-string member should also carry the hasMethod() accessory type since we know the method exists on whatever the value represents. This produces the more precise type (class-string&hasMethod(foo))|(object&hasMethod(foo)). Note: the same improvement is not possible for property_exists() because ClassStringType::hasInstanceProperty() returns No (class-strings cannot access instance properties), so class-string&hasProperty(prop) reduces to never via TypeCombinator::intersect(). Co-Authored-By: Claude Opus 4.6 --- src/Type/Php/MethodExistsTypeSpecifyingExtension.php | 5 ++++- tests/PHPStan/Analyser/nsrt/bug-14667.php | 4 ++-- tests/PHPStan/Analyser/nsrt/bug-4573.php | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php index 15db9aad87c..69e3cbd202a 100644 --- a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php @@ -92,7 +92,10 @@ public function specifyTypes( new ObjectWithoutClassType(), new HasMethodType($methodNameType->getValue()), ]), - new ClassStringType(), + new IntersectionType([ + new ClassStringType(), + new HasMethodType($methodNameType->getValue()), + ]), ]), $context, $scope, diff --git a/tests/PHPStan/Analyser/nsrt/bug-14667.php b/tests/PHPStan/Analyser/nsrt/bug-14667.php index 2852b201236..04d633c4589 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14667.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14667.php @@ -23,13 +23,13 @@ function testExplicitMixed(mixed $row): void function testMethodExistsMixed($row): void { if (method_exists($row, 'foo')) { - assertType('class-string|(object&hasMethod(foo))', $row); + assertType('(class-string&hasMethod(foo))|(object&hasMethod(foo))', $row); } } function testMethodExistsExplicitMixed(mixed $row): void { if (method_exists($row, 'foo')) { - assertType('class-string|(object&hasMethod(foo))', $row); + assertType('(class-string&hasMethod(foo))|(object&hasMethod(foo))', $row); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4573.php b/tests/PHPStan/Analyser/nsrt/bug-4573.php index 92fb321de56..b6b60122731 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4573.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4573.php @@ -23,7 +23,7 @@ class Foo public function doFoo($stringOrObject): void { if (is_callable([$stringOrObject, 'doFoo'])) { - assertType('Bug4573\Bar|class-string', $stringOrObject); + assertType('Bug4573\Bar|(class-string&hasMethod(doFoo))', $stringOrObject); } } @@ -33,7 +33,7 @@ public function doFoo($stringOrObject): void public function doBar($stringOrObject): void { if (method_exists($stringOrObject, 'doFoo')) { - assertType('Bug4573\Bar|class-string', $stringOrObject); + assertType('Bug4573\Bar|(class-string&hasMethod(doFoo))', $stringOrObject); } } From 4657cdf11eda7d2702a0992b78c7c55ea684cc64 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 23 May 2026 09:12:03 +0000 Subject: [PATCH 08/12] Merge method_exists rule test into nsrt bug-14667 test file Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-14667.php | 41 +++++++++++++ .../Rules/Methods/CallMethodsRuleTest.php | 4 +- .../PHPStan/Rules/Methods/data/bug-14667.php | 57 ------------------- 3 files changed, 43 insertions(+), 59 deletions(-) delete mode 100644 tests/PHPStan/Rules/Methods/data/bug-14667.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-14667.php b/tests/PHPStan/Analyser/nsrt/bug-14667.php index 04d633c4589..c24c5f89feb 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14667.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14667.php @@ -24,6 +24,7 @@ function testMethodExistsMixed($row): void { if (method_exists($row, 'foo')) { assertType('(class-string&hasMethod(foo))|(object&hasMethod(foo))', $row); + $row->foo(); } } @@ -31,5 +32,45 @@ function testMethodExistsExplicitMixed(mixed $row): void { if (method_exists($row, 'foo')) { assertType('(class-string&hasMethod(foo))|(object&hasMethod(foo))', $row); + $row->foo(); + } +} + +/** @param object|string $row */ +function testMethodExistsObjectOrString($row): void +{ + if (method_exists($row, 'foo')) { + $row->foo(); + } +} + +function testMethodExistsObject(object $row): void +{ + if (method_exists($row, 'bar')) { + $row->bar(); + } +} + +/** @param mixed $x */ +function testMethodExistsMixedChained($x): void +{ + if (method_exists($x, 'getName') && $x->getName() !== null) { + echo $x->getName(); + } +} + +/** @param class-string|object $row */ +function testMethodExistsClassStringOrObject($row): void +{ + if (method_exists($row, 'foo')) { + $row->foo(); + } +} + +/** @param class-string $row */ +function testMethodExistsClassString(string $row): void +{ + if (method_exists($row, 'foo')) { + $row->foo(); // error: Cannot call method foo() on class-string. } } diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 02cc7422ad3..add4de5990f 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -4107,10 +4107,10 @@ public function testBug14667(): void $this->checkThisOnly = false; $this->checkNullables = true; $this->checkUnionTypes = true; - $this->analyse([__DIR__ . '/data/bug-14667.php'], [ + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14667.php'], [ [ 'Cannot call method foo() on class-string.', - 55, + 74, ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/bug-14667.php b/tests/PHPStan/Rules/Methods/data/bug-14667.php deleted file mode 100644 index da374611c5f..00000000000 --- a/tests/PHPStan/Rules/Methods/data/bug-14667.php +++ /dev/null @@ -1,57 +0,0 @@ -= 8.0 - -namespace Bug14667Methods; - -/** @param mixed $row */ -function testImplicitMixed($row): void -{ - if (method_exists($row, 'foo')) { - $row->foo(); - } -} - -function testExplicitMixed(mixed $row): void -{ - if (method_exists($row, 'foo')) { - $row->foo(); - } -} - -/** @param object|string $row */ -function testObjectOrString($row): void -{ - if (method_exists($row, 'foo')) { - $row->foo(); - } -} - -function testObject(object $row): void -{ - if (method_exists($row, 'bar')) { - $row->bar(); - } -} - -/** @param mixed $x */ -function testMixedChained($x): void -{ - if (method_exists($x, 'getName') && $x->getName() !== null) { - echo $x->getName(); - } -} - -/** @param class-string|object $row */ -function testClassStringOrObject($row): void -{ - if (method_exists($row, 'foo')) { - $row->foo(); - } -} - -/** @param class-string $row */ -function testClassString(string $row): void -{ - if (method_exists($row, 'foo')) { - $row->foo(); // error: Cannot call method foo() on class-string. - } -} From 81cabd414729e705d4965affe38325ede45716ba Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 23 May 2026 11:16:37 +0200 Subject: [PATCH 09/12] improve variable names --- src/Type/Php/MethodExistsTypeSpecifyingExtension.php | 12 ++++++------ .../Php/PropertyExistsTypeSpecifyingExtension.php | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php index 69e3cbd202a..12a0cb9ee85 100644 --- a/src/Type/Php/MethodExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/MethodExistsTypeSpecifyingExtension.php @@ -56,10 +56,10 @@ public function specifyTypes( return $this->createFuncCallSpec($node, $context, $scope); } - $objectType = $scope->getType($args[0]->value); - if ($objectType->isString()->yes()) { - if ($objectType->isClassString()->yes()) { - foreach ($objectType->getClassStringObjectType()->getObjectClassReflections() as $classReflection) { + $objectOrStringType = $scope->getType($args[0]->value); + if ($objectOrStringType->isString()->yes()) { + if ($objectOrStringType->isClassString()->yes()) { + foreach ($objectOrStringType->getClassStringObjectType()->getObjectClassReflections() as $classReflection) { if ($classReflection->hasMethod($methodNameType->getValue()) && !$classReflection->hasNativeMethod($methodNameType->getValue())) { return $this->createFuncCallSpec($node, $context, $scope); } @@ -68,7 +68,7 @@ public function specifyTypes( return $this->typeSpecifier->create( $args[0]->value, new IntersectionType([ - $objectType, + $objectOrStringType, new HasMethodType($methodNameType->getValue()), ]), $context, @@ -79,7 +79,7 @@ public function specifyTypes( return new SpecifiedTypes([], []); } - foreach ($objectType->getObjectClassReflections() as $classReflection) { + foreach ($objectOrStringType->getObjectClassReflections() as $classReflection) { if ($classReflection->hasMethod($methodNameType->getValue()) && !$classReflection->hasNativeMethod($methodNameType->getValue())) { return $this->createFuncCallSpec($node, $context, $scope); } diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index 09043b17d56..a5ba636424c 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -67,8 +67,8 @@ public function specifyTypes( return new SpecifiedTypes([], []); } - $objectType = $scope->getType($args[0]->value); - if (!$objectType->isObject()->yes()) { + $objectOrStringType = $scope->getType($args[0]->value); + if (!$objectOrStringType->isObject()->yes()) { return $this->typeSpecifier->create( $args[0]->value, new UnionType([ From 2a943b9fc646e1e33f75cf5f300f3f6c25736114 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 23 May 2026 09:19:24 +0000 Subject: [PATCH 10/12] Add static property access and static method call tests for bug-14667 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Static property access (`$row::$prop`) after `property_exists()` guard produces no errors for any type — strings are stripped by AccessStaticPropertiesCheck before the canAccessProperties() check, and the property_exists() guard suppresses undefined property errors. Static method calls (`$row::foo()`) after `method_exists()` guard correctly report "Static call to instance method" for object types (mixed, object, object|string, class-string|object) since hasMethod() represents an instance method. Class-string-only case produces no error since static calls on class-strings are valid PHP. Co-Authored-By: Claude Opus 4.6 --- .../Methods/CallStaticMethodsRuleTest.php | 27 ++++++++++ .../Rules/Methods/data/bug-14667-static.php | 49 +++++++++++++++++++ .../AccessStaticPropertiesRuleTest.php | 5 ++ .../Properties/data/bug-14667-static.php | 49 +++++++++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-14667-static.php create mode 100644 tests/PHPStan/Rules/Properties/data/bug-14667-static.php diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 47a70c9d02a..e24fdb8799c 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -1033,4 +1033,31 @@ public function testBug14596(): void ]); } + public function testBug14667(): void + { + $this->checkThisOnly = false; + $this->analyse([__DIR__ . '/data/bug-14667-static.php'], [ + [ + 'Static call to instance method stdClass::foo().', + 9, + ], + [ + 'Static call to instance method stdClass::foo().', + 16, + ], + [ + 'Static call to instance method stdClass::foo().', + 24, + ], + [ + 'Static call to instance method stdClass::foo().', + 31, + ], + [ + 'Static call to instance method stdClass::foo().', + 39, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-14667-static.php b/tests/PHPStan/Rules/Methods/data/bug-14667-static.php new file mode 100644 index 00000000000..50ed44ec4aa --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-14667-static.php @@ -0,0 +1,49 @@ += 8.0 + +namespace Bug14667StaticMethods; + +/** @param mixed $row */ +function testStaticImplicitMixed($row): void +{ + if (method_exists($row, 'foo')) { + $row::foo(); + } +} + +function testStaticExplicitMixed(mixed $row): void +{ + if (method_exists($row, 'foo')) { + $row::foo(); + } +} + +/** @param object|string $row */ +function testStaticObjectOrString($row): void +{ + if (method_exists($row, 'foo')) { + $row::foo(); + } +} + +function testStaticObject(object $row): void +{ + if (method_exists($row, 'foo')) { + $row::foo(); + } +} + +/** @param class-string|object $row */ +function testStaticClassStringOrObject($row): void +{ + if (method_exists($row, 'foo')) { + $row::foo(); + } +} + +/** @param class-string $row */ +function testStaticClassString(string $row): void +{ + if (method_exists($row, 'foo')) { + $row::foo(); + } +} diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index a6e80a4dff5..71c7368147d 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -358,4 +358,9 @@ public function testBug2861(): void $this->analyse([__DIR__ . '/data/bug-2861.php'], []); } + public function testBug14667(): void + { + $this->analyse([__DIR__ . '/data/bug-14667-static.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-14667-static.php b/tests/PHPStan/Rules/Properties/data/bug-14667-static.php new file mode 100644 index 00000000000..8b6744b282c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-14667-static.php @@ -0,0 +1,49 @@ += 8.0 + +namespace Bug14667Static; + +/** @param mixed $row */ +function testStaticImplicitMixed($row): void +{ + if (property_exists($row, 'prop')) { + echo $row::$prop; + } +} + +function testStaticExplicitMixed(mixed $row): void +{ + if (property_exists($row, 'prop')) { + echo $row::$prop; + } +} + +/** @param object|string $row */ +function testStaticObjectOrString($row): void +{ + if (property_exists($row, 'prop')) { + echo $row::$prop; + } +} + +function testStaticObject(object $row): void +{ + if (property_exists($row, 'prop')) { + echo $row::$prop; + } +} + +/** @param class-string|object $row */ +function testStaticClassStringOrObject($row): void +{ + if (property_exists($row, 'prop')) { + echo $row::$prop; + } +} + +/** @param class-string $row */ +function testStaticClassString(string $row): void +{ + if (property_exists($row, 'prop')) { + echo $row::$prop; + } +} From 0cbc5fb3bd239ba9ec8605335e7ad23d3e49fa5a Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 23 May 2026 09:20:34 +0000 Subject: [PATCH 11/12] Narrow class-string to class-string&hasProperty/hasMethod in property_exists/method_exists guards When property_exists() or method_exists() narrows mixed to a union, the class-string member now also gets the HasPropertyType/HasMethodType accessory type, producing e.g. (class-string&hasProperty(prop))|(object&hasProperty(prop)) instead of class-string|(object&hasProperty(prop)). To make class-string&hasProperty work, StringType now overrides hasInstanceProperty() and hasStaticProperty() to return Maybe for class-strings (matching the existing hasMethod() pattern), and overrides getInstanceProperty()/getStaticProperty() to throw MissingPropertyFromReflectionException for class-strings. Co-Authored-By: Claude Opus 4.6 --- .../PropertyExistsTypeSpecifyingExtension.php | 5 ++- src/Type/StringType.php | 35 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-14667.php | 4 +-- tests/PHPStan/Analyser/nsrt/bug-2861.php | 4 +-- 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index a5ba636424c..d3bb527813a 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -76,7 +76,10 @@ public function specifyTypes( new ObjectWithoutClassType(), new HasPropertyType($propertyNameType->getValue()), ]), - new ClassStringType(), + new IntersectionType([ + new ClassStringType(), + new HasPropertyType($propertyNameType->getValue()), + ]), ]), $context, $scope, diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 8c5fc17f4a8..7474c6bfd5c 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -5,6 +5,9 @@ use PHPStan\Php\PhpVersion; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Reflection\ClassMemberAccessAnswerer; +use PHPStan\Reflection\ExtendedPropertyReflection; +use PHPStan\Reflection\MissingPropertyFromReflectionException; use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; @@ -291,6 +294,38 @@ public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType return new BooleanType(); } + public function hasInstanceProperty(string $propertyName): TrinaryLogic + { + if ($this->isClassString()->yes()) { + return TrinaryLogic::createMaybe(); + } + return TrinaryLogic::createNo(); + } + + public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + if ($this->isClassString()->yes()) { + throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName); + } + throw new ShouldNotHappenException(); + } + + public function hasStaticProperty(string $propertyName): TrinaryLogic + { + if ($this->isClassString()->yes()) { + return TrinaryLogic::createMaybe(); + } + return TrinaryLogic::createNo(); + } + + public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection + { + if ($this->isClassString()->yes()) { + throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName); + } + throw new ShouldNotHappenException(); + } + public function hasMethod(string $methodName): TrinaryLogic { if ($this->isClassString()->yes()) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-14667.php b/tests/PHPStan/Analyser/nsrt/bug-14667.php index c24c5f89feb..aa4cd62f13d 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14667.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14667.php @@ -8,14 +8,14 @@ function testMixed($row): void { if (property_exists($row, 'prop')) { - assertType('class-string|(object&hasProperty(prop))', $row); + assertType('(class-string&hasProperty(prop))|(object&hasProperty(prop))', $row); } } function testExplicitMixed(mixed $row): void { if (property_exists($row, 'prop')) { - assertType('class-string|(object&hasProperty(prop))', $row); + assertType('(class-string&hasProperty(prop))|(object&hasProperty(prop))', $row); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-2861.php b/tests/PHPStan/Analyser/nsrt/bug-2861.php index 8142e859f9f..ebb1440780d 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-2861.php +++ b/tests/PHPStan/Analyser/nsrt/bug-2861.php @@ -9,7 +9,7 @@ */ function testObjectOrString($objectOrClass): void { if (property_exists($objectOrClass, 'foo')) { - assertType('class-string|(object&hasProperty(foo))', $objectOrClass); + assertType('(class-string&hasProperty(foo))|(object&hasProperty(foo))', $objectOrClass); } } @@ -18,6 +18,6 @@ function testObjectOrString($objectOrClass): void { */ function testObjectOrClassString($objectOrClass): void { if (property_exists($objectOrClass, 'bar')) { - assertType('class-string|(object&hasProperty(bar))', $objectOrClass); + assertType('(class-string&hasProperty(bar))|(object&hasProperty(bar))', $objectOrClass); } } From 5f7bcd9f60fe0ccc9719c535c4150d3474e28e4a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 23 May 2026 11:55:45 +0200 Subject: [PATCH 12/12] mixed requires php 8.0 --- tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php | 1 + tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php | 1 + tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php | 1 + .../PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php | 2 ++ 4 files changed, 5 insertions(+) diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index add4de5990f..404ca6b378c 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -4102,6 +4102,7 @@ public function testBug14596(): void ]); } + #[RequiresPhp('>= 8.0.0')] public function testBug14667(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index e24fdb8799c..9df45d830fb 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -1033,6 +1033,7 @@ public function testBug14596(): void ]); } + #[RequiresPhp('>= 8.0.0')] public function testBug14667(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index c9c1323d200..8ed0a9ac853 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -1302,6 +1302,7 @@ public function testBug2861(): void $this->analyse([__DIR__ . '/data/bug-2861.php'], []); } + #[RequiresPhp('>= 8.0.0')] public function testBug14667(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index 71c7368147d..f79687d2cd3 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -9,6 +9,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\RequiresPhp; use const PHP_VERSION_ID; /** @@ -358,6 +359,7 @@ public function testBug2861(): void $this->analyse([__DIR__ . '/data/bug-2861.php'], []); } + #[RequiresPhp('>= 8.0.0')] public function testBug14667(): void { $this->analyse([__DIR__ . '/data/bug-14667-static.php'], []);