Skip to content

Commit ca76429

Browse files
phpstan-botclaude
authored andcommitted
Use isSuperTypeOf as guard in IntersectionType::isAcceptedBy() instead of special-casing callable&array
Replace the callable&array-specific narrowing logic with a general approach: after the standard lazyMaxMin check, if it returns Yes, verify with isSuperTypeOf that the accepting type is actually a supertype of the full intersection. This catches cases where MixedType's accepts-everything behavior causes false acceptances (e.g. array<int> falsely accepting array<mixed>&hasOffsetValue or array<mixed>&callable intersections). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8bbf9bb commit ca76429

3 files changed

Lines changed: 25 additions & 16 deletions

File tree

src/Type/IntersectionType.php

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -299,26 +299,18 @@ 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-
317302
$result = AcceptsResult::lazyMaxMin(
318-
$types,
303+
$this->types,
319304
static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes),
320305
);
321306

307+
if ($result->yes()) {
308+
$isSuperType = $acceptingType->isSuperTypeOf($this);
309+
if ($isSuperType->no()) {
310+
return $isSuperType->toAcceptsResult();
311+
}
312+
}
313+
322314
if ($this->isOversizedArray()->yes()) {
323315
if (!$result->no()) {
324316
return AcceptsResult::createYes();

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4055,6 +4055,10 @@ public function testBug14549(): void
40554055
'Parameter #1 $param of method Bug14549Bis\Foo::callConstantArrayObjectOrStringStringString() expects array{object|string, string, string}, array&callable(): mixed given.',
40564056
47,
40574057
],
4058+
[
4059+
'Parameter #1 $param of method Bug14549Bis\Foo::callArrayString() expects array<string>, array given.',
4060+
58,
4061+
],
40584062
]);
40594063
}
40604064

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,17 @@ public function doCallWithCallableAndArray(array $task): void
4747
$this->callConstantArrayObjectOrStringStringString($task);
4848
}
4949

50+
/** @param array<string> $param */
51+
public function callArrayString(array $param): void
52+
{
53+
}
54+
55+
public function doCallWithHasOffsetValue(array $arr): void
56+
{
57+
if (isset($arr[1]) && $arr[1] === 1) {
58+
$this->callArrayString($arr);
59+
$this->callArrayInt($arr);
60+
}
61+
}
62+
5063
}

0 commit comments

Comments
 (0)