From f995a1f02c8711a08ffbb62e062b82dc26cfaa47 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 15 Oct 2025 10:25:22 +0200 Subject: [PATCH 1/9] infer `non-empty-list/array` after `isset($arr[$i])` --- src/Analyser/TypeSpecifier.php | 12 +++++++++ tests/PHPStan/Analyser/nsrt/bug-13674.php | 30 +++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13674.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 587818cbb3..ba123e94bc 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -936,6 +936,18 @@ public function specifyTypesInCondition( && !$scope->getType($var->var) instanceof MixedType ) { $dimType = $scope->getType($var->dim); + $varType = $scope->getType($var->var); + + if ($varType->isArray()->yes()) { + $types = $types->unionWith( + $this->create( + $var->var, + new NonEmptyArrayType(), + $context, + $scope, + )->setRootExpr($expr), + ); + } if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) { $types = $types->unionWith( diff --git a/tests/PHPStan/Analyser/nsrt/bug-13674.php b/tests/PHPStan/Analyser/nsrt/bug-13674.php new file mode 100644 index 0000000000..eb3cec9da9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13674.php @@ -0,0 +1,30 @@ + $arrayA + * @param list $listA + */ + public function sayHello($arrayA, $listA, int $i): void + { + if (isset($arrayA[$i])) { + assertType('non-empty-array', $arrayA); + } else { + assertType('array', $arrayA); + } + assertType('array', $arrayA); + + if (isset($listA[$i])) { + assertType('non-empty-list', $listA); + } else { + assertType('list', $listA); + } + assertType('list', $listA); + } +} From d2dabeb84628745dc1d13f4f183ba806e2307aa6 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 15 Oct 2025 10:36:39 +0200 Subject: [PATCH 2/9] adjust test expectations --- tests/PHPStan/Analyser/nsrt/bug-12274.php | 14 +++++++------- tests/PHPStan/Analyser/nsrt/bug-7000.php | 2 +- .../PHPStan/Analyser/nsrt/has-offset-type-bug.php | 2 +- .../Analyser/nsrt/specified-types-closure-use.php | 8 ++++---- tests/PHPStan/Rules/Arrays/data/bug-11679.php | 2 +- tests/PHPStan/Rules/Variables/IssetRuleTest.php | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12274.php b/tests/PHPStan/Analyser/nsrt/bug-12274.php index d4ad2e302d..7e899600be 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12274.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12274.php @@ -40,7 +40,7 @@ function getItemsByModifiedIndex(array $items): array function testKeepListAfterIssetIndex(array $list, int $i): void { if (isset($list[$i])) { - assertType('list', $list); + assertType('non-empty-list', $list); $list[$i] = 21; assertType('non-empty-list', $list); $list[$i+1] = 21; @@ -53,8 +53,8 @@ function testKeepListAfterIssetIndex(array $list, int $i): void function testKeepNestedListAfterIssetIndex(array $nestedList, int $i, int $j): void { if (isset($nestedList[$i][$j])) { - assertType('list>', $nestedList); - assertType('list', $nestedList[$i]); + assertType('non-empty-list>', $nestedList); + assertType('non-empty-list', $nestedList[$i]); $nestedList[$i][$j] = 21; assertType('non-empty-list>', $nestedList); assertType('list', $nestedList[$i]); @@ -66,7 +66,7 @@ function testKeepNestedListAfterIssetIndex(array $nestedList, int $i, int $j): v function testKeepListAfterIssetIndexPlusOne(array $list, int $i): void { if (isset($list[$i])) { - assertType('list', $list); + assertType('non-empty-list', $list); $list[$i+1] = 21; assertType('non-empty-list', $list); } @@ -77,7 +77,7 @@ function testKeepListAfterIssetIndexPlusOne(array $list, int $i): void function testKeepListAfterIssetIndexOnePlus(array $list, int $i): void { if (isset($list[$i])) { - assertType('list', $list); + assertType('non-empty-list', $list); $list[1+$i] = 21; assertType('non-empty-list', $list); } @@ -90,7 +90,7 @@ function testShouldLooseListbyAst(array $list, int $i): void if (isset($list[$i])) { $i++; - assertType('list', $list); + assertType('non-empty-list', $list); $list[1+$i] = 21; assertType('non-empty-array, int>', $list); } @@ -101,7 +101,7 @@ function testShouldLooseListbyAst(array $list, int $i): void function testShouldLooseListbyAst2(array $list, int $i): void { if (isset($list[$i])) { - assertType('list', $list); + assertType('non-empty-list', $list); $list[2+$i] = 21; assertType('non-empty-array, int>', $list); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-7000.php b/tests/PHPStan/Analyser/nsrt/bug-7000.php index a2e536a6da..3ad9a1b2d8 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7000.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7000.php @@ -12,7 +12,7 @@ public function doBar(): void $composer = array(); foreach (array('require', 'require-dev') as $linkType) { if (isset($composer[$linkType])) { - assertType('array{require?: array, require-dev?: array}', $composer); + assertType('non-empty-array{require?: array, require-dev?: array}', $composer); foreach ($composer[$linkType] as $x) {} } } diff --git a/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php b/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php index eacfb06af6..09955bde2e 100644 --- a/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php +++ b/tests/PHPStan/Analyser/nsrt/has-offset-type-bug.php @@ -26,7 +26,7 @@ public function doFoo(array $errorMessages): void continue; } - assertType('array>', $fileErrorsCounts); + assertType('non-empty-array>', $fileErrorsCounts); assertType('int<1, max>', $fileErrorsCounts[$errorMessage]); $fileErrorsCounts[$errorMessage]++; diff --git a/tests/PHPStan/Analyser/nsrt/specified-types-closure-use.php b/tests/PHPStan/Analyser/nsrt/specified-types-closure-use.php index 9cd49e4522..f5f8189ded 100644 --- a/tests/PHPStan/Analyser/nsrt/specified-types-closure-use.php +++ b/tests/PHPStan/Analyser/nsrt/specified-types-closure-use.php @@ -48,10 +48,10 @@ function ($arr) use ($key): void { public function doBuzz(array $arr, string $key): void { if (isset($arr[$key])) { - assertType('array', $arr); + assertType('non-empty-array', $arr); assertType("mixed~null", $arr[$key]); function () use ($arr, $key): void { - assertType('array', $arr); + assertType('non-empty-array', $arr); assertType("mixed~null", $arr[$key]); }; } @@ -60,10 +60,10 @@ function () use ($arr, $key): void { public function doBuzz(array $arr, string $key): void { if (isset($arr[$key])) { - assertType('array', $arr); + assertType('non-empty-array', $arr); assertType("mixed~null", $arr[$key]); function ($key) use ($arr): void { - assertType('array', $arr); + assertType('non-empty-array', $arr); assertType("mixed", $arr[$key]); }; } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-11679.php b/tests/PHPStan/Rules/Arrays/data/bug-11679.php index 463362516a..c2badb5b75 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-11679.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-11679.php @@ -33,7 +33,7 @@ public function sayHello(int $index): bool $this->arr[$index]['foo'] = true; assertType('non-empty-array', $this->arr); } - assertType('array', $this->arr); + assertType('non-empty-array', $this->arr); return $this->arr[$index]['foo']; // PHPStan does not realize 'foo' is set } } diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index fac3234530..296c0cda70 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -493,7 +493,7 @@ public function testPr4374(): void $this->analyse([__DIR__ . '/data/pr-4374.php'], [ [ - 'Offset string on array in isset() always exists and is not nullable.', + 'Offset string on non-empty-array in isset() always exists and is not nullable.', 23, ], ]); From 236fe1c4ad654e3d773e46cea10ab25da31f2360 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 15 Oct 2025 10:47:38 +0200 Subject: [PATCH 3/9] fix --- src/Analyser/TypeSpecifier.php | 24 +++++++++---------- tests/PHPStan/Analyser/nsrt/bug-7000.php | 2 +- .../Rules/Variables/NullCoalesceRuleTest.php | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index ba123e94bc..129af0712f 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -936,18 +936,6 @@ public function specifyTypesInCondition( && !$scope->getType($var->var) instanceof MixedType ) { $dimType = $scope->getType($var->dim); - $varType = $scope->getType($var->var); - - if ($varType->isArray()->yes()) { - $types = $types->unionWith( - $this->create( - $var->var, - new NonEmptyArrayType(), - $context, - $scope, - )->setRootExpr($expr), - ); - } if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) { $types = $types->unionWith( @@ -960,6 +948,18 @@ public function specifyTypesInCondition( ); } else { $varType = $scope->getType($var->var); + + if ($varType->isArray()->yes() && $dimType->isConstantScalarValue()->no()) { + $types = $types->unionWith( + $this->create( + $var->var, + new NonEmptyArrayType(), + $context, + $scope, + )->setRootExpr($expr), + ); + } + $narrowedKey = AllowedArrayKeysTypes::narrowOffsetKeyType($varType, $dimType); if ($narrowedKey !== null) { $types = $types->unionWith( diff --git a/tests/PHPStan/Analyser/nsrt/bug-7000.php b/tests/PHPStan/Analyser/nsrt/bug-7000.php index 3ad9a1b2d8..a2e536a6da 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7000.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7000.php @@ -12,7 +12,7 @@ public function doBar(): void $composer = array(); foreach (array('require', 'require-dev') as $linkType) { if (isset($composer[$linkType])) { - assertType('non-empty-array{require?: array, require-dev?: array}', $composer); + assertType('array{require?: array, require-dev?: array}', $composer); foreach ($composer[$linkType] as $x) {} } } diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index de2e966abb..d6f81e9b1b 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -280,7 +280,7 @@ public function testBug7190(): void { $this->analyse([__DIR__ . '/../Properties/data/bug-7190.php'], [ [ - 'Offset int on array on left side of ?? always exists and is not nullable.', + 'Offset int on non-empty-array on left side of ?? always exists and is not nullable.', 20, ], ]); From 8df2f37a9e18d1201af714e1d4e6b46aa39f01ff Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 15 Oct 2025 11:27:39 +0200 Subject: [PATCH 4/9] make this code more uniform with array_key_exists() handling --- src/Analyser/TypeSpecifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 129af0712f..cd930e21fa 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -949,7 +949,7 @@ public function specifyTypesInCondition( } else { $varType = $scope->getType($var->var); - if ($varType->isArray()->yes() && $dimType->isConstantScalarValue()->no()) { + if ($varType->isArray()->yes() && count($dimType->getConstantScalarTypes()) <= 1) { $types = $types->unionWith( $this->create( $var->var, From 2432871c45aaa9beee17d7113e843532902d19f1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 15 Oct 2025 20:11:28 +0200 Subject: [PATCH 5/9] added test --- tests/PHPStan/Analyser/nsrt/bug-13674.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13674.php b/tests/PHPStan/Analyser/nsrt/bug-13674.php index eb3cec9da9..76f5bf0dc8 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13674.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13674.php @@ -26,5 +26,10 @@ public function sayHello($arrayA, $listA, int $i): void assertType('list', $listA); } assertType('list', $listA); + + if (!isset($listA[$i])) { + return; + } + assertType('non-empty-list', $listA); } } From 7275cffbd986ef98dea2fc50816ef14d3a2f3929 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 16 Oct 2025 13:26:14 +0200 Subject: [PATCH 6/9] add assert --- tests/PHPStan/Analyser/nsrt/bug-13674.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13674.php b/tests/PHPStan/Analyser/nsrt/bug-13674.php index 76f5bf0dc8..7e3c0a5a06 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13674.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13674.php @@ -28,6 +28,7 @@ public function sayHello($arrayA, $listA, int $i): void assertType('list', $listA); if (!isset($listA[$i])) { + assertType('list', $listA); return; } assertType('non-empty-list', $listA); From f9dd50aaec3edcbf7657cc0c67d6cd7739cfb2ec Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 17 Oct 2025 08:53:09 +0200 Subject: [PATCH 7/9] Update bug-13674.php --- tests/PHPStan/Analyser/nsrt/bug-13674.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13674.php b/tests/PHPStan/Analyser/nsrt/bug-13674.php index 7e3c0a5a06..cabbb99632 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13674.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13674.php @@ -32,5 +32,8 @@ public function sayHello($arrayA, $listA, int $i): void return; } assertType('non-empty-list', $listA); + + $emptyArray = []; + assertType('false', isset($emptyArray[$i])); } } From a558636f6903199ef91697e914924c8bf084d8ff Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Feb 2026 08:28:52 +0100 Subject: [PATCH 8/9] fix collision --- tests/PHPStan/Analyser/nsrt/bug-13674.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13674.php b/tests/PHPStan/Analyser/nsrt/bug-13674.php index cabbb99632..3baabc168b 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13674.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13674.php @@ -1,6 +1,6 @@ Date: Tue, 10 Feb 2026 08:29:05 +0100 Subject: [PATCH 9/9] Add bug-13674b.php test file --- tests/PHPStan/Analyser/nsrt/{bug-13674.php => bug-13674b.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/PHPStan/Analyser/nsrt/{bug-13674.php => bug-13674b.php} (100%) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13674.php b/tests/PHPStan/Analyser/nsrt/bug-13674b.php similarity index 100% rename from tests/PHPStan/Analyser/nsrt/bug-13674.php rename to tests/PHPStan/Analyser/nsrt/bug-13674b.php