From 8c85a32b51ff19fc5d10df8e5bdcfd6387860daf Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 24 Oct 2025 07:49:08 +0200 Subject: [PATCH 01/10] array_key_exist inference is lost when adding a false condition --- src/Analyser/TypeSpecifier.php | 20 +++++++++++++- tests/PHPStan/Analyser/TypeSpecifierTest.php | 6 ++--- tests/PHPStan/Analyser/nsrt/bug-11276.php | 26 +++++++++++++++++++ ...nexistentOffsetInArrayDimFetchRuleTest.php | 6 +++++ tests/PHPStan/Rules/Arrays/data/bug-11276.php | 26 +++++++++++++++++++ 5 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11276.php create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-11276.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 4ea8a595f5..b1f8b09cba 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -663,7 +663,25 @@ public function specifyTypesInCondition( $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr); $rightScope = $scope->filterByFalseyValue($expr->left); $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr); - $types = $context->true() ? $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope)) : $leftTypes->unionWith($rightTypes); + + if ($context->true()) { + if ( + $scope->getType($expr->left)->isFalse()->yes() + || $scope->getType($expr->right)->isTrue()->yes() + ) { + $types = $rightTypes->normalize($rightScope); + } elseif ( + $scope->getType($expr->left)->isTrue()->yes() + || $scope->getType($expr->right)->isFalse()->yes() + ) { + $types = $leftTypes->normalize($scope); + } else { + $types = $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope)); + } + } else { + $types = $leftTypes->unionWith($rightTypes); + } + if ($context->true()) { return (new SpecifiedTypes( $types->getSureTypes(), diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index 9edca89ab9..dfcfae42dc 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -353,7 +353,7 @@ public static function dataCondition(): iterable self::createFunctionCall('is_int', 'foo'), self::createFunctionCall('is_string', 'bar'), ), - [], + ['$foo' => 'int'], ['$foo' => '~int', '$bar' => '~string'], ], [ @@ -652,7 +652,7 @@ public static function dataCondition(): iterable [ new Expr\Empty_(new Variable('array')), [ - '$array' => 'array{}|null', + '$array' => 'array{}', ], [ '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', @@ -664,7 +664,7 @@ public static function dataCondition(): iterable '$array' => '~0|0.0|\'\'|\'0\'|array{}|false|null', ], [ - '$array' => 'array{}|null', + '$array' => 'array{}', ], ], [ diff --git a/tests/PHPStan/Analyser/nsrt/bug-11276.php b/tests/PHPStan/Analyser/nsrt/bug-11276.php new file mode 100644 index 0000000000..a13419fb77 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11276.php @@ -0,0 +1,26 @@ +analyse([__DIR__ . '/data/bug-6209.php'], []); } + public function testBug11276(): void + { + $this->reportPossiblyNonexistentConstantArrayOffset = true; + $this->analyse([__DIR__ . '/data/bug-11276.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-11276.php b/tests/PHPStan/Rules/Arrays/data/bug-11276.php new file mode 100644 index 0000000000..3a69fbed7c --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-11276.php @@ -0,0 +1,26 @@ +[a-z]{2})-(?[a-z]{1}[a-z0-9]{1})\/#', $url, $matches); + + foreach ($expected as $key => $value) { + if ($matches instanceof ArrayAccess || \array_key_exists($key, $matches)) { + $matches[$key]; + } + } + + foreach ($expected as $key => $value) { + if (\array_key_exists($key, $matches) || $matches instanceof ArrayAccess) { + $matches[$key]; + } + } + } +} From 6cf39628f2135f1b3204a23abbf29f8d769253f3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 24 Oct 2025 08:46:40 +0200 Subject: [PATCH 02/10] add more tests --- tests/PHPStan/Analyser/nsrt/bug-11276.php | 34 ++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-11276.php b/tests/PHPStan/Analyser/nsrt/bug-11276.php index a13419fb77..babf7e2db7 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11276.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11276.php @@ -1,8 +1,10 @@ ', $arr); + } + } + + if (!array_key_exists($i, $arr)) { + if (array_key_exists($j, $arr) || array_key_exists($i, $arr)) { + assertType('non-empty-array', $arr); + } + } +} From 74f27ca0a4e47d567296e40c7a54e08197d4f6bd Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 24 Oct 2025 08:47:40 +0200 Subject: [PATCH 03/10] fix --- src/Analyser/TypeSpecifier.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index b1f8b09cba..3fa0106b7c 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -667,7 +667,6 @@ public function specifyTypesInCondition( if ($context->true()) { if ( $scope->getType($expr->left)->isFalse()->yes() - || $scope->getType($expr->right)->isTrue()->yes() ) { $types = $rightTypes->normalize($rightScope); } elseif ( From 6cdeb451c3d04eff29e605176200a99e4f5ddacd Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 24 Oct 2025 08:53:27 +0200 Subject: [PATCH 04/10] fix php7 --- tests/PHPStan/Analyser/nsrt/bug-11276.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-11276.php b/tests/PHPStan/Analyser/nsrt/bug-11276.php index babf7e2db7..f03a1915ed 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11276.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11276.php @@ -42,7 +42,7 @@ function doFoo(int $i, int $j, $arr): void } } -function doBar($j, $arr) { +function doBar($j, array $arr) { $i = 1; if (!array_key_exists($i, $arr)) { if (array_key_exists($i, $arr) || array_key_exists($j, $arr)) { From d02bcc395482e1f3c1aaceba99ae954c7bdf74d5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 24 Oct 2025 08:59:48 +0200 Subject: [PATCH 05/10] fix build --- tests/PHPStan/Analyser/nsrt/bug-11276.php | 15 --------------- tests/PHPStan/Analyser/nsrt/bug-11276b.php | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 15 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11276b.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-11276.php b/tests/PHPStan/Analyser/nsrt/bug-11276.php index f03a1915ed..73f56d605d 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11276.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11276.php @@ -41,18 +41,3 @@ function doFoo(int $i, int $j, $arr): void assertType('non-empty-array', $arr); } } - -function doBar($j, array $arr) { - $i = 1; - if (!array_key_exists($i, $arr)) { - if (array_key_exists($i, $arr) || array_key_exists($j, $arr)) { - assertType('non-empty-array', $arr); - } - } - - if (!array_key_exists($i, $arr)) { - if (array_key_exists($j, $arr) || array_key_exists($i, $arr)) { - assertType('non-empty-array', $arr); - } - } -} diff --git a/tests/PHPStan/Analyser/nsrt/bug-11276b.php b/tests/PHPStan/Analyser/nsrt/bug-11276b.php new file mode 100644 index 0000000000..73c88815d4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11276b.php @@ -0,0 +1,20 @@ += 8.0 + +namespace Bug11276b; + +use function PHPStan\Testing\assertType; + +function doBar($j, array $arr) { + $i = 1; + if (!array_key_exists($i, $arr)) { + if (array_key_exists($i, $arr) || array_key_exists($j, $arr)) { + assertType('non-empty-array', $arr); + } + } + + if (!array_key_exists($i, $arr)) { + if (array_key_exists($j, $arr) || array_key_exists($i, $arr)) { + assertType('non-empty-array', $arr); + } + } +} From fbb001899d1e0ed5543867c196e8ad79026d16a7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Oct 2025 10:01:38 +0100 Subject: [PATCH 06/10] support truethy/falsey --- src/Analyser/TypeSpecifier.php | 6 +++--- tests/PHPStan/Analyser/nsrt/bug-11276.php | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 3fa0106b7c..1abb42b684 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -666,12 +666,12 @@ public function specifyTypesInCondition( if ($context->true()) { if ( - $scope->getType($expr->left)->isFalse()->yes() + $scope->getType($expr->left)->toBoolean()->isFalse()->yes() ) { $types = $rightTypes->normalize($rightScope); } elseif ( - $scope->getType($expr->left)->isTrue()->yes() - || $scope->getType($expr->right)->isFalse()->yes() + $scope->getType($expr->left)->toBoolean()->isTrue()->yes() + || $scope->getType($expr->right)->toBoolean()->isFalse()->yes() ) { $types = $leftTypes->normalize($scope); } else { diff --git a/tests/PHPStan/Analyser/nsrt/bug-11276.php b/tests/PHPStan/Analyser/nsrt/bug-11276.php index 73f56d605d..7104f4da31 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11276.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11276.php @@ -14,6 +14,14 @@ function doFoo(int $i, int $j, $arr): void assertType('non-empty-array', $arr); } + if ("0" || array_key_exists($i, $arr)) { + assertType('non-empty-array', $arr); + } + + if (array_key_exists($i, $arr) || "0") { + assertType('non-empty-array', $arr); + } + if (true || array_key_exists($i, $arr)) { assertType('mixed', $arr); } @@ -22,6 +30,14 @@ function doFoo(int $i, int $j, $arr): void assertType('mixed', $arr); } + if ("1" || array_key_exists($i, $arr)) { + assertType('mixed', $arr); + } + + if (array_key_exists($i, $arr) || "1") { + assertType('mixed', $arr); + } + if (array_key_exists($i, $arr) || array_key_exists($j, $arr)) { assertType('non-empty-array', $arr); } From 9d112f2815bbf89647001b38f87154a5ba4d311e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Oct 2025 10:05:04 +0100 Subject: [PATCH 07/10] Update BooleanOrConstantConditionRuleTest.php --- .../Rules/Comparison/BooleanOrConstantConditionRuleTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php index a1a399005e..1006feee88 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanOrConstantConditionRuleTest.php @@ -239,7 +239,6 @@ public function testReportPhpDoc(): void [ 'Right side of || is always true.', 33, - $tipText, ], ]); } From 602fc713e32336f4e15e05acdc665d9c32f46aa5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Oct 2025 11:13:43 +0100 Subject: [PATCH 08/10] give infection more time to run the tests --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ac667b45e2..18799c2a12 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -240,6 +240,7 @@ jobs: run: | php build-infection/bin/infection-config.php \ --source-directory='build/PHPStan/Build' \ + --timeout=180 \ > infection.json5 cat infection.json5 | jq From 208051734fefc5a5f57f47dabe27a662c4f52acf Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Oct 2025 11:33:05 +0100 Subject: [PATCH 09/10] Update tests.yml --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 18799c2a12..b32a33d4b7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -240,7 +240,7 @@ jobs: run: | php build-infection/bin/infection-config.php \ --source-directory='build/PHPStan/Build' \ - --timeout=180 \ + --timeout=300 \ > infection.json5 cat infection.json5 | jq From d63bcbafe17826795ec2491d30b29adb1f22dd68 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Oct 2025 13:29:21 +0100 Subject: [PATCH 10/10] Discard changes to .github/workflows/tests.yml --- .github/workflows/tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b32a33d4b7..ac667b45e2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -240,7 +240,6 @@ jobs: run: | php build-infection/bin/infection-config.php \ --source-directory='build/PHPStan/Build' \ - --timeout=300 \ > infection.json5 cat infection.json5 | jq