From 50abf2a6a0f05c121b0967c58a06be48a17266f6 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 25 Apr 2025 17:19:11 +0200 Subject: [PATCH 01/23] 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 435fd9f2ca..f91ddca2c7 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 11a044def0..ca4cc32e49 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -1224,6 +1224,11 @@ public function testBug1O580(): void } #[RequiresPhp('>= 8.0')] + public function testBug12927(): void + { + $this->analyse([__DIR__ . '/data/bug-12927.php'], []); + } + public function testBug4443(): void { $this->analyse([__DIR__ . '/data/bug-4443.php'], [ 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 89ff299270..d70d5e3951 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 fab2c9c2eef32c5ad92188ebf11ceb5a4ca24501 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 25 Apr 2025 17:42:44 +0200 Subject: [PATCH 02/23] 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 32d7746d54..fc025fec72 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -226,6 +226,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 caf5ed854e969eb780b1e919aa4b973e44190b8c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 25 Apr 2025 18:36:38 +0200 Subject: [PATCH 03/23] 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 f91ddca2c7..5b1b3a8019 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 6af2fb0e92007832aa29f07d7e3df5d58f4215a4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 09:28:09 +0200 Subject: [PATCH 04/23] 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 1eb577826d..c2af7472ea 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 9fa98792f063cfe9e357cb49aebed9cc268d1cbc Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 09:52:07 +0200 Subject: [PATCH 05/23] 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 7964f5b3ee..479ac007a2 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -800,4 +800,10 @@ public function testBug12675(): void $this->analyse([__DIR__ . '/data/bug-12675.php'], []); } + 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 1b109daf32b4da2002605e50acadecbc37d8488d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 09:56:57 +0200 Subject: [PATCH 06/23] 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 479ac007a2..c850da0fdb 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -806,4 +806,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 ddde2ff01385982d9190b5a80f484e527c290c73 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 10:00:19 +0200 Subject: [PATCH 07/23] Added regression test --- .../ParameterOutAssignedTypeRuleTest.php | 5 ++++ .../Rules/Variables/data/bug-12330.php | 2 +- .../Rules/Variables/data/bug-12754.php | 26 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) 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 d70d5e3951..d1c384e426 100644 --- a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php +++ b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php @@ -74,4 +74,9 @@ public function testBug13093b(): void $this->analyse([__DIR__ . '/data/bug-13093b.php'], []); } + public function testBug12754(): void + { + $this->analyse([__DIR__ . '/data/bug-12754.php'], []); + } + } 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']; 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 949b9ce5ba9be304564839e487a50ac9af01e56f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 10:09:47 +0200 Subject: [PATCH 08/23] 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 d1c384e426..b03d0e57a1 100644 --- a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php +++ b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php @@ -76,6 +76,9 @@ public function testBug13093b(): 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 dda3a817586e722b66008bc09ba6b47fd1f72c5e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 10:18:11 +0200 Subject: [PATCH 09/23] 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 b03d0e57a1..e228555ab8 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 0fd18cc128d771363fe71ff9c7dd98346c119387 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 10:30:39 +0200 Subject: [PATCH 10/23] 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 bf8d74298c..422e34801b 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -802,6 +802,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 3e2c32d150a5b79436995cc7237fab1afacba964 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 26 Apr 2025 14:38:29 +0200 Subject: [PATCH 11/23] 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 5b1b3a8019..67a63830eb 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 21178f2ce83a690fad9bee05b69738360e9024b7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 27 Apr 2025 11:51:40 +0200 Subject: [PATCH 12/23] 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 2b43c4f5b2..fc4d4de09f 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5936,9 +5936,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 67a63830eb..3e9c952933 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 5c60ac764c..dbf7a6b7ca 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 413c5c6ec3..c04a012c63 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -361,9 +361,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 9bfd5cd364..438d5f5f8d 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -695,11 +695,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 422e34801b..22b6c7b0e3 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -802,9 +802,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 @@ -832,6 +829,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 2020324e9b2fc23e3875f9a77e1de9337c1539b8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 27 Apr 2025 13:06:51 +0200 Subject: [PATCH 13/23] 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 78b960142e119bbfb21f5f387ed87dcdc0fc5532 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 27 Apr 2025 13:06:59 +0200 Subject: [PATCH 14/23] 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 079610b30cef7fb5acb4c29e7fb60ef1f391cd0c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 27 Apr 2025 13:08:06 +0200 Subject: [PATCH 15/23] 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 c04a012c63..3cb25ebc96 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -378,11 +378,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 3b734e26e28e78892f4ec3d013ea322f95dc817b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 27 Apr 2025 13:41:39 +0200 Subject: [PATCH 16/23] 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 3cb25ebc96..c85277f048 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -362,12 +362,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 438d5f5f8d..ba30e4f5fe 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -693,21 +693,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 0c3b0ff29f254a6e79f5e6cd4fd6de2d9d51694f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 8 May 2025 15:24:39 +0200 Subject: [PATCH 17/23] 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 fc4d4de09f..b7cd0d5d04 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -163,6 +163,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; @@ -170,6 +171,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; @@ -5943,7 +5945,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 c85277f048..a98514a30d 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -361,23 +361,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 5b589b428792c307eacb2f5378a236a91955662e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 8 May 2025 15:25:35 +0200 Subject: [PATCH 18/23] Update phpstan-baseline.neon --- phpstan-baseline.neon | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2ad06b8261..69261520fb 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 d035fbd9ae03bcddd1cd2e66d5f84d5c329b6ca5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 8 May 2025 15:30:00 +0200 Subject: [PATCH 19/23] 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 b7cd0d5d04..d00fb22aca 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5955,7 +5955,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 a98514a30d..413c5c6ec3 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -361,11 +361,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 a9a200829592d3f8b9de0c07c274c60f52cb9448 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 8 May 2025 16:13:31 +0200 Subject: [PATCH 20/23] 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 d00fb22aca..dbaa69adfb 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5956,7 +5956,7 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar $valueToWrite, $hasOffsetType, ); - } else { + } elseif ($valueToWrite->isArray()->yes()) { $valueToWrite = TypeCombinator::intersect( $valueToWrite, new NonEmptyArrayType(), From deb4beb163c42cf5cc2b189c4cb634a5f4091871 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 17 Jul 2025 15:05:09 +0200 Subject: [PATCH 21/23] Update ParameterOutAssignedTypeRuleTest.php --- .../Rules/Variables/ParameterOutAssignedTypeRuleTest.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php index e228555ab8..21b57cecf2 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 PHPUnit\Framework\Attributes\RequiresPhp; use const PHP_VERSION_ID; /** @@ -75,11 +76,9 @@ public function testBug13093b(): void $this->analyse([__DIR__ . '/data/bug-13093b.php'], []); } + #[RequiresPhp('>= 8.0')] 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 748f7deb0a2be9b61f8496b282426df928c25633 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 17 Jul 2025 15:06:09 +0200 Subject: [PATCH 22/23] use PHPUnit attributes --- .../Rules/Properties/TypesAssignedToPropertiesRuleTest.php | 6 ++---- .../Rules/Variables/ParameterOutAssignedTypeRuleTest.php | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index c850da0fdb..77534f28a5 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -806,12 +807,9 @@ public function testBug11171(): void $this->analyse([__DIR__ . '/data/bug-11171.php'], []); } + #[RequiresPhp('>= 8.0')] 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/Variables/ParameterOutAssignedTypeRuleTest.php b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php index 21b57cecf2..bdd78d2dfd 100644 --- a/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php +++ b/tests/PHPStan/Rules/Variables/ParameterOutAssignedTypeRuleTest.php @@ -6,7 +6,6 @@ use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; -use const PHP_VERSION_ID; /** * @extends RuleTestCase From 004e491071d850d803b65eadb2ee9dcac56a3f29 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 17 Jul 2025 15:09:17 +0200 Subject: [PATCH 23/23] Update TypesAssignedToPropertiesRuleTest.php --- .../Rules/Properties/TypesAssignedToPropertiesRuleTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index 77534f28a5..d5a4679aef 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -6,7 +6,6 @@ use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; -use const PHP_VERSION_ID; /** * @extends RuleTestCase