Skip to content

Commit 82fb419

Browse files
committed
Resolve ConditionalTypeForParameter children before converting to ConditionalType to prevent infinite traversal
- In `ResolvedFunctionVariantWithOriginal::resolveConditionalTypesForParameter()`, traverse children (target, if, else) of `ConditionalTypeForParameter` first, then convert to `ConditionalType` and return without further traversal. This prevents entering the subject (the passed argument type), which may contain its own `ConditionalTypeForParameter` with a colliding parameter name (e.g. `$value`), causing infinite recursion. - Applied the same fix to the analogous pattern in `TypeSpecifier::resolveAssertTypes()`. - Triggered by `is_X(is_Y(...))` first-class callable combinations where both functions have conditional return types using the same parameter name `$value`.
1 parent d2d6328 commit 82fb419

File tree

3 files changed

+51
-3
lines changed

3 files changed

+51
-3
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1849,8 +1849,12 @@ private function specifyTypesFromAsserts(TypeSpecifierContext $context, Expr\Cal
18491849
if ($type instanceof ConditionalTypeForParameter) {
18501850
$parameterName = substr($type->getParameterName(), 1);
18511851
if (array_key_exists($parameterName, $argsMap)) {
1852-
$argType = TypeCombinator::union(...array_map(static fn (Expr $expr) => $scope->getType($expr), $argsMap[$parameterName]));
1853-
$type = $type->toConditional($argType);
1852+
$type = $traverse($type);
1853+
if ($type instanceof ConditionalTypeForParameter) {
1854+
$argType = TypeCombinator::union(...array_map(static fn (Expr $expr) => $scope->getType($expr), $argsMap[substr($type->getParameterName(), 1)]));
1855+
return $type->toConditional($argType);
1856+
}
1857+
return $type;
18541858
}
18551859
}
18561860

src/Reflection/ResolvedFunctionVariantWithOriginal.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,13 @@ private function resolveConditionalTypesForParameter(Type $type): Type
290290
{
291291
return TypeTraverser::map($type, function (Type $type, callable $traverse): Type {
292292
if ($type instanceof ConditionalTypeForParameter && array_key_exists($type->getParameterName(), $this->passedArgs)) {
293-
$type = $type->toConditional($this->passedArgs[$type->getParameterName()]);
293+
// Traverse children first, then convert — avoids infinite loop when
294+
// the passed argument contains ConditionalTypeForParameter with a colliding parameter name.
295+
$type = $traverse($type);
296+
if ($type instanceof ConditionalTypeForParameter) {
297+
return $type->toConditional($this->passedArgs[$type->getParameterName()]);
298+
}
299+
return $type;
294300
}
295301

296302
return $traverse($type);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Bug13872;
4+
5+
use function is_array;
6+
use function is_bool;
7+
use function is_callable;
8+
use function is_countable;
9+
use function is_float;
10+
use function is_int;
11+
use function is_iterable;
12+
use function is_null;
13+
use function is_numeric;
14+
use function is_object;
15+
use function is_resource;
16+
use function is_scalar;
17+
use function is_string;
18+
use function PHPStan\Testing\assertType;
19+
20+
class Foo
21+
{
22+
public function check(): void
23+
{
24+
assertType('true', is_callable(is_callable(...)));
25+
assertType('true', is_callable(is_array(...)));
26+
assertType('false', is_array(is_string(...)));
27+
assertType('false', is_string(is_int(...)));
28+
assertType('false', is_int(is_callable(...)));
29+
assertType('true', is_object(is_callable(...)));
30+
assertType('true', is_callable(is_bool(...)));
31+
assertType('false', is_null(is_string(...)));
32+
assertType('false', is_float(is_int(...)));
33+
assertType('false', is_scalar(is_numeric(...)));
34+
assertType('false', is_countable(is_array(...)));
35+
assertType('false', is_iterable(is_string(...)));
36+
assertType('false', is_resource(is_callable(...)));
37+
}
38+
}

0 commit comments

Comments
 (0)