diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 288caefdc6..d0c85461ed 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -408,6 +408,24 @@ public function setExistingOffsetValueType(Type $offsetType, Type $valueType): T $newItemType, ); } + } elseif ( + $this->itemType->isArray()->yes() + && !$this->itemType->isConstantArray()->yes() + && $valueType->isArray()->yes() + && !$valueType->isConstantArray()->yes() + && $this->itemType->getIterableValueType()->isArray()->yes() + && $valueType->getIterableValueType()->isArray()->yes() + ) { + $newItemType = $this->itemType->setExistingOffsetValueType( + $valueType->getIterableKeyType(), + $valueType->getIterableValueType(), + ); + if ($newItemType !== $this->itemType) { + return new self( + $this->keyType, + $newItemType, + ); + } } return new self( diff --git a/tests/PHPStan/Analyser/nsrt/bug-13637.php b/tests/PHPStan/Analyser/nsrt/bug-13637.php new file mode 100644 index 0000000000..fe374bb1af --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13637.php @@ -0,0 +1,136 @@ +>> +*/ +function doesNotWork() : array { + $final = []; + + for ($i = 0; $i < 5; $i++) { + $j = $i * 2; + $k = $j +1; + $l = $i * 3; + $final[$i][$j][$k]['abc'] = $i; + $final[$i][$j][$k]['def'] = $i; + $final[$i][$j][$k]['ghi'] = $i; + + assertType("array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}", $final[$i][$j][$k]); + assertType("non-empty-array, non-empty-array, non-empty-array, array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}>>>", $final); + } + + assertType("non-empty-array, non-empty-array, non-empty-array, array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}>>>", $final); + return $final; +} + +/** +* @return array>>> +*/ +function fourLevelsDeep() : array { + $final = []; + + for ($i = 0; $i < 5; $i++) { + $j = $i * 2; + $k = $j + 1; + $l = $i * 3; + $final[$i][$j][$k][$l]['abc'] = $i; + $final[$i][$j][$k][$l]['def'] = $i; + $final[$i][$j][$k][$l]['ghi'] = $i; + + assertType("array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}", $final[$i][$j][$k][$l]); + assertType("non-empty-array, non-empty-array, non-empty-array, non-empty-array, array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}>>>>", $final); + } + + assertType("non-empty-array, non-empty-array, non-empty-array, non-empty-array, array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}>>>>", $final); + return $final; +} + +/** +* @return array>>>> +*/ +function fiveLevelsDeep() : array { + $final = []; + + for ($i = 0; $i < 5; $i++) { + $j = $i * 2; + $k = $j + 1; + $l = $i * 3; + $m = $i + 10; + $final[$i][$j][$k][$l][$m]['abc'] = $i; + $final[$i][$j][$k][$l][$m]['def'] = $i; + $final[$i][$j][$k][$l][$m]['ghi'] = $i; + + assertType("array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}", $final[$i][$j][$k][$l][$m]); + assertType("non-empty-array, non-empty-array, non-empty-array, non-empty-array, non-empty-array, array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}>>>>>", $final); + } + + assertType("non-empty-array, non-empty-array, non-empty-array, non-empty-array, non-empty-array, array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}>>>>>", $final); + return $final; +} + +/** +* @return array>>>>> +*/ +function sixLevelsDeep() : array { + $final = []; + + for ($i = 0; $i < 5; $i++) { + $j = $i * 2; + $k = $j + 1; + $l = $i * 3; + $m = $i + 10; + $n = $i + 20; + $final[$i][$j][$k][$l][$m][$n]['abc'] = $i; + $final[$i][$j][$k][$l][$m][$n]['def'] = $i; + $final[$i][$j][$k][$l][$m][$n]['ghi'] = $i; + + assertType("array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}", $final[$i][$j][$k][$l][$m][$n]); + assertType("non-empty-array, non-empty-array, non-empty-array, non-empty-array, non-empty-array, non-empty-array, array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}>>>>>>", $final); + } + + assertType("non-empty-array, non-empty-array, non-empty-array, non-empty-array, non-empty-array, non-empty-array, array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}>>>>>>", $final); + return $final; +} + +/** Tests that maybe-array item type (union with non-array) skips the recursive path */ +function maybeArrayItemType(bool $flag): void { + $final = []; + + for ($i = 0; $i < 5; $i++) { + $j = $i * 2; + $k = $j + 1; + if ($flag) { + $final[$i][$j][$k]['abc'] = $i; + $final[$i][$j][$k]['def'] = $i; + } else { + $final[$i] = $i; + } + } + + assertType("non-empty-array, non-empty-array, non-empty-array, array{abc: int<0, 4>, def: int<0, 4>}>>|int<0, 4>>", $final); +} + +/** +* @return array> +*/ +function thisWorks() : array { + $final = []; + + for ($i = 0; $i < 5; $i++) { + $j = $i * 2; + $k = $j +1; + $l = $i * 3; + $final[$i][$j]['abc'] = $i; + $final[$i][$j]['def'] = $i; + $final[$i][$j]['ghi'] = $i; + + assertType("array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}", $final[$i][$j]); + assertType("non-empty-array, non-empty-array, array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}>>", $final); + } + + assertType("non-empty-array, non-empty-array, array{abc: int<0, 4>, def: int<0, 4>, ghi: int<0, 4>}>>", $final); + return $final; +}