From b73b7a3dc0572745838054d42d7270f873c64acc Mon Sep 17 00:00:00 2001 From: David Scandurra <31861387+SplotyCode@users.noreply.github.com> Date: Sat, 8 Nov 2025 16:37:32 +0100 Subject: [PATCH 1/4] Fix non-existent offset when guarding --- src/Analyser/TypeSpecifier.php | 19 +++++++++++++++++ ...nexistentOffsetInArrayDimFetchRuleTest.php | 8 +++++++ ...port-possibly-nonexistent-array-offset.php | 21 +++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 6698a8a800..55e24d1b1a 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -291,6 +291,25 @@ public function specifyTypesInCondition( if ($specifiedTypes !== null) { $result = $result->unionWith($specifiedTypes); } + if ( + $context->true() + && $expr instanceof Node\Expr\BinaryOp\Smaller + && $argType->isList()->yes() + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() + ) { + $dimFetch = new ArrayDimFetch( + $expr->right->getArgs()[0]->value, + $expr->left, + ); + $result = $result->unionWith( + $this->create( + $dimFetch, + $argType->getIterableValueType(), + TypeSpecifierContext::createTrue(), + $scope, + )->setRootExpr($expr) + ); + } if ( $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 879c86f791..1b4658fa50 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -700,6 +700,10 @@ public static function dataReportPossiblyNonexistentArrayOffset(): iterable "Offset 'foo' might not exist on array.", 9, ], + [ + 'Offset int might not exist on list.', + 77 + ], ]]; yield [true, true, [ [ @@ -710,6 +714,10 @@ public static function dataReportPossiblyNonexistentArrayOffset(): iterable 'Offset string might not exist on array{foo: 1}.', 20, ], + [ + 'Offset int might not exist on list.', + 77 + ], ]]; } diff --git a/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php b/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php index fc54d96f00..967d913cea 100644 --- a/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php +++ b/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php @@ -57,4 +57,25 @@ public function nonEmpty(array $a): void echo $a[0]; } + /** + * @param list $array + * @param positive-int $index + */ + public function guard(array $array, int $index) { + if ($index < count($array)) { + return $array[$index]; + } + return null; + } + + + /** + * @param list $array + */ + public function guardNotSafe(array $array, int $index) { + if ($index < count($array)) { + return $array[$index]; + } + return null; + } } From 679d274fcb278b72faa14ad2b0f00a12fb23509f Mon Sep 17 00:00:00 2001 From: David Scandurra <31861387+SplotyCode@users.noreply.github.com> Date: Sat, 8 Nov 2025 16:57:47 +0100 Subject: [PATCH 2/4] Dont set root expression --- 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 55e24d1b1a..5f637a4e75 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -307,7 +307,7 @@ public function specifyTypesInCondition( $argType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope, - )->setRootExpr($expr) + ) ); } From fa337a98ed3385fd194e6dbe56512e647b2f3d16 Mon Sep 17 00:00:00 2001 From: David Scandurra <31861387+SplotyCode@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:22:01 +0100 Subject: [PATCH 3/4] add more test cases --- ...nexistentOffsetInArrayDimFetchRuleTest.php | 16 +++++++++++ ...port-possibly-nonexistent-array-offset.php | 27 +++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 1b4658fa50..b9f3b2c396 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -704,6 +704,14 @@ public static function dataReportPossiblyNonexistentArrayOffset(): iterable 'Offset int might not exist on list.', 77 ], + [ + 'Offset int<0, max> might not exist on list.', + 88 + ], + [ + 'Offset int<0, max> might not exist on array.', + 100 + ], ]]; yield [true, true, [ [ @@ -718,6 +726,14 @@ public static function dataReportPossiblyNonexistentArrayOffset(): iterable 'Offset int might not exist on list.', 77 ], + [ + 'Offset int<0, max> might not exist on list.', + 88 + ], + [ + 'Offset int<0, max> might not exist on array.', + 100 + ], ]]; } diff --git a/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php b/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php index 967d913cea..0a15b211d3 100644 --- a/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php +++ b/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php @@ -59,7 +59,7 @@ public function nonEmpty(array $a): void /** * @param list $array - * @param positive-int $index + * @param non-negative-int $index */ public function guard(array $array, int $index) { if ($index < count($array)) { @@ -72,10 +72,33 @@ public function guard(array $array, int $index) { /** * @param list $array */ - public function guardNotSafe(array $array, int $index) { + public function guardNotSafeLowerBound(array $array, int $index) { if ($index < count($array)) { return $array[$index]; } return null; } + + /** + * @param list $array + * @param non-negative-int $index + */ + public function guardNotSafeUpperBound(array $array, int $index) { + if ($index <= count($array)) { + return $array[$index]; + } + return null; + } + + + /** + * @param array $array + * @param non-negative-int $index + */ + public function guardNotSafeArray(array $array, int $index) { + if ($index <= count($array)) { + return $array[$index]; + } + return null; + } } From 2c7e290493147fb97360c73fb94d6a4304393371 Mon Sep 17 00:00:00 2001 From: David Scandurra <31861387+SplotyCode@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:48:16 +0100 Subject: [PATCH 4/4] Fix test because of more explicit type --- src/Analyser/TypeSpecifier.php | 6 ++++-- tests/PHPStan/Analyser/nsrt/bug-10264.php | 2 +- .../NonexistentOffsetInArrayDimFetchRuleTest.php | 8 ++++++++ .../report-possibly-nonexistent-array-offset.php | 12 ++++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 5f637a4e75..22934f082d 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -273,7 +273,8 @@ public function specifyTypesInCondition( && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true) && $leftType->isInteger()->yes() ) { - $argType = $scope->getType($expr->right->getArgs()[0]->value); + $argExpr = $expr->right->getArgs()[0]->value; + $argType = $scope->getType($argExpr); if ($leftType instanceof ConstantIntegerType) { if ($orEqual) { @@ -295,10 +296,11 @@ public function specifyTypesInCondition( $context->true() && $expr instanceof Node\Expr\BinaryOp\Smaller && $argType->isList()->yes() + && $argExpr instanceof Expr\Variable && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() ) { $dimFetch = new ArrayDimFetch( - $expr->right->getArgs()[0]->value, + $argExpr, $expr->left, ); $result = $result->unionWith( diff --git a/tests/PHPStan/Analyser/nsrt/bug-10264.php b/tests/PHPStan/Analyser/nsrt/bug-10264.php index 20b1361a25..52e44ccb88 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10264.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10264.php @@ -33,7 +33,7 @@ public function sayHello(array $c): void assertType('list', $c); if (count($c) > 0) { $c = array_map(fn() => new stdClass(), $c); - assertType('non-empty-list', $c); + assertType('non-empty-list&hasOffsetValue(0, stdClass)', $c); } else { assertType('array{}', $c); } diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index b9f3b2c396..e14e6e4f37 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -712,6 +712,10 @@ public static function dataReportPossiblyNonexistentArrayOffset(): iterable 'Offset int<0, max> might not exist on array.', 100 ], + [ + 'Offset int<0, max> might not exist on array|int<6, max>, int>.', + 112 + ], ]]; yield [true, true, [ [ @@ -734,6 +738,10 @@ public static function dataReportPossiblyNonexistentArrayOffset(): iterable 'Offset int<0, max> might not exist on array.', 100 ], + [ + 'Offset int<0, max> might not exist on array|int<6, max>, int>.', + 112 + ], ]]; } diff --git a/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php b/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php index 0a15b211d3..6ccc313ced 100644 --- a/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php +++ b/tests/PHPStan/Rules/Arrays/data/report-possibly-nonexistent-array-offset.php @@ -101,4 +101,16 @@ public function guardNotSafeArray(array $array, int $index) { } return null; } + + /** + * @param array $array + * @param non-negative-int $index + */ + public function guardNotSafeBecauseRewrite(array $array, int $index) { + if ($index <= count($array)) { + unset($array[5]); + return $array[$index]; + } + return null; + } }