From 810dce74281aeb2be3f3c4993b5f858bf6e6775e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Apr 2026 08:25:10 +0200 Subject: [PATCH 01/14] Don't report "no value type specified in iterable type array&callable" --- src/Rules/MissingTypehintCheck.php | 3 +++ .../MissingMethodParameterTypehintRuleTest.php | 10 ++++++++++ tests/PHPStan/Rules/Methods/data/bug-14549.php | 15 +++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-14549.php diff --git a/src/Rules/MissingTypehintCheck.php b/src/Rules/MissingTypehintCheck.php index 07f584fb8a1..1ef3fb87d28 100644 --- a/src/Rules/MissingTypehintCheck.php +++ b/src/Rules/MissingTypehintCheck.php @@ -73,6 +73,9 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array if ($type instanceof AccessoryType) { return $type; } + if ($type->isCallable()->yes() && $type->isArray()->yes()) { + return $type; + } if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) { $iterablesWithMissingValueTypehint = array_merge( $iterablesWithMissingValueTypehint, diff --git a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php index db4b818f924..634a0e1c156 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php @@ -148,4 +148,14 @@ public function testBug7662(): void ]); } + public function testBug14549(): void + { + $this->analyse([__DIR__ . '/data/bug-14549.php'], [ + [ + 'Method Bug14549\MondayMorning::call() has parameter $task with no signature specified for callable.', + 10 + ] + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-14549.php b/tests/PHPStan/Rules/Methods/data/bug-14549.php new file mode 100644 index 00000000000..6f8c97e94bb --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-14549.php @@ -0,0 +1,15 @@ + Date: Wed, 29 Apr 2026 08:25:40 +0200 Subject: [PATCH 02/14] cs --- .../Rules/Methods/MissingMethodParameterTypehintRuleTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php index 634a0e1c156..af77987c40c 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php @@ -153,8 +153,8 @@ public function testBug14549(): void $this->analyse([__DIR__ . '/data/bug-14549.php'], [ [ 'Method Bug14549\MondayMorning::call() has parameter $task with no signature specified for callable.', - 10 - ] + 10, + ], ]); } From f49a68aee78d09b01e0984c772f8ac638b3e865d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Apr 2026 14:24:51 +0200 Subject: [PATCH 03/14] improve IntersectionType->getIterableKey/ValueType --- src/Type/IntersectionType.php | 6 ++++++ tests/PHPStan/Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Rules/Methods/data/bug-14549.php | 8 ++++++++ 3 files changed, 15 insertions(+) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 7ca93e11767..e7931d1da7d 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -782,6 +782,9 @@ public function getArraySize(): Type public function getIterableKeyType(): Type { + if ($this->isCallable()->yes() && $this->isArray()->yes()) { + return new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]); + } return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType()); } @@ -797,6 +800,9 @@ public function getLastIterableKeyType(): Type public function getIterableValueType(): Type { + if ($this->isCallable()->yes() && $this->isArray()->yes()) { + return new UnionType([new ObjectWithoutClassType(), new StringType()]); + } return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType()); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index bdf01e4c526..810af0c7dd3 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -190,6 +190,7 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Variables/data/bug-7417.php'; yield __DIR__ . '/../Rules/Arrays/data/bug-7469.php'; yield __DIR__ . '/../Rules/Variables/data/bug-3391.php'; + yield __DIR__ . '/../Rules/Methods/data/bug-14549.php'; yield __DIR__ . '/../Rules/Functions/data/bug-anonymous-function-method-constant.php'; diff --git a/tests/PHPStan/Rules/Methods/data/bug-14549.php b/tests/PHPStan/Rules/Methods/data/bug-14549.php index 6f8c97e94bb..bffb0179506 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-14549.php +++ b/tests/PHPStan/Rules/Methods/data/bug-14549.php @@ -2,6 +2,8 @@ namespace Bug14549; +use function PHPStan\Testing\assertType; + class MondayMorning { /** @@ -9,6 +11,12 @@ class MondayMorning */ public function call(array $task): void { + foreach($task as $k => $v) { + assertType('0|1', $k); + assertType('object|string', $v); + } + assertType('class-string|object', $task[0]); + assertType('string', $task[1]); } } From 16d47565a03cdad5f35db19c91556d58d89b39e9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Apr 2026 14:44:16 +0200 Subject: [PATCH 04/14] Update MissingMethodParameterTypehintRuleTest.php --- .../Rules/Methods/MissingMethodParameterTypehintRuleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php index af77987c40c..a1407df3437 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php @@ -153,7 +153,7 @@ public function testBug14549(): void $this->analyse([__DIR__ . '/data/bug-14549.php'], [ [ 'Method Bug14549\MondayMorning::call() has parameter $task with no signature specified for callable.', - 10, + 12, ], ]); } From fe53cdb9024f7f503e62a0be4e93798a3a1f0265 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Apr 2026 15:05:25 +0200 Subject: [PATCH 05/14] more tests --- .../PHPStan/Rules/Methods/data/bug-14549.php | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Rules/Methods/data/bug-14549.php b/tests/PHPStan/Rules/Methods/data/bug-14549.php index bffb0179506..8e07b5ba9a1 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-14549.php +++ b/tests/PHPStan/Rules/Methods/data/bug-14549.php @@ -4,12 +4,12 @@ use function PHPStan\Testing\assertType; -class MondayMorning +class Foo { /** * @param callable-array $task */ - public function call(array $task): void + public function doFoo(array $task): void { foreach($task as $k => $v) { assertType('0|1', $k); @@ -18,6 +18,22 @@ public function call(array $task): void assertType('class-string|object', $task[0]); assertType('string', $task[1]); } + + /** + * @param non-empty-list $list + */ + public function doBar(array $list): void + { + if ($list[0] !== '') { + assertType('non-empty-list&hasOffsetValue(0, non-empty-string)', $list); + + if (is_callable($list)) { + assertType('non-empty-list&callable(): mixed&hasOffsetValue(0, non-empty-string)', $list); + assertType('non-empty-string', $list[0]); + assertType('string', $list[1]); + } + } + } } From dc62aaf322da3f521700751f547b664b0762ac27 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Apr 2026 15:08:46 +0200 Subject: [PATCH 06/14] fix --- src/Type/IntersectionType.php | 5 +++-- tests/PHPStan/Rules/Methods/data/bug-14549.php | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index e7931d1da7d..b8ff85a87cf 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -800,10 +800,11 @@ public function getLastIterableKeyType(): Type public function getIterableValueType(): Type { + $result = $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType()); if ($this->isCallable()->yes() && $this->isArray()->yes()) { - return new UnionType([new ObjectWithoutClassType(), new StringType()]); + return TypeCombinator::intersect($result, new UnionType([new ObjectWithoutClassType(), new StringType()])); } - return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType()); + return $result; } public function getFirstIterableValueType(): Type diff --git a/tests/PHPStan/Rules/Methods/data/bug-14549.php b/tests/PHPStan/Rules/Methods/data/bug-14549.php index 8e07b5ba9a1..6d0c5d40245 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-14549.php +++ b/tests/PHPStan/Rules/Methods/data/bug-14549.php @@ -31,6 +31,11 @@ public function doBar(array $list): void assertType('non-empty-list&callable(): mixed&hasOffsetValue(0, non-empty-string)', $list); assertType('non-empty-string', $list[0]); assertType('string', $list[1]); + + foreach($list as $k => $v) { + assertType('0|1', $k); + assertType('string', $v); + } } } } From d3062f18446aa914ebaab8925d9a6e4727e7567e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Apr 2026 17:11:54 +0200 Subject: [PATCH 07/14] fix --- src/Type/IntersectionType.php | 8 +++++++- .../Methods/MissingMethodParameterTypehintRuleTest.php | 2 +- tests/PHPStan/Rules/Methods/data/bug-14549.php | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index b8ff85a87cf..47d7061bcae 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -802,7 +802,13 @@ public function getIterableValueType(): Type { $result = $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType()); if ($this->isCallable()->yes() && $this->isArray()->yes()) { - return TypeCombinator::intersect($result, new UnionType([new ObjectWithoutClassType(), new StringType()])); + return TypeCombinator::intersect( + $result, + new UnionType([ + new ObjectWithoutClassType(), + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + ]) + ); } return $result; } diff --git a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php index a1407df3437..415f3a6d7de 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php @@ -152,7 +152,7 @@ public function testBug14549(): void { $this->analyse([__DIR__ . '/data/bug-14549.php'], [ [ - 'Method Bug14549\MondayMorning::call() has parameter $task with no signature specified for callable.', + 'Method Bug14549\Foo::doFoo() has parameter $task with no signature specified for callable.', 12, ], ]); diff --git a/tests/PHPStan/Rules/Methods/data/bug-14549.php b/tests/PHPStan/Rules/Methods/data/bug-14549.php index 6d0c5d40245..633c1f76cd2 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-14549.php +++ b/tests/PHPStan/Rules/Methods/data/bug-14549.php @@ -13,7 +13,7 @@ public function doFoo(array $task): void { foreach($task as $k => $v) { assertType('0|1', $k); - assertType('object|string', $v); + assertType('object|non-empty-string', $v); } assertType('class-string|object', $task[0]); assertType('string', $task[1]); @@ -34,7 +34,7 @@ public function doBar(array $list): void foreach($list as $k => $v) { assertType('0|1', $k); - assertType('string', $v); + assertType('non-empty-string', $v); } } } From 8139b0c39edc9d76bc8bb97cc5869fe6a943edf9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Apr 2026 17:14:56 +0200 Subject: [PATCH 08/14] cs --- 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 47d7061bcae..8eee1b6d4eb 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -807,7 +807,7 @@ public function getIterableValueType(): Type new UnionType([ new ObjectWithoutClassType(), new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), - ]) + ]), ); } return $result; From 752510321c6f3967d39662ea0da2878c44641c3f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Apr 2026 18:55:46 +0200 Subject: [PATCH 09/14] test ConstantStringType->isCallable() --- .../Type/Constant/ConstantStringTypeTest.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php index d07d72d48af..f8dfc9a9665 100644 --- a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php @@ -185,4 +185,21 @@ public function testSetInvalidValue(): void $this->assertInstanceOf(ErrorType::class, $result); } + #[DataProvider('dataIsCallable')] + public function testIsCallable(TrinaryLogic $trinaryLogic, string $constantValue): void + { + $this->assertSame( + $trinaryLogic, + (new ConstantStringType($constantValue))->isCallable() + ); + } + + public static function dataIsCallable(): iterable + { + yield [TrinaryLogic::createNo(), '']; + yield [TrinaryLogic::createNo(), '0']; + yield [TrinaryLogic::createYes(), 'substr']; + yield [TrinaryLogic::createYes(), self::class.'::dataIsCallable']; + yield [TrinaryLogic::createMaybe(), self::class.'::methodDoesNotExist']; + } } From 8bc7c388727873267d5332f41dc7b7fa09caae45 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Apr 2026 18:57:53 +0200 Subject: [PATCH 10/14] Update IntersectionType.php --- src/Type/IntersectionType.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 8eee1b6d4eb..abb7de1101f 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -806,7 +806,7 @@ public function getIterableValueType(): Type $result, new UnionType([ new ObjectWithoutClassType(), - new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), ]), ); } @@ -1009,9 +1009,9 @@ private function doGetOffsetValueType(Type $offsetType): Type if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) { $narrowedType = new UnionType([new ClassStringType(), new ObjectWithoutClassType()]); } elseif ((new ConstantIntegerType(1))->isSuperTypeOf($arrayKeyOffsetType)->yes()) { - $narrowedType = new StringType(); + $narrowedType = new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]); } else { - $narrowedType = new UnionType([new StringType(), new ObjectWithoutClassType()]); + $narrowedType = new UnionType([new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), new ObjectWithoutClassType()]); } $result = TypeCombinator::intersect($result, $narrowedType); } From 616d08a628a57917745c539b15a8cb362b14b833 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Apr 2026 19:04:47 +0200 Subject: [PATCH 11/14] fix expectations --- tests/PHPStan/Analyser/nsrt/bug-3842.php | 6 +++--- tests/PHPStan/Rules/Methods/data/bug-14549.php | 8 ++++---- tests/PHPStan/Type/Constant/ConstantStringTypeTest.php | 7 ++++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-3842.php b/tests/PHPStan/Analyser/nsrt/bug-3842.php index 51e607ea409..a752e41b301 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-3842.php +++ b/tests/PHPStan/Analyser/nsrt/bug-3842.php @@ -22,7 +22,7 @@ function testIsArrayOnCallable(callable $value): void { if (is_array($value)) { assertType('array&callable(): mixed', $value); assertType('class-string|object', $value[0]); - assertType('string', $value[1]); + assertType('non-falsy-string', $value[1]); } } @@ -30,7 +30,7 @@ function testIsArrayOnCallable(callable $value): void { function testCallableArrayPhpDoc(array $value): void { assertType('array&callable(): mixed', $value); assertType('class-string|object', $value[0]); - assertType('string', $value[1]); + assertType('non-falsy-string', $value[1]); } function testIsStringOnCallable(callable $value): void { @@ -50,7 +50,7 @@ function checkClassString(array $values): void { /** @param 0|1 $offset */ function testCallableArrayUnionOffset(callable $value, int $offset): void { if (is_array($value)) { - assertType('object|string', $value[$offset]); + assertType('object|non-falsy-string', $value[$offset]); } } diff --git a/tests/PHPStan/Rules/Methods/data/bug-14549.php b/tests/PHPStan/Rules/Methods/data/bug-14549.php index 633c1f76cd2..daebd13fe65 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-14549.php +++ b/tests/PHPStan/Rules/Methods/data/bug-14549.php @@ -13,10 +13,10 @@ public function doFoo(array $task): void { foreach($task as $k => $v) { assertType('0|1', $k); - assertType('object|non-empty-string', $v); + assertType('object|non-falsy-string', $v); } assertType('class-string|object', $task[0]); - assertType('string', $task[1]); + assertType('non-falsy-string', $task[1]); } /** @@ -30,11 +30,11 @@ public function doBar(array $list): void if (is_callable($list)) { assertType('non-empty-list&callable(): mixed&hasOffsetValue(0, non-empty-string)', $list); assertType('non-empty-string', $list[0]); - assertType('string', $list[1]); + assertType('non-falsy-string', $list[1]); foreach($list as $k => $v) { assertType('0|1', $k); - assertType('non-empty-string', $v); + assertType('non-falsy-string', $v); } } } diff --git a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php index f8dfc9a9665..5b11f9913e9 100644 --- a/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantStringTypeTest.php @@ -190,7 +190,7 @@ public function testIsCallable(TrinaryLogic $trinaryLogic, string $constantValue { $this->assertSame( $trinaryLogic, - (new ConstantStringType($constantValue))->isCallable() + (new ConstantStringType($constantValue))->isCallable(), ); } @@ -199,7 +199,8 @@ public static function dataIsCallable(): iterable yield [TrinaryLogic::createNo(), '']; yield [TrinaryLogic::createNo(), '0']; yield [TrinaryLogic::createYes(), 'substr']; - yield [TrinaryLogic::createYes(), self::class.'::dataIsCallable']; - yield [TrinaryLogic::createMaybe(), self::class.'::methodDoesNotExist']; + yield [TrinaryLogic::createYes(), self::class . '::dataIsCallable']; + yield [TrinaryLogic::createMaybe(), self::class . '::methodDoesNotExist']; } + } From ec26bc549bffc6b6955bcc13a334a44505ad4625 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Apr 2026 19:17:35 +0200 Subject: [PATCH 12/14] remove unnecessary isSuperTypeOf check --- src/Type/IntersectionType.php | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index abb7de1101f..db1c0888874 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -1004,17 +1004,14 @@ private function doGetOffsetValueType(Type $offsetType): Type if ($this->isCallable()->yes() && $this->isArray()->yes()) { $arrayKeyOffsetType = $offsetType->toArrayKey(); - $callableArrayOffsetType = new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]); - if ($callableArrayOffsetType->isSuperTypeOf($arrayKeyOffsetType)->yes()) { - if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) { - $narrowedType = new UnionType([new ClassStringType(), new ObjectWithoutClassType()]); - } elseif ((new ConstantIntegerType(1))->isSuperTypeOf($arrayKeyOffsetType)->yes()) { - $narrowedType = new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]); - } else { - $narrowedType = new UnionType([new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), new ObjectWithoutClassType()]); - } - $result = TypeCombinator::intersect($result, $narrowedType); + if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) { + $narrowedType = new UnionType([new ClassStringType(), new ObjectWithoutClassType()]); + } elseif ((new ConstantIntegerType(1))->isSuperTypeOf($arrayKeyOffsetType)->yes()) { + $narrowedType = new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]); + } else { + $narrowedType = new UnionType([new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), new ObjectWithoutClassType()]); } + $result = TypeCombinator::intersect($result, $narrowedType); } return $result; From 6bb6930e763dbfde6bbb1836255d9182d7c061ac Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Apr 2026 19:20:15 +0200 Subject: [PATCH 13/14] simplify IntersectionType->hasOffsetValueType --- src/Type/IntersectionType.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index db1c0888874..f5dceab6006 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -973,9 +973,7 @@ private function doHasOffsetValueType(Type $offsetType): TrinaryLogic } } - $result = $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType)); - - if (!$result->yes() && $this->isCallable()->yes() && $this->isArray()->yes()) { + if ($this->isCallable()->yes() && $this->isArray()->yes()) { $arrayKeyOffsetType = $offsetType->toArrayKey(); $callableArrayOffsetType = new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]); if ($callableArrayOffsetType->isSuperTypeOf($arrayKeyOffsetType)->yes()) { @@ -983,7 +981,7 @@ private function doHasOffsetValueType(Type $offsetType): TrinaryLogic } } - return $result; + return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType)); } public function getOffsetValueType(Type $offsetType): Type From c7e923030c71a337a7f90d0a0428aeeea568bbf8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 29 Apr 2026 22:29:12 +0200 Subject: [PATCH 14/14] still check callables parameter/return type, even if we skip the array --- src/Rules/MissingTypehintCheck.php | 18 ++++++++++++++++-- .../MissingMethodParameterTypehintRuleTest.php | 10 ++++++++++ tests/PHPStan/Rules/Methods/data/bug-14549.php | 8 ++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Rules/MissingTypehintCheck.php b/src/Rules/MissingTypehintCheck.php index 1ef3fb87d28..b43d51b6dd8 100644 --- a/src/Rules/MissingTypehintCheck.php +++ b/src/Rules/MissingTypehintCheck.php @@ -73,8 +73,22 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array if ($type instanceof AccessoryType) { return $type; } - if ($type->isCallable()->yes() && $type->isArray()->yes()) { - return $type; + if ( + $type instanceof IntersectionType + && $type->isCallable()->yes() + && $type->isArray()->yes() + ) { + $nonArrayInner = []; + foreach ($type->getTypes() as $innerType) { + if ($innerType->isArray()->yes()) { + continue; + } + $nonArrayInner[] = $innerType; + } + if (count($nonArrayInner) === 1) { + return $traverse($nonArrayInner[0]); + } + return $traverse(new IntersectionType($nonArrayInner)); } if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) { $iterablesWithMissingValueTypehint = array_merge( diff --git a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php index 415f3a6d7de..9f2056500d4 100644 --- a/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php @@ -155,6 +155,16 @@ public function testBug14549(): void 'Method Bug14549\Foo::doFoo() has parameter $task with no signature specified for callable.', 12, ], + [ + 'Method Bug14549\Foo::doIntersection() has parameter $array with no value type specified in iterable type array.', + 46, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], + [ + 'Method Bug14549\Foo::doIntersection() has parameter $array with no value type specified in iterable type array.', + 46, + MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP, + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/bug-14549.php b/tests/PHPStan/Rules/Methods/data/bug-14549.php index daebd13fe65..c713ac3092c 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-14549.php +++ b/tests/PHPStan/Rules/Methods/data/bug-14549.php @@ -39,6 +39,14 @@ public function doBar(array $list): void } } } + + /** + * @param (array&callable(array): array) $array + */ + public function doIntersection($array): void + { + } + }