Skip to content

Commit

Permalink
Bleeding edge - report useless array filter() calls
Browse files Browse the repository at this point in the history
  • Loading branch information
leongersen authored Mar 18, 2022
1 parent 8ba5f34 commit 610b396
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 1 deletion.
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ parameters:
bleedingEdge: true
skipCheckGenericClasses: []
explicitMixedInUnknownGenericNew: true
arrayFilter: true
stubFiles:
- ../stubs/bleedingEdge/Countable.stub
7 changes: 7 additions & 0 deletions conf/config.level5.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ parameters:
checkFunctionArgumentTypes: true
checkArgumentsPassedByReference: true

conditionalTags:
PHPStan\Rules\Functions\ArrayFilterRule:
phpstan.rules.rule: %featureToggles.arrayFilter%

rules:
- PHPStan\Rules\DateTimeInstantiationRule
- PHPStan\Rules\Functions\ImplodeFunctionRule
Expand All @@ -16,3 +20,6 @@ services:
reportMaybes: %reportMaybes%
tags:
- phpstan.rules.rule

-
class: PHPStan\Rules\Functions\ArrayFilterRule
2 changes: 2 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ parameters:
- FilterIterator
- RecursiveCallbackFilterIterator
explicitMixedInUnknownGenericNew: false
arrayFilter: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down Expand Up @@ -205,6 +206,7 @@ parametersSchema:
disableRuntimeReflectionProvider: bool(),
skipCheckGenericClasses: listOf(string()),
explicitMixedInUnknownGenericNew: bool(),
arrayFilter: bool(),
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
87 changes: 87 additions & 0 deletions src/Rules/Functions/ArrayFilterRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\StaticTypeFactory;
use PHPStan\Type\VerbosityLevel;
use function count;
use function sprintf;
use function strtolower;

/**
* @implements Rule<Node\Expr\FuncCall>
*/
class ArrayFilterRule implements Rule
{

public function __construct(private ReflectionProvider $reflectionProvider)
{
}

public function getNodeType(): string
{
return FuncCall::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!($node->name instanceof Node\Name)) {
return [];
}

$functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope);

if ($functionName === null || strtolower($functionName) !== 'array_filter') {
return [];
}

$args = $node->getArgs();
if (count($args) !== 1) {
return [];
}

$arrayType = $scope->getType($args[0]->value);

if ($arrayType->isIterableAtLeastOnce()->no()) {
$message = 'Parameter #1 $array (%s) to function array_filter is empty, call has no effect.';
return [
RuleErrorBuilder::message(sprintf(
$message,
$arrayType->describe(VerbosityLevel::value()),
))->build(),
];
}

$falsyType = StaticTypeFactory::falsey();
$isSuperType = $falsyType->isSuperTypeOf($arrayType->getIterableValueType());

if ($isSuperType->no()) {
$message = 'Parameter #1 $array (%s) to function array_filter does not contain falsy values, the array will always stay the same.';
return [
RuleErrorBuilder::message(sprintf(
$message,
$arrayType->describe(VerbosityLevel::value()),
))->build(),
];
}

if ($isSuperType->yes()) {
$message = 'Parameter #1 $array (%s) to function array_filter contains falsy values only, the result will always be an empty array.';
return [
RuleErrorBuilder::message(sprintf(
$message,
$arrayType->describe(VerbosityLevel::value()),
))->build(),
];
}

return [];
}

}
2 changes: 1 addition & 1 deletion tests/PHPStan/Command/data/file-without-errors.php
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<?php

array_filter([]);
array_filter([0, 1, 2]);
71 changes: 71 additions & 0 deletions tests/PHPStan/Rules/Functions/ArrayFilterRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<ArrayFilterRule>
*/
class ArrayFilterRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new ArrayFilterRule($this->createReflectionProvider());
}

public function testFile(): void
{
$expectedErrors = [
[
'Parameter #1 $array (array{1, 3}) to function array_filter does not contain falsy values, the array will always stay the same.',
11,
],
[
'Parameter #1 $array (array{\'test\'}) to function array_filter does not contain falsy values, the array will always stay the same.',
12,
],
[
'Parameter #1 $array (array{true, true}) to function array_filter does not contain falsy values, the array will always stay the same.',
17,
],
[
'Parameter #1 $array (array{stdClass}) to function array_filter does not contain falsy values, the array will always stay the same.',
18,
],
[
'Parameter #1 $array (array<stdClass>) to function array_filter does not contain falsy values, the array will always stay the same.',
20,
],
[
'Parameter #1 $array (array{0}) to function array_filter contains falsy values only, the result will always be an empty array.',
23,
],
[
'Parameter #1 $array (array{null}) to function array_filter contains falsy values only, the result will always be an empty array.',
24,
],
[
'Parameter #1 $array (array{null, null}) to function array_filter contains falsy values only, the result will always be an empty array.',
25,
],
[
'Parameter #1 $array (array{null, 0}) to function array_filter contains falsy values only, the result will always be an empty array.',
26,
],
[
'Parameter #1 $array (array<false|null>) to function array_filter contains falsy values only, the result will always be an empty array.',
27,
],
[
'Parameter #1 $array (array{}) to function array_filter is empty, call has no effect.',
28,
],
];

$this->analyse([__DIR__ . '/data/array_filter_empty.php'], $expectedErrors);
}

}
28 changes: 28 additions & 0 deletions tests/PHPStan/Rules/Functions/data/array_filter_empty.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

/** @var \stdClass[] $objects */
$objects = [];
/** @var array<\stdClass|null> $objectsOrNull */
$objectsOrNull = [];
/** @var array<false|null> $falsey */
$falsey = [];

array_filter([0,1,3]);
array_filter([1,3]);
array_filter(['test']);
array_filter(['', 'test']);
array_filter([null, 'test']);
array_filter([false, 'test']);
array_filter([true, false]);
array_filter([true, true]);
array_filter([new \stdClass()]);
array_filter([new \stdClass(), null]);
array_filter($objects);
array_filter($objectsOrNull);

array_filter([0]);
array_filter([null]);
array_filter([null, null]);
array_filter([null, 0]);
array_filter($falsey);
array_filter([]);

0 comments on commit 610b396

Please sign in to comment.