Skip to content

Commit 568210b

Browse files
kamil-zacekondrejmirtes
authored andcommitted
Introduce strict array_filter call (require callback method)
1 parent 4723149 commit 568210b

File tree

5 files changed

+231
-0
lines changed

5 files changed

+231
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ parameters:
7474
strictCalls: false
7575
switchConditionsMatchingType: false
7676
noVariableVariables: false
77+
strictArrayFilter: false
7778
```
7879

7980
## Enabling rules one-by-one

rules.neon

+10
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ parameters:
2929
strictCalls: %strictRules.allRules%
3030
switchConditionsMatchingType: %strictRules.allRules%
3131
noVariableVariables: %strictRules.allRules%
32+
strictArrayFilter: [%strictRules.allRules%, %featureToggles.bleedingEdge%]
3233

3334
parametersSchema:
3435
strictRules: structure([
@@ -45,6 +46,7 @@ parametersSchema:
4546
strictCalls: anyOf(bool(), arrayOf(bool()))
4647
switchConditionsMatchingType: anyOf(bool(), arrayOf(bool()))
4748
noVariableVariables: anyOf(bool(), arrayOf(bool()))
49+
strictArrayFilter: anyOf(bool(), arrayOf(bool()))
4850
])
4951

5052
conditionalTags:
@@ -78,6 +80,8 @@ conditionalTags:
7880
phpstan.rules.rule: %strictRules.overwriteVariablesWithLoop%
7981
PHPStan\Rules\ForLoop\OverwriteVariablesWithForLoopInitRule:
8082
phpstan.rules.rule: %strictRules.overwriteVariablesWithLoop%
83+
PHPStan\Rules\Functions\ArrayFilterStrictRule:
84+
phpstan.rules.rule: %strictRules.strictArrayFilter%
8185
PHPStan\Rules\Functions\ClosureUsesThisRule:
8286
phpstan.rules.rule: %strictRules.closureUsesThis%
8387
PHPStan\Rules\Methods\WrongCaseOfInheritedMethodRule:
@@ -184,6 +188,12 @@ services:
184188
-
185189
class: PHPStan\Rules\ForLoop\OverwriteVariablesWithForLoopInitRule
186190

191+
-
192+
class: PHPStan\Rules\Functions\ArrayFilterStrictRule
193+
arguments:
194+
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%
195+
checkNullables: %checkNullables%
196+
187197
-
188198
class: PHPStan\Rules\Functions\ClosureUsesThisRule
189199

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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 PhpParser\Node\Name;
8+
use PHPStan\Analyser\ArgumentsNormalizer;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Reflection\ParametersAcceptorSelector;
11+
use PHPStan\Reflection\ReflectionProvider;
12+
use PHPStan\Rules\Rule;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use PHPStan\Type\Type;
15+
use PHPStan\Type\VerbosityLevel;
16+
use function count;
17+
use function sprintf;
18+
19+
/**
20+
* @implements Rule<FuncCall>
21+
*/
22+
class ArrayFilterStrictRule implements Rule
23+
{
24+
25+
/** @var ReflectionProvider */
26+
private $reflectionProvider;
27+
28+
/** @var bool */
29+
private $treatPhpDocTypesAsCertain;
30+
31+
/** @var bool */
32+
private $checkNullables;
33+
34+
public function __construct(
35+
ReflectionProvider $reflectionProvider,
36+
bool $treatPhpDocTypesAsCertain,
37+
bool $checkNullables
38+
)
39+
{
40+
$this->reflectionProvider = $reflectionProvider;
41+
$this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain;
42+
$this->checkNullables = $checkNullables;
43+
}
44+
45+
public function getNodeType(): string
46+
{
47+
return FuncCall::class;
48+
}
49+
50+
public function processNode(Node $node, Scope $scope): array
51+
{
52+
if (!$node->name instanceof Name) {
53+
return [];
54+
}
55+
56+
if (!$this->reflectionProvider->hasFunction($node->name, $scope)) {
57+
return [];
58+
}
59+
60+
$functionReflection = $this->reflectionProvider->getFunction($node->name, $scope);
61+
62+
if ($functionReflection->getName() !== 'array_filter') {
63+
return [];
64+
}
65+
66+
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
67+
$scope,
68+
$node->getArgs(),
69+
$functionReflection->getVariants(),
70+
$functionReflection->getNamedArgumentsVariants()
71+
);
72+
73+
$normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node);
74+
75+
if ($normalizedFuncCall === null) {
76+
return [];
77+
}
78+
79+
$args = $normalizedFuncCall->getArgs();
80+
if (count($args) === 0) {
81+
return [];
82+
}
83+
84+
if (count($args) === 1) {
85+
return [RuleErrorBuilder::message('Call to function array_filter() requires parameter #2 to be passed to avoid loose comparison semantics.')->build()];
86+
}
87+
88+
$nativeCallbackType = $scope->getNativeType($args[1]->value);
89+
90+
if ($this->treatPhpDocTypesAsCertain) {
91+
$callbackType = $scope->getType($args[1]->value);
92+
} else {
93+
$callbackType = $nativeCallbackType;
94+
}
95+
96+
if ($this->isCallbackTypeNull($callbackType)) {
97+
$message = 'Parameter #2 of array_filter() cannot be null to avoid loose comparison semantics (%s given).';
98+
$errorBuilder = RuleErrorBuilder::message(sprintf(
99+
$message,
100+
$callbackType->describe(VerbosityLevel::typeOnly())
101+
));
102+
103+
if (!$this->isCallbackTypeNull($nativeCallbackType) && $this->treatPhpDocTypesAsCertain) {
104+
$errorBuilder->tip('Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.');
105+
}
106+
107+
return [$errorBuilder->build()];
108+
}
109+
110+
return [];
111+
}
112+
113+
private function isCallbackTypeNull(Type $callbackType): bool
114+
{
115+
if ($callbackType->isNull()->yes()) {
116+
return true;
117+
}
118+
119+
if ($callbackType->isNull()->no()) {
120+
return false;
121+
}
122+
123+
return $this->checkNullables;
124+
}
125+
126+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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<ArrayFilterStrictRule>
10+
*/
11+
class ArrayFilterStrictRuleTest extends RuleTestCase
12+
{
13+
14+
/** @var bool */
15+
private $treatPhpDocTypesAsCertain;
16+
17+
/** @var bool */
18+
private $checkNullables;
19+
20+
protected function getRule(): Rule
21+
{
22+
return new ArrayFilterStrictRule($this->createReflectionProvider(), $this->treatPhpDocTypesAsCertain, $this->checkNullables);
23+
}
24+
25+
protected function shouldTreatPhpDocTypesAsCertain(): bool
26+
{
27+
return $this->treatPhpDocTypesAsCertain;
28+
}
29+
30+
public function testRule(): void
31+
{
32+
$this->treatPhpDocTypesAsCertain = true;
33+
$this->checkNullables = true;
34+
$this->analyse([__DIR__ . '/data/array-filter-strict.php'], [
35+
[
36+
'Call to function array_filter() requires parameter #2 to be passed to avoid loose comparison semantics.',
37+
15,
38+
],
39+
[
40+
'Call to function array_filter() requires parameter #2 to be passed to avoid loose comparison semantics.',
41+
25,
42+
],
43+
[
44+
'Call to function array_filter() requires parameter #2 to be passed to avoid loose comparison semantics.',
45+
26,
46+
],
47+
[
48+
'Parameter #2 of array_filter() cannot be null to avoid loose comparison semantics (null given).',
49+
28,
50+
],
51+
[
52+
'Parameter #2 of array_filter() cannot be null to avoid loose comparison semantics ((Closure)|null given).',
53+
34,
54+
],
55+
]);
56+
}
57+
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ArrayFilterStrict;
4+
5+
/** @var list<int> $list */
6+
$list = [1, 2, 3];
7+
8+
/** @var array<string, int> $array */
9+
$array = ["a" => 1, "b" => 2, "c" => 3];
10+
11+
array_filter([1, 2, 3], function (int $value): bool {
12+
return $value > 1;
13+
});
14+
15+
array_filter([1, 2, 3]);
16+
17+
array_filter([1, 2, 3], function (int $value): bool {
18+
return $value > 1;
19+
}, ARRAY_FILTER_USE_KEY);
20+
21+
array_filter([1, 2, 3], function (int $value): int {
22+
return $value;
23+
});
24+
25+
array_filter($list);
26+
array_filter($array);
27+
28+
array_filter($array, null);
29+
30+
array_filter($list, 'intval');
31+
32+
/** @var bool $bool */
33+
$bool = doFoo();
34+
array_filter($list, foo() ? null : function (int $value): bool {
35+
return $value > 1;
36+
});

0 commit comments

Comments
 (0)