Skip to content

Commit a21012d

Browse files
committed
Fix handling unpacked argument with constant arrays
1 parent 6fd85e3 commit a21012d

File tree

3 files changed

+145
-20
lines changed

3 files changed

+145
-20
lines changed

src/Rules/FunctionCallParametersCheck.php

+60-17
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22

33
namespace PHPStan\Rules;
44

5+
use PhpParser\Node\Expr;
56
use PHPStan\Analyser\Scope;
67
use PHPStan\Reflection\ParametersAcceptor;
78
use PHPStan\Type\ErrorType;
89
use PHPStan\Type\NeverType;
910
use PHPStan\Type\Type;
11+
use PHPStan\Type\TypeCombinator;
12+
use PHPStan\Type\TypeUtils;
1013
use PHPStan\Type\VerbosityLevel;
1114
use PHPStan\Type\VoidType;
1215

@@ -66,10 +69,57 @@ public function check(
6669
$functionParametersMaxCount = -1;
6770
}
6871

69-
$errors = [];
70-
$invokedParametersCount = count($funcCall->args);
71-
foreach ($funcCall->args as $arg) {
72+
/** @var array<int, array{Expr, Type, bool}> $arguments */
73+
$arguments = [];
74+
/** @var array<int, \PhpParser\Node\Arg> $args */
75+
$args = $funcCall->args;
76+
foreach ($args as $i => $arg) {
77+
$type = $scope->getType($arg->value);
7278
if ($arg->unpack) {
79+
$arrays = TypeUtils::getConstantArrays($type);
80+
if (count($arrays) > 0) {
81+
$minKeys = null;
82+
foreach ($arrays as $array) {
83+
$keysCount = count($array->getKeyTypes());
84+
if ($minKeys !== null && $keysCount >= $minKeys) {
85+
continue;
86+
}
87+
88+
$minKeys = $keysCount;
89+
}
90+
91+
for ($j = 0; $j < $minKeys; $j++) {
92+
$types = [];
93+
foreach ($arrays as $constantArray) {
94+
$types[] = $constantArray->getValueTypes()[$j];
95+
}
96+
$arguments[] = [
97+
$arg->value,
98+
TypeCombinator::union(...$types),
99+
false,
100+
];
101+
}
102+
} else {
103+
$arguments[] = [
104+
$arg->value,
105+
$type->getIterableValueType(),
106+
true,
107+
];
108+
}
109+
continue;
110+
}
111+
112+
$arguments[] = [
113+
$arg->value,
114+
$type,
115+
false,
116+
];
117+
}
118+
119+
$errors = [];
120+
$invokedParametersCount = count($arguments);
121+
foreach ($arguments as $i => [$argumentValue, $argumentValueType, $unpack]) {
122+
if ($unpack) {
73123
$invokedParametersCount = max($functionParametersMinCount, $functionParametersMaxCount);
74124
break;
75125
}
@@ -115,13 +165,11 @@ public function check(
115165

116166
$parameters = $parametersAcceptor->getParameters();
117167

118-
/** @var array<int, \PhpParser\Node\Arg> $args */
119-
$args = $funcCall->args;
120-
foreach ($args as $i => $argument) {
121-
if ($this->checkArgumentTypes && $argument->unpack) {
168+
foreach ($arguments as $i => [$argumentValue, $argumentValueType, $unpack]) {
169+
if ($this->checkArgumentTypes && $unpack) {
122170
$iterableTypeResult = $this->ruleLevelHelper->findTypeToCheck(
123171
$scope,
124-
$argument->value,
172+
$argumentValue,
125173
'',
126174
static function (Type $type): bool {
127175
return $type->isIterable()->yes();
@@ -154,11 +202,6 @@ static function (Type $type): bool {
154202
}
155203

156204
$parameterType = $parameter->getType();
157-
158-
$argumentValueType = $scope->getType($argument->value);
159-
if ($argument->unpack) {
160-
$argumentValueType = $argumentValueType->getIterableValueType();
161-
}
162205
if (
163206
$this->checkArgumentTypes
164207
&& !$parameter->passedByReference()->createsNewVariable()
@@ -177,10 +220,10 @@ static function (Type $type): bool {
177220
if (
178221
!$this->checkArgumentsPassedByReference
179222
|| !$parameter->passedByReference()->yes()
180-
|| $argument->value instanceof \PhpParser\Node\Expr\Variable
181-
|| $argument->value instanceof \PhpParser\Node\Expr\ArrayDimFetch
182-
|| $argument->value instanceof \PhpParser\Node\Expr\PropertyFetch
183-
|| $argument->value instanceof \PhpParser\Node\Expr\StaticPropertyFetch
223+
|| $argumentValue instanceof \PhpParser\Node\Expr\Variable
224+
|| $argumentValue instanceof \PhpParser\Node\Expr\ArrayDimFetch
225+
|| $argumentValue instanceof \PhpParser\Node\Expr\PropertyFetch
226+
|| $argumentValue instanceof \PhpParser\Node\Expr\StaticPropertyFetch
184227
) {
185228
continue;
186229
}

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

+40-3
Original file line numberDiff line numberDiff line change
@@ -436,15 +436,15 @@ public function testCallMethods(): void
436436
1379,
437437
],
438438
[
439-
'Only iterables can be unpacked, array<int>|null given in argument #3.',
439+
'Only iterables can be unpacked, array<int>|null given in argument #5.',
440440
1456,
441441
],
442442
[
443-
'Only iterables can be unpacked, int given in argument #4.',
443+
'Only iterables can be unpacked, int given in argument #6.',
444444
1456,
445445
],
446446
[
447-
'Only iterables can be unpacked, string given in argument #5.',
447+
'Only iterables can be unpacked, string given in argument #7.',
448448
1456,
449449
],
450450
[
@@ -864,10 +864,30 @@ public function testCallVariadicMethods(): void
864864
'Parameter #4 ...$strings of method CallVariadicMethods\Foo::doVariadicString() expects string, int given.',
865865
42,
866866
],
867+
[
868+
'Parameter #5 ...$strings of method CallVariadicMethods\Foo::doVariadicString() expects string, int given.',
869+
42,
870+
],
871+
[
872+
'Parameter #6 ...$strings of method CallVariadicMethods\Foo::doVariadicString() expects string, int given.',
873+
42,
874+
],
875+
[
876+
'Method CallVariadicMethods\Foo::doIntegerParameters() invoked with 3 parameters, 2 required.',
877+
43,
878+
],
867879
[
868880
'Parameter #1 $foo of method CallVariadicMethods\Foo::doIntegerParameters() expects int, string given.',
869881
43,
870882
],
883+
[
884+
'Parameter #2 $bar of method CallVariadicMethods\Foo::doIntegerParameters() expects int, string given.',
885+
43,
886+
],
887+
[
888+
'Method CallVariadicMethods\Foo::doIntegerParameters() invoked with 3 parameters, 2 required.',
889+
44,
890+
],
871891
[
872892
'Parameter #1 ...$strings of method CallVariadicMethods\Bar::variadicStrings() expects string, int given.',
873893
85,
@@ -1433,4 +1453,21 @@ public function testBug3445(): void
14331453
]);
14341454
}
14351455

1456+
public function testBug3481(): void
1457+
{
1458+
$this->checkThisOnly = false;
1459+
$this->checkNullables = true;
1460+
$this->checkUnionTypes = true;
1461+
$this->analyse([__DIR__ . '/data/bug-3481.php'], [
1462+
[
1463+
'Method Bug3481\Foo::doSomething() invoked with 2 parameters, 3 required.',
1464+
34,
1465+
],
1466+
[
1467+
'Parameter #1 $a of method Bug3481\Foo::doSomething() expects string, int|string given.',
1468+
44,
1469+
],
1470+
]);
1471+
}
1472+
14361473
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace Bug3481;
4+
5+
class Foo
6+
{
7+
/**
8+
* @param string $a
9+
* @param int $b
10+
* @param string $c
11+
*/
12+
public function doSomething($a, $b, $c): void
13+
{
14+
}
15+
}
16+
17+
function (): void {
18+
$args = [
19+
'foo',
20+
1,
21+
'bar',
22+
];
23+
$foo = new Foo();
24+
$foo->doSomething(...$args);
25+
};
26+
27+
function (): void {
28+
$args = ['foo', 1];
29+
if (rand(0, 1)) {
30+
$args[] = 'bar';
31+
}
32+
33+
$foo = new Foo();
34+
$foo->doSomething(...$args);
35+
};
36+
37+
function (): void {
38+
$args = ['foo', 1, 'string'];
39+
if (rand(0, 1)) {
40+
$args[0] = 1;
41+
}
42+
43+
$foo = new Foo();
44+
$foo->doSomething(...$args);
45+
};

0 commit comments

Comments
 (0)