Skip to content

Check isFirstClassCallable() before calling getArgs() on nested FuncCall nodes#5564

Merged
staabm merged 8 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-zrmaq32
Apr 29, 2026
Merged

Check isFirstClassCallable() before calling getArgs() on nested FuncCall nodes#5564
staabm merged 8 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-zrmaq32

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

PHPStan crashes with assert(!$this->isFirstClassCallable()) when using first-class callable syntax with certain array functions like array_key_last(...), array_key_first(...), array_rand(...), etc. The crash occurs because getArgs() is called on nested FuncCall nodes without first checking whether the call is a first-class callable.

Changes

  • src/Analyser/TypeSpecifier.php — Added !$expr->isFirstClassCallable() checks before getArgs() calls in all patterns that match function names on nested FuncCall nodes within assignments and comparisons:

    • array_key_first/array_key_last assignment pattern (line ~829)
    • array_rand assignment pattern (line ~885)
    • array_search assignment pattern (line ~942)
    • count/sizeof - 1 assignment pattern (line ~917)
    • count/sizeof comparison patterns (lines ~251, ~279, ~382)
    • preg_match comparison pattern (line ~408)
    • strlen/mb_strlen comparison pattern (line ~425)
    • gettype, get_parent_class, get_class/get_debug_type equality patterns (lines ~1609, ~1649, ~2765)
    • trim/ltrim/rtrim equality pattern (line ~1691)
    • count/sizeof identical/equal comparison patterns (lines ~2909, ~2920)
  • src/Analyser/NodeScopeResolver.php — Added guards for:

    • array_keys in foreach expression handling (line ~4142)
    • count/sizeof in for-loop condition patterns (lines ~4818, ~4842)
  • src/Analyser/ExprHandler/AssignHandler.php — Added guards for:

    • count/sizeof in $list[count($list) - n] list-preservation check
    • array_key_first/array_key_last in $list[array_key_last($list)] list-preservation check
    • array_search in $list[array_search($needle, $list)] list-preservation check
  • src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php — Added guards for:

    • array_rand dim fetch pattern (already had the check for array_key_first/array_key_last)
    • count/sizeof - 1 dim fetch pattern

Analogous cases probed and found already correct

  • Parser visitors (ArrayMapArgVisitor, ArrayFilterArgVisitor, etc.) — all 14 visitors already check isFirstClassCallable()
  • ImpossibleCheckTypeHelper.php — protected by function-level guard at entry + callers that don't pass first-class callables
  • ConstantConditionRuleHelper.php — explicitly checks !$expr->isFirstClassCallable() before calling findSpecifiedType
  • InitializerExprTypeResolver.php — has top-level isFirstClassCallable() guard
  • FuncCallHandler.phpsupports() method already rejects first-class callables

Root cause

The TypeSpecifier is called with original AST nodes (not the converted FunctionCallableNode equivalents). When processing assignments like $fn = array_key_last(...), the TypeSpecifier checks if the RHS is a FuncCall named array_key_first/array_key_last and calls getArgs() to access the function arguments. However, array_key_last(...) is a first-class callable where getArgs() triggers an assertion failure because first-class callables use VariadicPlaceholder arguments, not real Arg nodes.

The top-level guard in TypeSpecifier::specifyTypesInCondition() (line 138) catches first-class callables that ARE the expression being analyzed, but not first-class callables that are NESTED inside assignments, comparisons, or array dim fetches. The same pattern exists in NodeScopeResolver, AssignHandler, and NonexistentOffsetInArrayDimFetchRule.

Test

Added tests/PHPStan/Analyser/nsrt/bug-14550.php — an NSRT test that creates first-class callables of array_key_first, array_key_last, array_rand, and array_search, and asserts their types. Without the fix, this crashes with assert(!$this->isFirstClassCallable()).

Fixes phpstan/phpstan#14550

…FuncCall` nodes

- Fix crash when using first-class callable syntax with array_key_first,
  array_key_last, array_rand, and array_search (e.g. `$fn = array_key_last(...)`)
- Add `!$expr->isFirstClassCallable()` guards in TypeSpecifier.php for all
  patterns that check function names on nested FuncCall nodes: array_key_first/last,
  array_rand, array_search, count/sizeof, strlen/mb_strlen, preg_match, gettype,
  get_parent_class, get_class, get_debug_type, trim/ltrim/rtrim
- Add same guards in NodeScopeResolver.php for array_keys in foreach and
  count/sizeof in for-loop conditions
- Add same guards in AssignHandler.php for count/sizeof, array_key_first/last,
  and array_search in array dim fetch list-preservation checks
- Add same guards in NonexistentOffsetInArrayDimFetchRule.php for array_rand
  and count/sizeof patterns
Comment thread src/Analyser/TypeSpecifier.php
Comment thread src/Analyser/TypeSpecifier.php
Comment thread src/Analyser/TypeSpecifier.php
Comment thread src/Analyser/ExprHandler/AssignHandler.php
phpstan-bot and others added 5 commits April 29, 2026 06:14
Cover first-class callable syntax for count, sizeof, strlen, mb_strlen,
preg_match, gettype, get_class, get_debug_type, get_parent_class,
trim, ltrim, rtrim, and array_keys.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…xpand tests

Add missing guards for strlen/mb_strlen, array_key_first/array_key_last,
get_class/get_debug_type, and substr/strstr/etc patterns in
resolveNormalizedIdentical that were not covered by the initial fix.

Expand tests to cover all affected functions: strlen, mb_strlen, count,
sizeof, preg_match, gettype, get_class, get_debug_type, get_parent_class,
trim, ltrim, rtrim, array_keys (foreach), and count (for-loop). Tests
exercise both assignment and comparison/equality code paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread tests/PHPStan/Analyser/data/bug-14550.php
Most tests were simple `$fn = someFunc(...); assertType(...)` patterns
that never triggered the assertion because standalone assignments only
exercise the TypeSpecifier assign patterns (array_key_first,
array_key_last, array_rand, count-1). Functions like strlen, count,
gettype, trim etc. need their first-class callables inside if-conditions
to exercise the comparison, identical, and string-equality guards.

Each test now uses the specific expression pattern that would crash
without the corresponding isFirstClassCallable() guard:
- Comparisons: `if (count(...) < 1)`, `if (0 < strlen(...))`
- Identicals: `if (count(...) === 0)`, `if (strlen(...) === 0)`
- Null checks: `if (array_key_first(...) !== null)`
- String equality: `if (gettype(...) === 'string')`, `if (trim(...) !== '')`
- Conditions: `if ($key = array_search(...))`
- Foreach: `foreach (array_keys(...) as $key)`
- For loops: `for ($i = 0; $i < count(...); $i++)`

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@staabm staabm requested a review from VincentLanglet April 29, 2026 07:20
@@ -0,0 +1,160 @@
<?php // lint >= 8.1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turn this into a AnalyserIntegrationTest as we are covering crash bugs

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Converted the bug-14550 test from an NSRT test (tests/PHPStan/Analyser/nsrt/bug-14550.php) to an AnalyserIntegrationTest (tests/PHPStan/Analyser/data/bug-14550.php), since this is a crash bug. The test data file was simplified to remove assertType() calls (not needed for crash prevention), and a testBug14550() method was added to AnalyserIntegrationTest that verifies the analysis completes without crashing. All 11,941 tests pass and make phpstan reports no errors.

Since this is a crash bug, an AnalyserIntegrationTest is more appropriate
than an NSRT test. The test verifies the analysis completes without
crashing on first-class callables in various TypeSpecifier code paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@staabm staabm merged commit b6a75c6 into phpstan:2.1.x Apr 29, 2026
659 of 661 checks passed
@staabm staabm deleted the create-pull-request/patch-zrmaq32 branch April 29, 2026 09:27
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.

3 participants