From 5e185c37d7fa12c94b9a23b41742e67be0441317 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Mon, 19 Jan 2026 14:15:41 +0100 Subject: [PATCH 01/10] Fix #13963 with unit tests. --- src/Type/Php/FilterFunctionReturnTypeHelper.php | 14 ++++++++++++++ .../nsrt/filter-var-returns-non-empty-string.php | 12 +++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 23d60cca01..ed7d45f947 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -389,6 +389,20 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp } if ($filterValue === $this->getConstant('FILTER_DEFAULT')) { + if ($this->hasFlag('FILTER_FLAG_EMPTY_STRING_NULL', $flagsType)->yes() && $in->isString()->yes()) { + if ($in->isNonEmptyString()->yes()) { + return $in; + } + + if ($in->isNonEmptyString()->maybe()) { + return new UnionType([new AccessoryNonEmptyStringType(), new NullType()]); + } + + if ($in->isNonEmptyString()->no()) { + return new NullType(); + } + } + if ($this->canStringBeSanitized($filterValue, $flagsType)->no() && $in->isString()->yes()) { return $in; } diff --git a/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php b/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php index dc6620b0ca..7338ba12a0 100644 --- a/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php @@ -8,16 +8,23 @@ class Foo { /** * @param non-empty-string $str + * @param string $maybe_empty_string * @param positive-int $positive_int * @param negative-int $negative_int */ - public function run(string $str, int $int, int $positive_int, int $negative_int): void + public function run(string $str, string $maybe_empty_string, int $int, int $positive_int, int $negative_int): void { assertType('non-empty-string', $str); $return = filter_var($str, FILTER_DEFAULT); assertType('non-empty-string', $return); + $return = filter_var($str, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string', $return); + + $return = filter_var($maybe_empty_string, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + $return = filter_var($str, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW); assertType('string|false', $return); @@ -100,6 +107,9 @@ public function run(string $str, int $int, int $positive_int, int $negative_int) $return = filter_var($str2, FILTER_DEFAULT); assertType("''", $return); + $return = filter_var('', FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('null', $return); + $return = filter_var($str2, FILTER_VALIDATE_URL); assertType('non-falsy-string|false', $return); From ee40a69303ef5c0c807b9c6b0664fbc04f744ffa Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Mon, 19 Jan 2026 18:12:04 +0100 Subject: [PATCH 02/10] Improve handling of input type for filter_var with option FILTER_FLAG_EMPTY_STRING_NULL. --- .../Php/FilterFunctionReturnTypeHelper.php | 41 ++++++++++--- .../filter-var-returns-non-empty-string.php | 60 ++++++++++++++++++- 2 files changed, 93 insertions(+), 8 deletions(-) diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index ed7d45f947..65b0c168b2 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -389,18 +389,45 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp } if ($filterValue === $this->getConstant('FILTER_DEFAULT')) { - if ($this->hasFlag('FILTER_FLAG_EMPTY_STRING_NULL', $flagsType)->yes() && $in->isString()->yes()) { - if ($in->isNonEmptyString()->yes()) { - return $in; + if ($this->hasFlag('FILTER_FLAG_EMPTY_STRING_NULL', $flagsType)->yes()) { + if ($in->isFalse()->yes() || $in->isNull()->yes()) { + return new NullType(); } - if ($in->isNonEmptyString()->maybe()) { - return new UnionType([new AccessoryNonEmptyStringType(), new NullType()]); + if ($in->isFloat()->yes() || $in->isInteger()->yes() || $in->isTrue()->yes()) { + return $in->toString(); } - if ($in->isNonEmptyString()->no()) { - return new NullType(); + if ($in->isString()->yes()) { + if ($in->isNonEmptyString()->yes()) { + return $in; + } + + if ($in->isNonEmptyString()->maybe()) { + return new UnionType([new AccessoryNonEmptyStringType(), new NullType()]); + } + + if ($in->isNonEmptyString()->no()) { + return new NullType(); + } } + + $inString = $in->toString(); + if ($inString instanceof UnionType) { + $inStringTypes = $inString->getTypes(); + // Replace ConstantStringType of value === "" by NullType. + $types = array_filter( + $inStringTypes, + static fn (Type $type): bool => $type instanceof ConstantStringType && $type->getValue() !== '', + ); + if ($types !== $inStringTypes) { + $types[] = new NullType(); + } + + return new UnionType($types); + } + + return new UnionType([new AccessoryNonEmptyStringType(), new NullType()]); } if ($this->canStringBeSanitized($filterValue, $flagsType)->no() && $in->isString()->yes()) { diff --git a/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php b/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php index 7338ba12a0..297f4d2393 100644 --- a/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php @@ -9,10 +9,23 @@ class Foo /** * @param non-empty-string $str * @param string $maybe_empty_string + * @param null|string $nullable_string + * @param null|non-empty-string $nullable_non_empty_string + * @param int $int * @param positive-int $positive_int * @param negative-int $negative_int + * @param bool $bool */ - public function run(string $str, string $maybe_empty_string, int $int, int $positive_int, int $negative_int): void + public function run( + string $str, + string $maybe_empty_string, + null|string $nullable_string, + null|string $nullable_non_empty_string, + int $int, + int $positive_int, + int $negative_int, + bool $bool, + ): void { assertType('non-empty-string', $str); @@ -139,5 +152,50 @@ public function run(string $str, string $maybe_empty_string, int $int, int $posi $return = filter_var('0x10', FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_HEX); assertType('16', $return); + + $return = filter_var(true, options: FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'1'", $return); + + $return = filter_var(false, options: FILTER_FLAG_EMPTY_STRING_NULL); + assertType('null', $return); + + $return = filter_var($bool, options: FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'1'|null", $return); + + $return = filter_var(0.0, options: FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'-0'|'0'", $return); + + $return = filter_var(0, options: FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'0'", $return); + + $return = filter_var(null, options: FILTER_FLAG_EMPTY_STRING_NULL); + assertType('null', $return); + + $return = filter_var($nullable_string, options: FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($nullable_non_empty_string, options: FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf(0.0, true), options: FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'-0'|'0'|'1'", $return); + + $return = filter_var($this->anyOf($bool, $maybe_empty_string), options: FILTER_FLAG_EMPTY_STRING_NULL); + assertType("non-empty-string|null", $return); + + $return = filter_var($this->anyOf(0, null), options: FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'0'|null", $return); + } + + /** + * @template T + * @template U + * @param T $a + * @param U $b + * @return T|U + */ + private function anyOf(mixed $a, mixed $b): mixed + { + return random_int(0, 1) ? $a : $b; } } From 56f11e43debd8b8b65e2db121866e3eb06ac4105 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Mon, 19 Jan 2026 18:20:22 +0100 Subject: [PATCH 03/10] Fix forgotten import of function. --- src/Type/Php/FilterFunctionReturnTypeHelper.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 65b0c168b2..e31e836dcb 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -28,6 +28,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; +use function array_filter; use function array_key_exists; use function array_merge; use function hexdec; From dfc3c1393ecbc72c85081c9d0af49443edb1c935 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Mon, 19 Jan 2026 18:49:47 +0100 Subject: [PATCH 04/10] Better usage of PHPStan API. --- src/Type/Php/FilterFunctionReturnTypeHelper.php | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index e31e836dcb..4862bef454 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -28,7 +28,6 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; -use function array_filter; use function array_key_exists; use function array_merge; use function hexdec; @@ -415,17 +414,11 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp $inString = $in->toString(); if ($inString instanceof UnionType) { - $inStringTypes = $inString->getTypes(); - // Replace ConstantStringType of value === "" by NullType. - $types = array_filter( - $inStringTypes, - static fn (Type $type): bool => $type instanceof ConstantStringType && $type->getValue() !== '', + return $inString->traverse( + static fn (Type $type): Type => $type->getConstantStrings() === [$type] && $type->getValue() === '' + ? new NullType() + : $type, ); - if ($types !== $inStringTypes) { - $types[] = new NullType(); - } - - return new UnionType($types); } return new UnionType([new AccessoryNonEmptyStringType(), new NullType()]); From eaa26a968c3d3eb116cbc33a70825aae244e7a77 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Mon, 19 Jan 2026 19:08:13 +0100 Subject: [PATCH 05/10] Code clean up. --- src/Type/Php/FilterFunctionReturnTypeHelper.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 4862bef454..cf2d3fc948 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -30,6 +30,7 @@ use PHPStan\Type\UnionType; use function array_key_exists; use function array_merge; +use function count; use function hexdec; use function is_int; use function octdec; @@ -415,9 +416,13 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp $inString = $in->toString(); if ($inString instanceof UnionType) { return $inString->traverse( - static fn (Type $type): Type => $type->getConstantStrings() === [$type] && $type->getValue() === '' - ? new NullType() - : $type, + static function (Type $type): Type { + $typeConstantStrings = $type->getConstantStrings(); + if (count($typeConstantStrings) === 1 && $typeConstantStrings[0]->getValue() === '') { + return new NullType(); + } + return $type; + }, ); } From 6cde0aa32013e0cf07b87b8b85d5085720526ca7 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Tue, 20 Jan 2026 10:56:34 +0100 Subject: [PATCH 06/10] Refactor thanks to @VincentLanglet + add new test cases. --- .../Php/FilterFunctionReturnTypeHelper.php | 86 +++++++++---------- .../filter-var-returns-non-empty-string.php | 23 ++++- 2 files changed, 60 insertions(+), 49 deletions(-) diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index cf2d3fc948..890ccdd272 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -30,7 +30,6 @@ use PHPStan\Type\UnionType; use function array_key_exists; use function array_merge; -use function count; use function hexdec; use function is_int; use function octdec; @@ -193,7 +192,8 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T } if ($exactType === null || $hasOptions->maybe() || (!$inputType->equals($type) && $inputType->isSuperTypeOf($type)->yes())) { - if (!$defaultType->isSuperTypeOf($type)->yes()) { + // Default type is handled as the exactType when flag FILTER_FLAG_EMPTY_STRING_NULL is set. + if (!$defaultType->isSuperTypeOf($type)->yes() && $this->hasFlag('FILTER_FLAG_EMPTY_STRING_NULL', $flagsType)->no()) { $type = TypeCombinator::union($type, $defaultType); } } @@ -209,7 +209,7 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T return new ArrayType($inputArrayKeyType ?? $mixedType, $type); } - if ($this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)->yes()) { + if ($this->isValidationFilter($filterValue) && $this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)->yes()) { $type = TypeCombinator::remove($type, $defaultType); } @@ -390,55 +390,44 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp } if ($filterValue === $this->getConstant('FILTER_DEFAULT')) { - if ($this->hasFlag('FILTER_FLAG_EMPTY_STRING_NULL', $flagsType)->yes()) { - if ($in->isFalse()->yes() || $in->isNull()->yes()) { - return new NullType(); + $scalarOrNull = new UnionType([ + new StringType(), + new FloatType(), + new BooleanType(), + new IntegerType(), + new NullType(), + ]); + + if ($scalarOrNull->isSuperTypeOf($in)->yes()) { + $canBeSanitized = $this->canStringBeSanitized($filterValue, $flagsType); + if ($canBeSanitized->no()) { + $stringType = $in->toString(); + } else { + $stringType = $in->isString()->no() + ? $in->toString() + : TypeCombinator::union(TypeCombinator::remove($in, new StringType()), new StringType()); } - if ($in->isFloat()->yes() || $in->isInteger()->yes() || $in->isTrue()->yes()) { - return $in->toString(); - } - - if ($in->isString()->yes()) { - if ($in->isNonEmptyString()->yes()) { - return $in; - } - - if ($in->isNonEmptyString()->maybe()) { - return new UnionType([new AccessoryNonEmptyStringType(), new NullType()]); - } - - if ($in->isNonEmptyString()->no()) { - return new NullType(); - } - } - - $inString = $in->toString(); - if ($inString instanceof UnionType) { - return $inString->traverse( - static function (Type $type): Type { - $typeConstantStrings = $type->getConstantStrings(); - if (count($typeConstantStrings) === 1 && $typeConstantStrings[0]->getValue() === '') { - return new NullType(); - } - return $type; - }, - ); - } - - return new UnionType([new AccessoryNonEmptyStringType(), new NullType()]); + return $this->handleEmptyStringNullFlag($stringType, $flagsType); } + } - if ($this->canStringBeSanitized($filterValue, $flagsType)->no() && $in->isString()->yes()) { - return $in; - } + return null; + } - if ($in->isBoolean()->yes() || $in->isFloat()->yes() || $in->isInteger()->yes() || $in->isNull()->yes()) { - return $in->toString(); - } + private function handleEmptyStringNullFlag(Type $in, ?Type $flagsType): Type + { + $hasFlag = $this->hasFlag('FILTER_FLAG_EMPTY_STRING_NULL', $flagsType); + if ($hasFlag->no()) { + return $in; } - return null; + $hasEmptyString = !$in->isSuperTypeOf(new ConstantStringType(''))->no(); + if ($hasFlag->maybe()) { + return $hasEmptyString ? TypeCombinator::addNull($in) : $in; + } + + return $hasEmptyString ? TypeCombinator::remove(TypeCombinator::addNull($in), new ConstantStringType('')) : $in; } /** @param array $typeOptions */ @@ -572,7 +561,7 @@ private function getFlagsValue(Type $exprType): Type private function canStringBeSanitized(int $filterValue, ?Type $flagsType): TrinaryLogic { // If it is a validation filter, the string will not be changed - if (($filterValue & self::VALIDATION_FILTER_BITMASK) !== 0) { + if ($this->isValidationFilter($filterValue)) { return TrinaryLogic::createNo(); } @@ -587,4 +576,9 @@ private function canStringBeSanitized(int $filterValue, ?Type $flagsType): Trina return TrinaryLogic::createYes(); } + private function isValidationFilter(int $filterValue): bool + { + return ($filterValue & self::VALIDATION_FILTER_BITMASK) !== 0; + } + } diff --git a/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php b/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php index 297f4d2393..0bdcbfbfa9 100644 --- a/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php @@ -27,6 +27,8 @@ public function run( bool $bool, ): void { + $object = (object)[]; + assertType('non-empty-string', $str); $return = filter_var($str, FILTER_DEFAULT); @@ -38,14 +40,23 @@ public function run( $return = filter_var($maybe_empty_string, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType('non-empty-string|null', $return); + $return = filter_var($object, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW); + assertType('false', $return); + $return = filter_var($str, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW); - assertType('string|false', $return); + assertType('string', $return); + + $return = filter_var($object, FILTER_DEFAULT, FILTER_FLAG_STRIP_HIGH); + assertType('false', $return); $return = filter_var($str, FILTER_DEFAULT, FILTER_FLAG_STRIP_HIGH); - assertType('string|false', $return); + assertType('string', $return); + + $return = filter_var($object, FILTER_DEFAULT, FILTER_FLAG_STRIP_BACKTICK); + assertType('false', $return); $return = filter_var($str, FILTER_DEFAULT, FILTER_FLAG_STRIP_BACKTICK); - assertType('string|false', $return); + assertType('string', $return); $return = filter_var($str, FILTER_VALIDATE_EMAIL); assertType('non-falsy-string|false', $return); @@ -71,6 +82,9 @@ public function run( $return = filter_var($str, FILTER_SANITIZE_STRING); assertType('string|false', $return); + $return = filter_var($object, FILTER_SANITIZE_STRING); + assertType('false', $return); + $return = filter_var($str, FILTER_VALIDATE_INT); assertType('int|false', $return); @@ -177,6 +191,9 @@ public function run( $return = filter_var($nullable_non_empty_string, options: FILTER_FLAG_EMPTY_STRING_NULL); assertType('non-empty-string|null', $return); + $return = filter_var($object, options: FILTER_FLAG_EMPTY_STRING_NULL); + assertType('false', $return); + $return = filter_var($this->anyOf(0.0, true), options: FILTER_FLAG_EMPTY_STRING_NULL); assertType("'-0'|'0'|'1'", $return); From daa1bd1df0e7e9cfe5296af6bad400cb1ce3db03 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Tue, 20 Jan 2026 11:18:51 +0100 Subject: [PATCH 07/10] Make unit tests complient with PHP 7.4. --- .../filter-var-returns-non-empty-string.php | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php b/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php index 0bdcbfbfa9..a163a35b21 100644 --- a/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php @@ -167,40 +167,40 @@ public function run( $return = filter_var('0x10', FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_HEX); assertType('16', $return); - $return = filter_var(true, options: FILTER_FLAG_EMPTY_STRING_NULL); + $return = filter_var(true, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType("'1'", $return); - $return = filter_var(false, options: FILTER_FLAG_EMPTY_STRING_NULL); + $return = filter_var(false, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType('null', $return); - $return = filter_var($bool, options: FILTER_FLAG_EMPTY_STRING_NULL); + $return = filter_var($bool, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType("'1'|null", $return); - $return = filter_var(0.0, options: FILTER_FLAG_EMPTY_STRING_NULL); + $return = filter_var(0.0, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType("'-0'|'0'", $return); - $return = filter_var(0, options: FILTER_FLAG_EMPTY_STRING_NULL); + $return = filter_var(0, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType("'0'", $return); - $return = filter_var(null, options: FILTER_FLAG_EMPTY_STRING_NULL); + $return = filter_var(null, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType('null', $return); - $return = filter_var($nullable_string, options: FILTER_FLAG_EMPTY_STRING_NULL); + $return = filter_var($nullable_string, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType('non-empty-string|null', $return); - $return = filter_var($nullable_non_empty_string, options: FILTER_FLAG_EMPTY_STRING_NULL); + $return = filter_var($nullable_non_empty_string, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType('non-empty-string|null', $return); - $return = filter_var($object, options: FILTER_FLAG_EMPTY_STRING_NULL); + $return = filter_var($object, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType('false', $return); - $return = filter_var($this->anyOf(0.0, true), options: FILTER_FLAG_EMPTY_STRING_NULL); + $return = filter_var($this->anyOf(0.0, true), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType("'-0'|'0'|'1'", $return); - $return = filter_var($this->anyOf($bool, $maybe_empty_string), options: FILTER_FLAG_EMPTY_STRING_NULL); + $return = filter_var($this->anyOf($bool, $maybe_empty_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType("non-empty-string|null", $return); - $return = filter_var($this->anyOf(0, null), options: FILTER_FLAG_EMPTY_STRING_NULL); + $return = filter_var($this->anyOf(0, null), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType("'0'|null", $return); } From 331e01ffbf922c0613b05e7e5151d3da99900ca7 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Tue, 20 Jan 2026 14:20:21 +0100 Subject: [PATCH 08/10] Apply comments. --- src/Type/Php/FilterFunctionReturnTypeHelper.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 890ccdd272..39467e49bc 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -191,9 +191,12 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T $type = TypeCombinator::intersect($type, $accessory); } - if ($exactType === null || $hasOptions->maybe() || (!$inputType->equals($type) && $inputType->isSuperTypeOf($type)->yes())) { - // Default type is handled as the exactType when flag FILTER_FLAG_EMPTY_STRING_NULL is set. - if (!$defaultType->isSuperTypeOf($type)->yes() && $this->hasFlag('FILTER_FLAG_EMPTY_STRING_NULL', $flagsType)->no()) { + if ( + $exactType === null + || $hasOptions->maybe() + || ($this->isValidationFilter($filterValue) && (!$inputType->equals($type) && $inputType->isSuperTypeOf($type)->yes())) + ) { + if (!$defaultType->isSuperTypeOf($type)->yes()) { $type = TypeCombinator::union($type, $defaultType); } } @@ -209,7 +212,7 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T return new ArrayType($inputArrayKeyType ?? $mixedType, $type); } - if ($this->isValidationFilter($filterValue) && $this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)->yes()) { + if ($this->hasFlag('FILTER_THROW_ON_FAILURE', $flagsType)->yes()) { $type = TypeCombinator::remove($type, $defaultType); } From 27351e82aed332a9d5279be42db8d4a1e08efa21 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Tue, 20 Jan 2026 15:02:27 +0100 Subject: [PATCH 09/10] Try to make unit test OK with PHP 7.4. --- .../Analyser/nsrt/filter-var-returns-non-empty-string.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php b/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php index a163a35b21..8e183213a2 100644 --- a/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php @@ -19,8 +19,8 @@ class Foo public function run( string $str, string $maybe_empty_string, - null|string $nullable_string, - null|string $nullable_non_empty_string, + ?string $nullable_string, + ?string $nullable_non_empty_string, int $int, int $positive_int, int $negative_int, @@ -211,7 +211,7 @@ public function run( * @param U $b * @return T|U */ - private function anyOf(mixed $a, mixed $b): mixed + private function anyOf($a, $b): mixed { return random_int(0, 1) ? $a : $b; } From edca7fef84e2f37b6565b37578fbf5ccd6339688 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Tue, 20 Jan 2026 15:09:22 +0100 Subject: [PATCH 10/10] Try to make unit test OK with PHP 7.4. --- .../Analyser/nsrt/filter-var-returns-non-empty-string.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php b/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php index 8e183213a2..3846a3bd3f 100644 --- a/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php +++ b/tests/PHPStan/Analyser/nsrt/filter-var-returns-non-empty-string.php @@ -211,7 +211,7 @@ public function run( * @param U $b * @return T|U */ - private function anyOf($a, $b): mixed + private function anyOf($a, $b) { return random_int(0, 1) ? $a : $b; }