Skip to content

Commit b6a75c6

Browse files
authored
Check isFirstClassCallable() before calling getArgs() on nested FuncCall nodes (#5564)
1 parent 5e793f7 commit b6a75c6

4 files changed

Lines changed: 160 additions & 1 deletion

File tree

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: 20 additions & 1 deletion
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
) {
@@ -2981,9 +2996,10 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
29812996
if (
29822997
!$context->null()
29832998
&& $unwrappedLeftExpr instanceof FuncCall
2984-
&& count($unwrappedLeftExpr->getArgs()) === 1
29852999
&& $unwrappedLeftExpr->name instanceof Name
3000+
&& !$unwrappedLeftExpr->isFirstClassCallable()
29863001
&& in_array(strtolower((string) $unwrappedLeftExpr->name), ['strlen', 'mb_strlen'], true)
3002+
&& count($unwrappedLeftExpr->getArgs()) === 1
29873003
&& $rightType->isInteger()->yes()
29883004
) {
29893005
if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) {
@@ -3019,6 +3035,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
30193035
if (
30203036
$unwrappedLeftExpr instanceof FuncCall
30213037
&& $unwrappedLeftExpr->name instanceof Name
3038+
&& !$unwrappedLeftExpr->isFirstClassCallable()
30223039
&& in_array($unwrappedLeftExpr->name->toLowerString(), ['array_key_first', 'array_key_last'], true)
30233040
&& isset($unwrappedLeftExpr->getArgs()[0])
30243041
&& $rightType->isNull()->yes()
@@ -3050,6 +3067,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
30503067
$context->true()
30513068
&& $unwrappedLeftExpr instanceof FuncCall
30523069
&& $unwrappedLeftExpr->name instanceof Name
3070+
&& !$unwrappedLeftExpr->isFirstClassCallable()
30533071
&& in_array(strtolower($unwrappedLeftExpr->name->toString()), ['get_class', 'get_debug_type'], true)
30543072
&& isset($unwrappedLeftExpr->getArgs()[0])
30553073
) {
@@ -3075,6 +3093,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
30753093
$context->truthy()
30763094
&& $unwrappedLeftExpr instanceof FuncCall
30773095
&& $unwrappedLeftExpr->name instanceof Name
3096+
&& !$unwrappedLeftExpr->isFirstClassCallable()
30783097
&& in_array(strtolower($unwrappedLeftExpr->name->toString()), [
30793098
'substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'ucfirst', 'lcfirst',
30803099
'mb_substr', 'mb_strstr', 'mb_stristr', 'mb_strchr', 'mb_strrchr', 'mb_strtolower', 'mb_strtoupper', 'mb_ucfirst', 'mb_lcfirst',

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1556,6 +1556,13 @@ public function testBug14542(): void
15561556
$this->assertNoErrors($errors);
15571557
}
15581558

1559+
public function testBug14550(): void
1560+
{
1561+
// crash
1562+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-14550.php');
1563+
$this->assertNotEmpty($errors);
1564+
}
1565+
15591566
/**
15601567
* @param string[]|null $allAnalysedFiles
15611568
* @return list<Error>
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug14550;
6+
7+
function testArrayKeyFirstAssign(): void
8+
{
9+
$fn = array_key_first(...);
10+
}
11+
12+
function testArrayKeyLastAssign(): void
13+
{
14+
$fn = array_key_last(...);
15+
}
16+
17+
function testArrayRandAssign(): void
18+
{
19+
$fn = array_rand(...);
20+
}
21+
22+
function testCountMinusOneAssign(): void
23+
{
24+
$idx = count(...) - 1;
25+
}
26+
27+
function testArraySearchInCondition(): void
28+
{
29+
if ($key = array_search(...)) {
30+
}
31+
}
32+
33+
function testCountInComparisons(): void
34+
{
35+
if (count(...) < 1) {}
36+
if (0 < count(...)) {}
37+
}
38+
39+
function testSizeofInComparisons(): void
40+
{
41+
if (sizeof(...) < 1) {}
42+
if (0 < sizeof(...)) {}
43+
}
44+
45+
function testCountMinusOneInComparison(): void
46+
{
47+
$i = 0;
48+
if ($i < count(...) - 1) {}
49+
}
50+
51+
function testStrlenInComparisons(): void
52+
{
53+
if (strlen(...) < 1) {}
54+
if (0 < strlen(...)) {}
55+
}
56+
57+
function testMbStrlenInComparisons(): void
58+
{
59+
if (mb_strlen(...) < 1) {}
60+
if (0 < mb_strlen(...)) {}
61+
}
62+
63+
function testPregMatchInComparisons(): void
64+
{
65+
if (preg_match(...) < 1) {}
66+
if (0 < preg_match(...)) {}
67+
}
68+
69+
function testCountIdentical(): void
70+
{
71+
if (count(...) === 0) {}
72+
}
73+
74+
function testStrlenIdentical(): void
75+
{
76+
if (strlen(...) === 0) {}
77+
if (mb_strlen(...) === 0) {}
78+
}
79+
80+
function testArrayKeyFirstNullComparison(): void
81+
{
82+
if (array_key_first(...) !== null) {}
83+
if (array_key_last(...) !== null) {}
84+
}
85+
86+
function testGetClassIdentical(): void
87+
{
88+
if (get_class(...) === 'stdClass') {}
89+
if (get_debug_type(...) === 'string') {}
90+
}
91+
92+
function testStringFuncIdentical(): void
93+
{
94+
if (strtolower(...) === 'test') {}
95+
}
96+
97+
function testGettypeEquality(): void
98+
{
99+
if (gettype(...) === 'string') {}
100+
if (gettype(...) == 'string') {}
101+
}
102+
103+
function testGetClassEquality(): void
104+
{
105+
if (get_class(...) == 'stdClass') {}
106+
if (get_debug_type(...) == 'string') {}
107+
}
108+
109+
function testGetParentClassEquality(): void
110+
{
111+
if (get_parent_class(...) === 'stdClass') {}
112+
}
113+
114+
function testTrimEquality(): void
115+
{
116+
if (trim(...) !== '') {}
117+
if (ltrim(...) !== '') {}
118+
if (rtrim(...) !== '') {}
119+
}
120+
121+
function testArrayKeysInForeach(): void
122+
{
123+
foreach (array_keys(...) as $key) {}
124+
}
125+
126+
function testCountInForLoop(): void
127+
{
128+
for ($i = 0; $i < count(...); $i++) {}
129+
for ($i = 0; count(...) > $i; $i++) {}
130+
}

0 commit comments

Comments
 (0)