diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 109d6f233a..81e6d3272d 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -943,7 +943,17 @@ public function hasOffsetValueType(Type $offsetType): TrinaryLogic } } - return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType)); + $result = $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType)); + + if (!$result->yes() && $this->isCallable()->yes() && $this->isArray()->yes()) { + $arrayKeyOffsetType = $offsetType->toArrayKey(); + $callableArrayOffsetType = new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]); + if ($callableArrayOffsetType->isSuperTypeOf($arrayKeyOffsetType)->yes()) { + return TrinaryLogic::createYes(); + } + } + + return $result; } public function getOffsetValueType(Type $offsetType): Type @@ -953,6 +963,21 @@ public function getOffsetValueType(Type $offsetType): Type return TypeUtils::toBenevolentUnion($result); } + if ($this->isCallable()->yes() && $this->isArray()->yes()) { + $arrayKeyOffsetType = $offsetType->toArrayKey(); + $callableArrayOffsetType = new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1)]); + if ($callableArrayOffsetType->isSuperTypeOf($arrayKeyOffsetType)->yes()) { + if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) { + $narrowedType = new UnionType([new ClassStringType(), new ObjectWithoutClassType()]); + } elseif ((new ConstantIntegerType(1))->isSuperTypeOf($arrayKeyOffsetType)->yes()) { + $narrowedType = new StringType(); + } else { + $narrowedType = new UnionType([new StringType(), new ObjectWithoutClassType()]); + } + $result = TypeCombinator::intersect($result, $narrowedType); + } + } + return $result; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-3842.php b/tests/PHPStan/Analyser/nsrt/bug-3842.php new file mode 100644 index 0000000000..51e607ea40 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3842.php @@ -0,0 +1,62 @@ +&callable(): mixed', $value); + assertType('class-string|object', $value[0]); + assertType('string', $value[1]); + } +} + +/** @param callable-array $value */ +function testCallableArrayPhpDoc(array $value): void { + assertType('array&callable(): mixed', $value); + assertType('class-string|object', $value[0]); + assertType('string', $value[1]); +} + +function testIsStringOnCallable(callable $value): void { + if (is_string($value)) { + assertType('callable-string', $value); + } +} + +/** @param array{string|object, string} $values */ +function check(array $values): void { +} + +/** @param array{class-string|object, string} $values */ +function checkClassString(array $values): void { +} + +/** @param 0|1 $offset */ +function testCallableArrayUnionOffset(callable $value, int $offset): void { + if (is_array($value)) { + assertType('object|string', $value[$offset]); + } +} + +function testPassCallableArray(callable $value): void { + if (is_array($value)) { + check($value); + checkClassString($value); + } +} diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index bfa38f42d5..3da9519243 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2866,4 +2866,9 @@ public function testBug13643(): void $this->analyse([__DIR__ . '/data/bug-13643.php'], []); } + public function testBug3842(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-3842.php'], []); + } + }