diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 23d60cca01..39467e49bc 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -191,7 +191,11 @@ 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())) { + 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); } @@ -389,18 +393,46 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp } if ($filterValue === $this->getConstant('FILTER_DEFAULT')) { - if ($this->canStringBeSanitized($filterValue, $flagsType)->no() && $in->isString()->yes()) { - return $in; - } + $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->isBoolean()->yes() || $in->isFloat()->yes() || $in->isInteger()->yes() || $in->isNull()->yes()) { - return $in->toString(); + return $this->handleEmptyStringNullFlag($stringType, $flagsType); } } return null; } + private function handleEmptyStringNullFlag(Type $in, ?Type $flagsType): Type + { + $hasFlag = $this->hasFlag('FILTER_FLAG_EMPTY_STRING_NULL', $flagsType); + if ($hasFlag->no()) { + return $in; + } + + $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 */ private function applyRangeOptions(Type $type, array $typeOptions, Type $defaultType): Type { @@ -532,7 +564,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(); } @@ -547,4 +579,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 dc6620b0ca..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 @@ -8,24 +8,55 @@ 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, int $int, int $positive_int, int $negative_int): void + public function run( + string $str, + string $maybe_empty_string, + ?string $nullable_string, + ?string $nullable_non_empty_string, + int $int, + int $positive_int, + int $negative_int, + bool $bool, + ): void { + $object = (object)[]; + 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($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); @@ -51,6 +82,9 @@ public function run(string $str, int $int, int $positive_int, int $negative_int) $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); @@ -100,6 +134,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); @@ -129,5 +166,53 @@ public function run(string $str, int $int, int $positive_int, int $negative_int) $return = filter_var('0x10', FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_HEX); assertType('16', $return); + + $return = filter_var(true, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'1'", $return); + + $return = filter_var(false, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('null', $return); + + $return = filter_var($bool, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'1'|null", $return); + + $return = filter_var(0.0, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'-0'|'0'", $return); + + $return = filter_var(0, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType("'0'", $return); + + $return = filter_var(null, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('null', $return); + + $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, FILTER_DEFAULT, FILTER_FLAG_EMPTY_STRING_NULL); + assertType('non-empty-string|null', $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); + 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(0, null), FILTER_DEFAULT, 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($a, $b) + { + return random_int(0, 1) ? $a : $b; } }