Check isFirstClassCallable() before calling getArgs() on nested FuncCall nodes#5564
Merged
staabm merged 8 commits intophpstan:2.1.xfrom Apr 29, 2026
Merged
Conversation
…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
staabm
reviewed
Apr 29, 2026
staabm
reviewed
Apr 29, 2026
staabm
reviewed
Apr 29, 2026
staabm
reviewed
Apr 29, 2026
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>
staabm
reviewed
Apr 29, 2026
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
approved these changes
Apr 29, 2026
staabm
reviewed
Apr 29, 2026
| @@ -0,0 +1,160 @@ | |||
| <?php // lint >= 8.1 | |||
Contributor
There was a problem hiding this comment.
Turn this into a AnalyserIntegrationTest as we are covering crash bugs
Collaborator
Author
There was a problem hiding this comment.
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
approved these changes
Apr 29, 2026
VincentLanglet
approved these changes
Apr 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PHPStan crashes with
assert(!$this->isFirstClassCallable())when using first-class callable syntax with certain array functions likearray_key_last(...),array_key_first(...),array_rand(...), etc. The crash occurs becausegetArgs()is called on nestedFuncCallnodes without first checking whether the call is a first-class callable.Changes
src/Analyser/TypeSpecifier.php— Added!$expr->isFirstClassCallable()checks beforegetArgs()calls in all patterns that match function names on nestedFuncCallnodes within assignments and comparisons:array_key_first/array_key_lastassignment pattern (line ~829)array_randassignment pattern (line ~885)array_searchassignment pattern (line ~942)count/sizeof - 1assignment pattern (line ~917)count/sizeofcomparison patterns (lines ~251, ~279, ~382)preg_matchcomparison pattern (line ~408)strlen/mb_strlencomparison pattern (line ~425)gettype,get_parent_class,get_class/get_debug_typeequality patterns (lines ~1609, ~1649, ~2765)trim/ltrim/rtrimequality pattern (line ~1691)count/sizeofidentical/equal comparison patterns (lines ~2909, ~2920)src/Analyser/NodeScopeResolver.php— Added guards for:array_keysin foreach expression handling (line ~4142)count/sizeofin for-loop condition patterns (lines ~4818, ~4842)src/Analyser/ExprHandler/AssignHandler.php— Added guards for:count/sizeofin$list[count($list) - n]list-preservation checkarray_key_first/array_key_lastin$list[array_key_last($list)]list-preservation checkarray_searchin$list[array_search($needle, $list)]list-preservation checksrc/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php— Added guards for:array_randdim fetch pattern (already had the check forarray_key_first/array_key_last)count/sizeof - 1dim fetch patternAnalogous cases probed and found already correct
ArrayMapArgVisitor,ArrayFilterArgVisitor, etc.) — all 14 visitors already checkisFirstClassCallable()ImpossibleCheckTypeHelper.php— protected by function-level guard at entry + callers that don't pass first-class callablesConstantConditionRuleHelper.php— explicitly checks!$expr->isFirstClassCallable()before callingfindSpecifiedTypeInitializerExprTypeResolver.php— has top-levelisFirstClassCallable()guardFuncCallHandler.php—supports()method already rejects first-class callablesRoot cause
The
TypeSpecifieris called with original AST nodes (not the convertedFunctionCallableNodeequivalents). When processing assignments like$fn = array_key_last(...), the TypeSpecifier checks if the RHS is aFuncCallnamedarray_key_first/array_key_lastand callsgetArgs()to access the function arguments. However,array_key_last(...)is a first-class callable wheregetArgs()triggers an assertion failure because first-class callables useVariadicPlaceholderarguments, not realArgnodes.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 inNodeScopeResolver,AssignHandler, andNonexistentOffsetInArrayDimFetchRule.Test
Added
tests/PHPStan/Analyser/nsrt/bug-14550.php— an NSRT test that creates first-class callables ofarray_key_first,array_key_last,array_rand, andarray_search, and asserts their types. Without the fix, this crashes withassert(!$this->isFirstClassCallable()).Fixes phpstan/phpstan#14550