From 6d752b4297d23c4e6e24e87173f6703fa5ba1c24 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 15 Oct 2025 10:25:22 +0200 Subject: [PATCH 01/10] 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 0aafa3956b..0efc81a936 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1065,6 +1065,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 4de94540ee83a6fe39afe9e8c23bf083aea3cf41 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 15 Oct 2025 10:36:39 +0200 Subject: [PATCH 02/10] 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 be2f2b3c68..11f2b39f1f 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 0afcd7b1663fd7a41cbb38999210b947876e9ec6 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 15 Oct 2025 10:47:38 +0200 Subject: [PATCH 03/10] 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 0efc81a936..9173dc3c90 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1065,18 +1065,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( @@ -1089,6 +1077,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 7a92e20046..3d8a7dee27 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 b08f7733abfdbed29a90150455dba8417287fc8c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 15 Oct 2025 11:27:39 +0200 Subject: [PATCH 04/10] 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 9173dc3c90..dd3a8c1746 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1078,7 +1078,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 15cae8cabe501c0940edfde6ba01985c9ab13e61 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 15 Oct 2025 20:11:28 +0200 Subject: [PATCH 05/10] 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 4a1e46cf121472510cf096e376cbf2486bb5d6e9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 16 Oct 2025 13:26:14 +0200 Subject: [PATCH 06/10] 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 d08533c625e18bbd2e6747929853071347af37ad Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 17 Oct 2025 08:53:09 +0200 Subject: [PATCH 07/10] 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 dd5825fbfe5416477e8bae0cb58cd5c6a39f2f5d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 10 Feb 2026 08:28:52 +0100 Subject: [PATCH 08/10] 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 09/10] 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 From 7d4d6af33ad828f31b405b69ed099461a05f5dbd Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 9 Mar 2026 15:48:29 +0100 Subject: [PATCH 10/10] Update TypeSpecifier.php --- src/Analyser/TypeSpecifier.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index dd3a8c1746..7d8666a464 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1078,28 +1078,29 @@ public function specifyTypesInCondition( } else { $varType = $scope->getType($var->var); - if ($varType->isArray()->yes() && count($dimType->getConstantScalarTypes()) <= 1) { + $narrowedKey = AllowedArrayKeysTypes::narrowOffsetKeyType($varType, $dimType); + if ($narrowedKey !== null) { $types = $types->unionWith( $this->create( - $var->var, - new NonEmptyArrayType(), + $var->dim, + $narrowedKey, $context, $scope, )->setRootExpr($expr), ); } - $narrowedKey = AllowedArrayKeysTypes::narrowOffsetKeyType($varType, $dimType); - if ($narrowedKey !== null) { + if ($scope->getNativeType($var->var)->isArray()->yes()) { $types = $types->unionWith( $this->create( - $var->dim, - $narrowedKey, + $var->var, + new NonEmptyArrayType(), $context, $scope, )->setRootExpr($expr), ); } + } }