From 9867a23472ec1a25dbaba52cad0f9d55b5c82b80 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 2 Nov 2025 10:49:33 +0100 Subject: [PATCH 1/3] Improve inference on FuncCall === FuncCall --- src/Analyser/TypeSpecifier.php | 19 +++++++++ tests/PHPStan/Analyser/nsrt/bug-13749.php | 49 +++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13749.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 6698a8a800..194df41c44 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -2241,6 +2241,25 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif } public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $specifiedTypes = $this->resolveNormalizedIdentical($expr, $scope, $context); + + // merge result of fn1() === fn2() and fn2() === fn1() + $leftExpr = $expr->left; + $rightExpr = $expr->right; + if ($rightExpr instanceof FuncCall && $leftExpr instanceof FuncCall) { + return $specifiedTypes->unionWith( + $this->resolveNormalizedIdentical(new Expr\BinaryOp\Identical( + $rightExpr, + $leftExpr, + ), $scope, $context), + ); + } + + return $specifiedTypes; + } + + private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { // Normalize to: fn() === expr $leftExpr = $expr->left; diff --git a/tests/PHPStan/Analyser/nsrt/bug-13749.php b/tests/PHPStan/Analyser/nsrt/bug-13749.php new file mode 100644 index 0000000000..13979b1127 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13749.php @@ -0,0 +1,49 @@ += strlen($s)) { + assertType('string', $s); // could be non-empty-string + } + + if (strlen($s) === strlen($nonES)) { + assertType('non-empty-string', $s); + } + if (strlen($s) >= strlen($nonES)) { + assertType('non-empty-string', $s); + } + } + + /** + * @param non-falsy-string $nonFalsy + */ + public function sayNonFalsy(string $s, string $nonFalsy): void + { + if (strlen($nonFalsy) === strlen($s)) { + assertType('non-empty-string', $s); + } + if (strlen($nonFalsy) >= strlen($s)) { + assertType('string', $s); // could be non-empty-string + } + + if (strlen($s) === strlen($nonFalsy)) { + assertType('non-empty-string', $s); + } + if (strlen($s) >= strlen($nonFalsy)) { + assertType('non-empty-string', $s); + } + } + +} From d267c7312c7b24d74eb74024e60e0fb9a2049ec1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 2 Nov 2025 10:54:32 +0100 Subject: [PATCH 2/3] cleanup --- src/Analyser/TypeSpecifier.php | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 194df41c44..a05715c28e 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -2242,11 +2242,23 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - $specifiedTypes = $this->resolveNormalizedIdentical($expr, $scope, $context); - - // merge result of fn1() === fn2() and fn2() === fn1() $leftExpr = $expr->left; $rightExpr = $expr->right; + + // Normalize to: fn() === expr + if ($rightExpr instanceof FuncCall && !$leftExpr instanceof FuncCall) { + $specifiedTypes = $this->resolveNormalizedIdentical(new Expr\BinaryOp\Identical( + $rightExpr, + $leftExpr, + ), $scope, $context); + } else { + $specifiedTypes = $this->resolveNormalizedIdentical(new Expr\BinaryOp\Identical( + $leftExpr, + $rightExpr, + ), $scope, $context); + } + + // merge result of fn1() === fn2() and fn2() === fn1() if ($rightExpr instanceof FuncCall && $leftExpr instanceof FuncCall) { return $specifiedTypes->unionWith( $this->resolveNormalizedIdentical(new Expr\BinaryOp\Identical( @@ -2261,12 +2273,8 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - // Normalize to: fn() === expr $leftExpr = $expr->left; $rightExpr = $expr->right; - if ($rightExpr instanceof FuncCall && !$leftExpr instanceof FuncCall) { - [$leftExpr, $rightExpr] = [$rightExpr, $leftExpr]; - } $unwrappedLeftExpr = $leftExpr; if ($leftExpr instanceof AlwaysRememberedExpr) { From 48f5ada4cc34b6eeafbeea206a7729f8e57234ff Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 2 Nov 2025 11:00:41 +0100 Subject: [PATCH 3/3] more tests --- tests/PHPStan/Analyser/nsrt/bug-13749.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13749.php b/tests/PHPStan/Analyser/nsrt/bug-13749.php index 13979b1127..25344ba0dd 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13749.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13749.php @@ -46,4 +46,24 @@ public function sayNonFalsy(string $s, string $nonFalsy): void } } + /** + * @param non-empty-array $nonEmptyArr + */ + public function sayCount(array $arr, array $nonEmptyArr): void + { + if (count($arr) === count($nonEmptyArr)) { + assertType('non-empty-array', $arr); + assertType('non-empty-array', $nonEmptyArr); + } + assertType('array', $arr); + assertType('non-empty-array', $nonEmptyArr); + + if (count($nonEmptyArr) === count($arr)) { + assertType('non-empty-array', $arr); + assertType('non-empty-array', $nonEmptyArr); + } + assertType('array', $arr); + assertType('non-empty-array', $nonEmptyArr); + } + }