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 2b764c7

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

File tree

5 files changed

+236
-0
lines changed

5 files changed

+236
-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: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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\RuleError;
14+
use PHPStan\Rules\RuleErrorBuilder;
15+
use PHPStan\Type\Type;
16+
use PHPStan\Type\VerbosityLevel;
17+
use function count;
18+
use function sprintf;
19+
20+
/**
21+
* @implements Rule<FuncCall>
22+
*/
23+
class ArrayFilterStrictRule implements Rule
24+
{
25+
26+
/** @var ReflectionProvider */
27+
private $reflectionProvider;
28+
29+
/** @var bool */
30+
private $treatPhpDocTypesAsCertain;
31+
32+
/** @var bool */
33+
private $checkNullables;
34+
35+
public function __construct(
36+
ReflectionProvider $reflectionProvider,
37+
bool $treatPhpDocTypesAsCertain,
38+
bool $checkNullables
39+
)
40+
{
41+
$this->reflectionProvider = $reflectionProvider;
42+
$this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain;
43+
$this->checkNullables = $checkNullables;
44+
}
45+
46+
public function getNodeType(): string
47+
{
48+
return FuncCall::class;
49+
}
50+
51+
/**
52+
* @param FuncCall $node
53+
* @return list<RuleError>
54+
*/
55+
public function processNode(Node $node, Scope $scope): array
56+
{
57+
if (!$node->name instanceof Name) {
58+
return [];
59+
}
60+
61+
if (!$this->reflectionProvider->hasFunction($node->name, $scope)) {
62+
return [];
63+
}
64+
65+
$functionReflection = $this->reflectionProvider->getFunction($node->name, $scope);
66+
67+
if ($functionReflection->getName() !== 'array_filter') {
68+
return [];
69+
}
70+
71+
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
72+
$scope,
73+
$node->getArgs(),
74+
$functionReflection->getVariants(),
75+
$functionReflection->getNamedArgumentsVariants()
76+
);
77+
78+
$normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node);
79+
80+
if ($normalizedFuncCall === null) {
81+
return [];
82+
}
83+
84+
$args = $normalizedFuncCall->getArgs();
85+
if (count($args) === 0) {
86+
return [];
87+
}
88+
89+
if (count($args) === 1) {
90+
return [RuleErrorBuilder::message('Call to function array_filter() requires parameter #2 to be passed to avoid loose comparison semantics.')->build()];
91+
}
92+
93+
$nativeCallbackType = $scope->getNativeType($args[1]->value);
94+
95+
if ($this->treatPhpDocTypesAsCertain) {
96+
$callbackType = $scope->getType($args[1]->value);
97+
} else {
98+
$callbackType = $nativeCallbackType;
99+
}
100+
101+
if ($this->isCallbackTypeNull($callbackType)) {
102+
$message = 'Parameter #2 of array_filter() cannot be null to avoid loose comparison semantics (%s given).';
103+
$errorBuilder = RuleErrorBuilder::message(sprintf(
104+
$message,
105+
$callbackType->describe(VerbosityLevel::typeOnly())
106+
));
107+
108+
if (!$this->isCallbackTypeNull($nativeCallbackType) && $this->treatPhpDocTypesAsCertain) {
109+
$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%</>.');
110+
}
111+
112+
return [$errorBuilder->build()];
113+
}
114+
115+
return [];
116+
}
117+
118+
private function isCallbackTypeNull(Type $callbackType): bool
119+
{
120+
if ($callbackType->isNull()->yes()) {
121+
return true;
122+
}
123+
124+
if ($callbackType->isNull()->no()) {
125+
return false;
126+
}
127+
128+
return $this->checkNullables;
129+
}
130+
131+
}
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 $reportMaybes;
19+
20+
protected function getRule(): Rule
21+
{
22+
return new ArrayFilterStrictRule($this->createReflectionProvider(), $this->treatPhpDocTypesAsCertain, $this->reportMaybes);
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->reportMaybes = 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 によって変換されたページ (->オリジナル) /