Skip to content

Resolve ConditionalTypeForParameter children before converting to ConditionalType to prevent infinite traversal#5507

Merged
ondrejmirtes merged 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-wvi7owy
Apr 21, 2026
Merged

Resolve ConditionalTypeForParameter children before converting to ConditionalType to prevent infinite traversal#5507
ondrejmirtes merged 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-wvi7owy

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

PHPStan goes OOM (infinite loop) when analysing is_callable(is_callable(...)) and similar patterns where a first-class callable of a function with a conditional return type is passed as an argument to another function with a conditional return type using the same parameter name.

Changes

  • src/Reflection/ResolvedFunctionVariantWithOriginal.php: In resolveConditionalTypesForParameter(), changed the traversal order — traverse children of ConditionalTypeForParameter (target, if, else) first via $traverse(), then convert to ConditionalType and return directly without further traversal into the subject (passed argument).
  • src/Analyser/TypeSpecifier.php: Applied the same fix to the analogous ConditionalTypeForParameterConditionalType conversion in assert type resolution.
  • tests/PHPStan/Analyser/nsrt/bug-13872.php: Regression test covering is_callable(is_callable(...)), is_callable(is_array(...)), is_array(is_string(...)), and many other is_X(is_Y(...)) combinations.

Root cause

When resolveConditionalTypesForParameter encounters a ConditionalTypeForParameter node, it converts it to a ConditionalType whose subject is the passed argument type. The TypeTraverser then recurses into the ConditionalType's children, including the subject. If the subject is a ClosureType (from a first-class callable like is_callable(...)), the traversal enters the closure's return type, which also contains a ConditionalTypeForParameter with the same parameter name ($value). This triggers the same conversion again with the same subject, creating an infinite loop.

All is_* type-checking functions (is_callable, is_array, is_string, is_int, is_float, is_bool, is_null, is_numeric, is_object, is_scalar, is_countable, is_iterable, is_resource) use $value as their parameter name in their conditional return type @return ($value is T ? true : false), so any is_X(is_Y(...)) combination triggers this.

The fix resolves children first (which only visits target/if/else — no subject), then converts, and returns without further traversal into the newly created subject.

Test

Added tests/PHPStan/Analyser/nsrt/bug-13872.php with 13 assertions covering various is_X(is_Y(...)) first-class callable combinations that previously caused infinite loops, verifying both the absence of the hang and correct type inference results.

Fixes phpstan/phpstan#13872

…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`.
@ondrejmirtes ondrejmirtes merged commit 82fb419 into phpstan:2.1.x Apr 21, 2026
363 checks passed
@ondrejmirtes ondrejmirtes deleted the create-pull-request/patch-wvi7owy branch April 21, 2026 20:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants