diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 5af3a763db..cde18d2a32 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -3,5 +3,6 @@ parameters: bleedingEdge: true skipCheckGenericClasses: [] explicitMixedInUnknownGenericNew: true + arrayFilter: true stubFiles: - ../stubs/bleedingEdge/Countable.stub diff --git a/conf/config.level5.neon b/conf/config.level5.neon index c890be88ec..3339b0a836 100644 --- a/conf/config.level5.neon +++ b/conf/config.level5.neon @@ -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 @@ -16,3 +20,6 @@ services: reportMaybes: %reportMaybes% tags: - phpstan.rules.rule + + - + class: PHPStan\Rules\Functions\ArrayFilterRule diff --git a/conf/config.neon b/conf/config.neon index 5f3f094aef..6a0898629d 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -28,6 +28,7 @@ parameters: - FilterIterator - RecursiveCallbackFilterIterator explicitMixedInUnknownGenericNew: false + arrayFilter: false fileExtensions: - php checkAdvancedIsset: false @@ -205,6 +206,7 @@ parametersSchema: disableRuntimeReflectionProvider: bool(), skipCheckGenericClasses: listOf(string()), explicitMixedInUnknownGenericNew: bool(), + arrayFilter: bool(), ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/src/Rules/Functions/ArrayFilterRule.php b/src/Rules/Functions/ArrayFilterRule.php new file mode 100644 index 0000000000..2ff8dd0e90 --- /dev/null +++ b/src/Rules/Functions/ArrayFilterRule.php @@ -0,0 +1,87 @@ + + */ +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 []; + } + +} diff --git a/tests/PHPStan/Command/data/file-without-errors.php b/tests/PHPStan/Command/data/file-without-errors.php index 4c4ad920ae..08929907d3 100644 --- a/tests/PHPStan/Command/data/file-without-errors.php +++ b/tests/PHPStan/Command/data/file-without-errors.php @@ -1,3 +1,3 @@ + */ +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) 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) 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); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/array_filter_empty.php b/tests/PHPStan/Rules/Functions/data/array_filter_empty.php new file mode 100644 index 0000000000..3bcb4dd6ea --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_filter_empty.php @@ -0,0 +1,28 @@ + $objectsOrNull */ +$objectsOrNull = []; +/** @var array $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([]);