Skip to content

Commit a9501e9

Browse files
phpstan-botclaude
andcommitted
Use narrowed iterable types in IntersectionType::isAcceptedBy() for array&callable
When an array&callable intersection is checked for acceptance, the raw ArrayType(mixed, mixed) component was used, causing array<int> to incorrectly accept array&callable. Now the array component uses the narrowed key/value types (int<0,1>, object|non-falsy-string) so the acceptance check correctly rejects incompatible array types. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3aa818d commit a9501e9

3 files changed

Lines changed: 54 additions & 1 deletion

File tree

src/Type/IntersectionType.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,23 @@ public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult
299299

300300
public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult
301301
{
302+
$types = $this->types;
303+
if ($this->isCallable()->yes() && $this->isArray()->yes()) {
304+
$narrowedKeyType = $this->getIterableKeyType();
305+
$narrowedValueType = $this->getIterableValueType();
306+
$types = array_map(static function (Type $innerType) use ($narrowedKeyType, $narrowedValueType): Type {
307+
if (!$innerType->isArray()->yes()) {
308+
return $innerType;
309+
}
310+
if (!$innerType->getIterableValueType() instanceof MixedType) {
311+
return $innerType;
312+
}
313+
return new ArrayType($narrowedKeyType, $narrowedValueType);
314+
}, $types);
315+
}
316+
302317
$result = AcceptsResult::lazyMaxMin(
303-
$this->types,
318+
$types,
304319
static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes),
305320
);
306321

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4017,4 +4017,21 @@ public function testBug13272(): void
40174017
$this->analyse([__DIR__ . '/data/bug-13272.php'], []);
40184018
}
40194019

4020+
public function testBug14549(): void
4021+
{
4022+
$this->checkThisOnly = false;
4023+
$this->checkNullables = true;
4024+
$this->checkUnionTypes = true;
4025+
$this->analyse([__DIR__ . '/data/bug-14549.php'], [
4026+
[
4027+
'Parameter #1 $param of method Bug14549\Foo::call() expects array<int>, array&callable given.',
4028+
67,
4029+
],
4030+
[
4031+
'Parameter #1 $param of method Bug14549\Foo::call() expects array<int>, array&callable given.',
4032+
75,
4033+
],
4034+
]);
4035+
}
4036+
40204037
}

tests/PHPStan/Rules/Methods/data/bug-14549.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,27 @@ public function doBaz(array $task): void
5454
{
5555
}
5656

57+
/** @param array<int> $param */
58+
public function call(array $param): void
59+
{
60+
}
61+
62+
/**
63+
* @param callable-array $task
64+
*/
65+
public function doCallWithCallableArray(array $task): void
66+
{
67+
$this->call($task);
68+
}
69+
70+
/**
71+
* @param callable&array $task
72+
*/
73+
public function doCallWithCallableAndArray(array $task): void
74+
{
75+
$this->call($task);
76+
}
77+
5778
}
5879

5980

0 commit comments

Comments
 (0)