Skip to content

Commit 95e0784

Browse files
phpstan-botVincentLangletclaude
authored
Fix callable array intersection type offset narrowing (phpstan#5548)
Co-authored-by: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4279f35 commit 95e0784

3 files changed

Lines changed: 93 additions & 1 deletion

File tree

src/Type/IntersectionType.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -943,7 +943,17 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic
943943
}
944944
}
945945

946-
return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
946+
$result = $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
947+
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+
956+
return $result;
947957
}
948958

949959
public function getOffsetValueType(Type $offsetType): Type
@@ -953,6 +963,21 @@ public function getOffsetValueType(Type $offsetType): Type
953963
return TypeUtils::toBenevolentUnion($result);
954964
}
955965

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+
956981
return $result;
957982
}
958983

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug3842;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class ClassA
8+
{
9+
public static function callback(): void
10+
{
11+
}
12+
}
13+
14+
class ClassB
15+
{
16+
public function callback(): void
17+
{
18+
}
19+
}
20+
21+
function testIsArrayOnCallable(callable $value): void {
22+
if (is_array($value)) {
23+
assertType('array<mixed, mixed>&callable(): mixed', $value);
24+
assertType('class-string|object', $value[0]);
25+
assertType('string', $value[1]);
26+
}
27+
}
28+
29+
/** @param callable-array $value */
30+
function testCallableArrayPhpDoc(array $value): void {
31+
assertType('array&callable(): mixed', $value);
32+
assertType('class-string|object', $value[0]);
33+
assertType('string', $value[1]);
34+
}
35+
36+
function testIsStringOnCallable(callable $value): void {
37+
if (is_string($value)) {
38+
assertType('callable-string', $value);
39+
}
40+
}
41+
42+
/** @param array{string|object, string} $values */
43+
function check(array $values): void {
44+
}
45+
46+
/** @param array{class-string|object, string} $values */
47+
function checkClassString(array $values): void {
48+
}
49+
50+
/** @param 0|1 $offset */
51+
function testCallableArrayUnionOffset(callable $value, int $offset): void {
52+
if (is_array($value)) {
53+
assertType('object|string', $value[$offset]);
54+
}
55+
}
56+
57+
function testPassCallableArray(callable $value): void {
58+
if (is_array($value)) {
59+
check($value);
60+
checkClassString($value);
61+
}
62+
}

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2866,4 +2866,9 @@ public function testBug13643(): void
28662866
$this->analyse([__DIR__ . '/data/bug-13643.php'], []);
28672867
}
28682868

2869+
public function testBug3842(): void
2870+
{
2871+
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3842.php'], []);
2872+
}
2873+
28692874
}

0 commit comments

Comments
 (0)