diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 76c40b1ae3..4e02b7bfcb 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -940,7 +940,9 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $offsetValueTypeStack[] = $offsetValueType; } - foreach (array_reverse($offsetTypes) as $i => [$offsetType]) { + $reversedOffsetTypes = array_reverse($offsetTypes); + $lastOffsetIndex = count($reversedOffsetTypes) - 1; + foreach ($reversedOffsetTypes as $i => [$offsetType]) { /** @var Type $offsetValueType */ $offsetValueType = array_pop($offsetValueTypeStack); if ( @@ -981,7 +983,19 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } } else { - $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); + // we iterate the offset-types in reversed order. + $isLastDimFetchInChain = $i === 0; + $isFirstDimFetchInChain = $i === $lastOffsetIndex; + + $unionValues = $isLastDimFetchInChain; + if ( + !$isLastDimFetchInChain + && $isFirstDimFetchInChain + && $offsetType !== null + ) { + $unionValues = $this->shouldUnionExistingItemType($offsetValueType, $valueToWrite); + } + $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $unionValues); } if ($arrayDimFetch === null || !$offsetValueType->isList()->yes()) { @@ -1076,4 +1090,34 @@ private function isSameVariable(Expr $a, Expr $b): bool return false; } + private function shouldUnionExistingItemType(Type $offsetValueType, Type $composedValue): bool + { + $existingItemType = $offsetValueType->getIterableValueType(); + + if (!$existingItemType->isConstantArray()->yes() || !$composedValue->isConstantArray()->yes()) { + return false; + } + + foreach ($existingItemType->getConstantArrays() as $existingArray) { + foreach ($existingArray->getKeyTypes() as $i => $keyType) { + if ($composedValue->hasOffsetValueType($keyType)->no()) { + continue; + } + $existingValue = $existingArray->getValueTypes()[$i]; + $newValue = $composedValue->getOffsetValueType($keyType); + + if ($existingValue->isSuperTypeOf($newValue)->yes()) { + continue; + } + if ($newValue->isSuperTypeOf($existingValue)->yes()) { + continue; + } + + return true; + } + } + + return false; + } + } diff --git a/tests/PHPStan/Analyser/nsrt/bug-13623.php b/tests/PHPStan/Analyser/nsrt/bug-13623.php new file mode 100644 index 0000000000..7d473a6830 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13623.php @@ -0,0 +1,24 @@ +}>", $customers); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-13857.php b/tests/PHPStan/Analyser/nsrt/bug-13857.php new file mode 100644 index 0000000000..737f17763e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13857.php @@ -0,0 +1,67 @@ + $array + */ +function test(array $array, int $id): void { + $array[$id]['state'] = 'foo'; + // only one element was set to 'foo', not all of them. + assertType("non-empty-array", $array); +} + +/** + * @param array $array + */ +function testMaybe(array $array, int $id): void { + $array[$id]['state'] = 'foo'; + // only one element was set to 'foo', not all of them. + assertType("non-empty-array", $array); +} + +/** + * @param array $array + */ +function testUnionValue(array $array, int $id): void { + $array[$id]['state'] = 'foo'; + // only one element was set to 'foo', not all of them. + assertType("non-empty-array", $array); +} + +/** + * @param array $array + */ +function testUnionArray(array $array, int $id): void { + $array[$id]['state'] = 'foo'; + // only one element was set to 'foo', not all of them. + assertType("non-empty-array", $array); +} + +/** + * @param array $array + */ +function testUnionArrayDifferentType(array $array, int $id): void { + $array[$id]['state'] = true; + assertType("non-empty-array", $array); +} + +/** + * @param array $array + */ +function testConstantArray(array $array, int $id): void { + $array[$id]['state'] = 'bar'; + assertType("non-empty-array", $array); +} + +/** + * @param array $array + */ +function testConstantArrayNonScalarAssign(array $array, int $id, bool $b): void { + $array[$id]['state'] = $b; + assertType("non-empty-array", $array); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-8270.php b/tests/PHPStan/Analyser/nsrt/bug-8270.php new file mode 100644 index 0000000000..f8bf03e4a1 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8270.php @@ -0,0 +1,47 @@ + $list */ + $list = []; + $list[0]['test'] = true; + + foreach ($list as $item) { + assertType('array{test: bool, value: int}', $item); + if ($item['test']) { + assertType('true', $item['test']); + echo $item['value']; + } + } +} + +function doBar() { + $list = []; + + for ($i = 0; $i < 10; $i++) { + $list[] = [ + 'test' => false, + 'value' => rand(), + ]; + } + + if ($list === []) { + return; + } + + $k = array_key_first($list); + assertType('int<0, max>', $k); + $list[$k]['test'] = true; + + foreach ($list as $item) { + assertType('array{test: bool, value: int<0, max>}', $item); + if ($item['test']) { + echo $item['value']; + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/shopware-connection-profiler.php b/tests/PHPStan/Analyser/nsrt/shopware-connection-profiler.php index ced23ea733..b28e473cc3 100644 --- a/tests/PHPStan/Analyser/nsrt/shopware-connection-profiler.php +++ b/tests/PHPStan/Analyser/nsrt/shopware-connection-profiler.php @@ -29,7 +29,7 @@ public function getGroupedQueries(): void $connectionGroupedQueries[$key]['index'] = $i; // "Explain query" relies on query index in 'queries'. } - assertType("array, count: 0, index: int}|array{sql: string, executionMS: float, types: array, count: int<1, max>, index: int}>", $connectionGroupedQueries); + assertType("array, count: int<0, max>, index: int}>", $connectionGroupedQueries); $connectionGroupedQueries[$key]['executionMS'] += $query['executionMS']; assertType("non-empty-array, count: int<0, max>, index: int}>", $connectionGroupedQueries); ++$connectionGroupedQueries[$key]['count']; diff --git a/tests/PHPStan/Rules/Arrays/data/bug-11679.php b/tests/PHPStan/Rules/Arrays/data/bug-11679.php index 463362516a..1255d3d8e4 100644 --- a/tests/PHPStan/Rules/Arrays/data/bug-11679.php +++ b/tests/PHPStan/Rules/Arrays/data/bug-11679.php @@ -32,6 +32,7 @@ public function sayHello(int $index): bool if (!isset($this->arr[$index]['foo'])) { $this->arr[$index]['foo'] = true; assertType('non-empty-array', $this->arr); + assertType('array{foo: true}', $this->arr[$index]); } assertType('array', $this->arr); return $this->arr[$index]['foo']; // PHPStan does not realize 'foo' is set diff --git a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php index 79b7b484e1..480b24f472 100644 --- a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php @@ -213,4 +213,10 @@ public function testBug4284(): void ]); } + public function testBug8270(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-8270.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php index 1833e4f681..918a6ade23 100644 --- a/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php +++ b/tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php @@ -377,4 +377,9 @@ public function testBug13921(): void ]); } + public function testBug13623(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13623.php'], []); + } + }