diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 0949e9eaf9..ec9cb809c0 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -333,6 +333,22 @@ public function specifyTypesInCondition( ); } } + + // infer $list[$index] after $index < count($list) + if ( + $context->true() + && !$orEqual + // constant offsets are handled via HasOffsetType/HasOffsetValueType + && !$leftType instanceof ConstantIntegerType + && $argType->isList()->yes() + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() + ) { + $arrayArg = $expr->right->getArgs()[0]->value; + $dimFetch = new ArrayDimFetch($arrayArg, $expr->left); + $result = $result->unionWith( + $this->create($dimFetch, $argType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr), + ); + } } if ( diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index f877c5dec7..282e7a2402 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1155,4 +1155,32 @@ public function testBug11276(): void $this->analyse([__DIR__ . '/data/bug-11276.php'], []); } + public function testBug13770(): void + { + $this->reportPossiblyNonexistentGeneralArrayOffset = true; + + $this->analyse([__DIR__ . '/data/bug-13770.php'], [ + [ + 'Offset int<1, max> might not exist on non-empty-list.', + 53, + ], + [ + 'Offset int might not exist on list.', + 66, + ], + [ + 'Offset int might not exist on list.', + 91, + ], + [ + 'Offset int might not exist on array.', + 100, + ], + [ + 'Offset -1|3|6|10 might not exist on list.', + 126, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13770.php b/tests/PHPStan/Rules/Arrays/data/bug-13770.php new file mode 100644 index 0000000000..cbd1748dfb --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-13770.php @@ -0,0 +1,131 @@ + $array + * @param positive-int $index + */ + public function positiveIntLessThanCount(array $array, int $index): int + { + if ($index < count($array)) { + return $array[$index]; // should not report + } + + return 0; + } + + /** + * @param list $array + * @param positive-int $index + */ + public function positiveIntLessThanCountInversed(array $array, int $index): int + { + if (count($array) > $index) { + return $array[$index]; // should not report + } + + return 0; + } + + /** + * @param list $array + * @param int<0, max> $index + */ + public function nonNegativeIntLessThanCount(array $array, int $index): int + { + if ($index < count($array)) { + return $array[$index]; // should not report + } + + return 0; + } + + /** + * @param list $array + * @param positive-int $index + */ + public function positiveIntLessThanOrEqualCount(array $array, int $index): int + { + if ($index <= count($array)) { + return $array[$index]; // SHOULD still report - off by one + } + + return 0; + } + + /** + * @param list $array + * @param int $index + */ + public function anyIntLessThanCount(array $array, int $index): int + { + if ($index < count($array)) { + return $array[$index]; // SHOULD still report - could be negative + } + + return 0; + } + + /** + * @param list $array + * @param int<0, max> $index + */ + public function positiveIntOnNormalCountMode(array $array, int $index): int + { + if ($index < count($array, COUNT_NORMAL)) { + return $array[$index]; // should not error + } + + return 0; + } + + /** + * @param list $array + */ + public function anyIntOnUnknownCountMode(array $array, int $index, $countMode): int + { + if ($index < count($array, $countMode)) { + return $array[$index]; // SHOULD still report - could be negative + } + + return 0; + } + + public function anyIntOnRecursiveCount(array $array, int $index): int + { + if ($index < count($array, COUNT_RECURSIVE)) { + return $array[$index]; // SHOULD still report - could be negative + } + + return 0; + } + + /** + * @param list $array + * @param 3|6|10 $index + */ + public function constantPositiveIntLessThanCount(array $array, int $index): int + { + if ($index < count($array)) { + return $array[$index]; // should not report + } + + return 0; + } + + /** + * @param list $array + * @param -1|3|6|10 $index + */ + public function constantMaybeNegativeIntLessThanCount(array $array, int $index): int + { + if ($index < count($array)) { + return $array[$index]; // SHOULD still report - could be negative + } + + return 0; + } +}