diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 23d60cca01..436efa8fd2 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -24,6 +24,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; @@ -191,7 +192,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 +394,70 @@ 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(), + ]); + + $scalarOrNullType = null; + $useDefaultType = false; + + if ($scalarOrNull->isSuperTypeOf($in)->yes()) { + $scalarOrNullType = $in; } - if ($in->isBoolean()->yes() || $in->isFloat()->yes() || $in->isInteger()->yes() || $in->isNull()->yes()) { - return $in->toString(); + 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 = $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; } + 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 +589,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 +604,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..607971f98e 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,52 @@ 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 + * @param mixed $mixed */ - 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, + $mixed, + ): void { + $array = []; + $object = (object)[]; + assertType('non-empty-string', $str); $return = filter_var($str, FILTER_DEFAULT); assertType('non-empty-string', $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 +79,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); @@ -129,5 +160,293 @@ 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($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); + + $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($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($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(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); + } + + /** + * @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; } }