Skip to content

Commit 275d81e

Browse files
committed
Check isFirstClassCallable() before calling getArgs() on nested 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
1 parent dc2650e commit 275d81e

5 files changed

Lines changed: 66 additions & 0 deletions

File tree

src/Analyser/ExprHandler/AssignHandler.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,6 +1221,7 @@ private function shouldKeepList(ArrayDimFetch $arrayDimFetch, Scope $scope, Type
12211221
&& $arrayDimFetch->dim->right instanceof Node\Scalar\Int_
12221222
&& $arrayDimFetch->dim->left instanceof Expr\FuncCall
12231223
&& $arrayDimFetch->dim->left->name instanceof Name
1224+
&& !$arrayDimFetch->dim->left->isFirstClassCallable()
12241225
&& in_array($arrayDimFetch->dim->left->name->toLowerString(), ['count', 'sizeof'], true)
12251226
&& count($arrayDimFetch->dim->left->getArgs()) === 1 // could support COUNT_RECURSIVE, COUNT_NORMAL
12261227
&& $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->left->getArgs()[0]->value)
@@ -1231,6 +1232,7 @@ private function shouldKeepList(ArrayDimFetch $arrayDimFetch, Scope $scope, Type
12311232
} elseif ( // keep list for $list[array_key_last($list)] and $list[array_key_first($list)] assignments
12321233
$arrayDimFetch->dim instanceof Expr\FuncCall
12331234
&& $arrayDimFetch->dim->name instanceof Name
1235+
&& !$arrayDimFetch->dim->isFirstClassCallable()
12341236
&& in_array($arrayDimFetch->dim->name->toLowerString(), ['array_key_last', 'array_key_first'], true)
12351237
&& count($arrayDimFetch->dim->getArgs()) >= 1
12361238
&& $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->getArgs()[0]->value)
@@ -1239,6 +1241,7 @@ private function shouldKeepList(ArrayDimFetch $arrayDimFetch, Scope $scope, Type
12391241
} elseif ( // keep list for $list[array_search($needle, $list)] assignments
12401242
$arrayDimFetch->dim instanceof Expr\FuncCall
12411243
&& $arrayDimFetch->dim->name instanceof Name
1244+
&& !$arrayDimFetch->dim->isFirstClassCallable()
12421245
&& $arrayDimFetch->dim->name->toLowerString() === 'array_search'
12431246
&& count($arrayDimFetch->dim->getArgs()) >= 1
12441247
&& $this->isSameVariable($arrayDimFetch->var, $arrayDimFetch->dim->getArgs()[1]->value)

src/Analyser/NodeScopeResolver.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4141,6 +4141,7 @@ private function enterForeach(MutatingScope $scope, ExpressionResultStorage $sto
41414141
if (
41424142
$stmt->expr instanceof FuncCall
41434143
&& $stmt->expr->name instanceof Name
4144+
&& !$stmt->expr->isFirstClassCallable()
41444145
&& $stmt->expr->name->toLowerString() === 'array_keys'
41454146
&& $stmt->valueVar instanceof Variable
41464147
) {
@@ -4816,6 +4817,7 @@ private function inferForLoopExpressions(For_ $stmt, Expr $lastCondExpr, Mutatin
48164817
&& $lastCondExpr->left instanceof Variable
48174818
&& $lastCondExpr->right instanceof FuncCall
48184819
&& $lastCondExpr->right->name instanceof Name
4820+
&& !$lastCondExpr->right->isFirstClassCallable()
48194821
&& in_array($lastCondExpr->right->name->toLowerString(), ['count', 'sizeof'], true)
48204822
&& count($lastCondExpr->right->getArgs()) > 0
48214823
&& $lastCondExpr->right->getArgs()[0]->value instanceof Variable
@@ -4840,6 +4842,7 @@ private function inferForLoopExpressions(For_ $stmt, Expr $lastCondExpr, Mutatin
48404842
&& $lastCondExpr->right instanceof Variable
48414843
&& $lastCondExpr->left instanceof FuncCall
48424844
&& $lastCondExpr->left->name instanceof Name
4845+
&& !$lastCondExpr->left->isFirstClassCallable()
48434846
&& in_array($lastCondExpr->left->name->toLowerString(), ['count', 'sizeof'], true)
48444847
&& count($lastCondExpr->left->getArgs()) > 0
48454848
&& $lastCondExpr->left->getArgs()[0]->value instanceof Variable

src/Analyser/TypeSpecifier.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ public function specifyTypesInCondition(
247247
if (
248248
$expr->left instanceof FuncCall
249249
&& $expr->left->name instanceof Name
250+
&& !$expr->left->isFirstClassCallable()
250251
&& in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true)
251252
&& count($expr->left->getArgs()) >= 1
252253
&& (
@@ -275,6 +276,7 @@ public function specifyTypesInCondition(
275276
!$context->null()
276277
&& $expr->right instanceof FuncCall
277278
&& $expr->right->name instanceof Name
279+
&& !$expr->right->isFirstClassCallable()
278280
&& in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true)
279281
&& count($expr->right->getArgs()) >= 1
280282
&& $leftType->isInteger()->yes()
@@ -378,6 +380,7 @@ public function specifyTypesInCondition(
378380
&& $expr->right instanceof Expr\BinaryOp\Minus
379381
&& $expr->right->left instanceof FuncCall
380382
&& $expr->right->left->name instanceof Name
383+
&& !$expr->right->left->isFirstClassCallable()
381384
&& in_array(strtolower((string) $expr->right->left->name), ['count', 'sizeof'], true)
382385
&& count($expr->right->left->getArgs()) >= 1
383386
// constant offsets are handled via HasOffsetType/HasOffsetValueType
@@ -404,6 +407,7 @@ public function specifyTypesInCondition(
404407
!$context->null()
405408
&& $expr->right instanceof FuncCall
406409
&& $expr->right->name instanceof Name
410+
&& !$expr->right->isFirstClassCallable()
407411
&& in_array(strtolower((string) $expr->right->name), ['preg_match'], true)
408412
&& count($expr->right->getArgs()) >= 3
409413
&& (
@@ -421,6 +425,7 @@ public function specifyTypesInCondition(
421425
!$context->null()
422426
&& $expr->right instanceof FuncCall
423427
&& $expr->right->name instanceof Name
428+
&& !$expr->right->isFirstClassCallable()
424429
&& in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true)
425430
&& count($expr->right->getArgs()) === 1
426431
&& $leftType->isInteger()->yes()
@@ -825,6 +830,7 @@ public function specifyTypesInCondition(
825830
if (
826831
$expr->expr instanceof FuncCall
827832
&& $expr->expr->name instanceof Name
833+
&& !$expr->expr->isFirstClassCallable()
828834
&& in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true)
829835
&& count($expr->expr->getArgs()) >= 1
830836
) {
@@ -881,6 +887,7 @@ public function specifyTypesInCondition(
881887
if (
882888
$expr->expr instanceof FuncCall
883889
&& $expr->expr->name instanceof Name
890+
&& !$expr->expr->isFirstClassCallable()
884891
&& in_array($expr->expr->name->toLowerString(), ['array_rand'], true)
885892
&& count($expr->expr->getArgs()) >= 1
886893
) {
@@ -911,6 +918,7 @@ public function specifyTypesInCondition(
911918
$expr->expr instanceof Expr\BinaryOp\Minus
912919
&& $expr->expr->left instanceof FuncCall
913920
&& $expr->expr->left->name instanceof Name
921+
&& !$expr->expr->left->isFirstClassCallable()
914922
&& $expr->expr->right instanceof Node\Scalar\Int_
915923
&& $expr->expr->right->value === 1
916924
&& in_array($expr->expr->left->name->toLowerString(), ['count', 'sizeof'], true)
@@ -938,6 +946,7 @@ public function specifyTypesInCondition(
938946
if (
939947
$expr->expr instanceof FuncCall
940948
&& $expr->expr->name instanceof Name
949+
&& !$expr->expr->isFirstClassCallable()
941950
&& $expr->expr->name->toLowerString() === 'array_search'
942951
&& count($expr->expr->getArgs()) >= 2
943952
) {
@@ -1596,6 +1605,7 @@ private function specifyTypesForConstantStringBinaryExpression(
15961605
if (
15971606
$exprNode instanceof FuncCall
15981607
&& $exprNode->name instanceof Name
1608+
&& !$exprNode->isFirstClassCallable()
15991609
&& strtolower($exprNode->name->toString()) === 'gettype'
16001610
&& isset($exprNode->getArgs()[0])
16011611
) {
@@ -1636,6 +1646,7 @@ private function specifyTypesForConstantStringBinaryExpression(
16361646
$context->true()
16371647
&& $exprNode instanceof FuncCall
16381648
&& $exprNode->name instanceof Name
1649+
&& !$exprNode->isFirstClassCallable()
16391650
&& strtolower((string) $exprNode->name) === 'get_parent_class'
16401651
&& isset($exprNode->getArgs()[0])
16411652
) {
@@ -1673,6 +1684,7 @@ private function specifyTypesForConstantStringBinaryExpression(
16731684
$context->false()
16741685
&& $exprNode instanceof FuncCall
16751686
&& $exprNode->name instanceof Name
1687+
&& !$exprNode->isFirstClassCallable()
16761688
&& in_array(strtolower((string) $exprNode->name), [
16771689
'trim', 'ltrim', 'rtrim', 'chop',
16781690
'mb_trim', 'mb_ltrim', 'mb_rtrim',
@@ -2750,6 +2762,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif
27502762
if (
27512763
$exprNode instanceof FuncCall
27522764
&& $exprNode->name instanceof Name
2765+
&& !$exprNode->isFirstClassCallable()
27532766
&& in_array(strtolower($exprNode->name->toString()), ['gettype', 'get_class', 'get_debug_type'], true)
27542767
&& isset($exprNode->getArgs()[0])
27552768
&& $constantType->isString()->yes()
@@ -2897,6 +2910,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
28972910
if (
28982911
!$context->null()
28992912
&& $unwrappedLeftExpr instanceof FuncCall
2913+
&& !$unwrappedLeftExpr->isFirstClassCallable()
29002914
&& count($unwrappedLeftExpr->getArgs()) >= 1
29012915
&& $unwrappedLeftExpr->name instanceof Name
29022916
&& in_array(strtolower((string) $unwrappedLeftExpr->name), ['count', 'sizeof'], true)
@@ -2907,6 +2921,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
29072921
$context->true()
29082922
&& $unwrappedRightExpr instanceof FuncCall
29092923
&& $unwrappedRightExpr->name instanceof Name
2924+
&& !$unwrappedRightExpr->isFirstClassCallable()
29102925
&& in_array($unwrappedRightExpr->name->toLowerString(), ['count', 'sizeof'], true)
29112926
&& count($unwrappedRightExpr->getArgs()) >= 1
29122927
) {

src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ public function processNode(Node $node, Scope $scope): array
134134
if (
135135
$node->dim instanceof Node\Expr\FuncCall
136136
&& $node->dim->name instanceof Node\Name
137+
&& !$node->dim->isFirstClassCallable()
137138
&& $node->dim->name->toLowerString() === 'array_rand'
138139
&& count($node->dim->getArgs()) >= 1
139140
) {
@@ -162,6 +163,7 @@ public function processNode(Node $node, Scope $scope): array
162163
$node->dim instanceof Node\Expr\BinaryOp\Minus
163164
&& $node->dim->left instanceof Node\Expr\FuncCall
164165
&& $node->dim->left->name instanceof Node\Name
166+
&& !$node->dim->left->isFirstClassCallable()
165167
&& in_array($node->dim->left->name->toLowerString(), ['count', 'sizeof'], true)
166168
&& count($node->dim->left->getArgs()) >= 1
167169
&& $node->dim->right instanceof Node\Scalar\Int_
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug14550;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @param list<string> $list
11+
*/
12+
function crashArrayKeyFirst(array $list): void
13+
{
14+
$fn = array_key_first(...);
15+
assertType('Closure(array): (int|string|null)', $fn);
16+
}
17+
18+
/**
19+
* @param list<string> $list
20+
*/
21+
function crashArrayKeyLast(array $list): void
22+
{
23+
$fn = array_key_last(...);
24+
assertType('Closure(array): (int|string|null)', $fn);
25+
}
26+
27+
/**
28+
* @param list<string> $list
29+
*/
30+
function crashArrayRand(array $list): void
31+
{
32+
$fn = array_rand(...);
33+
assertType('(Closure(non-empty-array): (int|string))|(Closure(non-empty-array, int<1, max>): (array<int, int|string>|int|string))', $fn);
34+
}
35+
36+
/**
37+
* @param list<string> $list
38+
*/
39+
function crashArraySearch(array $list, string $s): void
40+
{
41+
$fn = array_search(...);
42+
assertType('Closure(mixed, array, bool=): (int|string|false)', $fn);
43+
}

0 commit comments

Comments
 (0)