Skip to content

Commit 610b396

Browse files
authored
Bleeding edge - report useless array filter() calls
1 parent 8ba5f34 commit 610b396

File tree

7 files changed

+197
-1
lines changed

7 files changed

+197
-1
lines changed

conf/bleedingEdge.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ parameters:
33
bleedingEdge: true
44
skipCheckGenericClasses: []
55
explicitMixedInUnknownGenericNew: true
6+
arrayFilter: true
67
stubFiles:
78
- ../stubs/bleedingEdge/Countable.stub

conf/config.level5.neon

Lines changed: 7 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\ArrayFilterRule:
10+
phpstan.rules.rule: %featureToggles.arrayFilter%
11+
812
rules:
913
- PHPStan\Rules\DateTimeInstantiationRule
1014
- PHPStan\Rules\Functions\ImplodeFunctionRule
@@ -16,3 +20,6 @@ services:
1620
reportMaybes: %reportMaybes%
1721
tags:
1822
- phpstan.rules.rule
23+
24+
-
25+
class: PHPStan\Rules\Functions\ArrayFilterRule

conf/config.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ parameters:
2828
- FilterIterator
2929
- RecursiveCallbackFilterIterator
3030
explicitMixedInUnknownGenericNew: false
31+
arrayFilter: false
3132
fileExtensions:
3233
- php
3334
checkAdvancedIsset: false
@@ -205,6 +206,7 @@ parametersSchema:
205206
disableRuntimeReflectionProvider: bool(),
206207
skipCheckGenericClasses: listOf(string()),
207208
explicitMixedInUnknownGenericNew: bool(),
209+
arrayFilter: bool(),
208210
])
209211
fileExtensions: listOf(string())
210212
checkAdvancedIsset: bool()
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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\ReflectionProvider;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
use PHPStan\Type\StaticTypeFactory;
12+
use PHPStan\Type\VerbosityLevel;
13+
use function count;
14+
use function sprintf;
15+
use function strtolower;
16+
17+
/**
18+
* @implements Rule<Node\Expr\FuncCall>
19+
*/
20+
class ArrayFilterRule implements Rule
21+
{
22+
23+
public function __construct(private ReflectionProvider $reflectionProvider)
24+
{
25+
}
26+
27+
public function getNodeType(): string
28+
{
29+
return FuncCall::class;
30+
}
31+
32+
public function processNode(Node $node, Scope $scope): array
33+
{
34+
if (!($node->name instanceof Node\Name)) {
35+
return [];
36+
}
37+
38+
$functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope);
39+
40+
if ($functionName === null || strtolower($functionName) !== 'array_filter') {
41+
return [];
42+
}
43+
44+
$args = $node->getArgs();
45+
if (count($args) !== 1) {
46+
return [];
47+
}
48+
49+
$arrayType = $scope->getType($args[0]->value);
50+
51+
if ($arrayType->isIterableAtLeastOnce()->no()) {
52+
$message = 'Parameter #1 $array (%s) to function array_filter is empty, call has no effect.';
53+
return [
54+
RuleErrorBuilder::message(sprintf(
55+
$message,
56+
$arrayType->describe(VerbosityLevel::value()),
57+
))->build(),
58+
];
59+
}
60+
61+
$falsyType = StaticTypeFactory::falsey();
62+
$isSuperType = $falsyType->isSuperTypeOf($arrayType->getIterableValueType());
63+
64+
if ($isSuperType->no()) {
65+
$message = 'Parameter #1 $array (%s) to function array_filter does not contain falsy values, the array will always stay the same.';
66+
return [
67+
RuleErrorBuilder::message(sprintf(
68+
$message,
69+
$arrayType->describe(VerbosityLevel::value()),
70+
))->build(),
71+
];
72+
}
73+
74+
if ($isSuperType->yes()) {
75+
$message = 'Parameter #1 $array (%s) to function array_filter contains falsy values only, the result will always be an empty array.';
76+
return [
77+
RuleErrorBuilder::message(sprintf(
78+
$message,
79+
$arrayType->describe(VerbosityLevel::value()),
80+
))->build(),
81+
];
82+
}
83+
84+
return [];
85+
}
86+
87+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
<?php
22

3-
array_filter([]);
3+
array_filter([0, 1, 2]);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<ArrayFilterRule>
10+
*/
11+
class ArrayFilterRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): Rule
15+
{
16+
return new ArrayFilterRule($this->createReflectionProvider());
17+
}
18+
19+
public function testFile(): void
20+
{
21+
$expectedErrors = [
22+
[
23+
'Parameter #1 $array (array{1, 3}) to function array_filter does not contain falsy values, the array will always stay the same.',
24+
11,
25+
],
26+
[
27+
'Parameter #1 $array (array{\'test\'}) to function array_filter does not contain falsy values, the array will always stay the same.',
28+
12,
29+
],
30+
[
31+
'Parameter #1 $array (array{true, true}) to function array_filter does not contain falsy values, the array will always stay the same.',
32+
17,
33+
],
34+
[
35+
'Parameter #1 $array (array{stdClass}) to function array_filter does not contain falsy values, the array will always stay the same.',
36+
18,
37+
],
38+
[
39+
'Parameter #1 $array (array<stdClass>) to function array_filter does not contain falsy values, the array will always stay the same.',
40+
20,
41+
],
42+
[
43+
'Parameter #1 $array (array{0}) to function array_filter contains falsy values only, the result will always be an empty array.',
44+
23,
45+
],
46+
[
47+
'Parameter #1 $array (array{null}) to function array_filter contains falsy values only, the result will always be an empty array.',
48+
24,
49+
],
50+
[
51+
'Parameter #1 $array (array{null, null}) to function array_filter contains falsy values only, the result will always be an empty array.',
52+
25,
53+
],
54+
[
55+
'Parameter #1 $array (array{null, 0}) to function array_filter contains falsy values only, the result will always be an empty array.',
56+
26,
57+
],
58+
[
59+
'Parameter #1 $array (array<false|null>) to function array_filter contains falsy values only, the result will always be an empty array.',
60+
27,
61+
],
62+
[
63+
'Parameter #1 $array (array{}) to function array_filter is empty, call has no effect.',
64+
28,
65+
],
66+
];
67+
68+
$this->analyse([__DIR__ . '/data/array_filter_empty.php'], $expectedErrors);
69+
}
70+
71+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/** @var \stdClass[] $objects */
4+
$objects = [];
5+
/** @var array<\stdClass|null> $objectsOrNull */
6+
$objectsOrNull = [];
7+
/** @var array<false|null> $falsey */
8+
$falsey = [];
9+
10+
array_filter([0,1,3]);
11+
array_filter([1,3]);
12+
array_filter(['test']);
13+
array_filter(['', 'test']);
14+
array_filter([null, 'test']);
15+
array_filter([false, 'test']);
16+
array_filter([true, false]);
17+
array_filter([true, true]);
18+
array_filter([new \stdClass()]);
19+
array_filter([new \stdClass(), null]);
20+
array_filter($objects);
21+
array_filter($objectsOrNull);
22+
23+
array_filter([0]);
24+
array_filter([null]);
25+
array_filter([null, null]);
26+
array_filter([null, 0]);
27+
array_filter($falsey);
28+
array_filter([]);

0 commit comments

Comments
 (0)