Skip to content

Commit c7a5e59

Browse files
committed
CallToFunctionStatementWithoutSideEffectsRule checks the purity of array_filter(), array_map(), and array_reduce()
1 parent 4f7beff commit c7a5e59

File tree

5 files changed

+98
-2
lines changed

5 files changed

+98
-2
lines changed

resources/functionMap_bleedingEdge.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@
1111
'bcpowmod' => ['numeric-string|null', 'base'=>'numeric-string', 'exponent'=>'numeric-string', 'modulus'=>'string', 'scale='=>'int'],
1212
'bcsqrt' => ['numeric-string', 'operand'=>'numeric-string', 'scale='=>'int'],
1313
'bcsub' => ['numeric-string', 'left_operand'=>'numeric-string', 'right_operand'=>'numeric-string', 'scale='=>'int'],
14+
'array_filter' => ['array', 'input'=>'array', 'callback='=>'pure-callable(mixed,mixed):bool|pure-callable(mixed):bool', 'flag='=>'ARRAY_FILTER_USE_BOTH|ARRAY_FILTER_USE_KEY'],
15+
'array_reduce' => ['mixed', 'input'=>'array', 'callback'=>'pure-callable(mixed,mixed):mixed', 'initial='=>'mixed'],
16+
'array_udiff_assoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'key_comp_func'=>'pure-callable(mixed,mixed):int'],
17+
'array_udiff_assoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|pure-callable(mixed,mixed):int', '...rest='=>'array|pure-callable(mixed,mixed):int'],
18+
'array_udiff_uassoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_comp_func'=>'pure-callable', 'key_comp_func'=>'pure-callable(mixed,mixed):int'],
19+
'array_udiff_uassoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|pure-callable(mixed,mixed):int', 'arg5'=>'array|pure-callable(mixed,mixed):int', '...rest='=>'array|pure-callable(mixed,mixed):int'],
20+
'array_uintersect' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_compare_func'=>'pure-callable(mixed,mixed):int'],
21+
'array_uintersect\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|pure-callable(mixed,mixed):int', '...rest='=>'array|pure-callable(mixed,mixed):int'],
22+
'array_uintersect_assoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_compare_func'=>'pure-callable(mixed,mixed):int'],
23+
'array_uintersect_assoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|pure-callable(mixed,mixed):int', '...rest='=>'array|pure-callable(mixed,mixed):int'],
24+
'array_uintersect_uassoc' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'data_compare_func'=>'pure-callable(mixed,mixed):int', 'key_compare_func'=>'pure-callable(mixed,mixed):int'],
25+
'array_uintersect_uassoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|pure-callable(mixed,mixed):int', 'arg5'=>'array|pure-callable(mixed,mixed):int', '...rest='=>'array|pure-callable(mixed,mixed):int'],
1426
'Closure::bind' => ['Closure', 'old'=>'Closure', 'to'=>'?object', 'scope='=>'object|class-string|\'static\'|null'],
1527
'Closure::bindTo' => ['Closure', 'new'=>'?object', 'newscope='=>'object|class-string|\'static\'|null'],
1628
'error_log' => ['bool', 'message'=>'string', 'message_type='=>'0|1|2|3|4', 'destination='=>'string', 'extra_headers='=>'string'],

src/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRule.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Rules\RuleErrorBuilder;
1111
use PHPStan\Type\NeverType;
1212
use PHPStan\Type\Type;
13+
use function count;
1314
use function in_array;
1415
use function sprintf;
1516

@@ -21,10 +22,12 @@ final class CallToFunctionStatementWithoutSideEffectsRule implements Rule
2122

2223
private const SIDE_EFFECT_FLIP_PARAMETERS = [
2324
// functionName => [name, pos, testName]
25+
'array_filter' => ['callback', 1, 'isPure'],
26+
'array_map' => ['callback', 0, 'isPure'],
27+
'array_reduce' => ['callback', 1, 'isPure'],
2428
'print_r' => ['return', 1, 'isTruthy'],
2529
'var_export' => ['return', 1, 'isTruthy'],
2630
'highlight_string' => ['return', 1, 'isTruthy'],
27-
2831
];
2932

3033
public const PHPSTAN_TESTING_FUNCTIONS = [
@@ -76,6 +79,22 @@ public function processNode(Node $node, Scope $scope): array
7679
$sideEffectFlipped = false;
7780
$hasNamedParameter = false;
7881
$checker = [
82+
'isPure' => static function (Type $type) use ($scope) {
83+
if ($type->isCallable()->no()) {
84+
return false;
85+
}
86+
$callableParametersAcceptors = $type->getCallableParametersAcceptors($scope);
87+
if (count($callableParametersAcceptors) === 0) {
88+
return false;
89+
}
90+
foreach ($callableParametersAcceptors as $callableParametersAcceptor) {
91+
if (!$callableParametersAcceptor->isPure()->yes()) {
92+
return false;
93+
}
94+
}
95+
96+
return true;
97+
},
7998
'isNotNull' => static fn (Type $type) => $type->isNull()->no(),
8099
'isTruthy' => static fn (Type $type) => $type->toBoolean()->isTrue()->yes(),
81100
][$testName];

tests/PHPStan/Rules/Functions/CallToFunctionStatementWithoutSideEffectsRuleTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,22 @@ public function testRule(): void
3232
'Call to function print_r() on a separate line has no effect.',
3333
26,
3434
],
35+
[
36+
'Call to function array_filter() on a separate line has no effect.',
37+
29,
38+
],
39+
[
40+
'Call to function array_map() on a separate line has no effect.',
41+
33,
42+
],
43+
[
44+
'Call to function array_reduce() on a separate line has no effect.',
45+
37,
46+
],
47+
[
48+
'Call to function array_reduce() on a separate line has no effect.',
49+
40,
50+
],
3551
]);
3652

3753
if (PHP_VERSION_ID < 80000) {
@@ -51,6 +67,26 @@ public function testRule(): void
5167
'Call to function highlight_string() on a separate line has no effect.',
5268
21,
5369
],
70+
[
71+
'Call to function array_filter() on a separate line has no effect.',
72+
22,
73+
],
74+
[
75+
'Call to function array_filter() on a separate line has no effect.',
76+
23,
77+
],
78+
[
79+
'Call to function array_map() on a separate line has no effect.',
80+
24,
81+
],
82+
[
83+
'Call to function array_map() on a separate line has no effect.',
84+
25,
85+
],
86+
[
87+
'Call to function array_reduce() on a separate line has no effect.',
88+
26,
89+
],
5490
]);
5591
}
5692

tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects-8.0.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ public function noEffect(string $url, $resourceOrNull)
1919
var_export([], return: true);
2020
print_r([], return: true);
2121
highlight_string($url, return: true);
22+
array_filter([], callback: 'is_string');
23+
array_filter([], is_string(...));
24+
array_map(array: [], callback: 'is_string');
25+
array_map(is_string(...), []);
26+
array_reduce([], callback: fn($carry, $item) => $carry + $item);
2227
}
2328

2429
/**
@@ -31,6 +36,14 @@ public function hasSideEffect(string $url, $resource)
3136
var_export(value: []);
3237
print_r(value: []);
3338
highlight_string($url);
39+
$callback = rand() === 0 ? is_string(...) : var_dump(...);
40+
array_filter([], callback: $callback);
41+
array_filter([], callback: 'var_dump');
42+
array_filter([], var_dump(...));
43+
array_map(array: [], callback: $callback);
44+
array_map(array: [], callback: 'var_dump');
45+
array_map(var_dump(...), array: []);
46+
array_reduce([], callback: $callback);
3447
}
3548

3649
}

tests/PHPStan/Rules/Functions/data/function-call-statement-no-side-effects.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
class Foo
88
{
99

10-
public function doFoo(string $url)
10+
public function doFoo(string $url, $mixed)
1111
{
1212
printf('%s', 'test');
1313
sprintf('%s', 'test');
@@ -24,6 +24,22 @@ public function doFoo(string $url)
2424
var_export([], true);
2525
print_r([]);
2626
print_r([], true);
27+
$callback = rand() === 0 ? 'is_string' : 'var_dump';
28+
array_filter([], 'var_dump');
29+
array_filter([], 'is_string');
30+
array_filter([], $mixed);
31+
array_filter([], $callback);
32+
array_map('var_dump', []);
33+
array_map('is_string', []);
34+
array_map($mixed, []);
35+
array_map($callback, []);
36+
array_reduce([], 'var_dump');
37+
array_reduce([], 'is_string');
38+
array_reduce([], $mixed);
39+
array_reduce([], $callback);
40+
array_reduce([], function ($carry, $item) {
41+
return $carry + $item;
42+
});
2743
}
2844

2945
public function doBar(string $s)

0 commit comments

Comments
 (0)