diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index e0d7f63cdf..bbcc5d5a4e 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -25,6 +25,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; use function array_key_exists; use function array_merge; use function hexdec; @@ -56,9 +57,14 @@ public function __construct(private ReflectionProvider $reflectionProvider, priv private function getOffsetValueType(Type $inputType, Type $offsetType, ?Type $filterType, ?Type $flagsType): Type { - $inexistentOffsetType = $this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType) - ? new ConstantBooleanType(false) - : new NullType(); + $hasNullOnFailure = $this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType); + if ($hasNullOnFailure->yes()) { + $inexistentOffsetType = new ConstantBooleanType(false); + } elseif ($hasNullOnFailure->no()) { + $inexistentOffsetType = new NullType(); + } else { + $inexistentOffsetType = new UnionType([new ConstantBooleanType(false), new NullType()]); + } $hasOffsetValueType = $inputType->hasOffsetValueType($offsetType); if ($hasOffsetValueType->no()) { @@ -122,18 +128,37 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T $hasOptions = $this->hasOptions($flagsType); $options = $hasOptions->yes() ? $this->getOptions($flagsType, $filterValue) : []; - $defaultType = $options['default'] ?? ($this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType) - ? new NullType() - : new ConstantBooleanType(false)); + if (isset($options['default'])) { + $defaultType = $options['default']; + } else { + $hasNullOnFailure = $this->hasFlag('FILTER_NULL_ON_FAILURE', $flagsType); + if ($hasNullOnFailure->yes()) { + $defaultType = new NullType(); + } elseif ($hasNullOnFailure->no()) { + $defaultType = new ConstantBooleanType(false); + } else { + $defaultType = new UnionType([new ConstantBooleanType(false), new NullType()]); + } + } - $inputIsArray = $inputType->isArray(); $hasRequireArrayFlag = $this->hasFlag('FILTER_REQUIRE_ARRAY', $flagsType); - if ($inputIsArray->no() && $hasRequireArrayFlag) { + if ($hasRequireArrayFlag->maybe()) { + // Too complicated + return $mixedType; + } + + $inputIsArray = $inputType->isArray(); + if ($inputIsArray->no() && $hasRequireArrayFlag->yes()) { return $defaultType; } $hasForceArrayFlag = $this->hasFlag('FILTER_FORCE_ARRAY', $flagsType); - if ($inputIsArray->yes() && ($hasRequireArrayFlag || $hasForceArrayFlag)) { + if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->maybe()) { + // Too complicated + return $mixedType; + } + + if ($inputIsArray->yes() && ($hasRequireArrayFlag->yes() || $hasForceArrayFlag->yes())) { $inputArrayKeyType = $inputType->getIterableKeyType(); $inputType = $inputType->getIterableValueType(); } @@ -147,9 +172,11 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T $type = $exactType ?? $this->getFilterTypeMap()[$filterValue] ?? $mixedType; $type = $this->applyRangeOptions($type, $options, $defaultType); - if ($inputType->isNonEmptyString()->yes() + if ( + $inputType->isNonEmptyString()->yes() && $type->isString()->yes() - && !$this->canStringBeSanitized($filterValue, $flagsType)) { + && $this->canStringBeSanitized($filterValue, $flagsType)->no() + ) { $accessory = new AccessoryNonEmptyStringType(); if ($inputType->isNonFalsyString()->yes()) { $accessory = new AccessoryNonFalsyStringType(); @@ -163,14 +190,14 @@ public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): T } } - if ($hasRequireArrayFlag) { + if ($hasRequireArrayFlag->yes()) { $type = new ArrayType($inputArrayKeyType ?? $mixedType, $type); if (!$inputIsArray->yes()) { $type = TypeCombinator::union($type, $defaultType); } } - if (!$hasRequireArrayFlag && $hasForceArrayFlag) { + if ($hasRequireArrayFlag->no() && $hasForceArrayFlag->yes()) { return new ArrayType($inputArrayKeyType ?? $mixedType, $type); } @@ -329,16 +356,19 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp } if ($in instanceof ConstantStringType) { - $value = $in->getValue(); $allowOctal = $this->hasFlag('FILTER_FLAG_ALLOW_OCTAL', $flagsType); $allowHex = $this->hasFlag('FILTER_FLAG_ALLOW_HEX', $flagsType); + if ($allowOctal->maybe() || $allowHex->maybe()) { + return null; + } - if ($allowOctal && preg_match('/\A0[oO][0-7]+\z/', $value) === 1) { + $value = $in->getValue(); + if ($allowOctal->yes() && preg_match('/\A0[oO][0-7]+\z/', $value) === 1) { $octalValue = octdec($value); return is_int($octalValue) ? new ConstantIntegerType($octalValue) : $defaultType; } - if ($allowHex && preg_match('/\A0[xX][0-9A-Fa-f]+\z/', $value) === 1) { + if ($allowHex->yes() && preg_match('/\A0[xX][0-9A-Fa-f]+\z/', $value) === 1) { $hexValue = hexdec($value); return is_int($hexValue) ? new ConstantIntegerType($hexValue) : $defaultType; } @@ -348,7 +378,7 @@ private function determineExactType(Type $in, int $filterValue, Type $defaultTyp } if ($filterValue === $this->getConstant('FILTER_DEFAULT')) { - if (!$this->canStringBeSanitized($filterValue, $flagsType) && $in->isString()->yes()) { + if ($this->canStringBeSanitized($filterValue, $flagsType)->no() && $in->isString()->yes()) { return $in; } @@ -443,20 +473,23 @@ private function getOptions(Type $flagsType, int $filterValue): array /** * @param non-empty-string $flagName */ - private function hasFlag(string $flagName, ?Type $flagsType): bool + private function hasFlag(string $flagName, ?Type $flagsType): TrinaryLogic { $flag = $this->getConstant($flagName); if ($flag === null) { - return false; + return TrinaryLogic::createNo(); } - if ($flagsType === null) { - return false; + if ($flagsType === null) { // Will default to 0 + return TrinaryLogic::createNo(); } $type = $this->getFlagsValue($flagsType); + if (!$type instanceof ConstantIntegerType) { + return TrinaryLogic::createMaybe(); + } - return $type instanceof ConstantIntegerType && ($type->getValue() & $flag) === $flag; + return TrinaryLogic::createFromBoolean(($type->getValue() & $flag) === $flag); } private function getFlagsValue(Type $exprType): Type @@ -465,25 +498,36 @@ private function getFlagsValue(Type $exprType): Type return $exprType; } - return $exprType->getOffsetValueType($this->flagsString); + $hasOffsetValue = $exprType->hasOffsetValueType($this->flagsString); + if ($hasOffsetValue->no()) { + return new ConstantIntegerType(0); + } + if ($hasOffsetValue->yes()) { + return $exprType->getOffsetValueType($this->flagsString); + } + + return TypeCombinator::union( + new ConstantIntegerType(0), + $exprType->getOffsetValueType($this->flagsString), + ); } - private function canStringBeSanitized(int $filterValue, ?Type $flagsType): bool + 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) { - return false; + return TrinaryLogic::createNo(); } // 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) - || $this->hasFlag('FILTER_FLAG_STRIP_HIGH', $flagsType) - || $this->hasFlag('FILTER_FLAG_STRIP_BACKTICK', $flagsType); + ->or($this->hasFlag('FILTER_FLAG_STRIP_HIGH', $flagsType)) + ->or($this->hasFlag('FILTER_FLAG_STRIP_BACKTICK', $flagsType)); } - return true; + return TrinaryLogic::createYes(); } } diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 22f21b50a2..9be65a5493 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -7354,8 +7354,8 @@ public static function dataFilterVar(): Generator $typeAndFlags = [ ['%s|false', ''], - ['%s|false', ', $mixed'], - ['%s|false', ', ["flags" => $mixed]'], + ['mixed', ', $mixed'], + ['mixed', ', ["flags" => $mixed]'], ['%s|null', ', FILTER_NULL_ON_FAILURE'], ['%s|null', ', ["flags" => FILTER_NULL_ON_FAILURE]'], ['%s|null', ', ["flags" => FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4]'], @@ -7384,8 +7384,8 @@ public static function dataFilterVar(): Generator $boolFlags = [ ['bool', ''], - ['bool', ', $mixed'], - ['bool', ', ["flags" => $mixed]'], + ['mixed', ', $mixed'], + ['mixed', ', ["flags" => $mixed]'], ['bool|null', ', FILTER_NULL_ON_FAILURE'], ['bool|null', ', ["flags" => FILTER_NULL_ON_FAILURE]'], ['bool|null', ', ["flags" => FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4]'], diff --git a/tests/PHPStan/Analyser/nsrt/filter-var.php b/tests/PHPStan/Analyser/nsrt/filter-var.php index d677b0de9c..1935c117aa 100644 --- a/tests/PHPStan/Analyser/nsrt/filter-var.php +++ b/tests/PHPStan/Analyser/nsrt/filter-var.php @@ -90,7 +90,7 @@ public function invalidInput(array $arr, object $object, $resource): void public function intToInt(int $int, array $options): void { assertType('int', filter_var($int, FILTER_VALIDATE_INT)); - assertType('int|false', filter_var($int, FILTER_VALIDATE_INT, $options)); + assertType('mixed', filter_var($int, FILTER_VALIDATE_INT, $options)); assertType('int<0, max>|false', filter_var($int, FILTER_VALIDATE_INT, ['options' => ['min_range' => 0]])); } diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 75529afc48..9971f74981 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -1009,6 +1009,11 @@ public function testBug11019(): void $this->analyse([__DIR__ . '/data/bug-11019.php'], []); } + public function testBug11485(): void + { + $this->analyse([__DIR__ . '/data/bug-11485.php'], []); + } + public function testBug12946(): void { $this->analyse([__DIR__ . '/data/bug-12946.php'], []); diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11485.php b/tests/PHPStan/Rules/Comparison/data/bug-11485.php new file mode 100644 index 0000000000..93d550d422 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11485.php @@ -0,0 +1,18 @@ +