diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index df779e633e..fcba95fbac 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -992,6 +992,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar && $arrayDimFetch !== null && $scope->hasExpressionType($arrayDimFetch)->yes() && !$offsetValueType->hasOffsetValueType($offsetType)->no() + && (!$offsetValueType->isList()->yes() || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($offsetType)->yes()) ) { $hasOffsetType = null; if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) { diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 2e5514dc02..bd8cd20d9b 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -981,7 +981,9 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni } } - if ($this->isList()->yes() && $this->getIterableValueType()->isArray()->yes()) { + if ($this->isList()->yes() && $this->getIterableValueType()->isArray()->yes() + && ($offsetType === null || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($offsetType)->yes()) + ) { $result = TypeCombinator::intersect($result, new AccessoryArrayListType()); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-10089.php b/tests/PHPStan/Analyser/nsrt/bug-10089.php index 21122cdfc3..ff8d55eeed 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10089.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10089.php @@ -22,7 +22,7 @@ protected function create_matrix(int $size): array $matrix[$size - 1][8] = 3; // non-empty-array&hasOffsetValue(8, 3)> - assertType('non-empty-list, 0|3>>', $matrix); + assertType('non-empty-array, 0|3>>', $matrix); for ($i = 0; $i <= $size; $i++) { if ($matrix[$i][8] === 0) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-13629.php b/tests/PHPStan/Analyser/nsrt/bug-13629.php index 621b4169bb..ee2724f4a1 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13629.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13629.php @@ -31,7 +31,7 @@ function test(array $xsdFiles, array $groupedByNamespace, array $extraNamespaces } } // After assigning with string keys ($viewHelper['name']), $xsdFiles[$xmlNamespace] should NOT be a list - assertType('array, array{xmlNamespace: string, namespace: string, name: string}>', $xsdFiles[$xmlNamespace]); + assertType('array|string, array{xmlNamespace: string, namespace: string, name: string}>', $xsdFiles[$xmlNamespace]); $xsdFiles[$xmlNamespace] = array_values($xsdFiles[$xmlNamespace]); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14245.php b/tests/PHPStan/Analyser/nsrt/bug-14245.php index 63333b5de2..a7855fc6e0 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14245.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14245.php @@ -144,5 +144,5 @@ function ArrayKeyExistsKeepsList($needle): void { if (array_key_exists($needle, $list)) { $list[$needle] = 37; } - assertType('list', $list); + assertType('array', $list); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14336.php b/tests/PHPStan/Analyser/nsrt/bug-14336.php new file mode 100644 index 0000000000..e606ca92d5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14336.php @@ -0,0 +1,207 @@ + $list + */ +function test(array $list, int $int): void { + $list[$int] = 'foo'; + assertType('non-empty-array', $list); +} + +/** + * @param array> $xsdFiles + * @param array> $groupedByNamespace + * @param array> $extraNamespaces + */ +function test2(array $xsdFiles, array $groupedByNamespace, array $extraNamespaces, int $int): void { + foreach ($extraNamespaces as $mergedNamespace) { + if (count($mergedNamespace) < 2) { + continue; + } + + $targetNamespace = end($mergedNamespace); + if (!isset($groupedByNamespace[$targetNamespace])) { + continue; + } + $xmlNamespace = $groupedByNamespace[$targetNamespace][0]['xmlNamespace']; + + $xsdFiles[$xmlNamespace] = []; + assertType('array{}', $xsdFiles[$xmlNamespace]); + foreach ($mergedNamespace as $namespace) { + foreach ($groupedByNamespace[$namespace] ?? [] as $viewHelper) { + assertType('array{xmlNamespace: string, namespace: string, name: string}', $viewHelper); + $xsdFiles[$xmlNamespace][$int] = $viewHelper; + assertType('non-empty-array', $xsdFiles[$xmlNamespace]); + } + assertType('array', $xsdFiles[$xmlNamespace]); + } + assertType('array', $xsdFiles[$xmlNamespace]); + } +} + +/** + * @param list $list + */ +function testInLoop(array $list, int $int): void { + foreach ([1, 2, 3] as $item) { + $list[$int] = 'foo'; + } + assertType('non-empty-array', $list); +} + +/** + * @param array> $map + */ +function testNestedDimFetchInLoop(array $map, string $key, int $int): void { + $map[$key] = []; + foreach ([1, 2, 3] as $item) { + $map[$key][$int] = 'foo'; + } + assertType('non-empty-array', $map[$key]); +} + +/** + * @param array> $map + * @param list $items + * @param list $items2 + */ +function testDoubleNestedForeachDimFetch(array $map, string $key, int $int, array $items, array $items2): void { + $map[$key] = []; + foreach ($items as $item) { + foreach ($items2 as $item2) { + $map[$key][$int] = $item2; + } + } + assertType('array', $map[$key]); +} + +/** + * @param array> $map + * @param list $items + */ +function testSingleVariableForeach(array $map, string $key, int $int, array $items): void { + $map[$key] = []; + foreach ($items as $item) { + $map[$key][$int] = $item; + } + assertType('array', $map[$key]); +} + +/** + * @param array> $map + * @param list $items + * @param list $outerItems + */ +function testOuterForeach(array $map, string $key, int $int, array $items, array $outerItems): void { + foreach ($outerItems as $outerItem) { + $map[$key] = []; + foreach ($items as $item) { + $map[$key][$int] = $item; + } + assertType('array', $map[$key]); + } +} + +/** + * @param array> $map + * @param list $items + * @param list $outerItems + */ +function testOuterForeachWithContinue(array $map, string $key, int $int, array $items, array $outerItems): void { + foreach ($outerItems as $outerItem) { + if (strlen($outerItem) < 2) { + continue; + } + $map[$key] = []; + foreach ($items as $item) { + $map[$key][$int] = $item; + } + assertType('array', $map[$key]); + } +} + +/** + * @param array> $map + * @param list> $nestedItems + * @param list $outerItems + */ +function testNestedInnerForeach(array $map, string $key, int $int, array $nestedItems, array $outerItems): void { + foreach ($outerItems as $outerItem) { + if (strlen($outerItem) < 2) { + continue; + } + $map[$key] = []; + foreach ($nestedItems as $items) { + foreach ($items as $item) { + $map[$key][$int] = $item; + } + } + assertType('array', $map[$key]); + } +} + +/** + * @param array> $map + * @param array> $nestedItems + * @param list $outerItems + */ +function testNestedInnerForeachNullCoalesce(array $map, string $key, int $int, array $nestedItems, array $outerItems): void { + foreach ($outerItems as $outerItem) { + if (strlen($outerItem) < 2) { + continue; + } + $map[$key] = []; + foreach ($outerItems as $ns) { + foreach ($nestedItems[$ns] ?? [] as $item) { + $map[$key][$int] = $item; + } + } + assertType('array', $map[$key]); + } +} + +/** + * @param array> $map + * @param array> $grouped + * @param array> $extra + */ +function testCloseToOriginal(array $map, array $grouped, array $extra, int $int): void { + foreach ($extra as $merged) { + if (count($merged) < 2) { + continue; + } + $target = end($merged); + if (!isset($grouped[$target])) { + continue; + } + $key = $grouped[$target][0]['ns']; + + $map[$key] = []; + foreach ($merged as $ns) { + foreach ($grouped[$ns] ?? [] as $item) { + $map[$key][$int] = $item; + } + } + assertType('array', $map[$key]); + } +} + +/** + * @param list $list + */ +function testAppend(array $list): void { + $list[] = 'foo'; + assertType('non-empty-list', $list); +} + +/** + * @param list $list + */ +function testLiteralZero(array $list): void { + $list[0] = 'foo'; + assertType("non-empty-list&hasOffsetValue(0, 'foo')", $list); +}