Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#6840 Report useless array filter #1077

Merged
merged 4 commits into from
Mar 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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([]);