From 56a8799924d4c25008701bbdc3d99dfa04dd01b2 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 7 Apr 2025 21:12:56 +0200 Subject: [PATCH 01/10] non-empty-array does not except array with hasOffsetValue --- src/Type/Accessory/NonEmptyArrayType.php | 6 +++ .../CallToFunctionParametersRuleTest.php | 19 ++++++++ .../Rules/Functions/data/bug-12847.php | 48 +++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-12847.php diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index d4726fd5c2..f510de287b 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -76,6 +76,12 @@ public function getConstantStrings(): array public function accepts(Type $type, bool $strictTypes): AcceptsResult { + if ($type instanceof HasOffsetType + || $type instanceof HasOffsetValueType + ) { + return AcceptsResult::createYes(); + } + if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 0eea3694d1..3c8f970910 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2053,4 +2053,23 @@ public function testBug7522(): void $this->analyse([__DIR__ . '/data/bug-7522.php'], []); } + public function testBug12847(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-12847.php'], [ + [ + 'Parameter #1 $array of function Bug12847\doSomething expects non-empty-array, mixed given.', + 32, + 'mixed is empty.', + ], + [ + 'Parameter #1 $array of function Bug12847\doSomething expects non-empty-array, mixed given.', + 39, + 'mixed is empty.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-12847.php b/tests/PHPStan/Rules/Functions/data/bug-12847.php new file mode 100644 index 0000000000..c9b168386a --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-12847.php @@ -0,0 +1,48 @@ + $array + */ + $array = [ + 'abc' => 'def' + ]; + + if (isset($array['def'])) { + doSomething($array); + } +} + +function doFoo(array $array):void { + if (isset($array['def'])) { + doSomething($array); + } +} + +function doFooBar(array $array):void { + if (array_key_exists('foo', $array) && $array['foo'] === 17) { + doSomething($array); + } +} + +function doImplicitMixed($mixed):void { + if (isset($mixed['def'])) { + doSomething($mixed); + } +} + +function doExplicitMixed(mixed $mixed): void +{ + if (isset($mixed['def'])) { + doSomething($mixed); + } +} +/** + * @param non-empty-array $array + */ +function doSomething(array $array): void +{ + +} From 4dc1a7e7cdef5df68872acd88c3f4878ce4c03e0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 7 Apr 2025 21:21:17 +0200 Subject: [PATCH 02/10] Update bug-12847.php --- tests/PHPStan/Rules/Functions/data/bug-12847.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Functions/data/bug-12847.php b/tests/PHPStan/Rules/Functions/data/bug-12847.php index c9b168386a..af6be557be 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-12847.php +++ b/tests/PHPStan/Rules/Functions/data/bug-12847.php @@ -1,4 +1,4 @@ -= 7.4 namespace Bug12847; @@ -39,6 +39,7 @@ function doExplicitMixed(mixed $mixed): void doSomething($mixed); } } + /** * @param non-empty-array $array */ From 060bd6fabfd0fe4394b8c697265f101ffdbf9c58 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 7 Apr 2025 21:27:21 +0200 Subject: [PATCH 03/10] min php 7.4 --- .../Rules/Functions/CallToFunctionParametersRuleTest.php | 4 ++++ tests/PHPStan/Rules/Functions/data/bug-12847.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 3c8f970910..a69d3637f5 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2055,6 +2055,10 @@ public function testBug7522(): void public function testBug12847(): void { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + $this->checkExplicitMixed = true; $this->checkImplicitMixed = true; diff --git a/tests/PHPStan/Rules/Functions/data/bug-12847.php b/tests/PHPStan/Rules/Functions/data/bug-12847.php index af6be557be..8c2f3394ac 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-12847.php +++ b/tests/PHPStan/Rules/Functions/data/bug-12847.php @@ -1,4 +1,4 @@ -= 7.4 + Date: Wed, 9 Apr 2025 09:34:47 +0200 Subject: [PATCH 04/10] more tests --- .../Rules/Functions/CallToFunctionParametersRuleTest.php | 4 ++++ tests/PHPStan/Rules/Functions/data/bug-12847.php | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index a69d3637f5..7b29533f2f 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2073,6 +2073,10 @@ public function testBug12847(): void 39, 'mixed is empty.', ], + [ + 'Parameter #1 $array of function Bug12847\doSomething expects non-empty-array, stdClass given.', + 46, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/bug-12847.php b/tests/PHPStan/Rules/Functions/data/bug-12847.php index 8c2f3394ac..92b509ac5f 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-12847.php +++ b/tests/PHPStan/Rules/Functions/data/bug-12847.php @@ -40,6 +40,13 @@ function doExplicitMixed(mixed $mixed): void } } +function doStdClass(\stdClass $stdClass): void +{ + if (isset($stdClass['def'])) { + doSomething($stdClass); + } +} + /** * @param non-empty-array $array */ From b4188016d805cc40aa0738434b6f68b34aa9fe2c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 9 Apr 2025 09:43:07 +0200 Subject: [PATCH 05/10] Revert "more tests" This reverts commit 5a62d9afb2da7189fd06b8aad2ce4a076eeef658. --- .../Rules/Functions/CallToFunctionParametersRuleTest.php | 4 ---- tests/PHPStan/Rules/Functions/data/bug-12847.php | 7 ------- 2 files changed, 11 deletions(-) diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 7b29533f2f..a69d3637f5 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2073,10 +2073,6 @@ public function testBug12847(): void 39, 'mixed is empty.', ], - [ - 'Parameter #1 $array of function Bug12847\doSomething expects non-empty-array, stdClass given.', - 46, - ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/bug-12847.php b/tests/PHPStan/Rules/Functions/data/bug-12847.php index 92b509ac5f..8c2f3394ac 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-12847.php +++ b/tests/PHPStan/Rules/Functions/data/bug-12847.php @@ -40,13 +40,6 @@ function doExplicitMixed(mixed $mixed): void } } -function doStdClass(\stdClass $stdClass): void -{ - if (isset($stdClass['def'])) { - doSomething($stdClass); - } -} - /** * @param non-empty-array $array */ From bbcce3f9ec051619eaeb61072aee17af964ffb04 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 15 Apr 2025 16:09:13 +0200 Subject: [PATCH 06/10] move logic into HasOffset* --- src/Type/Accessory/HasOffsetType.php | 3 +++ src/Type/Accessory/HasOffsetValueType.php | 4 ++++ src/Type/Accessory/NonEmptyArrayType.php | 6 ------ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 455f0de86e..e187a2226d 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -111,6 +111,9 @@ public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { + if ($acceptingType instanceof NonEmptyArrayType) { + return AcceptsResult::createYes(); + } return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index ec6e822a31..3ca2d6ac5b 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -122,6 +122,10 @@ public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { + if ($acceptingType instanceof NonEmptyArrayType) { + return AcceptsResult::createYes(); + } + return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index f510de287b..d4726fd5c2 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -76,12 +76,6 @@ public function getConstantStrings(): array public function accepts(Type $type, bool $strictTypes): AcceptsResult { - if ($type instanceof HasOffsetType - || $type instanceof HasOffsetValueType - ) { - return AcceptsResult::createYes(); - } - if ($type instanceof CompoundType) { return $type->isAcceptedBy($this, $strictTypes); } From bf5ba33a7cdb0252f5a29a8a3265b1b7f9b9adf8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 15 Apr 2025 16:13:31 +0200 Subject: [PATCH 07/10] Don't use instanceof --- src/Type/Accessory/HasOffsetType.php | 2 +- src/Type/Accessory/HasOffsetValueType.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index e187a2226d..37b01a9b5f 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -111,7 +111,7 @@ public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - if ($acceptingType instanceof NonEmptyArrayType) { + if ($acceptingType->isArray()->yes() && $acceptingType->isIterableAtLeastOnce()->yes()) { return AcceptsResult::createYes(); } return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index 3ca2d6ac5b..2b75c6bb8f 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -122,7 +122,7 @@ public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - if ($acceptingType instanceof NonEmptyArrayType) { + if ($acceptingType->isArray()->yes() && $acceptingType->isIterableAtLeastOnce()->yes()) { return AcceptsResult::createYes(); } From 30567c9e3807e0b0d94719ca1fb61689d66284a4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 15 Apr 2025 16:34:26 +0200 Subject: [PATCH 08/10] add failling test --- .../CallToFunctionParametersRuleTest.php | 4 ++++ .../Rules/Functions/data/bug-12847.php | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index a69d3637f5..51f7a5f0df 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2073,6 +2073,10 @@ public function testBug12847(): void 39, 'mixed is empty.', ], + [ + "Parameter #1 \$array of function doSomethingWithInt expects non-empty-array, non-empty-array&hasOffsetValue('foo', 'hello') given.", + 73, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/bug-12847.php b/tests/PHPStan/Rules/Functions/data/bug-12847.php index 8c2f3394ac..3f37baaab5 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-12847.php +++ b/tests/PHPStan/Rules/Functions/data/bug-12847.php @@ -47,3 +47,23 @@ function doSomething(array $array): void { } + +/** + * @param non-empty-array $array + */ +function doSomethingWithInt(array $array): void +{ + +} + +function doFooBarInt(array $array):void { + if (array_key_exists('foo', $array) && $array['foo'] === 17) { + doSomethingWithInt($array); + } +} + +function doFooBarString(array $array):void { + if (array_key_exists('foo', $array) && $array['foo'] === "hello") { + doSomethingWithInt($array); + } +} From 29ba0788aaebb6c4439129abfa2362eac7e8a07d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 15 Apr 2025 16:54:51 +0200 Subject: [PATCH 09/10] fix in IntersectionType --- src/Type/Accessory/HasOffsetType.php | 3 --- src/Type/Accessory/HasOffsetValueType.php | 4 ---- src/Type/IntersectionType.php | 10 ++++++++++ .../Functions/CallToFunctionParametersRuleTest.php | 8 ++++++-- tests/PHPStan/Rules/Functions/data/bug-12847.php | 4 ++-- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 37b01a9b5f..455f0de86e 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -111,9 +111,6 @@ public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - if ($acceptingType->isArray()->yes() && $acceptingType->isIterableAtLeastOnce()->yes()) { - return AcceptsResult::createYes(); - } return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index 2b75c6bb8f..ec6e822a31 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -122,10 +122,6 @@ public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult { - if ($acceptingType->isArray()->yes() && $acceptingType->isIterableAtLeastOnce()->yes()) { - return AcceptsResult::createYes(); - } - return $this->isSubTypeOf($acceptingType)->toAcceptsResult(); } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 149536a573..9079a5d9bb 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -194,6 +194,16 @@ public function accepts(Type $otherType, bool $strictTypes): AcceptsResult $result = $result->and($type->accepts($otherType, $strictTypes)); } + if (!$result->yes() + && $this->isArray()->yes() + && $this->isIterableAtLeastOnce()->yes() + && $otherType->isArray()->yes() + && $otherType->isIterableAtLeastOnce()->yes() + && $this->isSuperTypeOf($otherType)->yes() + ) { + return AcceptsResult::createYes(); + } + if (!$result->yes()) { $isList = $otherType->isList(); $reasons = $result->reasons; diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 51f7a5f0df..c5fce4e73a 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2074,8 +2074,12 @@ public function testBug12847(): void 'mixed is empty.', ], [ - "Parameter #1 \$array of function doSomethingWithInt expects non-empty-array, non-empty-array&hasOffsetValue('foo', 'hello') given.", - 73, + 'Parameter #1 $array of function Bug12847\doSomethingWithInt expects non-empty-array, non-empty-array given.', + 61, + ], + [ + 'Parameter #1 $array of function Bug12847\doSomethingWithInt expects non-empty-array, non-empty-array given.', + 67, ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/bug-12847.php b/tests/PHPStan/Rules/Functions/data/bug-12847.php index 3f37baaab5..c4880d83f6 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-12847.php +++ b/tests/PHPStan/Rules/Functions/data/bug-12847.php @@ -58,12 +58,12 @@ function doSomethingWithInt(array $array): void function doFooBarInt(array $array):void { if (array_key_exists('foo', $array) && $array['foo'] === 17) { - doSomethingWithInt($array); + doSomethingWithInt($array); // expect error, because our array is not sealed } } function doFooBarString(array $array):void { if (array_key_exists('foo', $array) && $array['foo'] === "hello") { - doSomethingWithInt($array); + doSomethingWithInt($array); // expect error, because our array is not sealed } } From 02de6587adb8b08495a11117e176e7587bfca3c4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 15 Apr 2025 16:59:53 +0200 Subject: [PATCH 10/10] Discard changes to src/Type/IntersectionType.php --- src/Type/IntersectionType.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 9079a5d9bb..149536a573 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -194,16 +194,6 @@ public function accepts(Type $otherType, bool $strictTypes): AcceptsResult $result = $result->and($type->accepts($otherType, $strictTypes)); } - if (!$result->yes() - && $this->isArray()->yes() - && $this->isIterableAtLeastOnce()->yes() - && $otherType->isArray()->yes() - && $otherType->isIterableAtLeastOnce()->yes() - && $this->isSuperTypeOf($otherType)->yes() - ) { - return AcceptsResult::createYes(); - } - if (!$result->yes()) { $isList = $otherType->isList(); $reasons = $result->reasons;