From 55702891270f92923c3553ff93d942b85c90f672 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Wed, 27 Nov 2024 15:51:30 +0100 Subject: [PATCH 1/3] Retain list type when assigning to offset 1 of non-empty-list --- src/Type/IntersectionType.php | 9 +++++- .../TypesAssignedToPropertiesRuleTest.php | 6 ++++ .../Rules/Properties/data/bug-12131.php | 29 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100755 tests/PHPStan/Rules/Properties/data/bug-12131.php diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 72fecc7de6..32c194660b 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -756,7 +756,14 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni ); }); } - return $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); + + $result = $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); + + if ($offsetType !== null && $this->isList()->yes() && $this->isIterableAtLeastOnce()->yes() && (new ConstantIntegerType(1))->isSuperTypeOf($offsetType)->yes()) { + $result = TypeCombinator::intersect($result, new AccessoryArrayListType()); + } + + return $result; } public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index ff9f5be5ae..e140807b35 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -692,4 +692,10 @@ public function testBug11617(): void ]); } + public function testBug12131(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-12131.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-12131.php b/tests/PHPStan/Rules/Properties/data/bug-12131.php new file mode 100755 index 0000000000..1442cce55d --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-12131.php @@ -0,0 +1,29 @@ += 7.4 + +namespace Bug12131; + +class Test +{ + /** + * @var non-empty-list + */ + public array $array; + + public function __construct() + { + $this->array = array_fill(0, 10, 1); + } + + public function setAtZero(): void + { + $this->array[0] = 1; + } + + public function setAtOne(): void + { + $this->array[1] = 1; + } +} + +$a = new Test(); +$a->setAtOne(); From 2223acdc741a51ca533dc68e15b884d9d804f21b Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Wed, 27 Nov 2024 21:08:46 +0100 Subject: [PATCH 2/3] Add more tests --- tests/PHPStan/Analyser/nsrt/list-type.php | 25 +++++++++++++++++++ .../TypesAssignedToPropertiesRuleTest.php | 8 +++++- .../Rules/Properties/data/bug-12131.php | 8 +++--- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/list-type.php b/tests/PHPStan/Analyser/nsrt/list-type.php index a80e8b066d..26640a5141 100644 --- a/tests/PHPStan/Analyser/nsrt/list-type.php +++ b/tests/PHPStan/Analyser/nsrt/list-type.php @@ -106,4 +106,29 @@ public function testUnset(array $list): void assertType('array|int<3, max>, int>', $list); } + /** @param list $list */ + public function testSetOffsetExplicitlyWithoutGap(array $list): void + { + assertType('list', $list); + $list[0] = 17; + assertType('non-empty-list&hasOffsetValue(0, 17)', $list); + $list[1] = 19; + assertType('non-empty-list&hasOffsetValue(0, 17)&hasOffsetValue(1, 19)', $list); + $list[0] = 21; + assertType('non-empty-list&hasOffsetValue(0, 21)&hasOffsetValue(1, 19)', $list); + + $list[2] = 23; + assertType('non-empty-array, int>&hasOffsetValue(0, 21)&hasOffsetValue(1, 19)&hasOffsetValue(2, 23)', $list); + } + + /** @param list $list */ + public function testSetOffsetExplicitlyWithGap(array $list): void + { + assertType('list', $list); + $list[0] = 17; + assertType('non-empty-list&hasOffsetValue(0, 17)', $list); + $list[2] = 21; + assertType('non-empty-array, int>&hasOffsetValue(0, 17)&hasOffsetValue(2, 21)', $list); + } + } diff --git a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php index e140807b35..4c87845d24 100644 --- a/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/TypesAssignedToPropertiesRuleTest.php @@ -695,7 +695,13 @@ public function testBug11617(): void public function testBug12131(): void { $this->checkExplicitMixed = true; - $this->analyse([__DIR__ . '/data/bug-12131.php'], []); + $this->analyse([__DIR__ . '/data/bug-12131.php'], [ + [ + 'Property Bug12131\Test::$array (non-empty-list) does not accept non-empty-array, int>.', + 29, + 'non-empty-array, int> might not be a list.', + ], + ]); } } diff --git a/tests/PHPStan/Rules/Properties/data/bug-12131.php b/tests/PHPStan/Rules/Properties/data/bug-12131.php index 1442cce55d..6f7f8d83d8 100755 --- a/tests/PHPStan/Rules/Properties/data/bug-12131.php +++ b/tests/PHPStan/Rules/Properties/data/bug-12131.php @@ -23,7 +23,9 @@ public function setAtOne(): void { $this->array[1] = 1; } -} -$a = new Test(); -$a->setAtOne(); + public function setAtTwo(): void + { + $this->array[2] = 1; + } +} From d1584887a2dac0d849ce0e7111a4399f2c17651a Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Wed, 27 Nov 2024 21:22:46 +0100 Subject: [PATCH 3/3] Use AccessoryArrayListType for intersection --- src/Type/IntersectionType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 32c194660b..d41b79e951 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -760,7 +760,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni $result = $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues)); if ($offsetType !== null && $this->isList()->yes() && $this->isIterableAtLeastOnce()->yes() && (new ConstantIntegerType(1))->isSuperTypeOf($offsetType)->yes()) { - $result = TypeCombinator::intersect($result, new AccessoryArrayListType()); + $result = AccessoryArrayListType::intersectWith($result); } return $result;