From e88370fdbb6d8b89331d5ce5017141728605b0a0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 25 Apr 2025 17:19:11 +0200 Subject: [PATCH 01/21] Keep list on unset() with nested dim-fetch --- src/Type/Accessory/AccessoryArrayListType.php | 7 +++++++ .../Rules/Methods/ReturnTypeRuleTest.php | 5 +++++ tests/PHPStan/Rules/Methods/data/bug-12927.php | 18 ++++++++++++++++++ .../ParameterOutAssignedTypeRuleTest.php | 2 +- 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-12927.php diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 5f60fe8eb7..bad91200cd 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -160,6 +160,13 @@ public function setExistingOffsetValueType(Type $offsetType, Type $valueType): T return $this; } + if ( + $valueType->isArray()->yes() + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($offsetType)->yes() + ) { + return $this; + } + return new ErrorType(); } diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index ec7bf57f05..aad2f581eb 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -1242,6 +1242,11 @@ public function testBug1O580(): void ]); } + public function testBug12927(): void + { + $this->analyse([__DIR__ . '/data/bug-12927.php'], []); + } + public function testBug4443(): void { if (PHP_VERSION_ID < 80000) { diff --git a/tests/PHPStan/Rules/Methods/data/bug-12927.php b/tests/PHPStan/Rules/Methods/data/bug-12927.php new file mode 100644 index 0000000000..5510cc5096 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12927.php @@ -0,0 +1,18 @@ + $list + * @return list> + */ + public function sayHello(array $list): array + { + foreach($list as $k => $v) { + unset($list[$k]['abc']); + } + return $list; + } +} diff --git a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php index f8268f8fcd..222960d077 100644 --- a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php +++ b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php @@ -43,7 +43,7 @@ public function testRule(): void 47, ], [ - 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doBaz3() expects list>, array, array, int>> given.', + 'Parameter &$p @param-out type of method ParameterOutAssignedType\Foo::doBaz3() expects list>, list, int>> given.', 56, ], [ From 7f6c24ededf999fc68f218fbb008c3c549c08829 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 25 Apr 2025 17:42:44 +0200 Subject: [PATCH 02/21] added type assertions --- .../PHPStan/Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Rules/Methods/data/bug-12927.php | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 2bfafc8404..1df4b1a0e3 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -214,6 +214,7 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Arrays/data/bug-11679.php'; yield __DIR__ . '/../Rules/Methods/data/bug-4801.php'; yield __DIR__ . '/../Rules/Arrays/data/narrow-superglobal.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-12927.php'; } /** diff --git a/tests/PHPStan/Rules/Methods/data/bug-12927.php b/tests/PHPStan/Rules/Methods/data/bug-12927.php index 5510cc5096..9293ebc0a9 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-12927.php +++ b/tests/PHPStan/Rules/Methods/data/bug-12927.php @@ -2,6 +2,8 @@ namespace Bug12927; +use function PHPStan\Testing\assertType; + class HelloWorld { /** @@ -12,6 +14,22 @@ public function sayHello(array $list): array { foreach($list as $k => $v) { unset($list[$k]['abc']); + assertType('non-empty-list', $list); + assertType('array{}|array{abc: string}', $list[$k]); + } + return $list; + } + + /** + * @param list> $list + * @return list> + */ + public function sayFoo(array $list): array + { + foreach($list as $k => $v) { + unset($list[$k]['abc']); + assertType('non-empty-list>', $list); + assertType('array', $list[$k]); } return $list; } From 8b4760f2a1515b5a3c380dde139db84f9fa7656a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 25 Apr 2025 18:36:38 +0200 Subject: [PATCH 03/21] test overwriting elements --- src/Type/Accessory/AccessoryArrayListType.php | 7 ++++ .../PHPStan/Rules/Methods/data/bug-12927.php | 33 +++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index bad91200cd..babaf69aca 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -151,6 +151,13 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } + if ( + $valueType->isArray()->yes() + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($offsetType)->yes() + ) { + return $this; + } + return new ErrorType(); } diff --git a/tests/PHPStan/Rules/Methods/data/bug-12927.php b/tests/PHPStan/Rules/Methods/data/bug-12927.php index 9293ebc0a9..0331446aec 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-12927.php +++ b/tests/PHPStan/Rules/Methods/data/bug-12927.php @@ -22,15 +22,42 @@ public function sayHello(array $list): array /** * @param list> $list - * @return list> */ - public function sayFoo(array $list): array + public function sayFoo(array $list): void { foreach($list as $k => $v) { unset($list[$k]['abc']); assertType('non-empty-list>', $list); assertType('array', $list[$k]); } - return $list; + assertType('list>', $list); + } + + /** + * @param list> $list + */ + public function sayFoo2(array $list): void + { + foreach($list as $k => $v) { + $list[$k]['abc'] = 'world'; + assertType("non-empty-list&hasOffsetValue('abc', 'world')>", $list); + assertType("non-empty-array&hasOffsetValue('abc', 'world')", $list[$k]); + } + assertType("list&hasOffsetValue('abc', 'world')>", $list); + } + + /** + * @param list> $list + */ + public function sayFooBar(array $list): void + { + foreach($list as $k => $v) { + if (rand(0,1)) { + unset($list[$k]); + } + assertType('array, array>', $list); + assertType('array', $list[$k]); + } + assertType('array', $list[$k]); } } From c16b2246cc1d76c4c588888c41f80780e06e7d84 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 09:28:09 +0200 Subject: [PATCH 04/21] Added regression test --- .../ParameterOutExecutionEndTypeRuleTest.php | 5 ++++ .../Rules/Variables/data/bug-12330.php | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-12330.php diff --git a/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php index 5929aad03a..8bee23c88b 100644 --- a/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php +++ b/tests/PHPStan/Rules/Variables/ParameterOutExecutionEndTypeRuleTest.php @@ -58,4 +58,9 @@ public function testBug11363(): void $this->analyse([__DIR__ . '/data/bug-11363.php'], []); } + public function testBug12330(): void + { + $this->analyse([__DIR__ . '/data/bug-12330.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-12330.php b/tests/PHPStan/Rules/Variables/data/bug-12330.php new file mode 100644 index 0000000000..46fe32e09f --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-12330.php @@ -0,0 +1,25 @@ +>} $options + * @param-out array{items: list>} $options + */ +function alterItems(array &$options): void +{ + foreach ($options['items'] as $i => $item) { + $options['items'][$i]['options']['title'] = $item['name']; + } +} + +/** + * @param array{items: array>} $options + * @param-out array{items: array>} $options + */ +function alterItems(array &$options): void +{ + foreach ($options['items'] as $i => $item) { + $options['items'][$i]['options']['title'] = $item['name']; + } +} From 3fd8ce55fdd63c4d1f802c2622918a7fafd87d1b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 09:52:07 +0200 Subject: [PATCH 05/21] Added regression test --- .../TypesAssignedToPropertiesRuleTest.php | 6 +++ .../Rules/Properties/data/bug-11171.php | 41 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-11171.php diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 8d050e7636..ec84d02764 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -779,4 +779,10 @@ public function testPropertyHooks(): void ]); } + public function testBug11171(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-11171.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-11171.php b/tests/PHPStan/Rules/Properties/data/bug-11171.php new file mode 100644 index 0000000000..688e1c501c --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-11171.php @@ -0,0 +1,41 @@ + + */ + public array $innerTypeExpressions = []; + + /** + * @param \Closure(self): void $callback + */ + public function walkTypes(\Closure $callback): void + { + $startIndexOffset = 0; + + foreach ($this->innerTypeExpressions as $k => ['start_index' => $startIndexOrig, + 'expression' => $inner,]) { + $this->innerTypeExpressions[$k]['start_index'] += $startIndexOffset; + + $innerLengthOrig = \strlen($inner->value); + + $inner->walkTypes($callback); + + $this->value = substr_replace( + $this->value, + $inner->value, + $startIndexOrig + $startIndexOffset, + $innerLengthOrig + ); + + $startIndexOffset += \strlen($inner->value) - $innerLengthOrig; + } + + $callback($this); + } +} From f17a8003b1676921870224959f142992bee71fc1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 09:56:57 +0200 Subject: [PATCH 06/21] Added regression test --- .../TypesAssignedToPropertiesRuleTest.php | 10 ++++++++ .../Rules/Properties/data/bug-8282.php | 25 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-8282.php diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index ec84d02764..90c1bdf9ae 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -785,4 +785,14 @@ public function testBug11171(): void $this->analyse([__DIR__ . '/data/bug-11171.php'], []); } + public function testBug8282(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-8282.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-8282.php b/tests/PHPStan/Rules/Properties/data/bug-8282.php new file mode 100644 index 0000000000..f7276bb55e --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8282.php @@ -0,0 +1,25 @@ + $list */ + public function __construct( + public array $list + ) + { + } + + public function updateNameById(int $id, string $name): void + { + foreach ($this->list as $index => $entry) { + if ($entry['id'] === $id) { + $this->list[$index]['name'] = $name; + } + } + } +} From 0b230df998f25d0af237ca64cfc3c559e38ce395 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 10:00:19 +0200 Subject: [PATCH 07/21] Added regression test --- .../ParameterOutAssignedTypeRuleTest.php | 5 ++++ .../Rules/Variables/data/bug-12754.php | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-12754.php diff --git a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php index 222960d077..1f001e437c 100644 --- a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php +++ b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php @@ -64,4 +64,9 @@ public function testBenevolentArrayKey(): void $this->analyse([__DIR__ . '/data/benevolent-array-key.php'], []); } + public function testBug12754(): void + { + $this->analyse([__DIR__ . '/data/bug-12754.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-12754.php b/tests/PHPStan/Rules/Variables/data/bug-12754.php new file mode 100644 index 0000000000..e8269ff4d0 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-12754.php @@ -0,0 +1,26 @@ + $list + * @return void + */ + public function modify(array &$list): void + { + foreach ($list as $int => $array) { + $list[$int][1] = $this->apply($array[1]); + } + } + + /** + * @param string $value + * @return string + */ + public function apply(string $value): mixed + { + return $value; + } +} From dda1203c5c6cc1151359e46ea2a66962df0a34c3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 10:04:30 +0200 Subject: [PATCH 08/21] Update bug-12330.php --- tests/PHPStan/Rules/Variables/data/bug-12330.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Variables/data/bug-12330.php b/tests/PHPStan/Rules/Variables/data/bug-12330.php index 46fe32e09f..d2e2f08a38 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-12330.php +++ b/tests/PHPStan/Rules/Variables/data/bug-12330.php @@ -17,7 +17,7 @@ function alterItems(array &$options): void * @param array{items: array>} $options * @param-out array{items: array>} $options */ -function alterItems(array &$options): void +function alterItems2(array &$options): void { foreach ($options['items'] as $i => $item) { $options['items'][$i]['options']['title'] = $item['name']; From e7978fac39536f39ce913c51cc55fc84cc3bbeb3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 10:09:47 +0200 Subject: [PATCH 09/21] fix min php version --- tests/PHPStan/Rules/Properties/data/bug-8282.php | 2 +- .../Rules/Variables/ParameterOutAssignedTypeRuleTest.php | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Properties/data/bug-8282.php b/tests/PHPStan/Rules/Properties/data/bug-8282.php index f7276bb55e..b82c8f5ab1 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-8282.php +++ b/tests/PHPStan/Rules/Properties/data/bug-8282.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug8282; diff --git a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php index 1f001e437c..fa6c8636b5 100644 --- a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php +++ b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php @@ -66,6 +66,9 @@ public function testBenevolentArrayKey(): void public function testBug12754(): void { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('PHP 8.0+ is required for this test.'); + } $this->analyse([__DIR__ . '/data/bug-12754.php'], []); } From ee5f26b8824c3099e20ebcb0995b09cec69eec3b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 10:18:11 +0200 Subject: [PATCH 10/21] cs --- .../PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php index fa6c8636b5..3c6585eade 100644 --- a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php +++ b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\Rule as TRule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase From 2f622b72b3644bf456570c0813c98d9ca22d7a35 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 10:30:39 +0200 Subject: [PATCH 11/21] fix remaining part of bug8282 --- src/Type/IntersectionType.php | 3 +++ tests/PHPStan/Rules/Properties/data/bug-8282.php | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index bfe0e31471..49952f64ed 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -799,6 +799,9 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni } $result = $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); + if ($this->isList()->yes() && $valueType->isArray()->yes()) { + $result = TypeCombinator::intersect($result, new AccessoryArrayListType()); + } if ( $offsetType !== null diff --git a/tests/PHPStan/Rules/Properties/data/bug-8282.php b/tests/PHPStan/Rules/Properties/data/bug-8282.php index b82c8f5ab1..faaa9a103a 100644 --- a/tests/PHPStan/Rules/Properties/data/bug-8282.php +++ b/tests/PHPStan/Rules/Properties/data/bug-8282.php @@ -14,6 +14,12 @@ public function __construct( { } + public function updateName(int $index, string $name): void + { + assert(isset($this->list[$index])); + $this->list[$index]['name'] = $name; + } + public function updateNameById(int $id, string $name): void { foreach ($this->list as $index => $entry) { From 194652c5e3b4f342690a5bd68c204cad2f9a0060 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 14:38:29 +0200 Subject: [PATCH 12/21] simplify --- src/Type/Accessory/AccessoryArrayListType.php | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index babaf69aca..5c92cb2d4f 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -163,18 +163,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()) { - return $this; - } - - if ( - $valueType->isArray()->yes() - && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($offsetType)->yes() - ) { - return $this; - } - - return new ErrorType(); + return $this; } public function unsetOffset(Type $offsetType): Type From 1016b288610bb991025f9a28cbad2a1706d19777 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 27 Apr 2025 11:51:40 +0200 Subject: [PATCH 13/21] Use Type->setExistingOffsetValueType() more --- src/Analyser/NodeScopeResolver.php | 11 +++++++- src/Type/Accessory/AccessoryArrayListType.php | 7 ----- src/Type/Accessory/HasOffsetValueType.php | 4 +++ src/Type/ArrayType.php | 27 ++++++++++++++++--- src/Type/Constant/ConstantArrayType.php | 6 +++++ src/Type/IntersectionType.php | 7 ++--- 6 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 58aa455c4a..43f6ec079d 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5924,9 +5924,18 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar } $offsetValueType = TypeCombinator::intersect($offsetValueType, TypeCombinator::union(...$types)); } - $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); $arrayDimFetch = $dimFetchStack[$i] ?? null; + if ( + $offsetType !== null + && $arrayDimFetch !== null + && $scope->hasExpressionType($arrayDimFetch)->yes() + ) { + $valueToWrite = $offsetValueType->setExistingOffsetValueType($offsetType, $valueToWrite); + } else { + $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); + } + if ($arrayDimFetch === null || !$offsetValueType->isList()->yes()) { continue; } diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 5c92cb2d4f..eb08e0c8c9 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -151,13 +151,6 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni return $this; } - if ( - $valueType->isArray()->yes() - && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($offsetType)->yes() - ) { - return $this; - } - return new ErrorType(); } diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index ec6e822a31..4e476d7e2e 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -184,6 +184,10 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { + if (!$offsetType->equals($this->offsetType)) { + return $this; + } + return new self($this->offsetType, $valueType); } diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index e6c0097db7..5081c38a1f 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -356,9 +356,30 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - return new self( - $this->keyType, - TypeCombinator::union($this->itemType, $valueType), + if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) { + if ($offsetType->isSuperTypeOf($this->keyType)->yes()) { + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->setOffsetValueType($offsetType, $valueType); + return $builder->getArray(); + } + + return TypeCombinator::intersect( + new self( + TypeCombinator::union($this->keyType, $offsetType), + TypeCombinator::union($this->itemType, $valueType), + ), + new HasOffsetValueType($offsetType, $valueType), + new NonEmptyArrayType(), + ); + } + + + return TypeCombinator::intersect( + new self( + $this->keyType, + TypeCombinator::union($this->itemType, $valueType) + ), + new NonEmptyArrayType(), ); } diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 01476d4d01..a021215015 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -694,11 +694,17 @@ public function setExistingOffsetValueType(Type $offsetType, Type $valueType): T { $offsetType = $offsetType->toArrayKey(); $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + $unionValues = $offsetType instanceof UnionType && count($offsetType->getTypes()) > 1; foreach ($this->keyTypes as $keyType) { if ($offsetType->isSuperTypeOf($keyType)->no()) { continue; } + if ($unionValues) { + $builder->setOffsetValueType($keyType, TypeCombinator::union($this->getOffsetValueType($keyType), $valueType)); + continue; + } + $builder->setOffsetValueType($keyType, $valueType); } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 49952f64ed..dee2515ad8 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -799,9 +799,6 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni } $result = $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); - if ($this->isList()->yes() && $valueType->isArray()->yes()) { - $result = TypeCombinator::intersect($result, new AccessoryArrayListType()); - } if ( $offsetType !== null @@ -829,6 +826,10 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni } } + if ($this->isList()->yes() && $this->getIterableValueType()->isArray()->yes()) { + $result = TypeCombinator::intersect($result, new AccessoryArrayListType()); + } + return $result; } From ab29821338f475c551faa66649ff24da6d934dd4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 27 Apr 2025 13:06:51 +0200 Subject: [PATCH 14/21] adjust bug8113 expectations --- tests/PHPStan/Rules/Variables/data/bug-8113.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Variables/data/bug-8113.php b/tests/PHPStan/Rules/Variables/data/bug-8113.php index 27ebe729ae..49bbbc89bb 100644 --- a/tests/PHPStan/Rules/Variables/data/bug-8113.php +++ b/tests/PHPStan/Rules/Variables/data/bug-8113.php @@ -34,7 +34,7 @@ function () { ]; assertType("non-empty-array>&hasOffsetValue('Review', array{id: null, text: null, answer: null})&hasOffsetValue('SurveyInvitation', non-empty-array&hasOffsetValue('review', null))", $review); unset($review['SurveyInvitation']['review']); - assertType("non-empty-array>&hasOffsetValue('Review', array)&hasOffsetValue('SurveyInvitation', array)", $review); + assertType("non-empty-array>&hasOffsetValue('Review', array{id: null, text: null, answer: null})&hasOffsetValue('SurveyInvitation', array)", $review); } assertType('array>', $review); if (array_key_exists('User', $review['Review'])) { @@ -42,7 +42,7 @@ function () { $review['User'] = $review['Review']['User']; assertType("non-empty-array&hasOffsetValue('Review', non-empty-array&hasOffset('User'))&hasOffsetValue('User', mixed)", $review); unset($review['Review']['User']); - assertType("non-empty-array&hasOffsetValue('Review', array)&hasOffsetValue('User', array)", $review); + assertType("non-empty-array&hasOffsetValue('Review', array)&hasOffsetValue('User', mixed)", $review); } assertType("non-empty-array&hasOffsetValue('Review', array)", $review); }; From 8f866933782038f3fc325b28e1ddb674c4fcd097 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 27 Apr 2025 13:06:59 +0200 Subject: [PATCH 15/21] less precise types --- tests/PHPStan/Analyser/nsrt/bug-12274.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12274.php b/tests/PHPStan/Analyser/nsrt/bug-12274.php index 437dc09ae3..f0536a0c15 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12274.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12274.php @@ -56,8 +56,8 @@ function testKeepNestedListAfterIssetIndex(array $nestedList, int $i, int $j): v assertType('list>', $nestedList); assertType('list', $nestedList[$i]); $nestedList[$i][$j] = 21; - assertType('non-empty-list>', $nestedList); - assertType('non-empty-list', $nestedList[$i]); + assertType('non-empty-list>', $nestedList); + assertType('list', $nestedList[$i]); } assertType('list>', $nestedList); } From adff669efc548230d782f696e13453ff0814dfa6 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 27 Apr 2025 13:08:06 +0200 Subject: [PATCH 16/21] fix cs --- src/Type/ArrayType.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 5081c38a1f..e498577309 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -373,11 +373,10 @@ public function setExistingOffsetValueType(Type $offsetType, Type $valueType): T ); } - return TypeCombinator::intersect( new self( $this->keyType, - TypeCombinator::union($this->itemType, $valueType) + TypeCombinator::union($this->itemType, $valueType), ), new NonEmptyArrayType(), ); From 88741229ee5420467ef3c4e3c1b09bd5a1960900 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 27 Apr 2025 13:41:39 +0200 Subject: [PATCH 17/21] simplify --- src/Type/ArrayType.php | 6 ------ src/Type/Constant/ConstantArrayType.php | 15 +-------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index e498577309..ca745fdf09 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -357,12 +357,6 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) { - if ($offsetType->isSuperTypeOf($this->keyType)->yes()) { - $builder = ConstantArrayTypeBuilder::createEmpty(); - $builder->setOffsetValueType($offsetType, $valueType); - return $builder->getArray(); - } - return TypeCombinator::intersect( new self( TypeCombinator::union($this->keyType, $offsetType), diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index a021215015..3dde11b16e 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -692,21 +692,8 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - $offsetType = $offsetType->toArrayKey(); $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); - $unionValues = $offsetType instanceof UnionType && count($offsetType->getTypes()) > 1; - foreach ($this->keyTypes as $keyType) { - if ($offsetType->isSuperTypeOf($keyType)->no()) { - continue; - } - - if ($unionValues) { - $builder->setOffsetValueType($keyType, TypeCombinator::union($this->getOffsetValueType($keyType), $valueType)); - continue; - } - - $builder->setOffsetValueType($keyType, $valueType); - } + $builder->setOffsetValueType($offsetType, $valueType); return $builder->getArray(); } From e1d6a3952455163b8a3d72540002ccae8eb09ff9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 8 May 2025 15:24:39 +0200 Subject: [PATCH 18/21] fix setExistingOffsetValueType() for unset() use-case --- src/Analyser/NodeScopeResolver.php | 20 ++++++++++++++++++++ src/Type/ArrayType.php | 12 ------------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 43f6ec079d..02843ca116 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -160,6 +160,7 @@ use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; +use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\ClosureType; @@ -167,6 +168,7 @@ use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\GeneralizePrecision; @@ -5931,7 +5933,25 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar && $arrayDimFetch !== null && $scope->hasExpressionType($arrayDimFetch)->yes() ) { + $hasOffsetType = null; + if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) { + $hasOffsetType = new HasOffsetValueType($offsetType, $valueToWrite); + } $valueToWrite = $offsetValueType->setExistingOffsetValueType($offsetType, $valueToWrite); + + if ($hasOffsetType !== null) { + $valueToWrite = TypeCombinator::intersect( + $valueToWrite, + $hasOffsetType, + new NonEmptyArrayType(), + ); + } else { + $valueToWrite = TypeCombinator::intersect( + $valueToWrite, + new NonEmptyArrayType(), + ); + } + } else { $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0); } diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index ca745fdf09..92be673986 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -356,23 +356,11 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) { - return TypeCombinator::intersect( - new self( - TypeCombinator::union($this->keyType, $offsetType), - TypeCombinator::union($this->itemType, $valueType), - ), - new HasOffsetValueType($offsetType, $valueType), - new NonEmptyArrayType(), - ); - } - return TypeCombinator::intersect( new self( $this->keyType, TypeCombinator::union($this->itemType, $valueType), ), - new NonEmptyArrayType(), ); } From 0329ac79ca91755fc96db8e98252ad5b38008d8c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 8 May 2025 15:25:35 +0200 Subject: [PATCH 19/21] Update phpstan-baseline.neon --- phpstan-baseline.neon | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 035be525fe..c8878ce9a4 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -48,6 +48,12 @@ parameters: count: 2 path: src/Analyser/NodeScopeResolver.php + - + message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#' + identifier: phpstanApi.instanceofType + count: 1 + path: src/Analyser/NodeScopeResolver.php + - message: '#^Parameter \#2 \$node of method PHPStan\\BetterReflection\\SourceLocator\\Ast\\Strategy\\NodeToReflection\:\:__invoke\(\) expects PhpParser\\Node\\Expr\\ArrowFunction\|PhpParser\\Node\\Expr\\Closure\|PhpParser\\Node\\Expr\\FuncCall\|PhpParser\\Node\\Stmt\\Class_\|PhpParser\\Node\\Stmt\\Const_\|PhpParser\\Node\\Stmt\\Enum_\|PhpParser\\Node\\Stmt\\Function_\|PhpParser\\Node\\Stmt\\Interface_\|PhpParser\\Node\\Stmt\\Trait_, PhpParser\\Node\\Stmt\\ClassLike given\.$#' identifier: argument.type From 936d3cd55eb8764e743edb61c87668a3958ba73c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 8 May 2025 15:30:00 +0200 Subject: [PATCH 20/21] simplify --- src/Analyser/NodeScopeResolver.php | 1 - src/Type/ArrayType.php | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 02843ca116..bfc07099a9 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5943,7 +5943,6 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $valueToWrite = TypeCombinator::intersect( $valueToWrite, $hasOffsetType, - new NonEmptyArrayType(), ); } else { $valueToWrite = TypeCombinator::intersect( diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 92be673986..e6c0097db7 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -356,11 +356,9 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type { - return TypeCombinator::intersect( - new self( - $this->keyType, - TypeCombinator::union($this->itemType, $valueType), - ), + return new self( + $this->keyType, + TypeCombinator::union($this->itemType, $valueType), ); } From 35b0a154ba6229aefa3c8f35568ccc50e6582fe7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 8 May 2025 16:13:31 +0200 Subject: [PATCH 21/21] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index bfc07099a9..681bd88ceb 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5944,7 +5944,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $valueToWrite, $hasOffsetType, ); - } else { + } elseif ($valueToWrite->isArray()->yes()) { $valueToWrite = TypeCombinator::intersect( $valueToWrite, new NonEmptyArrayType(),