From fa75094b0f2e76c48e44af88857fad760387eac0 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:59:11 +0000 Subject: [PATCH 1/9] Fix phpstan/phpstan#14397: FILTER_FLAG_GLOBAL_RANGE false positive on PHP 8.3/8.4 - FILTER_FLAG_GLOBAL_RANGE (268435456) has the same numeric value as FILTER_THROW_ON_FAILURE on PHP 8.3/8.4 - This caused PHPStan to incorrectly think filter_var would throw, removing false from the return type - Gate all FILTER_THROW_ON_FAILURE checks behind PHP >= 8.5 (where the constant was introduced) - Applied fix to FilterFunctionReturnTypeHelper, FilterVarThrowTypeExtension, and FilterVarRule - Updated FilterVarRuleTest to pass new PhpVersion constructor parameter - New regression test in tests/PHPStan/Analyser/nsrt/bug-14397.php --- src/Rules/Functions/FilterVarRule.php | 4 +++- src/Type/Php/FilterFunctionReturnTypeHelper.php | 4 +++- src/Type/Php/FilterVarThrowTypeExtension.php | 3 +++ tests/PHPStan/Analyser/nsrt/bug-14397.php | 13 +++++++++++++ tests/PHPStan/Rules/Functions/FilterVarRuleTest.php | 2 ++ 5 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14397.php diff --git a/src/Rules/Functions/FilterVarRule.php b/src/Rules/Functions/FilterVarRule.php index 283cae9d0ce..a63cc4553bf 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,7 +46,7 @@ public function processNode(Node $node, Scope $scope): array $args = $node->getArgs(); - if ($this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null)) { + if ($this->phpVersion->getVersionId() >= 80500 && $this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null)) { if (count($args) < 3) { return []; } diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 39a0009125c..8a97f41c593 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->getVersionId() >= 80500 + ? $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..087e7633632 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, ) { } @@ -29,6 +31,7 @@ public function isFunctionSupported( ): bool { return $functionReflection->getName() === 'filter_var' + && $this->phpVersion->getVersionId() >= 80500 && $this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14397.php b/tests/PHPStan/Analyser/nsrt/bug-14397.php new file mode 100644 index 00000000000..0b65874310d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14397.php @@ -0,0 +1,13 @@ += 8.2 + +declare(strict_types = 1); + +namespace Bug14397; + +use function filter_var; +use function PHPStan\Testing\assertType; + +function test(string $ipAddress): void +{ + assertType('non-falsy-string|false', filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_GLOBAL_RANGE)); +} diff --git a/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php b/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php index 40c445cf58c..f7c1ec48a38 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), ); } From 94b3785b511dcf269d58fa9e50a62e606ed34943 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 30 Mar 2026 09:59:56 +0000 Subject: [PATCH 2/9] Replace raw version check with PhpVersion::hasFilterThrowOnFailureConstant() Co-Authored-By: Claude Opus 4.6 --- src/Php/PhpVersion.php | 5 +++++ src/Rules/Functions/FilterVarRule.php | 2 +- src/Type/Php/FilterFunctionReturnTypeHelper.php | 2 +- src/Type/Php/FilterVarThrowTypeExtension.php | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) 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 a63cc4553bf..21a0f9c2cb2 100644 --- a/src/Rules/Functions/FilterVarRule.php +++ b/src/Rules/Functions/FilterVarRule.php @@ -46,7 +46,7 @@ public function processNode(Node $node, Scope $scope): array $args = $node->getArgs(); - if ($this->phpVersion->getVersionId() >= 80500 && $this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null)) { + if ($this->phpVersion->hasFilterThrowOnFailureConstant() && $this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null)) { if (count($args) < 3) { return []; } diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 8a97f41c593..8608ac867ef 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -151,7 +151,7 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T $inputIsArray = $inputType->isArray(); $hasRequireArrayFlag = $this->hasFlag('FILTER_REQUIRE_ARRAY', $flagsType); - $hasThrowOnFailureFlag = $this->phpVersion->getVersionId() >= 80500 + $hasThrowOnFailureFlag = $this->phpVersion->hasFilterThrowOnFailureConstant() ? $this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType) : TrinaryLogic::createNo(); if ($inputIsArray->no() && $hasRequireArrayFlag->yes()) { diff --git a/src/Type/Php/FilterVarThrowTypeExtension.php b/src/Type/Php/FilterVarThrowTypeExtension.php index 087e7633632..7b40dbb9a99 100644 --- a/src/Type/Php/FilterVarThrowTypeExtension.php +++ b/src/Type/Php/FilterVarThrowTypeExtension.php @@ -31,7 +31,7 @@ public function isFunctionSupported( ): bool { return $functionReflection->getName() === 'filter_var' - && $this->phpVersion->getVersionId() >= 80500 + && $this->phpVersion->hasFilterThrowOnFailureConstant() && $this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null); } From 67dec56bf001a9349cc3968c40ad59214d311356 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 30 Mar 2026 10:11:36 +0000 Subject: [PATCH 3/9] Add tests for FilterVarThrowTypeExtension and FilterVarRule on PHP < 8.5 - Add nsrt test verifying filter_var with FILTER_THROW_ON_FAILURE still returns false on PHP < 8.5 (throw type extension correctly inactive) - Add FilterVarRule test verifying no error for FILTER_NULL_ON_FAILURE + FILTER_THROW_ON_FAILURE combination on PHP < 8.5 Co-Authored-By: Claude Opus 4.6 --- .../Analyser/nsrt/bug-14397-pre-php85.php | 16 ++++++++++++++++ .../Rules/Functions/FilterVarRuleTest.php | 6 ++++++ .../data/filter_var_null_and_throw_pre_php85.php | 16 ++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14397-pre-php85.php create mode 100644 tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_pre_php85.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-14397-pre-php85.php b/tests/PHPStan/Analyser/nsrt/bug-14397-pre-php85.php new file mode 100644 index 00000000000..238510a599e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14397-pre-php85.php @@ -0,0 +1,16 @@ + FILTER_THROW_ON_FAILURE])); +} diff --git a/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php b/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php index f7c1ec48a38..9971adada29 100644 --- a/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php @@ -31,4 +31,10 @@ public function testRule(): void ]); } + #[RequiresPhp('< 8.5')] + public function testRuleBeforePhp85(): void + { + $this->analyse([__DIR__ . '/data/filter_var_null_and_throw_pre_php85.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_pre_php85.php b/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_pre_php85.php new file mode 100644 index 00000000000..8ae392790eb --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_pre_php85.php @@ -0,0 +1,16 @@ + ['regexp' => '/^[a-z]+$/'], 'flags' => FILTER_THROW_ON_FAILURE|FILTER_NULL_ON_FAILURE] +); From 0608ee0307d3c71e5d1597cc46a0bc9d3b2c7b7c Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 30 Mar 2026 12:17:25 +0200 Subject: [PATCH 4/9] Update tests --- .../Analyser/nsrt/bug-14397-pre-php85.php | 16 ++++++++++++---- .../data/filter_var_null_and_throw_pre_php85.php | 8 +++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-14397-pre-php85.php b/tests/PHPStan/Analyser/nsrt/bug-14397-pre-php85.php index 238510a599e..5134dc009ca 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14397-pre-php85.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14397-pre-php85.php @@ -4,13 +4,21 @@ namespace Bug14397PrePhp85; +use PHPStan\TrinaryLogic; + use function filter_var; use function PHPStan\Testing\assertType; +use function PHPStan\Testing\assertVariableCertainty; function test(mixed $mixed): void { - // On PHP < 8.5, FILTER_THROW_ON_FAILURE doesn't truly exist - // so it shouldn't remove false from the return type - assertType('int|false', filter_var($mixed, FILTER_VALIDATE_INT, FILTER_THROW_ON_FAILURE)); - assertType('int|false', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_THROW_ON_FAILURE])); + 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/data/filter_var_null_and_throw_pre_php85.php b/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_pre_php85.php index 8ae392790eb..96362f1ef28 100644 --- a/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_pre_php85.php +++ b/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_pre_php85.php @@ -2,15 +2,13 @@ namespace FilterVarNullAndThrowPrePhp85; -// On PHP < 8.5, FILTER_THROW_ON_FAILURE doesn't truly exist -// so combining it with FILTER_NULL_ON_FAILURE should not be an error -filter_var('foo@bar.test', FILTER_VALIDATE_EMAIL, FILTER_THROW_ON_FAILURE|FILTER_NULL_ON_FAILURE); +filter_var('foo@bar.test', FILTER_VALIDATE_EMAIL, FILTER_FLAG_GLOBAL_RANGE|FILTER_NULL_ON_FAILURE); -$flag = FILTER_NULL_ON_FAILURE|FILTER_THROW_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_THROW_ON_FAILURE|FILTER_NULL_ON_FAILURE] + ['options' => ['regexp' => '/^[a-z]+$/'], 'flags' => FILTER_FLAG_GLOBAL_RANGE|FILTER_NULL_ON_FAILURE] ); From 5d6f582cdac1fe848fec5ce3b08dece56117b9bc Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 30 Mar 2026 12:29:16 +0200 Subject: [PATCH 5/9] Fix --- src/Type/Php/FilterVarThrowTypeExtension.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Type/Php/FilterVarThrowTypeExtension.php b/src/Type/Php/FilterVarThrowTypeExtension.php index 7b40dbb9a99..bcd8ed1a092 100644 --- a/src/Type/Php/FilterVarThrowTypeExtension.php +++ b/src/Type/Php/FilterVarThrowTypeExtension.php @@ -30,9 +30,7 @@ public function isFunctionSupported( FunctionReflection $functionReflection, ): bool { - return $functionReflection->getName() === 'filter_var' - && $this->phpVersion->hasFilterThrowOnFailureConstant() - && $this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null); + return $functionReflection->getName() === 'filter_var'; } public function getThrowTypeFromFunctionCall( @@ -45,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); From b46cca1ad9739fa8b3e3a28404f92a0ebcf0a636 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 30 Mar 2026 12:48:28 +0200 Subject: [PATCH 6/9] Fix --- .../Analyser/nsrt/bug-14397-pre-php85.php | 24 ------------------- tests/PHPStan/Analyser/nsrt/bug-14397.php | 16 +++++++++++++ 2 files changed, 16 insertions(+), 24 deletions(-) delete mode 100644 tests/PHPStan/Analyser/nsrt/bug-14397-pre-php85.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-14397-pre-php85.php b/tests/PHPStan/Analyser/nsrt/bug-14397-pre-php85.php deleted file mode 100644 index 5134dc009ca..00000000000 --- a/tests/PHPStan/Analyser/nsrt/bug-14397-pre-php85.php +++ /dev/null @@ -1,24 +0,0 @@ - FILTER_FLAG_GLOBAL_RANGE])); -} diff --git a/tests/PHPStan/Analyser/nsrt/bug-14397.php b/tests/PHPStan/Analyser/nsrt/bug-14397.php index 0b65874310d..e607dd8a378 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14397.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14397.php @@ -4,10 +4,26 @@ 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])); +} From 001b55d6c4f7e0bea66e2928d8936c6a91f12497 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 30 Mar 2026 13:40:42 +0200 Subject: [PATCH 7/9] Refacto --- src/Rules/Functions/FilterVarRule.php | 35 +++++++++++-------- .../Rules/Functions/FilterVarRuleTest.php | 6 ++-- ...ilter_var_null_and_throw_global_range.php} | 4 +-- 3 files changed, 25 insertions(+), 20 deletions(-) rename tests/PHPStan/Rules/Functions/data/{filter_var_null_and_throw_pre_php85.php => filter_var_null_and_throw_global_range.php} (84%) diff --git a/src/Rules/Functions/FilterVarRule.php b/src/Rules/Functions/FilterVarRule.php index 21a0f9c2cb2..294d067e876 100644 --- a/src/Rules/Functions/FilterVarRule.php +++ b/src/Rules/Functions/FilterVarRule.php @@ -46,23 +46,28 @@ public function processNode(Node $node, Scope $scope): array $args = $node->getArgs(); - if ($this->phpVersion->hasFilterThrowOnFailureConstant() && $this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null)) { - if (count($args) < 3) { - return []; - } + if ( + !$this->phpVersion->hasFilterThrowOnFailureConstant() + || !$this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null) + ) { + return []; + } + + if (count($args) < 3) { + 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/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php b/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php index 9971adada29..585e3436740 100644 --- a/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php @@ -31,10 +31,10 @@ public function testRule(): void ]); } - #[RequiresPhp('< 8.5')] - public function testRuleBeforePhp85(): void + #[RequiresPhp('>= 8.2')] + public function testRuleWithGlobalRange(): void { - $this->analyse([__DIR__ . '/data/filter_var_null_and_throw_pre_php85.php'], []); + $this->analyse([__DIR__ . '/data/filter_var_null_and_throw_global_range.php'], []); } } diff --git a/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_pre_php85.php b/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_global_range.php similarity index 84% rename from tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_pre_php85.php rename to tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_global_range.php index 96362f1ef28..e8d95ca8e88 100644 --- a/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_pre_php85.php +++ b/tests/PHPStan/Rules/Functions/data/filter_var_null_and_throw_global_range.php @@ -1,6 +1,6 @@ -= 8.2 -namespace FilterVarNullAndThrowPrePhp85; +namespace FilterVarNullAndThrowGlobalRange; filter_var('foo@bar.test', FILTER_VALIDATE_EMAIL, FILTER_FLAG_GLOBAL_RANGE|FILTER_NULL_ON_FAILURE); From b4f3ef595cd0edec53be0b68bbeddd3468620951 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 30 Mar 2026 11:41:42 +0000 Subject: [PATCH 8/9] Add FilterVarRule test for FILTER_FLAG_GLOBAL_RANGE on PHP 8.5+ Ensures FILTER_FLAG_GLOBAL_RANGE|FILTER_NULL_ON_FAILURE does not falsely trigger the "cannot use both" error on PHP 8.5+ where the constants have distinct values. Co-Authored-By: Claude Opus 4.6 --- .../PHPStan/Rules/Functions/FilterVarRuleTest.php | 6 ++++++ .../filter_var_null_and_global_range_php85.php | 14 ++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 tests/PHPStan/Rules/Functions/data/filter_var_null_and_global_range_php85.php diff --git a/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php b/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php index 585e3436740..4594db91a79 100644 --- a/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php +++ b/tests/PHPStan/Rules/Functions/FilterVarRuleTest.php @@ -37,4 +37,10 @@ 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] +); From d657e3be04ef1e494dfed698f64a04b718669d67 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 30 Mar 2026 11:46:03 +0000 Subject: [PATCH 9/9] Move args count check before expensive phpVersion/reflectionProvider checks in FilterVarRule Co-Authored-By: Claude Opus 4.6 --- src/Rules/Functions/FilterVarRule.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Rules/Functions/FilterVarRule.php b/src/Rules/Functions/FilterVarRule.php index 294d067e876..d8f80ed90f6 100644 --- a/src/Rules/Functions/FilterVarRule.php +++ b/src/Rules/Functions/FilterVarRule.php @@ -46,6 +46,10 @@ public function processNode(Node $node, Scope $scope): array $args = $node->getArgs(); + if (count($args) < 3) { + return []; + } + if ( !$this->phpVersion->hasFilterThrowOnFailureConstant() || !$this->reflectionProvider->hasConstant(new Name\FullyQualified('FILTER_THROW_ON_FAILURE'), null) @@ -53,10 +57,6 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (count($args) < 3) { - return []; - } - $flagsType = $scope->getType($args[2]->value); if ($this->filterFunctionReturnTypeHelper->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType)