diff --git a/src/Rules/Functions/FilterVarRule.php b/src/Rules/Functions/FilterVarRule.php new file mode 100644 index 0000000000..283cae9d0c --- /dev/null +++ b/src/Rules/Functions/FilterVarRule.php @@ -0,0 +1,69 @@ + + */ +#[RegisteredRule(level: 0)] +final class FilterVarRule implements Rule +{ + + public function __construct( + private ReflectionProvider $reflectionProvider, + private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if ($this->reflectionProvider->resolveFunctionName($node->name, $scope) !== 'filter_var') { + return []; + } + + $args = $node->getArgs(); + + if ($this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null)) { + if (count($args) < 3) { + return []; + } + + $flagsType = $scope->getType($args[2]->value); + + if ($this->filterFunctionReturnTypeHelper->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType) + ->and($this->filterFunctionReturnTypeHelper->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)) + ->yes() + ) { + return [ + RuleErrorBuilder::message('Cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE.') + ->identifier('filterVar.nullOnFailureAndThrowOnFailure') + ->build(), + ]; + } + } + + return []; + } + +} diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 98834338b5..e942a0d28f 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -483,7 +483,7 @@ private function getOptions(Type $flagsType, int $filterValue): array /** * @param non-empty-string $flagName */ - private function hasFlag(string $flagName, ?Type $flagsType): TrinaryLogic + public function hasFlag(string $flagName, ?Type $flagsType): TrinaryLogic { $flag = $this->getConstant($flagName); if ($flag === null) { diff --git a/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php b/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php new file mode 100644 index 0000000000..08596f61ba --- /dev/null +++ b/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php @@ -0,0 +1,32 @@ + */ +class FilterVarRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new FilterVarRule( + self::createReflectionProvider(), + self::getContainer()->getByType(FilterFunctionReturnTypeHelper::class), + ); + } + + #[RequiresPhp('8.5')] + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/filter_var_null_and_throw.php'], [ + ['Cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE.', 5], + ['Cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE.', 8], + ['Cannot use both FILTER_NULL_ON_FAILURE and FILTER_THROW_ON_FAILURE.', 10], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw.php b/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw.php new file mode 100644 index 0000000000..a1f8d33cd6 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw.php @@ -0,0 +1,17 @@ += 8.5 + +namespace FilterVarNullAndThrow; + +filter_var('foo@bar.test', FILTER_VALIDATE_EMAIL, FILTER_THROW_ON_FAILURE|FILTER_NULL_ON_FAILURE); + +$flag = FILTER_NULL_ON_FAILURE|FILTER_THROW_ON_FAILURE; +filter_var(100, FILTER_VALIDATE_INT, $flag); + +filter_var( + 'johndoe', + FILTER_VALIDATE_REGEXP, + ['options' => ['regexp' => '/^[a-z]+$/'], 'flags' => FILTER_THROW_ON_FAILURE|FILTER_NULL_ON_FAILURE] +); +filter_var('foo@bar.test', FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE); +filter_var('foo@bar.test', FILTER_VALIDATE_EMAIL, FILTER_THROW_ON_FAILURE); +