Skip to content

Commit fae693f

Browse files
phpstan-botclaude
andcommitted
Check key type compatibility when narrowing ArrayType with callable
A plain ArrayType carries a key type (e.g. array<string, mixed> has StringType keys). Since callable arrays always require integer keys 0 and 1, verify the key type is compatible with integers before narrowing. Extract the inline logic into narrowArrayTypeWithCallable for clarity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 65daf7c commit fae693f

2 files changed

Lines changed: 35 additions & 20 deletions

File tree

src/Type/TypeCombinator.php

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1688,18 +1688,11 @@ public static function intersect(Type ...$types): Type
16881688
&& get_class($types[$i]) === ArrayType::class
16891689
&& $types[$j] instanceof CallableType
16901690
) {
1691-
$existingValueType = $types[$i]->getItemType();
1692-
$offset0ValueType = self::intersect($existingValueType, new UnionType([new ClassStringType(), new ObjectWithoutClassType()]));
1693-
$offset1ValueType = self::intersect($existingValueType, new StringType());
1694-
if ($offset0ValueType instanceof NeverType || $offset1ValueType instanceof NeverType) {
1691+
$narrowed = self::narrowArrayTypeWithCallable($types[$i]);
1692+
if ($narrowed instanceof NeverType) {
16951693
return new NeverType();
16961694
}
1697-
$types[$i] = new ConstantArrayType(
1698-
[new ConstantIntegerType(0), new ConstantIntegerType(1)],
1699-
[$offset0ValueType, $offset1ValueType],
1700-
[2],
1701-
isList: TrinaryLogic::createYes(),
1702-
);
1695+
$types[$i] = $narrowed;
17031696
continue;
17041697
}
17051698

@@ -1708,18 +1701,11 @@ public static function intersect(Type ...$types): Type
17081701
&& get_class($types[$j]) === ArrayType::class
17091702
&& $types[$i] instanceof CallableType
17101703
) {
1711-
$existingValueType = $types[$j]->getItemType();
1712-
$offset0ValueType = self::intersect($existingValueType, new UnionType([new ClassStringType(), new ObjectWithoutClassType()]));
1713-
$offset1ValueType = self::intersect($existingValueType, new StringType());
1714-
if ($offset0ValueType instanceof NeverType || $offset1ValueType instanceof NeverType) {
1704+
$narrowed = self::narrowArrayTypeWithCallable($types[$j]);
1705+
if ($narrowed instanceof NeverType) {
17151706
return new NeverType();
17161707
}
1717-
$types[$j] = new ConstantArrayType(
1718-
[new ConstantIntegerType(0), new ConstantIntegerType(1)],
1719-
[$offset0ValueType, $offset1ValueType],
1720-
[2],
1721-
isList: TrinaryLogic::createYes(),
1722-
);
1708+
$types[$j] = $narrowed;
17231709
continue;
17241710
}
17251711

@@ -1841,6 +1827,27 @@ public static function removeTruthy(Type $type): Type
18411827
return self::remove($type, StaticTypeFactory::truthy());
18421828
}
18431829

1830+
private static function narrowArrayTypeWithCallable(ArrayType $arrayType): Type
1831+
{
1832+
if ($arrayType->getKeyType()->isSuperTypeOf(new IntegerType())->no()) {
1833+
return new NeverType();
1834+
}
1835+
1836+
$existingValueType = $arrayType->getItemType();
1837+
$offset0ValueType = self::intersect($existingValueType, new UnionType([new ClassStringType(), new ObjectWithoutClassType()]));
1838+
$offset1ValueType = self::intersect($existingValueType, new StringType());
1839+
if ($offset0ValueType instanceof NeverType || $offset1ValueType instanceof NeverType) {
1840+
return new NeverType();
1841+
}
1842+
1843+
return new ConstantArrayType(
1844+
[new ConstantIntegerType(0), new ConstantIntegerType(1)],
1845+
[$offset0ValueType, $offset1ValueType],
1846+
[2],
1847+
isList: TrinaryLogic::createYes(),
1848+
);
1849+
}
1850+
18441851
private static function narrowConstantArrayWithCallable(ConstantArrayType $constantArray): Type
18451852
{
18461853
$keyTypes = $constantArray->getKeyTypes();

tests/PHPStan/Analyser/nsrt/bug-14549.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ public function testTypedArrayNarrowing(array $task): void
5252
}
5353
}
5454

55+
/** @param array<string, mixed> $task */
56+
public function testStringKeyedArrayNarrowing(array $task): void
57+
{
58+
if (\is_callable($task)) {
59+
assertType('*NEVER*', $task);
60+
}
61+
}
62+
5563
/** @param callable-array $value */
5664
public function testCallableArrayPhpDoc(array $value): void
5765
{

0 commit comments

Comments
 (0)