diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 51995ac665..d8216a07ca 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -1057,11 +1057,13 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } $arrayDimFetch = $dimFetchStack[$i] ?? null; + $isContainerLevel = $i + 1 < count($dimFetchStack); if ( $offsetType !== null && $arrayDimFetch !== null && $scope->hasExpressionType($arrayDimFetch)->yes() && !$offsetValueType->hasOffsetValueType($offsetType)->no() + && (!$isContainerLevel || !$scope->getType($arrayDimFetch)->hasOffsetValueType($offsetType)->no()) ) { $hasOffsetType = null; if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) { diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 10482dae04..b7fd5a2b0e 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -987,7 +987,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni if ( $this->isList()->yes() && $offsetType !== null - && $offsetType->toArrayKey()->isInteger()->yes() + && $this->getIterableKeyType()->isSuperTypeOf($offsetType)->yes() && $this->getIterableValueType()->isArray()->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 01aafa9790..ff23c96194 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-10089.php +++ b/tests/PHPStan/Analyser/nsrt/bug-10089.php @@ -24,7 +24,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-14336.php b/tests/PHPStan/Analyser/nsrt/bug-14336.php new file mode 100644 index 0000000000..2470a3b255 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14336.php @@ -0,0 +1,156 @@ +> $xsdFiles + * @param array> $groupedByNamespace + * @param array> $extraNamespaces + */ +function testIssueReproducer(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']; + + assertType('string', $xmlNamespace); + assertType('non-empty-list&hasOffsetValue(1, string)', $mergedNamespace); + + $xsdFiles[$xmlNamespace] = []; + foreach ($mergedNamespace as $namespace) { + foreach ($groupedByNamespace[$namespace] ?? [] as $viewHelper) { + $xsdFiles[$xmlNamespace][$int] = $viewHelper; + } + } + // After assigning any int, $xsdFiles[$xmlNamespace] should NOT be a list + assertType('array', $xsdFiles[$xmlNamespace]); + $xsdFiles[$xmlNamespace] = array_values($xsdFiles[$xmlNamespace]); + } +} + +/** + * Simplified: nested dim fetch after reset with arbitrary int key. + * + * @param array> $arr + */ +function testNestedDimFetchAfterReset(array $arr, int $int, string $key): void +{ + $arr[$key] = []; + $arr[$key][$int] = ['name' => 'test']; + assertType("non-empty-array", $arr[$key]); +} + +/** + * Assigning with arbitrary int key in a loop should degrade list to array. + * + * @param list $list + * @param array $intMap + */ +function testAssignAnyIntInLoop(array $list, array $intMap): void +{ + foreach ($intMap as $intKey => $intValue) { + $list[$intKey] = ['abc' => 'def']; + } + assertType("array", $list); +} + +/** + * @param list $list + * @param int $intKey + */ +function testAssignAnyIntOutsideLoop(array $list, int $intKey): void +{ + $list[$intKey] = 'foo'; + assertType("non-empty-array", $list); +} + +/** + * Safe patterns should still preserve list. + * + * @param list $list + */ +function testKeepListWithAppend(array $list): void +{ + $list[] = 'foo'; + assertType("non-empty-list", $list); +} + +/** + * @param list $list + */ +function testKeepListWithConstantZero(array $list): void +{ + $list[0] = 'foo'; + assertType("non-empty-list&hasOffsetValue(0, 'foo')", $list); +} + +/** + * Nested array assignment in loop should keep outer list when key comes from iteration. + * + * @param list> $list + */ +function testNestedAssignKeepsList(array $list): void +{ + foreach ($list as $k => $v) { + $list[$k]['abc'] = 'world'; + } + assertType("list&hasOffsetValue('abc', 'world')>", $list); +} + +/** + * @param list> $list + * @param int $intKey + */ +function testNestedListAssignWithAnyInt(array $list, int $intKey): void +{ + $list[$intKey] = ['foo']; + assertType("non-empty-array>", $list); +} + +/** + * Assigning with negative int key should also degrade list. + * + * @param list $list + * @param int $negativeKey + */ +function testAssignNegativeInt(array $list, int $negativeKey): void +{ + $list[$negativeKey] = 'foo'; + assertType("non-empty-array", $list); +} + +/** + * Assigning with int<0, max> should still keep list (valid range). + * + * @param list> $list + * @param int<0, max> $nonNegativeKey + */ +function testAssignNonNegativeIntWithArrayValue(array $list, int $nonNegativeKey): void +{ + $list[$nonNegativeKey] = ['foo']; + assertType("non-empty-list>", $list); +} + +/** + * Direct scalar assignment with int<0, max> key. + * + * @param list $list + * @param int<0, max> $nonNegativeKey + */ +function testAssignNonNegativeIntWithScalarValue(array $list, int $nonNegativeKey): void +{ + $list[$nonNegativeKey] = 'foo'; + assertType("non-empty-array, string>", $list); +}