From 67e510d25b45aad77b2d40aa96b932a00361cb51 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:41:35 +0000 Subject: [PATCH 01/13] Fix #14234: Infer list offset exists for $i < count($list) - N - Added TypeSpecifier handling for BinaryOp\Minus with count() on the left side - When $i < count($list) - N (N >= 0), $list[$i] is narrowed as existing - New regression test in tests/PHPStan/Rules/Arrays/data/bug-14234.php --- src/Analyser/TypeSpecifier.php | 27 +++++++++++++++++++ ...nexistentOffsetInArrayDimFetchRuleTest.php | 7 +++++ tests/PHPStan/Rules/Arrays/data/bug-14234.php | 25 +++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-14234.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 6f8731e5ad..88807927ad 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -351,6 +351,33 @@ public function specifyTypesInCondition( } } + // infer $list[$index] after $index < count($list) - N + if ( + $context->true() + && !$orEqual + && $expr->right instanceof Expr\BinaryOp\Minus + && $expr->right->left instanceof FuncCall + && $expr->right->left->name instanceof Name + && in_array(strtolower((string) $expr->right->left->name), ['count', 'sizeof'], true) + && count($expr->right->left->getArgs()) >= 1 + && $leftType->isInteger()->yes() + && !$leftType instanceof ConstantIntegerType + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() + ) { + $countArgType = $scope->getType($expr->right->left->getArgs()[0]->value); + $subtractedType = $scope->getType($expr->right->right); + if ( + $countArgType->isList()->yes() + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($subtractedType)->yes() + ) { + $arrayArg = $expr->right->left->getArgs()[0]->value; + $dimFetch = new ArrayDimFetch($arrayArg, $expr->left); + $result = $result->unionWith( + $this->create($dimFetch, $countArgType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr), + ); + } + } + if ( !$context->null() && $expr->right instanceof FuncCall diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index dc168211dd..f309bc501c 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1189,4 +1189,11 @@ public function testBug13770(): void ]); } + public function testBug14234(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-14234.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-14234.php b/tests/PHPStan/Rules/Arrays/data/bug-14234.php new file mode 100644 index 0000000000..a88e8c5bff --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-14234.php @@ -0,0 +1,25 @@ + Date: Fri, 6 Mar 2026 07:48:18 +0100 Subject: [PATCH 02/13] more tests --- .../Analyser/NodeScopeResolverTest.php | 1 + ...nexistentOffsetInArrayDimFetchRuleTest.php | 7 ++++- tests/PHPStan/Rules/Arrays/data/bug-14234.php | 27 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index d0983dbb84..df3c5c6e92 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -248,6 +248,7 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Classes/data/bug-11591-property-tag.php'; yield __DIR__ . '/../Rules/Classes/data/mixin-trait-use.php'; + yield __DIR__ . '/../Rules/Arrays/data/bug-14234.php'; yield __DIR__ . '/../Rules/Arrays/data/bug-11679.php'; yield __DIR__ . '/../Rules/Methods/data/bug-4801.php'; yield __DIR__ . '/../Rules/Arrays/data/narrow-superglobal.php'; diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index f309bc501c..47edec6017 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1193,7 +1193,12 @@ public function testBug14234(): void { $this->reportPossiblyNonexistentGeneralArrayOffset = true; - $this->analyse([__DIR__ . '/data/bug-14234.php'], []); + $this->analyse([__DIR__ . '/data/bug-14234.php'], [ + [ + 'Offset int<0, max> might not exist on array.', + 48, + ], + ]); } } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-14234.php b/tests/PHPStan/Rules/Arrays/data/bug-14234.php index a88e8c5bff..29e8566b4c 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-14234.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-14234.php @@ -2,9 +2,12 @@ namespace Bug14234; +use function PHPStan\Testing\assertType; + function getShortenedPath(string $identifier): string { $parts = explode('/', $identifier); + assertType('non-empty-list', $parts); for ($i = 0; $i < count($parts) - 1; $i++) { $parts[$i] = substr($parts[$i], 0, 1); @@ -16,6 +19,7 @@ function getShortenedPath(string $identifier): string function getShortenedPath2(string $identifier): string { $parts = explode('/', $identifier); + assertType('non-empty-list', $parts); for ($i = 0; $i < count($parts); $i++) { $parts[$i] = substr($parts[$i], 0, 1); @@ -23,3 +27,26 @@ function getShortenedPath2(string $identifier): string return implode("/", $parts); } + +function getShortenedPath3(string $identifier): string +{ + $parts = explode('/', $identifier); + assertType('non-empty-list', $parts); + + for ($i = 0; $i < count($parts) - 4; $i++) { + $parts[$i] = substr($parts[$i], 0, 1); + } + + return implode("/", $parts); +} + +function getShortenedPath4(array $parts): string +{ + assertType('array', $parts); + + for ($i = 0; $i < count($parts) - 4; $i++) { + $parts[$i] = substr($parts[$i], 0, 1); + } + + return implode("/", $parts); +} From e940c0ddfaddcfe09c395133d5343cfa642f0b4a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 6 Mar 2026 07:52:40 +0100 Subject: [PATCH 03/13] reorder --- src/Analyser/TypeSpecifier.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 88807927ad..6cdc7fa41b 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -360,8 +360,9 @@ public function specifyTypesInCondition( && $expr->right->left->name instanceof Name && in_array(strtolower((string) $expr->right->left->name), ['count', 'sizeof'], true) && count($expr->right->left->getArgs()) >= 1 - && $leftType->isInteger()->yes() + // constant offsets are handled via HasOffsetType/HasOffsetValueType && !$leftType instanceof ConstantIntegerType + && $leftType->isInteger()->yes() && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() ) { $countArgType = $scope->getType($expr->right->left->getArgs()[0]->value); From c42f502bdef86884d19de74a5e628edc38b007f3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 6 Mar 2026 07:59:27 +0100 Subject: [PATCH 04/13] more tests --- .../NonexistentOffsetInArrayDimFetchRuleTest.php | 6 +++++- tests/PHPStan/Rules/Arrays/data/bug-14234.php | 14 +++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 47edec6017..50f49f7dca 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1194,9 +1194,13 @@ public function testBug14234(): void $this->reportPossiblyNonexistentGeneralArrayOffset = true; $this->analyse([__DIR__ . '/data/bug-14234.php'], [ + [ + 'Offset int<2, max> might not exist on non-empty-array, string>.', + 49, + ], [ 'Offset int<0, max> might not exist on array.', - 48, + 60, ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-14234.php b/tests/PHPStan/Rules/Arrays/data/bug-14234.php index 29e8566b4c..fe012c76b6 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-14234.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-14234.php @@ -40,7 +40,19 @@ function getShortenedPath3(string $identifier): string return implode("/", $parts); } -function getShortenedPath4(array $parts): string +function getShortenedPath4(string $identifier): string +{ + $parts = explode('/', $identifier); + assertType('non-empty-list', $parts); + + for ($i = 2; $i < count($parts) - 4; $i++) { + $parts[$i] = substr($parts[$i], 0, 1); // should not error + } + + return implode("/", $parts); +} + +function errorOnRegularError(array $parts): string { assertType('array', $parts); From 3517a5573b5031fc510f2ea55c7bbcef2cafc1a7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 6 Mar 2026 08:04:56 +0100 Subject: [PATCH 05/13] Update NonexistentOffsetInArrayDimFetchRuleTest.php --- .../Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 50f49f7dca..5ca507cd4c 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1189,6 +1189,7 @@ public function testBug13770(): void ]); } + #[RequiresPhp('>= 8.0')] public function testBug14234(): void { $this->reportPossiblyNonexistentGeneralArrayOffset = true; From 9208a02f3e6dc6e9eb3b304b92f75932006d4e7e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 6 Mar 2026 15:03:15 +0100 Subject: [PATCH 06/13] test more cases --- src/Analyser/TypeSpecifier.php | 4 +-- ...nexistentOffsetInArrayDimFetchRuleTest.php | 4 +++ tests/PHPStan/Rules/Arrays/data/bug-13770.php | 26 +++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 6cdc7fa41b..c79aeea0d4 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -352,9 +352,9 @@ public function specifyTypesInCondition( } // infer $list[$index] after $index < count($list) - N + // infer $list[$index] after $index <= count($list) - N if ( $context->true() - && !$orEqual && $expr->right instanceof Expr\BinaryOp\Minus && $expr->right->left instanceof FuncCall && $expr->right->left->name instanceof Name @@ -369,7 +369,7 @@ public function specifyTypesInCondition( $subtractedType = $scope->getType($expr->right->right); if ( $countArgType->isList()->yes() - && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($subtractedType)->yes() + && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($subtractedType)->yes() ) { $arrayArg = $expr->right->left->getArgs()[0]->value; $dimFetch = new ArrayDimFetch($arrayArg, $expr->left); diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 5ca507cd4c..6c34bedb83 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1186,6 +1186,10 @@ public function testBug13770(): void 'Offset -1|3|6|10 might not exist on list.', 126, ], + [ + 'Offset int<0, max> might not exist on list.', + 139, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13770.php b/tests/PHPStan/Rules/Arrays/data/bug-13770.php index cbd1748dfb..c17fd88b68 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-13770.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-13770.php @@ -128,4 +128,30 @@ public function constantMaybeNegativeIntLessThanCount(array $array, int $index): return 0; } + + /** + * @param list $array + * @param 0|positive-int $index + */ + public function ZeroOrMoreIntLessThanOrEqualCount(array $array, int $index): int + { + if ($index <= count($array)) { + return $array[$index]; // SHOULD still report - off by one + } + + return 0; + } + + /** + * @param list $array + * @param 0|positive-int $index + */ + public function ZeroOrMoreMinusOneIntLessThanOrEqualCount(array $array, int $index): int + { + if ($index <= count($array) - 1) { + return $array[$index]; + } + + return 0; + } } From 0a326004a3942eeb55b2979872b07ce0c23d3f82 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 6 Mar 2026 15:03:58 +0100 Subject: [PATCH 07/13] Update bug-13770.php --- tests/PHPStan/Rules/Arrays/data/bug-13770.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13770.php b/tests/PHPStan/Rules/Arrays/data/bug-13770.php index c17fd88b68..d59a2be4b1 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-13770.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-13770.php @@ -146,7 +146,7 @@ public function ZeroOrMoreIntLessThanOrEqualCount(array $array, int $index): int * @param list $array * @param 0|positive-int $index */ - public function ZeroOrMoreMinusOneIntLessThanOrEqualCount(array $array, int $index): int + public function ZeroOrMoreIntLessThanOrEqualCountMinusOne(array $array, int $index): int { if ($index <= count($array) - 1) { return $array[$index]; From 4604390aef592c9a47674ad46a7350048dcb45da Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 6 Mar 2026 15:07:06 +0100 Subject: [PATCH 08/13] Update bug-14234.php --- tests/PHPStan/Rules/Arrays/data/bug-14234.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Arrays/data/bug-14234.php b/tests/PHPStan/Rules/Arrays/data/bug-14234.php index fe012c76b6..2a3dffa7ff 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-14234.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-14234.php @@ -52,7 +52,7 @@ function getShortenedPath4(string $identifier): string return implode("/", $parts); } -function errorOnRegularError(array $parts): string +function errorOnRegularArray(array $parts): string { assertType('array', $parts); From f02b5c14b4b285718f12a9df3586510b597de1a7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 6 Mar 2026 15:08:15 +0100 Subject: [PATCH 09/13] Update TypeSpecifier.php --- src/Analyser/TypeSpecifier.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index c79aeea0d4..39862bdaf4 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -351,8 +351,8 @@ public function specifyTypesInCondition( } } - // infer $list[$index] after $index < count($list) - N - // infer $list[$index] after $index <= count($list) - N + // infer $list[$index] after $zeroOrMore < count($list) - N + // infer $list[$index] after $zeroOrMore <= count($list) - N if ( $context->true() && $expr->right instanceof Expr\BinaryOp\Minus From 2aed77d36100aebea6cc7fd045cf78a1c6c5ec35 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 6 Mar 2026 15:14:02 +0100 Subject: [PATCH 10/13] more tests --- ...nexistentOffsetInArrayDimFetchRuleTest.php | 4 +++ tests/PHPStan/Rules/Arrays/data/bug-13770.php | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 6c34bedb83..2c3dc6f80c 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1190,6 +1190,10 @@ public function testBug13770(): void 'Offset int<0, max> might not exist on list.', 139, ], + [ + 'Offset int<0, max> might not exist on array.', + 177, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13770.php b/tests/PHPStan/Rules/Arrays/data/bug-13770.php index d59a2be4b1..db7e0414e1 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-13770.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-13770.php @@ -154,4 +154,29 @@ public function ZeroOrMoreIntLessThanOrEqualCountMinusOne(array $array, int $ind return 0; } + + /** + * @param list $array + * @param 0|positive-int $index + */ + public function ZeroOrMoreIntLessThanOrEqualCountMinusFive(array $array, int $index): int + { + if ($index <= count($array) - 5) { + return $array[$index]; + } + + return 0; + } + + /** + * @param 0|positive-int $index + */ + public function errorsBecauseNotList(array $array, int $index): int + { + if ($index <= count($array) - 5) { + return $array[$index]; + } + + return 0; + } } From 193a4f60ccf671092484443cefec3fbc8e65e4b6 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 6 Mar 2026 15:46:47 +0100 Subject: [PATCH 11/13] another test --- .../NonexistentOffsetInArrayDimFetchRuleTest.php | 4 ++++ tests/PHPStan/Rules/Arrays/data/bug-13770.php | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 2c3dc6f80c..8bcc5307df 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1194,6 +1194,10 @@ public function testBug13770(): void 'Offset int<0, max> might not exist on array.', 177, ], + [ + 'Offset -5|int<0, max> might not exist on list.', + 190, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13770.php b/tests/PHPStan/Rules/Arrays/data/bug-13770.php index db7e0414e1..0962b7cdca 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-13770.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-13770.php @@ -179,4 +179,17 @@ public function errorsBecauseNotList(array $array, int $index): int return 0; } + + /** + * @param list $array + * @param -5|0|positive-int $index + */ + public function errorsBecauseMaybeTooSmall(array $array, int $index): int + { + if ($index <= count($array) - 5) { + return $array[$index]; + } + + return 0; + } } From e23471bb90b291de7a4d7be314cdb923c615316e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 6 Mar 2026 16:17:26 +0100 Subject: [PATCH 12/13] errors on recursive count --- src/Analyser/TypeSpecifier.php | 20 +++++++++++-------- ...nexistentOffsetInArrayDimFetchRuleTest.php | 4 ++++ tests/PHPStan/Rules/Arrays/data/bug-13770.php | 13 ++++++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 39862bdaf4..1d8ca2608a 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -369,6 +369,7 @@ public function specifyTypesInCondition( $subtractedType = $scope->getType($expr->right->right); if ( $countArgType->isList()->yes() + && $this->isNormalCountCall($expr->right->left, $countArgType, $scope)->yes() && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($subtractedType)->yes() ) { $arrayArg = $expr->right->left->getArgs()[0]->value; @@ -1239,6 +1240,16 @@ public function specifyTypesInCondition( return (new SpecifiedTypes([], []))->setRootExpr($expr); } + private function isNormalCountCall(FuncCall $countFuncCall, Type $typeToCount, Scope $scope): TrinaryLogic + { + if (count($countFuncCall->getArgs()) === 1) { + return TrinaryLogic::createYes(); + } + + $mode = $scope->getType($countFuncCall->getArgs()[1]->value); + return (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($typeToCount->getIterableValueType()->isArray()->negate()); + } + private function specifyTypesForCountFuncCall( FuncCall $countFuncCall, Type $type, @@ -1248,18 +1259,11 @@ private function specifyTypesForCountFuncCall( Expr $rootExpr, ): ?SpecifiedTypes { - if (count($countFuncCall->getArgs()) === 1) { - $isNormalCount = TrinaryLogic::createYes(); - } else { - $mode = $scope->getType($countFuncCall->getArgs()[1]->value); - $isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($type->getIterableValueType()->isArray()->negate()); - } - $isConstantArray = $type->isConstantArray(); $isList = $type->isList(); $oneOrMore = IntegerRangeType::fromInterval(1, null); if ( - !$isNormalCount->yes() + !$this->isNormalCountCall($countFuncCall, $type, $scope)->yes() || (!$isConstantArray->yes() && !$isList->yes()) || !$oneOrMore->isSuperTypeOf($sizeType)->yes() || $sizeType->isSuperTypeOf($type->getArraySize())->yes() diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 8bcc5307df..5ec3e79613 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1198,6 +1198,10 @@ public function testBug13770(): void 'Offset -5|int<0, max> might not exist on list.', 190, ], + [ + 'Offset int<0, max> might not exist on list.', + 203, + ], ]); } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13770.php b/tests/PHPStan/Rules/Arrays/data/bug-13770.php index 0962b7cdca..e14c726959 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-13770.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-13770.php @@ -192,4 +192,17 @@ public function errorsBecauseMaybeTooSmall(array $array, int $index): int return 0; } + + /** + * @param list $array + * @param 0|positive-int $index + */ + public function errorsOnRecursiveCount(array $array, int $index): int + { + if ($index <= count($array, COUNT_RECURSIVE) - 5) { + return $array[$index]; + } + + return 0; + } } From 6f3fd948a1285d7b5b708007f013dfd02a8bafbc Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 6 Mar 2026 16:20:00 +0100 Subject: [PATCH 13/13] taking over tests --- ...nexistentOffsetInArrayDimFetchRuleTest.php | 12 ++ tests/PHPStan/Rules/Arrays/data/bug-14215.php | 122 ++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 tests/PHPStan/Rules/Arrays/data/bug-14215.php diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 5ec3e79613..c066553f27 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1161,6 +1161,18 @@ public function testBug13526(): void $this->analyse([__DIR__ . '/data/bug-13526.php'], []); } + public function testBug14215(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-14215.php'], [ + [ + 'Offset int might not exist on list.', + 39, + ], + ]); + } + public function testBug13770(): void { $this->reportPossiblyNonexistentGeneralArrayOffset = true; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-14215.php b/tests/PHPStan/Rules/Arrays/data/bug-14215.php new file mode 100644 index 0000000000..c8042198d4 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-14215.php @@ -0,0 +1,122 @@ + $array + * @param positive-int $index + */ + public function positiveIntLessThanCountMinusOne(array $array, int $index): int + { + if ($index < count($array) - 1) { + return $array[$index]; // should not report + } + + return 0; + } + + /** + * @param list $array + * @param positive-int $index + */ + public function positiveIntLessThanOrEqualCountMinusOne(array $array, int $index): int + { + if ($index <= count($array) - 1) { + return $array[$index]; // should not report + } + + return 0; + } + + /** + * @param list $array + */ + public function intLessThanOrEqualCountMinusOne(array $array, int $index): int + { + if ($index <= count($array) - 1) { + return $array[$index]; // should error report, could be negative int + } + + return 0; + } + + /** + * @param list $array + * @param int<0, max> $index + */ + public function nonNegativeIntLessThanCountMinusOne(array $array, int $index): int + { + if ($index < count($array) - 1) { + return $array[$index]; // should not report + } + + return 0; + } + + /** + * @param list $array + * @param int<0, max> $index + */ + public function nonNegativeIntLessThanOrEqualCountMinusOne(array $array, int $index): int + { + if ($index <= count($array) - 1) { + return $array[$index]; // should not report + } + + return 0; + } + + /** + * @param list $array + * @param positive-int $index + */ + public function positiveIntLessThanCountMinusTwo(array $array, int $index): int + { + if ($index < count($array) - 2) { + return $array[$index]; // should not report + } + + return 0; + } + + /** + * @param list $array + * @param positive-int $index + */ + public function positiveIntLessThanOrEqualCountMinusTwo(array $array, int $index): int + { + if ($index <= count($array) - 2) { + return $array[$index]; // should not report + } + + return 0; + } + + /** + * @param list $array + * @param positive-int $index + */ + public function positiveIntLessThanSizeofMinusOne(array $array, int $index): int + { + if ($index < sizeof($array) - 1) { + return $array[$index]; // should not report + } + + return 0; + } + + /** + * @param list $array + * @param positive-int $index + */ + public function positiveIntGreaterThanCountMinusOneInversed(array $array, int $index): int + { + if (count($array) - 1 > $index) { + return $array[$index]; // should not report + } + + return 0; + } +}