diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 21945b2b38f..f24800b9a4d 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -501,4 +501,9 @@ public function supportsObjectsInArraySumProduct(): bool return $this->versionId >= 80300; } + public function hasFilterThrowOnFailureConstant(): bool + { + return $this->versionId >= 80500; + } + } diff --git a/src/Rules/Functions/FilterVarRule.php b/src/Rules/Functions/FilterVarRule.php index 283cae9d0ce..d8f80ed90f6 100644 --- a/src/Rules/Functions/FilterVarRule.php +++ b/src/Rules/Functions/FilterVarRule.php @@ -7,6 +7,7 @@ use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; @@ -23,6 +24,7 @@ final class FilterVarRule implements Rule public function __construct( private ReflectionProvider $reflectionProvider, private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper, + private PhpVersion $phpVersion, ) { } @@ -44,23 +46,28 @@ public function processNode(Node $node, Scope $scope): array $args = $node->getArgs(); - if ($this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null)) { - if (count($args) < 3) { - return []; - } + if (count($args) < 3) { + return []; + } + + if ( + !$this->phpVersion->hasFilterThrowOnFailureConstant() + || !$this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null) + ) { + return []; + } - $flagsType = $scope->getType($args[2]->value); + $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(), - ]; - } + 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 39a0009125c..8608ac867ef 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -151,7 +151,9 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T $inputIsArray = $inputType->isArray(); $hasRequireArrayFlag = $this->hasFlag('FILTER_REQUIRE_ARRAY', $flagsType); - $hasThrowOnFailureFlag = $this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType); + $hasThrowOnFailureFlag = $this->phpVersion->hasFilterThrowOnFailureConstant() + ? $this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType) + : TrinaryLogic::createNo(); if ($inputIsArray->no() && $hasRequireArrayFlag->yes()) { if ($hasThrowOnFailureFlag->yes()) { return new ErrorType(); diff --git a/src/Type/Php/FilterVarThrowTypeExtension.php b/src/Type/Php/FilterVarThrowTypeExtension.php index 677a90a2b12..bcd8ed1a092 100644 --- a/src/Type/Php/FilterVarThrowTypeExtension.php +++ b/src/Type/Php/FilterVarThrowTypeExtension.php @@ -6,6 +6,7 @@ use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\Constant\ConstantIntegerType; @@ -20,6 +21,7 @@ final class FilterVarThrowTypeExtension implements DynamicFunctionThrowTypeExten public function __construct( private ReflectionProvider $reflectionProvider, + private PhpVersion $phpVersion, ) { } @@ -28,8 +30,7 @@ public function isFunctionSupported( FunctionReflection $functionReflection, ): bool { - return $functionReflection->getName() === 'filter_var' - && $this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null); + return $functionReflection->getName() === 'filter_var'; } public function getThrowTypeFromFunctionCall( @@ -42,6 +43,13 @@ public function getThrowTypeFromFunctionCall( return null; } + if ( + !$this->phpVersion->hasFilterThrowOnFailureConstant() + || !$this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null) + ) { + return null; + } + $flagsExpr = $funcCall->getArgs()[3]->value; $flagsType = $scope->getType($flagsExpr); diff --git a/tests/PHPStan/Analyser/nsrt/bug-14397.php b/tests/PHPStan/Analyser/nsrt/bug-14397.php new file mode 100644 index 00000000000..e607dd8a378 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14397.php @@ -0,0 +1,29 @@ += 8.2 + +declare(strict_types = 1); + +namespace Bug14397; + +use PHPStan\TrinaryLogic; + +use function filter_var; +use function PHPStan\Testing\assertType; +use function PHPStan\Testing\assertVariableCertainty; + +function test(string $ipAddress): void +{ + assertType('non-falsy-string|false', filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_GLOBAL_RANGE)); +} + +function test2(mixed $mixed): void +{ + try { + filter_var($mixed, FILTER_VALIDATE_INT, FILTER_FLAG_GLOBAL_RANGE); + $foo = 1; + } catch (\Filter\FilterFailedException $e) { + assertVariableCertainty(TrinaryLogic::createYes(), $foo); + } + + assertType('int|false', filter_var($mixed, FILTER_VALIDATE_INT, FILTER_FLAG_GLOBAL_RANGE)); + assertType('int|false', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_FLAG_GLOBAL_RANGE])); +} diff --git a/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php b/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php index 40c445cf58c..4594db91a79 100644 --- a/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php @@ -2,6 +2,7 @@ namespace PHPStan\Rules\Functions; +use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Php\FilterFunctionReturnTypeHelper; @@ -16,6 +17,7 @@ protected function getRule(): Rule return new FilterVarRule( self::createReflectionProvider(), self::getContainer()->getByType(FilterFunctionReturnTypeHelper::class), + self::getContainer()->getByType(PhpVersion::class), ); } @@ -29,4 +31,16 @@ public function testRule(): void ]); } + #[RequiresPhp('>= 8.2')] + public function testRuleWithGlobalRange(): void + { + $this->analyse([__DIR__ . '/data/filter_var_null_and_throw_global_range.php'], []); + } + + #[RequiresPhp('>= 8.5')] + public function testRuleGlobalRangePhp85(): void + { + $this->analyse([__DIR__ . '/data/filter_var_null_and_global_range_php85.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/filter_var_null_and_global_range_php85.php b/tests/PHPStan/Rules/Functions/data/filter_var_null_and_global_range_php85.php new file mode 100644 index 00000000000..b52de8fbb7a --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/filter_var_null_and_global_range_php85.php @@ -0,0 +1,14 @@ += 8.5 + +namespace FilterVarNullAndGlobalRangePhp85; + +filter_var('foo@bar.test', FILTER_VALIDATE_EMAIL, FILTER_FLAG_GLOBAL_RANGE|FILTER_NULL_ON_FAILURE); + +$flag = FILTER_NULL_ON_FAILURE|FILTER_FLAG_GLOBAL_RANGE; +filter_var(100, FILTER_VALIDATE_INT, $flag); + +filter_var( + 'johndoe', + FILTER_VALIDATE_REGEXP, + ['options' => ['regexp' => '/^[a-z]+$/'], 'flags' => FILTER_FLAG_GLOBAL_RANGE|FILTER_NULL_ON_FAILURE] +); diff --git a/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_global_range.php b/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_global_range.php new file mode 100644 index 00000000000..e8d95ca8e88 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_global_range.php @@ -0,0 +1,14 @@ += 8.2 + +namespace FilterVarNullAndThrowGlobalRange; + +filter_var('foo@bar.test', FILTER_VALIDATE_EMAIL, FILTER_FLAG_GLOBAL_RANGE|FILTER_NULL_ON_FAILURE); + +$flag = FILTER_NULL_ON_FAILURE|FILTER_FLAG_GLOBAL_RANGE; +filter_var(100, FILTER_VALIDATE_INT, $flag); + +filter_var( + 'johndoe', + FILTER_VALIDATE_REGEXP, + ['options' => ['regexp' => '/^[a-z]+$/'], 'flags' => FILTER_FLAG_GLOBAL_RANGE|FILTER_NULL_ON_FAILURE] +);