Skip to content

Commit 2f785ba

Browse files
committed
check that values passed to array_sum/product are castable to number
1 parent adcabb3 commit 2f785ba

File tree

7 files changed

+309
-0
lines changed

7 files changed

+309
-0
lines changed

conf/config.level5.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ parameters:
55
checkFunctionArgumentTypes: true
66
checkArgumentsPassedByReference: true
77

8+
conditionalTags:
9+
PHPStan\Rules\Functions\ParameterCastableToNumberRule:
10+
phpstan.rules.rule: %featureToggles.bleedingEdge%
11+
812
rules:
913
- PHPStan\Rules\DateTimeInstantiationRule
1014
- PHPStan\Rules\Functions\CallUserFuncRule
@@ -36,3 +40,5 @@ services:
3640
treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain%
3741
tags:
3842
- phpstan.rules.rule
43+
-
44+
class: PHPStan\Rules\Functions\ParameterCastableToNumberRule
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\ParametersAcceptorSelector;
9+
use PHPStan\Reflection\ReflectionProvider;
10+
use PHPStan\Rules\ParameterCastableToStringCheck;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Type\Type;
13+
use function count;
14+
use function in_array;
15+
16+
/**
17+
* @implements Rule<Node\Expr\FuncCall>
18+
*/
19+
final class ParameterCastableToNumberRule implements Rule
20+
{
21+
22+
public function __construct(
23+
private ReflectionProvider $reflectionProvider,
24+
private ParameterCastableToStringCheck $parameterCastableToStringCheck,
25+
)
26+
{
27+
}
28+
29+
public function getNodeType(): string
30+
{
31+
return FuncCall::class;
32+
}
33+
34+
public function processNode(Node $node, Scope $scope): array
35+
{
36+
if (!($node->name instanceof Node\Name)) {
37+
return [];
38+
}
39+
40+
if (!$this->reflectionProvider->hasFunction($node->name, $scope)) {
41+
return [];
42+
}
43+
44+
$functionReflection = $this->reflectionProvider->getFunction($node->name, $scope);
45+
$functionName = $functionReflection->getName();
46+
47+
if (!in_array($functionName, ['array_sum', 'array_product'], true)) {
48+
return [];
49+
}
50+
51+
$origArgs = $node->getArgs();
52+
53+
if (count($origArgs) !== 1) {
54+
return [];
55+
}
56+
57+
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
58+
$scope,
59+
$origArgs,
60+
$functionReflection->getVariants(),
61+
$functionReflection->getNamedArgumentsVariants(),
62+
);
63+
64+
$errorMessage = 'Parameter %s of function %s expects an array of values castable to number, %s given.';
65+
$functionParameters = $parametersAcceptor->getParameters();
66+
$error = $this->parameterCastableToStringCheck->checkParameter(
67+
$origArgs[0],
68+
$scope,
69+
$errorMessage,
70+
static fn (Type $t) => $t->toNumber(),
71+
$functionName,
72+
$this->parameterCastableToStringCheck->getParameterName(
73+
$origArgs[0],
74+
0,
75+
$functionParameters[0] ?? null,
76+
),
77+
);
78+
79+
return $error !== null
80+
? [$error]
81+
: [];
82+
}
83+
84+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use PHPStan\Rules\ParameterCastableToStringCheck;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Rules\RuleLevelHelper;
8+
use PHPStan\Testing\RuleTestCase;
9+
use const PHP_VERSION_ID;
10+
11+
/**
12+
* @extends RuleTestCase<ParameterCastableToNumberRule>
13+
*/
14+
class ParameterCastableToNumberRuleTest extends RuleTestCase
15+
{
16+
17+
protected function getRule(): Rule
18+
{
19+
$broker = $this->createReflectionProvider();
20+
return new ParameterCastableToNumberRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, false)));
21+
}
22+
23+
public function testRule(): void
24+
{
25+
$this->analyse([__DIR__ . '/data/param-castable-to-number-functions.php'], [
26+
[
27+
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, array<int, int>> given.',
28+
20,
29+
],
30+
[
31+
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, stdClass> given.',
32+
21,
33+
],
34+
[
35+
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, string> given.',
36+
22,
37+
],
38+
[
39+
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, resource|false> given.',
40+
23,
41+
],
42+
[
43+
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, CurlHandle> given.',
44+
24,
45+
],
46+
[
47+
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, ParamCastableToNumberFunctions\\ClassWithToString> given.',
48+
25,
49+
],
50+
[
51+
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, array<int, int>> given.',
52+
27,
53+
],
54+
[
55+
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, stdClass> given.',
56+
28,
57+
],
58+
[
59+
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, string> given.',
60+
29,
61+
],
62+
[
63+
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, resource|false> given.',
64+
30,
65+
],
66+
[
67+
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, CurlHandle> given.',
68+
31,
69+
],
70+
[
71+
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, ParamCastableToNumberFunctions\\ClassWithToString> given.',
72+
32,
73+
],
74+
]);
75+
}
76+
77+
public function testNamedArguments(): void
78+
{
79+
if (PHP_VERSION_ID < 80000) {
80+
$this->markTestSkipped('Test requires PHP 8.0.');
81+
}
82+
83+
$this->analyse([__DIR__ . '/data/param-castable-to-number-functions-named-args.php'], [
84+
[
85+
'Parameter $array of function array_sum expects an array of values castable to number, array<int, array<int, int>> given.',
86+
7,
87+
],
88+
[
89+
'Parameter $array of function array_product expects an array of values castable to number, array<int, array<int, int>> given.',
90+
8,
91+
],
92+
]);
93+
}
94+
95+
public function testEnum(): void
96+
{
97+
if (PHP_VERSION_ID < 80100) {
98+
$this->markTestSkipped('Test requires PHP 8.1.');
99+
}
100+
101+
$this->analyse([__DIR__ . '/data/param-castable-to-number-functions-enum.php'], [
102+
[
103+
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, ParamCastableToNumberFunctionsEnum\\FooEnum::A> given.',
104+
12,
105+
],
106+
[
107+
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, ParamCastableToNumberFunctionsEnum\\FooEnum::A> given.',
108+
13,
109+
],
110+
]);
111+
}
112+
113+
public function testBug11883(): void
114+
{
115+
if (PHP_VERSION_ID < 80100) {
116+
$this->markTestSkipped('Test requires PHP 8.1.');
117+
}
118+
119+
$this->analyse([__DIR__ . '/data/bug-11883.php'], [
120+
[
121+
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, Bug11883\\SomeEnum::A|Bug11883\\SomeEnum::B> given.',
122+
13,
123+
],
124+
[
125+
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, Bug11883\\SomeEnum::A|Bug11883\\SomeEnum::B> given.',
126+
14,
127+
],
128+
]);
129+
}
130+
131+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php declare(strict_types = 1); // lint >= 8.1
2+
3+
namespace Bug11883;
4+
5+
enum SomeEnum: int
6+
{
7+
case A = 1;
8+
case B = 2;
9+
}
10+
11+
$enums1 = [SomeEnum::A, SomeEnum::B];
12+
13+
var_dump(array_sum($enums1));
14+
var_dump(array_product($enums1));
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php declare(strict_types = 1); // lint >= 8.1
2+
3+
namespace ParamCastableToNumberFunctionsEnum;
4+
5+
enum FooEnum
6+
{
7+
case A;
8+
}
9+
10+
function invalidUsages()
11+
{
12+
array_sum([FooEnum::A]);
13+
array_product([FooEnum::A]);
14+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php declare(strict_types = 1); // lint >= 8.0
2+
3+
namespace ParamCastableToNumberFunctionsNamedArgs;
4+
5+
function invalidUsages()
6+
{
7+
var_dump(array_sum(array: [[0]]));
8+
var_dump(array_product(array: [[0]]));
9+
}
10+
11+
function validUsages()
12+
{
13+
var_dump(array_sum(array: [1]));
14+
var_dump(array_product(array: [1]));
15+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ParamCastableToNumberFunctions;
4+
5+
class ClassWithoutToString {}
6+
class ClassWithToString
7+
{
8+
public function __toString(): string
9+
{
10+
return 'foo';
11+
}
12+
}
13+
14+
function invalidUsages(): void
15+
{
16+
$curlHandle = curl_init();
17+
// curl_init returns benevolent union and false is castable to number.
18+
assert($curlHandle !== false);
19+
20+
var_dump(array_sum([[0]]));
21+
var_dump(array_sum([new \stdClass()]));
22+
var_dump(array_sum(['ttt']));
23+
var_dump(array_sum([fopen('php://input', 'r')]));
24+
var_dump(array_sum([$curlHandle]));
25+
var_dump(array_sum([new ClassWithToString()]));
26+
27+
var_dump(array_product([[0]]));
28+
var_dump(array_product([new \stdClass()]));
29+
var_dump(array_product(['ttt']));
30+
var_dump(array_product([fopen('php://input', 'r')]));
31+
var_dump(array_product([$curlHandle]));
32+
var_dump(array_product([new ClassWithToString()]));
33+
}
34+
35+
function wrongNumberOfArguments(): void
36+
{
37+
array_sum();
38+
array_product();
39+
}
40+
41+
function validUsages(): void
42+
{
43+
var_dump(array_sum(['5.5', false, true, new \SimpleXMLElement('<a>7.7</a>'), 5, 5.5, null]));
44+
var_dump(array_product(['5.5', false, true, new \SimpleXMLElement('<a>7.7</a>'), 5, 5.5, null]));
45+
}

0 commit comments

Comments
 (0)