From 71071d40a49495cf2d33b63db57701cf9345c9d5 Mon Sep 17 00:00:00 2001 From: Liam Duckett Date: Wed, 23 Jul 2025 19:55:13 +0100 Subject: [PATCH 01/10] add failing test --- tests/PHPStan/Analyser/nsrt/bug-13304.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13304.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-13304.php b/tests/PHPStan/Analyser/nsrt/bug-13304.php new file mode 100644 index 0000000000..d3f5e236d2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13304.php @@ -0,0 +1,16 @@ + Date: Wed, 23 Jul 2025 20:11:08 +0100 Subject: [PATCH 02/10] first stab at allowing constant string unions in narrowing calls to property exists --- .../PropertyExistsTypeSpecifyingExtension.php | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index e62a04d409..1257314781 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -55,8 +55,9 @@ public function specifyTypes( TypeSpecifierContext $context, ): SpecifiedTypes { - $propertyNameType = $scope->getType($node->getArgs()[1]->value); - if (!$propertyNameType instanceof ConstantStringType) { + $propertyNameTypes = $scope->getType($node->getArgs()[1]->value)->getConstantStrings(); + + if ($propertyNameTypes === []) { return $this->typeSpecifier->create( new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()), new ConstantBooleanType(true), @@ -65,26 +66,33 @@ public function specifyTypes( ); } - if ($propertyNameType->getValue() === '') { - return new SpecifiedTypes([], []); + $hasPropertyTypes = []; + foreach ($propertyNameTypes as $propertyNameType) { + $hasPropertyTypes[] = new HasPropertyType($propertyNameType->getValue()); } $objectType = $scope->getType($node->getArgs()[0]->value); if ($objectType instanceof ConstantStringType) { return new SpecifiedTypes([], []); } elseif ($objectType->isObject()->yes()) { - $propertyNode = new PropertyFetch( - $node->getArgs()[0]->value, - new Identifier($propertyNameType->getValue()), - ); + $propertyNodes = []; + + foreach($propertyNameTypes as $propertyNameType) { + $propertyNodes[] = new PropertyFetch( + $node->getArgs()[0]->value, + new Identifier($propertyNameType->getValue()), + ); + } } else { return new SpecifiedTypes([], []); } - $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyNode, $scope); - if ($propertyReflection !== null) { - if (!$propertyReflection->isNative()) { - return new SpecifiedTypes([], []); + foreach($propertyNodes as $propertyNode) { + $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyNode, $scope); + if ($propertyReflection !== null) { + if (!$propertyReflection->isNative()) { + return new SpecifiedTypes([], []); + } } } @@ -92,7 +100,7 @@ public function specifyTypes( $node->getArgs()[0]->value, new IntersectionType([ new ObjectWithoutClassType(), - new HasPropertyType($propertyNameType->getValue()), + ...$hasPropertyTypes, ]), $context, $scope, From 648700218a5c32161987af914c328496cd1bfc0d Mon Sep 17 00:00:00 2001 From: Liam Duckett Date: Wed, 23 Jul 2025 20:23:44 +0100 Subject: [PATCH 03/10] add new case - appear to have reintroduced 12778 --- tests/PHPStan/Analyser/nsrt/bug-13304.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13304.php b/tests/PHPStan/Analyser/nsrt/bug-13304.php index d3f5e236d2..092e2174bb 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13304.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13304.php @@ -4,13 +4,22 @@ use function PHPStan\Testing\assertType; -function foo(object $bar): void +function foo(object $foo): void { foreach (['qux', 'quux'] as $property) { - if (!property_exists($bar, $property)) { + if (!property_exists($foo, $property)) { throw new \Exception; } - assertType("object&hasProperty(quux)&hasProperty(qux)", $bar); + assertType("object&hasProperty(quux)&hasProperty(qux)", $foo); } } + +function bar(object $bar): void +{ + if (!property_exists($bar, '')) { + throw new \Exception; + } + + assertType("object", $bar); +} From a7746f558624f0ca8259481e4bbfe83fb1759b1a Mon Sep 17 00:00:00 2001 From: Liam Duckett Date: Wed, 23 Jul 2025 20:26:18 +0100 Subject: [PATCH 04/10] resolve reintroduced variant of 12778 --- src/Type/Php/PropertyExistsTypeSpecifyingExtension.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index 1257314781..892d9b86eb 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -68,6 +68,10 @@ public function specifyTypes( $hasPropertyTypes = []; foreach ($propertyNameTypes as $propertyNameType) { + if($propertyNameType->getValue() === '') { + return new SpecifiedTypes([], []); + } + $hasPropertyTypes[] = new HasPropertyType($propertyNameType->getValue()); } From ecff9a12e36eb89fe8e256f64dbb975a734a3801 Mon Sep 17 00:00:00 2001 From: Liam Duckett Date: Wed, 23 Jul 2025 20:32:01 +0100 Subject: [PATCH 05/10] change namespace for test --- tests/PHPStan/Analyser/nsrt/bug-13304.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13304.php b/tests/PHPStan/Analyser/nsrt/bug-13304.php index 092e2174bb..cb5a1d198e 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13304.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13304.php @@ -1,6 +1,6 @@ Date: Thu, 24 Jul 2025 18:22:15 +0100 Subject: [PATCH 06/10] move empty string check, to satisfy static analysis --- src/Type/Php/PropertyExistsTypeSpecifyingExtension.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index 892d9b86eb..d2302b6eb6 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -68,10 +68,6 @@ public function specifyTypes( $hasPropertyTypes = []; foreach ($propertyNameTypes as $propertyNameType) { - if($propertyNameType->getValue() === '') { - return new SpecifiedTypes([], []); - } - $hasPropertyTypes[] = new HasPropertyType($propertyNameType->getValue()); } @@ -82,6 +78,10 @@ public function specifyTypes( $propertyNodes = []; foreach($propertyNameTypes as $propertyNameType) { + if($propertyNameType->getValue() === '') { + return new SpecifiedTypes([], []); + } + $propertyNodes[] = new PropertyFetch( $node->getArgs()[0]->value, new Identifier($propertyNameType->getValue()), From e69e5f379ad6bf82f031c09e474ae3f45ca25368 Mon Sep 17 00:00:00 2001 From: Liam Duckett Date: Thu, 24 Jul 2025 18:22:41 +0100 Subject: [PATCH 07/10] update baseline --- phpstan-baseline.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 47101ea9fe..67bc4a6d8b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1650,7 +1650,7 @@ parameters: - message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' identifier: phpstanApi.instanceofType - count: 2 + count: 1 path: src/Type/Php/PropertyExistsTypeSpecifyingExtension.php - From 29a73c11f08f8a7d00c0ff9e19f58ff4b43886d3 Mon Sep 17 00:00:00 2001 From: Liam Duckett Date: Thu, 24 Jul 2025 18:33:19 +0100 Subject: [PATCH 08/10] lint --- .../PropertyExistsTypeSpecifyingExtension.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index d2302b6eb6..e93881aa19 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -77,8 +77,8 @@ public function specifyTypes( } elseif ($objectType->isObject()->yes()) { $propertyNodes = []; - foreach($propertyNameTypes as $propertyNameType) { - if($propertyNameType->getValue() === '') { + foreach ($propertyNameTypes as $propertyNameType) { + if ($propertyNameType->getValue() === '') { return new SpecifiedTypes([], []); } @@ -91,12 +91,14 @@ public function specifyTypes( return new SpecifiedTypes([], []); } - foreach($propertyNodes as $propertyNode) { + foreach ($propertyNodes as $propertyNode) { $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyNode, $scope); - if ($propertyReflection !== null) { - if (!$propertyReflection->isNative()) { - return new SpecifiedTypes([], []); - } + if ($propertyReflection === null) { + continue; + } + + if (!$propertyReflection->isNative()) { + return new SpecifiedTypes([], []); } } From 1f40e1863f0e3f53e02f2ef7969553bf6fb24eb9 Mon Sep 17 00:00:00 2001 From: Liam Duckett Date: Thu, 24 Jul 2025 18:33:42 +0100 Subject: [PATCH 09/10] remove second test, as covered by static analysis --- tests/PHPStan/Analyser/nsrt/bug-13304.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13304.php b/tests/PHPStan/Analyser/nsrt/bug-13304.php index cb5a1d198e..0c7f2163b1 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13304.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13304.php @@ -14,12 +14,3 @@ function foo(object $foo): void assertType("object&hasProperty(quux)&hasProperty(qux)", $foo); } } - -function bar(object $bar): void -{ - if (!property_exists($bar, '')) { - throw new \Exception; - } - - assertType("object", $bar); -} From 40ba34094b41ba06d6cc7dbb357705c73018b179 Mon Sep 17 00:00:00 2001 From: Liam Duckett Date: Thu, 24 Jul 2025 20:48:13 +0100 Subject: [PATCH 10/10] remove redundant check for object being a constant string --- phpstan-baseline.neon | 6 ---- .../PropertyExistsTypeSpecifyingExtension.php | 30 +++++++++---------- 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 67bc4a6d8b..527ed0c002 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1647,12 +1647,6 @@ parameters: count: 1 path: src/Type/Php/NumberFormatFunctionDynamicReturnTypeExtension.php - - - message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' - identifier: phpstanApi.instanceofType - count: 1 - path: src/Type/Php/PropertyExistsTypeSpecifyingExtension.php - - message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' identifier: phpstanApi.instanceofType diff --git a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php index e93881aa19..5b9c3ee4d2 100644 --- a/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php +++ b/src/Type/Php/PropertyExistsTypeSpecifyingExtension.php @@ -16,7 +16,6 @@ use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FunctionTypeSpecifyingExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\ObjectWithoutClassType; @@ -72,23 +71,22 @@ public function specifyTypes( } $objectType = $scope->getType($node->getArgs()[0]->value); - if ($objectType instanceof ConstantStringType) { + + if (!$objectType->isObject()->yes()) { return new SpecifiedTypes([], []); - } elseif ($objectType->isObject()->yes()) { - $propertyNodes = []; - - foreach ($propertyNameTypes as $propertyNameType) { - if ($propertyNameType->getValue() === '') { - return new SpecifiedTypes([], []); - } - - $propertyNodes[] = new PropertyFetch( - $node->getArgs()[0]->value, - new Identifier($propertyNameType->getValue()), - ); + } + + $propertyNodes = []; + + foreach ($propertyNameTypes as $propertyNameType) { + if ($propertyNameType->getValue() === '') { + return new SpecifiedTypes([], []); } - } else { - return new SpecifiedTypes([], []); + + $propertyNodes[] = new PropertyFetch( + $node->getArgs()[0]->value, + new Identifier($propertyNameType->getValue()), + ); } foreach ($propertyNodes as $propertyNode) {