Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit e3867c0

Browse files
authored
Bleeding edge - check that values passed to array_sum/product are castable to number
1 parent 993db81 commit e3867c0

10 files changed

+343
-0
lines changed

‎conf/bleedingEdge.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
parameters:
22
featureToggles:
33
bleedingEdge: true
4+
checkParameterCastableToNumberFunctions: true
45
skipCheckGenericClasses!: []
56
stricterFunctionMap: true

‎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.checkParameterCastableToNumberFunctions%
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

‎conf/config.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ parameters:
2222
tooWideThrowType: true
2323
featureToggles:
2424
bleedingEdge: false
25+
checkParameterCastableToNumberFunctions: false
2526
skipCheckGenericClasses: []
2627
stricterFunctionMap: false
2728
fileExtensions:

‎conf/parametersSchema.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ parametersSchema:
2828
])
2929
featureToggles: structure([
3030
bleedingEdge: bool(),
31+
checkParameterCastableToNumberFunctions: bool(),
3132
skipCheckGenericClasses: listOf(string()),
3233
stricterFunctionMap: bool()
3334
])
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: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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 function array_map;
10+
use function str_replace;
11+
use const PHP_VERSION_ID;
12+
13+
/**
14+
* @extends RuleTestCase<ParameterCastableToNumberRule>
15+
*/
16+
class ParameterCastableToNumberRuleTest extends RuleTestCase
17+
{
18+
19+
protected function getRule(): Rule
20+
{
21+
$broker = $this->createReflectionProvider();
22+
return new ParameterCastableToNumberRule($broker, new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, false, false, false)));
23+
}
24+
25+
public function testRule(): void
26+
{
27+
$this->analyse([__DIR__ . '/data/param-castable-to-number-functions.php'], $this->hackPhp74ErrorMessages([
28+
[
29+
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, array<int, int>> given.',
30+
20,
31+
],
32+
[
33+
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, stdClass> given.',
34+
21,
35+
],
36+
[
37+
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, string> given.',
38+
22,
39+
],
40+
[
41+
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, resource|false> given.',
42+
23,
43+
],
44+
[
45+
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, CurlHandle> given.',
46+
24,
47+
],
48+
[
49+
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, ParamCastableToNumberFunctions\\ClassWithToString> given.',
50+
25,
51+
],
52+
[
53+
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, array<int, int>> given.',
54+
27,
55+
],
56+
[
57+
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, stdClass> given.',
58+
28,
59+
],
60+
[
61+
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, string> given.',
62+
29,
63+
],
64+
[
65+
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, resource|false> given.',
66+
30,
67+
],
68+
[
69+
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, CurlHandle> given.',
70+
31,
71+
],
72+
[
73+
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, ParamCastableToNumberFunctions\\ClassWithToString> given.',
74+
32,
75+
],
76+
]));
77+
}
78+
79+
public function testNamedArguments(): void
80+
{
81+
if (PHP_VERSION_ID < 80000) {
82+
$this->markTestSkipped('Test requires PHP 8.0.');
83+
}
84+
85+
$this->analyse([__DIR__ . '/data/param-castable-to-number-functions-named-args.php'], [
86+
[
87+
'Parameter $array of function array_sum expects an array of values castable to number, array<int, array<int, int>> given.',
88+
7,
89+
],
90+
[
91+
'Parameter $array of function array_product expects an array of values castable to number, array<int, array<int, int>> given.',
92+
8,
93+
],
94+
]);
95+
}
96+
97+
public function testEnum(): void
98+
{
99+
if (PHP_VERSION_ID < 80100) {
100+
$this->markTestSkipped('Test requires PHP 8.1.');
101+
}
102+
103+
$this->analyse([__DIR__ . '/data/param-castable-to-number-functions-enum.php'], [
104+
[
105+
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, ParamCastableToNumberFunctionsEnum\\FooEnum::A> given.',
106+
12,
107+
],
108+
[
109+
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, ParamCastableToNumberFunctionsEnum\\FooEnum::A> given.',
110+
13,
111+
],
112+
]);
113+
}
114+
115+
public function testBug11883(): void
116+
{
117+
if (PHP_VERSION_ID < 80100) {
118+
$this->markTestSkipped('Test requires PHP 8.1.');
119+
}
120+
121+
$this->analyse([__DIR__ . '/data/bug-11883.php'], [
122+
[
123+
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, Bug11883\\SomeEnum::A|Bug11883\\SomeEnum::B> given.',
124+
13,
125+
],
126+
[
127+
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, Bug11883\\SomeEnum::A|Bug11883\\SomeEnum::B> given.',
128+
14,
129+
],
130+
]);
131+
}
132+
133+
/**
134+
* @param list<array{0: string, 1: int, 2?: string|null}> $errors
135+
* @return list<array{0: string, 1: int, 2?: string|null}>
136+
*/
137+
private function hackPhp74ErrorMessages(array $errors): array
138+
{
139+
if (PHP_VERSION_ID >= 80000) {
140+
return $errors;
141+
}
142+
143+
return array_map(static function (array $error): array {
144+
$error[0] = str_replace(
145+
[
146+
'$array of function array_sum',
147+
'$array of function array_product',
148+
'array<int, CurlHandle>',
149+
],
150+
[
151+
'$input of function array_sum',
152+
'$input of function array_product',
153+
'array<int, resource>',
154+
],
155+
$error[0],
156+
);
157+
158+
return $error;
159+
}, $errors);
160+
}
161+
162+
}
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)

AltStyle によって変換されたページ (->オリジナル) /