Skip to content

Commit a9983f4

Browse files
committed
Merge branch 2.1.x into 2.2.x
2 parents 58ffa2d + b6c48a0 commit a9983f4

10 files changed

Lines changed: 354 additions & 10 deletions

phpstan-baseline.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ parameters:
1212
count: 1
1313
path: src/Analyser/AnalyserResultFinalizer.php
1414

15+
-
16+
rawMessage: PHPDoc tag @var with type int|string is not subtype of type string.
17+
identifier: varTag.type
18+
count: 1
19+
path: src/Analyser/ArgumentsNormalizer.php
20+
1521
-
1622
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
1723
identifier: phpstanApi.instanceofType

src/Analyser/ArgumentsNormalizer.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
namespace PHPStan\Analyser;
44

55
use PhpParser\Node\Arg;
6+
use PhpParser\Node\Expr\Array_;
67
use PhpParser\Node\Expr\FuncCall;
78
use PhpParser\Node\Expr\MethodCall;
89
use PhpParser\Node\Expr\New_;
910
use PhpParser\Node\Expr\StaticCall;
11+
use PhpParser\Node\Identifier;
12+
use PhpParser\Node\Scalar\Int_;
13+
use PhpParser\Node\Scalar\String_;
1014
use PHPStan\Node\Expr\TypeExpr;
1115
use PHPStan\Reflection\ParametersAcceptor;
1216
use PHPStan\Reflection\ParametersAcceptorSelector;
@@ -18,6 +22,8 @@
1822
use function array_keys;
1923
use function array_values;
2024
use function count;
25+
use function is_string;
26+
use function key;
2127
use function ksort;
2228
use function max;
2329
use function sprintf;
@@ -89,6 +95,102 @@ public static function reorderCallUserFuncArguments(
8995
), $acceptsNamedArguments];
9096
}
9197

98+
/**
99+
* @return array{ParametersAcceptor, FuncCall, TrinaryLogic}|null
100+
*/
101+
public static function reorderCallUserFuncArrayArguments(
102+
FuncCall $callUserFuncArrayCall,
103+
Scope $scope,
104+
): ?array
105+
{
106+
$args = $callUserFuncArrayCall->getArgs();
107+
if (count($args) < 2) {
108+
return null;
109+
}
110+
111+
$callbackArg = null;
112+
$argsArrayArg = null;
113+
foreach ($args as $i => $arg) {
114+
if ($callbackArg === null) {
115+
if ($arg->name === null && $i === 0) {
116+
$callbackArg = $arg;
117+
continue;
118+
}
119+
if ($arg->name !== null && $arg->name->toString() === 'callback') {
120+
$callbackArg = $arg;
121+
continue;
122+
}
123+
}
124+
125+
if ($argsArrayArg !== null) {
126+
continue;
127+
}
128+
if ($arg->name === null && $i === 1) {
129+
$argsArrayArg = $arg;
130+
continue;
131+
}
132+
if ($arg->name === null || $arg->name->toString() !== 'args') {
133+
continue;
134+
}
135+
$argsArrayArg = $arg;
136+
}
137+
138+
if ($callbackArg === null || $argsArrayArg === null) {
139+
return null;
140+
}
141+
142+
if (!$argsArrayArg->value instanceof Array_) {
143+
return null;
144+
}
145+
146+
$passThruArgs = [];
147+
foreach ($argsArrayArg->value->items as $item) {
148+
$key = null;
149+
if ($item->key instanceof String_) {
150+
/** @var int|string $key */
151+
$key = key([$item->key->value => null]);
152+
if ($key === '') {
153+
return null;
154+
}
155+
} elseif ($item->key !== null && !$item->key instanceof Int_) {
156+
// Dynamic key, we cannot be sure.
157+
return null;
158+
}
159+
160+
$passThruArgs[] = new Arg(
161+
$item->value,
162+
$item->byRef,
163+
$item->unpack,
164+
$item->getAttributes(),
165+
is_string($key) ? new Identifier($key) : null,
166+
);
167+
}
168+
169+
$calledOnType = $scope->getType($callbackArg->value);
170+
if (!$calledOnType->isCallable()->yes()) {
171+
return null;
172+
}
173+
174+
$callableParametersAcceptors = $calledOnType->getCallableParametersAcceptors($scope);
175+
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
176+
$scope,
177+
$passThruArgs,
178+
$callableParametersAcceptors,
179+
null,
180+
);
181+
182+
$acceptsNamedArguments = TrinaryLogic::createYes();
183+
foreach ($callableParametersAcceptors as $callableParametersAcceptor) {
184+
$acceptsNamedArguments = $acceptsNamedArguments->and($callableParametersAcceptor->acceptsNamedArguments());
185+
}
186+
187+
return [$parametersAcceptor, new FuncCall(
188+
$callbackArg->value,
189+
$passThruArgs,
190+
$callUserFuncArrayCall->getAttributes(),
191+
), $acceptsNamedArguments];
192+
}
193+
92194
public static function reorderFuncArguments(
93195
ParametersAcceptor $parametersAcceptor,
94196
FuncCall $functionCall,

src/Analyser/ExprHandler/FuncCallHandler.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,15 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type
770770
}
771771
}
772772

773+
if ($functionReflection->getName() === 'call_user_func_array') {
774+
$result = ArgumentsNormalizer::reorderCallUserFuncArrayArguments($expr, $scope);
775+
if ($result !== null) {
776+
[, $innerFuncCall] = $result;
777+
778+
return $scope->getType($innerFuncCall);
779+
}
780+
}
781+
773782
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
774783
$scope,
775784
$expr->getArgs(),

src/Rules/Functions/CallUserFuncRule.php

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\Rules\FunctionCallParametersCheck;
1414
use PHPStan\Rules\Rule;
1515
use function count;
16+
use function sprintf;
1617
use function ucfirst;
1718

1819
/**
@@ -49,20 +50,27 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE
4950
}
5051

5152
$functionReflection = $this->reflectionProvider->getFunction($node->name, $scope);
52-
if ($functionReflection->getName() !== 'call_user_func') {
53+
54+
$functionName = $functionReflection->getName();
55+
if ($functionName === 'call_user_func') {
56+
$result = ArgumentsNormalizer::reorderCallUserFuncArguments(
57+
$node,
58+
$scope,
59+
);
60+
} elseif ($functionName === 'call_user_func_array') {
61+
$result = ArgumentsNormalizer::reorderCallUserFuncArrayArguments(
62+
$node,
63+
$scope,
64+
);
65+
} else {
5366
return [];
5467
}
55-
56-
$result = ArgumentsNormalizer::reorderCallUserFuncArguments(
57-
$node,
58-
$scope,
59-
);
6068
if ($result === null) {
6169
return [];
6270
}
6371
[$parametersAcceptor, $funcCall, $acceptsNamedArguments] = $result;
6472

65-
$callableDescription = 'callable passed to call_user_func()';
73+
$callableDescription = sprintf('callable passed to %s()', $functionName);
6674

6775
return $this->check->check(
6876
$parametersAcceptor,
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php // lint >= 8.0
2+
3+
namespace CallUserFuncArrayPhp8;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @template T
9+
* @param T $t
10+
* @return T
11+
*/
12+
function generic($t) {
13+
return $t;
14+
}
15+
16+
/**
17+
* @template T
18+
* @param T $t
19+
* @return T
20+
*/
21+
function generic3($t = '', int $b = 100, string $c = '') {
22+
return $t;
23+
}
24+
25+
26+
function fun3($a = '', $b = '', $c = ''): int {
27+
return 1;
28+
}
29+
30+
class Foo {
31+
function doNamed() {
32+
assertType('1', call_user_func_array('CallUserFuncPhp8\generic', ['t' => 1]));
33+
assertType('array{1, 2, 3}', call_user_func_array('CallUserFuncPhp8\generic', ['t' => [1, 2, 3]]));
34+
35+
assertType('array{1, 2, 3}', call_user_func_array('CallUserFuncPhp8\generic3', ['t' => [1, 2, 3]]));
36+
assertType('\'\'', call_user_func_array('CallUserFuncPhp8\generic3', ['b' => 150]));
37+
assertType('\'\'', call_user_func_array('CallUserFuncPhp8\generic3', ['c' => 'lala']));
38+
assertType('\'\'', call_user_func_array(args: ['c' => 'lala'], callback: 'CallUserFuncPhp8\generic3'));
39+
40+
assertType('int', call_user_func_array('CallUserFuncPhp8\fun3', ['a' => [1, 2, 3]]));
41+
assertType('int', call_user_func_array('CallUserFuncPhp8\fun3', ['b' => [1, 2, 3]]));
42+
assertType('int', call_user_func_array('CallUserFuncPhp8\fun3', ['c' => [1, 2, 3]]));
43+
assertType('int', call_user_func_array('CallUserFuncPhp8\fun3', ['a' => [1, 2, 3], 'c' => 'c']));
44+
}
45+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace CallUserFuncArray;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @template T
9+
* @param T $t
10+
* @return T
11+
*/
12+
function generic($t) {
13+
return $t;
14+
}
15+
16+
function fun(): int
17+
{
18+
return 3;
19+
}
20+
21+
function fun3($i, $x, $y): int
22+
{
23+
return 3;
24+
}
25+
26+
class c {
27+
static function m(): string
28+
{
29+
return 'hello';
30+
}
31+
}
32+
33+
class Foo {
34+
function proxy() {
35+
$params = [
36+
'CallUserFunc\generic',
37+
[123, 456],
38+
];
39+
40+
assertType('mixed', call_user_func_array(...$params));
41+
}
42+
43+
/**
44+
* @param string[] $strings
45+
*/
46+
function doFunc($strings) {
47+
assertType('true', call_user_func_array('CallUserFunc\generic', [true]));
48+
assertType('\'hello\'', call_user_func_array('CallUserFunc\generic', ['hello']));
49+
assertType('array<string>', call_user_func_array('CallUserFunc\generic', [$strings]));
50+
51+
assertType('int', call_user_func_array('CallUserFunc\fun', []));
52+
assertType('int', call_user_func_array('CallUserFunc\fun3', [1 ,2 ,3]));
53+
assertType('string', call_user_func_array(['CallUserFunc\c', 'm'], []));
54+
}
55+
}

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,7 @@ public function testCallUserFuncArray(): void
11281128
],
11291129
];
11301130
}
1131-
$this->analyse([__DIR__ . '/data/call-user-func-array.php'], $errors);
1131+
$this->analyse([__DIR__ . '/data/call-user-func-array-named-args.php'], $errors);
11321132
}
11331133

11341134
public function testFirstClassCallables(): void

tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,65 @@ public function testRule(): void
7474
]);
7575
}
7676

77+
#[RequiresPhp('>= 8.0')]
78+
public function testRuleCallUserFuncArray(): void
79+
{
80+
$this->analyse([__DIR__ . '/data/call-user-func-array.php'], [
81+
[
82+
'Callable passed to call_user_func_array() invoked with 0 parameters, 1 required.',
83+
15,
84+
],
85+
[
86+
'Parameter #1 $i of callable passed to call_user_func_array() expects int, string given.',
87+
17,
88+
],
89+
[
90+
'Parameter $i of callable passed to call_user_func_array() expects int, string given.',
91+
18,
92+
],
93+
[
94+
'Parameter $i of callable passed to call_user_func_array() expects int, string given.',
95+
19,
96+
],
97+
[
98+
'Unknown parameter $j in call to callable passed to call_user_func_array().',
99+
22,
100+
],
101+
[
102+
'Missing parameter $i (int) in call to callable passed to call_user_func_array().',
103+
22,
104+
],
105+
[
106+
'Callable passed to call_user_func_array() invoked with 0 parameters, 2-4 required.',
107+
30,
108+
],
109+
[
110+
'Callable passed to call_user_func_array() invoked with 1 parameter, 2-4 required.',
111+
31,
112+
],
113+
[
114+
'Callable passed to call_user_func_array() invoked with 0 parameters, at least 2 required.',
115+
40,
116+
],
117+
[
118+
'Callable passed to call_user_func_array() invoked with 1 parameter, at least 2 required.',
119+
41,
120+
],
121+
[
122+
'Result of callable passed to call_user_func_array() (void) is used.',
123+
43,
124+
],
125+
[
126+
'Parameter #1 $i of callable passed to call_user_func_array() expects int|null, string given.',
127+
52,
128+
],
129+
[
130+
'Parameter #1 $i of callable passed to call_user_func_array() expects int|null, string given.',
131+
53,
132+
],
133+
]);
134+
}
135+
77136
public function testBug7057(): void
78137
{
79138
$this->analyse([__DIR__ . '/data/bug-7057.php'], []);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?php
2+
3+
call_user_func_array('array_merge', ['foo' => ['bar' => 2]]);

0 commit comments

Comments
 (0)