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 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‎

Lines changed: 1 addition & 0 deletions
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‎

Lines changed: 10 additions & 0 deletions
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

Lines changed: 126 additions & 0 deletions
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+
}
Lines changed: 58 additions & 0 deletions
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+
}
Lines changed: 36 additions & 0 deletions
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)

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