From 6a4f2b64f628b85032c3d52735b1a2d9e5e0de9f Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Mon, 19 Jan 2026 14:15:41 +0100 Subject: [PATCH 01/16] 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 31da7cb5e1e..eba307b2a40 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -393,6 +393,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 dc6620b0ca7..7338ba12a08 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 23152619d9d8e52c89e9d1dd894fb438984070aa Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Mon, 19 Jan 2026 18:12:04 +0100 Subject: [PATCH 02/16] 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 eba307b2a40..23a03f9ef50 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -393,18 +393,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 7338ba12a08..297f4d23933 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 affdbd86a4e153b33784a6e792983e63f02996d4 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Mon, 19 Jan 2026 18:20:22 +0100 Subject: [PATCH 03/16] 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 23a03f9ef50..5e28688122b 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -27,6 +27,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 count; From 3c63be522fefd3e272124ce65195a8a721338b18 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Mon, 19 Jan 2026 18:49:47 +0100 Subject: [PATCH 04/16] 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 5e28688122b..dd00d496516 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -27,7 +27,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 count; @@ -419,17 +418,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 1ea85c084e0ffe1297aa0bfe4905661671cc3205 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Mon, 19 Jan 2026 19:08:13 +0100 Subject: [PATCH 05/16] Code clean up. --- src/Type/Php/FilterFunctionReturnTypeHelper.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index dd00d496516..10bd4646857 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -419,9 +419,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 b7040988cc7552f6871c0fa5f740108ae10381b9 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Tue, 20 Jan 2026 10:56:34 +0100 Subject: [PATCH 06/16] Refactor thanks to @VincentLanglet + add new test cases. --- .../Php/FilterFunctionReturnTypeHelper.php | 83 +++++++++---------- .../filter-var-returns-non-empty-string.php | 23 ++++- 2 files changed, 59 insertions(+), 47 deletions(-) diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 10bd4646857..849b9da4981 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -196,7 +196,8 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType, Ph } 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); } } @@ -393,55 +394,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 */ @@ -585,7 +575,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(); } @@ -600,4 +590,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 297f4d23933..0bdcbfbfa98 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 88f1856f7969a1000aeb7945a8fb49b8c869955c Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Tue, 20 Jan 2026 11:18:51 +0100 Subject: [PATCH 07/16] 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 0bdcbfbfa98..a163a35b213 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 14c405445cc93f8e1ac7f5730d2b9156326d539f Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Tue, 20 Jan 2026 14:20:21 +0100 Subject: [PATCH 08/16] Apply comments. --- src/Type/Php/FilterFunctionReturnTypeHelper.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 849b9da4981..69a978f6a20 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -195,9 +195,12 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType, Ph $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); } } From 57d13bfdbba12654574ca3cdec750f37e9d10a25 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Tue, 20 Jan 2026 15:02:27 +0100 Subject: [PATCH 09/16] 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 a163a35b213..8e183213a2c 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 dc1fce7f4dee7377f5daada1e4eac15503e406e3 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Tue, 20 Jan 2026 15:09:22 +0100 Subject: [PATCH 10/16] 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 8e183213a2c..3846a3bd3fd 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; } From 0fee410a962afa15830fe5388c370ea158cfb743 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Tue, 27 Jan 2026 10:47:13 +0100 Subject: [PATCH 11/16] Add lots of unit tests and combinations to cover all kinds of types. --- .../Php/FilterFunctionReturnTypeHelper.php | 35 ++- .../filter-var-returns-non-empty-string.php | 258 +++++++++++++++++- 2 files changed, 276 insertions(+), 17 deletions(-) diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 69a978f6a20..e33170e41f9 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -23,6 +23,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectShapeType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -405,17 +406,41 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp new NullType(), ]); + $scalarOrNullType = null; + $useDefaultType = false; + if ($scalarOrNull->isSuperTypeOf($in)->yes()) { + $scalarOrNullType = $in; + } + + if ($scalarOrNull->isSuperTypeOf($in)->maybe()) { + // $in is (array or object) or (scalar or null). + $scalarOrNullType = TypeCombinator::remove( + TypeCombinator::remove($in, new ObjectShapeType([], [])), + new ArrayType(new MixedType(), new MixedType()), + ); + // Combine future results with defaultType as $in might be an array or an object. + $useDefaultType = true; + } + + if ( + $scalarOrNullType !== null + && ($in->isSuperTypeOf(new MixedType())->yes() || $scalarOrNull->isSuperTypeOf($scalarOrNullType)->yes()) + ) { $canBeSanitized = $this->canStringBeSanitized($filterValue, $flagsType); if ($canBeSanitized->no()) { - $stringType = $in->toString(); + $stringType = $scalarOrNullType->toString(); + } elseif ($scalarOrNullType->isString()->no()) { + $stringType = $scalarOrNullType->toString(); } else { - $stringType = $in->isString()->no() - ? $in->toString() - : TypeCombinator::union(TypeCombinator::remove($in, new StringType()), new StringType()); + $stringType = TypeCombinator::union( + TypeCombinator::remove($scalarOrNullType, new StringType()), + new StringType(), + ); } - return $this->handleEmptyStringNullFlag($stringType, $flagsType); + $returnType = $this->handleEmptyStringNullFlag($stringType, $flagsType); + return $useDefaultType ? TypeCombinator::union($defaultType, $returnType) : $returnType; } } 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 3846a3bd3fd..ba080e7a9dd 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 @@ -15,6 +15,7 @@ class Foo * @param positive-int $positive_int * @param negative-int $negative_int * @param bool $bool + * @param mixed $mixed */ public function run( string $str, @@ -25,8 +26,10 @@ public function run( int $positive_int, int $negative_int, bool $bool, + mixed $mixed, ): void { + $array = []; $object = (object)[]; assertType('non-empty-string', $str); @@ -34,12 +37,6 @@ public function run( $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($object, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW); assertType('false', $return); @@ -134,9 +131,6 @@ public function run( $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); @@ -167,6 +161,15 @@ public function run( $return = filter_var('0x10', FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_HEX); assertType('16', $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('', FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('null', $return); + $return = filter_var(true, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType("'1'", $return); @@ -191,17 +194,248 @@ public function run( $return = filter_var($nullable_non_empty_string, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType('non-empty-string|null', $return); + $return = filter_var($array, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('false', $return); + $return = filter_var($object, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType('false', $return); - $return = filter_var($this->anyOf(0.0, true), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + $return = filter_var($this->anyOf($str, $maybe_empty_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($str, ''), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($str, true), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string', $return); + + $return = filter_var($this->anyOf($str, false), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($str, $bool), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($str, 0.0), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string', $return); + + $return = filter_var($this->anyOf($str, 0), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string', $return); + + $return = filter_var($this->anyOf($str, null), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($str, $nullable_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($str, $nullable_non_empty_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($str, $array), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|false', $return); + + $return = filter_var($this->anyOf($str, $object), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|false', $return); + + $return = filter_var($this->anyOf($maybe_empty_string, ''), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($maybe_empty_string, true), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($maybe_empty_string, false), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($maybe_empty_string, $bool), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($maybe_empty_string, 0.0), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($maybe_empty_string, 0), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($maybe_empty_string, null), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($maybe_empty_string, $nullable_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($maybe_empty_string, $nullable_non_empty_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($maybe_empty_string, $array), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|false|null', $return); + + $return = filter_var($this->anyOf($maybe_empty_string, $object), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|false|null', $return); + + $return = filter_var($this->anyOf('', true), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'1'|null", $return); + + $return = filter_var($this->anyOf('', false), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('null', $return); + + $return = filter_var($this->anyOf('', $bool), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'1'|null", $return); + + $return = filter_var($this->anyOf('', 0.0), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'-0'|'0'|null", $return); + + $return = filter_var($this->anyOf('', 0), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'0'|null", $return); + + $return = filter_var($this->anyOf('', null), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('null', $return); + + $return = filter_var($this->anyOf('', $nullable_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf('', $nullable_non_empty_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf('', $array), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('false|null', $return); + + $return = filter_var($this->anyOf('', $object), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('false|null', $return); + + $return = filter_var($this->anyOf(true, false), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'1'|null", $return); + + $return = filter_var($this->anyOf(true, $bool), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'1'|null", $return); + + $return = filter_var($this->anyOf(true, 0.0), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType("'-0'|'0'|'1'", $return); - $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(true, 0), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'0'|'1'", $return); + + $return = filter_var($this->anyOf(true, null), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'1'|null", $return); + + $return = filter_var($this->anyOf(true, $nullable_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf(true, $nullable_non_empty_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf(true, $array), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'1'|false", $return); + + $return = filter_var($this->anyOf(true, $object), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'1'|false", $return); + + $return = filter_var($this->anyOf(false, $bool), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'1'|null", $return); + + $return = filter_var($this->anyOf(false, 0.0), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'-0'|'0'|null", $return); + + $return = filter_var($this->anyOf(false, 0), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'0'|null", $return); + + $return = filter_var($this->anyOf(false, null), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('null', $return); + + $return = filter_var($this->anyOf(false, $nullable_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf(false, $nullable_non_empty_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf(false, $array), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('false|null', $return); + + $return = filter_var($this->anyOf(false, $object), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('false|null', $return); + + $return = filter_var($this->anyOf($bool, 0.0), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'-0'|'0'|'1'|null", $return); + + $return = filter_var($this->anyOf($bool, 0), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'0'|'1'|null", $return); + + $return = filter_var($this->anyOf($bool, null), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'1'|null", $return); + + $return = filter_var($this->anyOf($bool, $nullable_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($bool, $nullable_non_empty_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($bool, $array), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'1'|false|null", $return); + + $return = filter_var($this->anyOf($bool, $object), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'1'|false|null", $return); + + $return = filter_var($this->anyOf(0.0, 0), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'-0'|'0'", $return); + + $return = filter_var($this->anyOf(0.0, null), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'-0'|'0'|null", $return); + + $return = filter_var($this->anyOf(0.0, $nullable_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf(0.0, $nullable_non_empty_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf(0.0, $array), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'-0'|'0'|false", $return); + + $return = filter_var($this->anyOf(0.0, $object), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'-0'|'0'|false", $return); $return = filter_var($this->anyOf(0, null), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); assertType("'0'|null", $return); + + $return = filter_var($this->anyOf(0, $nullable_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf(0, $nullable_non_empty_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf(0, $array), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'0'|false", $return); + + $return = filter_var($this->anyOf(0, $object), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'0'|false", $return); + + $return = filter_var($this->anyOf(null, $nullable_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf(null, $nullable_non_empty_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf(null, $array), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('false|null', $return); + + $return = filter_var($this->anyOf(null, $object), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('false|null', $return); + + $return = filter_var($this->anyOf($nullable_string, $nullable_non_empty_string), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $return); + + $return = filter_var($this->anyOf($nullable_string, $array), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|false|null', $return); + + $return = filter_var($this->anyOf($nullable_string, $object), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|false|null', $return); + + $return = filter_var($this->anyOf($nullable_non_empty_string, $array), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|false|null', $return); + + $return = filter_var($this->anyOf($nullable_non_empty_string, $object), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|false|null', $return); + + $return = filter_var($this->anyOf($array, $object), FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('false', $return); + + $return = filter_var($mixed, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|false|null', $return); } /** From 5214b2492910b78c3c4c3f858b4b6659e05e9e43 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Tue, 27 Jan 2026 11:13:50 +0100 Subject: [PATCH 12/16] Remove forced type mixed on PHPUnit test to be compliant 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 ba080e7a9dd..607971f98ed 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 @@ -26,7 +26,7 @@ public function run( int $positive_int, int $negative_int, bool $bool, - mixed $mixed, + $mixed, ): void { $array = []; From df064bf60954186b47f3b7f96d2baf7986301ee7 Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Tue, 10 Feb 2026 16:35:07 +0100 Subject: [PATCH 13/16] Simplify some type detection. --- .../Php/FilterFunctionReturnTypeHelper.php | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index e33170e41f9..22b729c3a98 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -406,15 +406,12 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp new NullType(), ]); - $scalarOrNullType = null; + $scalarOrNullType = $in; $useDefaultType = false; - if ($scalarOrNull->isSuperTypeOf($in)->yes()) { - $scalarOrNullType = $in; - } - if ($scalarOrNull->isSuperTypeOf($in)->maybe()) { - // $in is (array or object) or (scalar or null). + // $in is a union of maybe null, maybe scalar, and a non-scalar. + // Force it to a union of maybe null and maybe scalar. $scalarOrNullType = TypeCombinator::remove( TypeCombinator::remove($in, new ObjectShapeType([], [])), new ArrayType(new MixedType(), new MixedType()), @@ -423,25 +420,20 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp $useDefaultType = true; } - if ( - $scalarOrNullType !== null - && ($in->isSuperTypeOf(new MixedType())->yes() || $scalarOrNull->isSuperTypeOf($scalarOrNullType)->yes()) - ) { - $canBeSanitized = $this->canStringBeSanitized($filterValue, $flagsType); - if ($canBeSanitized->no()) { - $stringType = $scalarOrNullType->toString(); - } elseif ($scalarOrNullType->isString()->no()) { - $stringType = $scalarOrNullType->toString(); - } else { - $stringType = TypeCombinator::union( - TypeCombinator::remove($scalarOrNullType, new StringType()), - new StringType(), - ); - } - - $returnType = $this->handleEmptyStringNullFlag($stringType, $flagsType); - return $useDefaultType ? TypeCombinator::union($defaultType, $returnType) : $returnType; + $canBeSanitized = $this->canStringBeSanitized($filterValue, $flagsType); + if ($canBeSanitized->no()) { + $stringType = $scalarOrNullType->toString(); + } elseif ($scalarOrNullType->isString()->no()) { + $stringType = $scalarOrNullType->toString(); + } else { + $stringType = TypeCombinator::union( + TypeCombinator::remove($scalarOrNullType, new StringType()), + new StringType(), + ); } + + $returnType = $this->handleEmptyStringNullFlag($stringType, $flagsType); + return $useDefaultType ? TypeCombinator::union($defaultType, $returnType) : $returnType; } return null; @@ -454,12 +446,19 @@ private function handleEmptyStringNullFlag(Type $in, ?Type $flagsType): Type return $in; } - $hasEmptyString = !$in->isSuperTypeOf(new ConstantStringType(''))->no(); if ($hasFlag->maybe()) { - return $hasEmptyString ? TypeCombinator::addNull($in) : $in; + return $in->isNonEmptyString()->maybe() ? TypeCombinator::addNull($in) : $in; + } + + if ($in->isNonEmptyString()->yes()) { + return $in; + } + + if ($in->isNonEmptyString()->maybe()) { + return TypeCombinator::remove(TypeCombinator::addNull($in), new ConstantStringType('')); } - return $hasEmptyString ? TypeCombinator::remove(TypeCombinator::addNull($in), new ConstantStringType('')) : $in; + return new NullType(); } /** @param array $typeOptions */ From 4d919621d49a6cab5fed1e7d52d8ccf8ce9628eb Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Tue, 10 Feb 2026 19:38:40 +0100 Subject: [PATCH 14/16] Simplify comparisions. --- src/Type/Php/FilterFunctionReturnTypeHelper.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 22b729c3a98..590dd8bcbea 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -420,10 +420,7 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp $useDefaultType = true; } - $canBeSanitized = $this->canStringBeSanitized($filterValue, $flagsType); - if ($canBeSanitized->no()) { - $stringType = $scalarOrNullType->toString(); - } elseif ($scalarOrNullType->isString()->no()) { + if ($this->canStringBeSanitized($filterValue, $flagsType)->no()) { $stringType = $scalarOrNullType->toString(); } else { $stringType = TypeCombinator::union( From e4a777d81c8262f6bf38a4881583c068441666fd Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Tue, 10 Feb 2026 20:07:02 +0100 Subject: [PATCH 15/16] Removing mutants. --- .../Php/FilterFunctionReturnTypeHelper.php | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 590dd8bcbea..7cde6671c0d 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -398,33 +398,23 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp } if ($filterValue === $this->getConstant('FILTER_DEFAULT')) { - $scalarOrNull = new UnionType([ - new StringType(), - new FloatType(), - new BooleanType(), - new IntegerType(), - new NullType(), - ]); - - $scalarOrNullType = $in; + $inputType = $in; $useDefaultType = false; - if ($scalarOrNull->isSuperTypeOf($in)->maybe()) { - // $in is a union of maybe null, maybe scalar, and a non-scalar. - // Force it to a union of maybe null and maybe scalar. - $scalarOrNullType = TypeCombinator::remove( - TypeCombinator::remove($in, new ObjectShapeType([], [])), - new ArrayType(new MixedType(), new MixedType()), - ); - // Combine future results with defaultType as $in might be an array or an object. + if ($inputType->isArray()->maybe()) { + $inputType = TypeCombinator::remove($inputType, new ArrayType(new MixedType(), new MixedType())); + $useDefaultType = true; + } + if ($inputType->isObject()->maybe()) { + $inputType = TypeCombinator::remove($inputType, new ObjectShapeType([], [])); $useDefaultType = true; } if ($this->canStringBeSanitized($filterValue, $flagsType)->no()) { - $stringType = $scalarOrNullType->toString(); + $stringType = $inputType->toString(); } else { $stringType = TypeCombinator::union( - TypeCombinator::remove($scalarOrNullType, new StringType()), + TypeCombinator::remove($inputType, new StringType()), new StringType(), ); } From 2ff80baf75e83569d78d44932b89fc0b5fe64b1a Mon Sep 17 00:00:00 2001 From: Nicolas Giraud Date: Wed, 11 Feb 2026 14:37:18 +0100 Subject: [PATCH 16/16] Simplify method `canStringBeSanitized`. --- .../Php/FilterFunctionReturnTypeHelper.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 7cde6671c0d..68eaf0527fb 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -185,9 +185,9 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType, Ph $type = $this->applyRangeOptions($type, $options, $defaultType); if ( - $inputType->isNonEmptyString()->yes() + !$this->canStringBeSanitized($filterValue, $flagsType) + && $inputType->isNonEmptyString()->yes() && $type->isString()->yes() - && $this->canStringBeSanitized($filterValue, $flagsType)->no() ) { $accessory = new AccessoryNonEmptyStringType(); if ($inputType->isNonFalsyString()->yes()) { @@ -410,7 +410,7 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp $useDefaultType = true; } - if ($this->canStringBeSanitized($filterValue, $flagsType)->no()) { + if (!$this->canStringBeSanitized($filterValue, $flagsType)) { $stringType = $inputType->toString(); } else { $stringType = TypeCombinator::union( @@ -586,22 +586,27 @@ private function getFlagsValue(Type $exprType): Type ); } - private function canStringBeSanitized(int $filterValue, ?Type $flagsType): TrinaryLogic + private function canStringBeSanitized(int $filterValue, ?Type $flagsType): bool { // If it is a validation filter, the string will not be changed if ($this->isValidationFilter($filterValue)) { - return TrinaryLogic::createNo(); + return false; } // FILTER_DEFAULT will not sanitize, unless it has FILTER_FLAG_STRIP_LOW, // FILTER_FLAG_STRIP_HIGH, or FILTER_FLAG_STRIP_BACKTICK if ($filterValue === $this->getConstant('FILTER_DEFAULT')) { - return $this->hasFlag('FILTER_FLAG_STRIP_LOW', $flagsType) + $hasStripFlags = $this->hasFlag('FILTER_FLAG_STRIP_LOW', $flagsType) ->or($this->hasFlag('FILTER_FLAG_STRIP_HIGH', $flagsType)) ->or($this->hasFlag('FILTER_FLAG_STRIP_BACKTICK', $flagsType)); + if ($hasStripFlags->maybe()) { + // Too complicated, considering strip flags are not defined (default behavior). + return false; + } + return $hasStripFlags->yes(); } - return TrinaryLogic::createYes(); + return true; } private function isValidationFilter(int $filterValue): bool