Skip to content

Commit 0f240cc

Browse files
phpstan-botclaude
andcommitted
Narrow ConstantArrayType with callable and remove IntersectionType special behavior
Move callable-array narrowing logic entirely into TypeCombinator::intersect(): - Add ConstantArrayType + CallableType handling to narrow value types at offsets 0 and 1 - Update callable-array PHPDoc resolution to use TypeCombinator::intersect() - Remove redundant hasOffsetValueType/getOffsetValueType callable logic from IntersectionType Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 64fbc7b commit 0f240cc

7 files changed

Lines changed: 58 additions & 31 deletions

File tree

phpstan-baseline.neon

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,13 +1698,13 @@ parameters:
16981698
-
16991699
rawMessage: 'Doing instanceof PHPStan\Type\CallableType is error-prone and deprecated. Use Type::isCallable() and Type::getCallableParametersAcceptors() instead.'
17001700
identifier: phpstanApi.instanceofType
1701-
count: 3
1701+
count: 5
17021702
path: src/Type/TypeCombinator.php
17031703

17041704
-
17051705
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantArrayType is error-prone and deprecated. Use Type::getConstantArrays() instead.'
17061706
identifier: phpstanApi.instanceofType
1707-
count: 19
1707+
count: 21
17081708
path: src/Type/TypeCombinator.php
17091709

17101710
-

src/PhpDoc/TypeNodeResolver.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco
429429
return new IntersectionType([new ObjectWithoutClassType(), new CallableType()]);
430430

431431
case 'callable-array':
432-
return new IntersectionType([new ArrayType(new MixedType(), new MixedType()), new CallableType()]);
432+
return TypeCombinator::intersect(new ArrayType(new MixedType(), new MixedType()), new CallableType());
433433

434434
case 'never':
435435
case 'noreturn':

src/Type/IntersectionType.php

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -945,14 +945,6 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic
945945

946946
$result = $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
947947

948-
if (!$result->yes() && $this->isCallable()->yes() && $this->isArray()->yes()) {
949-
$arrayKeyOffsetType = $offsetType->toArrayKey();
950-
$callableArrayOffsetType = new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]);
951-
if ($callableArrayOffsetType->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
952-
return TrinaryLogic::createYes();
953-
}
954-
}
955-
956948
return $result;
957949
}
958950

@@ -963,21 +955,6 @@ public function getOffsetValueType(Type $offsetType): Type
963955
return TypeUtils::toBenevolentUnion($result);
964956
}
965957

966-
if ($this->isCallable()->yes() && $this->isArray()->yes()) {
967-
$arrayKeyOffsetType = $offsetType->toArrayKey();
968-
$callableArrayOffsetType = new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]);
969-
if ($callableArrayOffsetType->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
970-
if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
971-
$narrowedType = new UnionType([new ClassStringType(), new ObjectWithoutClassType()]);
972-
} elseif ((new ConstantIntegerType(1))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
973-
$narrowedType = new StringType();
974-
} else {
975-
$narrowedType = new UnionType([new StringType(), new ObjectWithoutClassType()]);
976-
}
977-
$result = TypeCombinator::intersect($result, $narrowedType);
978-
}
979-
}
980-
981958
return $result;
982959
}
983960

src/Type/TypeCombinator.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1723,6 +1723,28 @@ public static function intersect(Type ...$types): Type
17231723
continue;
17241724
}
17251725

1726+
if (
1727+
$types[$i] instanceof ConstantArrayType
1728+
&& $types[$j] instanceof CallableType
1729+
) {
1730+
$types[$i] = self::narrowConstantArrayWithCallable($types[$i]);
1731+
if ($types[$i] instanceof NeverType) {
1732+
return new NeverType();
1733+
}
1734+
continue;
1735+
}
1736+
1737+
if (
1738+
$types[$j] instanceof ConstantArrayType
1739+
&& $types[$i] instanceof CallableType
1740+
) {
1741+
$types[$j] = self::narrowConstantArrayWithCallable($types[$j]);
1742+
if ($types[$j] instanceof NeverType) {
1743+
return new NeverType();
1744+
}
1745+
continue;
1746+
}
1747+
17261748
continue;
17271749
}
17281750

@@ -1819,4 +1841,33 @@ public static function removeTruthy(Type $type): Type
18191841
return self::remove($type, StaticTypeFactory::truthy());
18201842
}
18211843

1844+
private static function narrowConstantArrayWithCallable(ConstantArrayType $constantArray): Type
1845+
{
1846+
$keyTypes = $constantArray->getKeyTypes();
1847+
$valueTypes = $constantArray->getValueTypes();
1848+
$newValueTypes = $valueTypes;
1849+
1850+
foreach ($keyTypes as $k => $keyType) {
1851+
if ((new ConstantIntegerType(0))->isSuperTypeOf($keyType)->yes()) {
1852+
$newValueTypes[$k] = self::intersect($valueTypes[$k], new UnionType([new ClassStringType(), new ObjectWithoutClassType()]));
1853+
if ($newValueTypes[$k] instanceof NeverType) {
1854+
return new NeverType();
1855+
}
1856+
} elseif ((new ConstantIntegerType(1))->isSuperTypeOf($keyType)->yes()) {
1857+
$newValueTypes[$k] = self::intersect($valueTypes[$k], new StringType());
1858+
if ($newValueTypes[$k] instanceof NeverType) {
1859+
return new NeverType();
1860+
}
1861+
}
1862+
}
1863+
1864+
return new ConstantArrayType(
1865+
$keyTypes,
1866+
$newValueTypes,
1867+
$constantArray->getNextAutoIndexes(),
1868+
$constantArray->getOptionalKeys(),
1869+
$constantArray->isList(),
1870+
);
1871+
}
1872+
18221873
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ public function testCallableArrayIterableTypes(callable $value): void
3636
public function testConstantArrayNarrowing(array $task): void
3737
{
3838
if (\is_callable($task)) {
39-
// ConstantArrayType keeps its shape, callable narrows offset access
40-
assertType('list{string, string}&callable(): mixed', $task);
39+
assertType('list{class-string, string}&callable(): mixed', $task);
4140
assertType('class-string', $task[0]);
4241
assertType('string', $task[1]);
4342
}
@@ -56,7 +55,7 @@ public function testTypedArrayNarrowing(array $task): void
5655
/** @param callable-array $value */
5756
public function testCallableArrayPhpDoc(array $value): void
5857
{
59-
assertType('array&callable(): mixed', $value);
58+
assertType('list{class-string|object, string}&callable(): mixed', $value);
6059
assertType('class-string|object', $value[0]);
6160
assertType('string', $value[1]);
6261
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function testIsArrayOnCallable(callable $value): void {
2828

2929
/** @param callable-array $value */
3030
function testCallableArrayPhpDoc(array $value): void {
31-
assertType('array&callable(): mixed', $value);
31+
assertType('list{class-string|object, string}&callable(): mixed', $value);
3232
assertType('class-string|object', $value[0]);
3333
assertType('string', $value[1]);
3434
}

tests/PHPStan/Analyser/nsrt/more-types.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function doFoo(
3939
): void
4040
{
4141
assertType('pure-callable(): mixed', $pureCallable);
42-
assertType('array&callable(): mixed', $callableArray);
42+
assertType('list{class-string|object, string}&callable(): mixed', $callableArray);
4343
assertType('resource', $closedResource);
4444
assertType('resource', $openResource);
4545
assertType('class-string<UnitEnum>', $enumString);

0 commit comments

Comments
 (0)