From 7d723e89a06aa4e00fa36c78216d702a6e918cb7 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Sun, 12 Dec 2021 22:21:30 +0100 Subject: [PATCH 1/3] Support ArrayAccess in AppendedArrayItemTypeRule --- .../Arrays/AppendedArrayItemTypeRule.php | 6 ++++-- src/Type/ObjectType.php | 9 +++++++++ .../Arrays/AppendedArrayItemTypeRuleTest.php | 12 ++++++++++++ .../Rules/Arrays/data/appended-array-item.php | 19 +++++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/Rules/Arrays/AppendedArrayItemTypeRule.php b/src/Rules/Arrays/AppendedArrayItemTypeRule.php index 07bdc2239c..4ca7872458 100644 --- a/src/Rules/Arrays/AppendedArrayItemTypeRule.php +++ b/src/Rules/Arrays/AppendedArrayItemTypeRule.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Arrays; +use ArrayAccess; use PhpParser\Node; use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\Assign; @@ -13,6 +14,7 @@ use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\ArrayType; +use PHPStan\Type\ObjectType; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -67,7 +69,7 @@ public function processNode(Node $node, Scope $scope): array } $assignedToType = $propertyReflection->getWritableType(); - if (!($assignedToType instanceof ArrayType)) { + if (!($assignedToType instanceof ArrayType) && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($assignedToType)->yes()) { return []; } @@ -77,7 +79,7 @@ public function processNode(Node $node, Scope $scope): array $assignedValueType = $scope->getType($node); } - $itemType = $assignedToType->getItemType(); + $itemType = $assignedToType->getIterableValueType(); if (!$this->ruleLevelHelper->accepts($itemType, $assignedValueType, $scope->isDeclareStrictTypes())) { $verbosityLevel = VerbosityLevel::getRecommendedLevelByType($itemType, $assignedValueType); return [ diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 633d7a8c2f..1b7b8a8939 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -746,6 +746,15 @@ public function getIterableValueType(): Type return new MixedType(); } + if ($this->isInstanceOf(ArrayAccess::class)->yes()) { + $tValue = GenericTypeVariableResolver::getType($this, ArrayAccess::class, 'TValue'); + if ($tValue !== null) { + return $tValue; + } + + return new MixedType(); + } + return new ErrorType(); } diff --git a/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php b/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php index 7dd894f4cd..f93ef65e8a 100644 --- a/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php @@ -58,6 +58,18 @@ public function testAppendedArrayItemType(): void 'Array (array) does not accept AppendedArrayItem\Baz.', 79, ], + [ + 'Array (ArrayAccess) does not accept int.', + 95, + ], + [ + 'Array (ArrayAccess&Countable) does not accept int.', + 96, + ], + [ + 'Array (ArrayAccess&Countable&iterable) does not accept int.', + 97, + ], ] ); } diff --git a/tests/PHPStan/Rules/Arrays/data/appended-array-item.php b/tests/PHPStan/Rules/Arrays/data/appended-array-item.php index 4d77299b03..6921da4667 100644 --- a/tests/PHPStan/Rules/Arrays/data/appended-array-item.php +++ b/tests/PHPStan/Rules/Arrays/data/appended-array-item.php @@ -78,3 +78,22 @@ function (Lorem $lorem) { $lorem->staticProperty[] = new Baz(); }; + +class ArrayAccess +{ + /** @var \ArrayAccess */ + private $collection1; + + /** @var \ArrayAccess&\Countable */ + private $collection2; + + /** @var \ArrayAccess&\Countable&iterable */ + private $collection3; + + public function doFoo() + { + $this->collection1[] = 1; + $this->collection2[] = 2; + $this->collection3[] = 3; + } +} From 6ad3e4404fd412ddc948cd2c672ad5d0e5971a5e Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Sun, 12 Dec 2021 22:46:19 +0100 Subject: [PATCH 2/3] Handle ArrayAccess also in getIterableKeyType --- src/Type/ObjectType.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 1b7b8a8939..e1346795e3 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -713,6 +713,15 @@ public function getIterableKeyType(): Type return new MixedType(); } + if ($this->isInstanceOf(ArrayAccess::class)->yes()) { + $tKey = GenericTypeVariableResolver::getType($this, ArrayAccess::class, 'TKey'); + if ($tKey !== null) { + return $tKey; + } + + return new MixedType(); + } + return new ErrorType(); } From dc39538060ed854d361fbd7b87d57ca45608a81b Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Mon, 13 Dec 2021 14:13:54 +0100 Subject: [PATCH 3/3] Resolve ArrayAccess generic types before Iterator types --- src/Type/ObjectType.php | 34 +++++++++---------- tests/PHPStan/Analyser/data/bug-2676.php | 2 +- .../Arrays/AppendedArrayItemTypeRuleTest.php | 10 ++++-- .../Rules/Arrays/data/appended-array-item.php | 11 ++++++ 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index e1346795e3..05efbaf59a 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -680,6 +680,15 @@ public function isIterableAtLeastOnce(): TrinaryLogic public function getIterableKeyType(): Type { + if ($this->isInstanceOf(ArrayAccess::class)->yes()) { + $tKey = GenericTypeVariableResolver::getType($this, ArrayAccess::class, 'TKey'); + if ($tKey !== null) { + return $tKey; + } + + return new MixedType(); + } + $classReflection = $this->getClassReflection(); if ($classReflection === null) { return new ErrorType(); @@ -713,20 +722,20 @@ public function getIterableKeyType(): Type return new MixedType(); } + return new ErrorType(); + } + + public function getIterableValueType(): Type + { if ($this->isInstanceOf(ArrayAccess::class)->yes()) { - $tKey = GenericTypeVariableResolver::getType($this, ArrayAccess::class, 'TKey'); - if ($tKey !== null) { - return $tKey; + $tValue = GenericTypeVariableResolver::getType($this, ArrayAccess::class, 'TValue'); + if ($tValue !== null) { + return $tValue; } return new MixedType(); } - return new ErrorType(); - } - - public function getIterableValueType(): Type - { if ($this->isInstanceOf(Iterator::class)->yes()) { return RecursionGuard::run($this, function (): Type { return ParametersAcceptorSelector::selectSingle( @@ -755,15 +764,6 @@ public function getIterableValueType(): Type return new MixedType(); } - if ($this->isInstanceOf(ArrayAccess::class)->yes()) { - $tValue = GenericTypeVariableResolver::getType($this, ArrayAccess::class, 'TValue'); - if ($tValue !== null) { - return $tValue; - } - - return new MixedType(); - } - return new ErrorType(); } diff --git a/tests/PHPStan/Analyser/data/bug-2676.php b/tests/PHPStan/Analyser/data/bug-2676.php index 4daa2b5552..8613d0d594 100644 --- a/tests/PHPStan/Analyser/data/bug-2676.php +++ b/tests/PHPStan/Analyser/data/bug-2676.php @@ -39,7 +39,7 @@ function (Wallet $wallet): void assertType('DoctrineIntersectionTypeIsSupertypeOf\Collection&iterable', $bankAccounts); foreach ($bankAccounts as $key => $bankAccount) { - assertType('(int|string)', $key); + assertType('int|string|null', $key); assertType('Bug2676\BankAccount', $bankAccount); } }; diff --git a/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php b/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php index f93ef65e8a..f0bbe8b7e0 100644 --- a/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/AppendedArrayItemTypeRuleTest.php @@ -60,15 +60,19 @@ public function testAppendedArrayItemType(): void ], [ 'Array (ArrayAccess) does not accept int.', - 95, + 99, ], [ 'Array (ArrayAccess&Countable) does not accept int.', - 96, + 102, ], [ 'Array (ArrayAccess&Countable&iterable) does not accept int.', - 97, + 105, + ], + [ + 'Array (SplObjectStorage) does not accept int.', + 108, ], ] ); diff --git a/tests/PHPStan/Rules/Arrays/data/appended-array-item.php b/tests/PHPStan/Rules/Arrays/data/appended-array-item.php index 6921da4667..d2b5c700fa 100644 --- a/tests/PHPStan/Rules/Arrays/data/appended-array-item.php +++ b/tests/PHPStan/Rules/Arrays/data/appended-array-item.php @@ -90,10 +90,21 @@ class ArrayAccess /** @var \ArrayAccess&\Countable&iterable */ private $collection3; + /** @var \SplObjectStorage */ + private $collection4; + public function doFoo() { + $this->collection1[] = 'foo'; $this->collection1[] = 1; + + $this->collection2[] = 'foo'; $this->collection2[] = 2; + + $this->collection3[] = 'foo'; $this->collection3[] = 3; + + $this->collection4[] = 'foo'; + $this->collection4[] = 4; } }