diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 0eabf31bdb..2fedc486d8 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -75,6 +75,18 @@ public function getTypeFromFunctionCall(Type $inputType, ?Type $filterType, ?Typ ? new NullType() : new ConstantBooleanType(false)); + $inputIsArray = $inputType->isArray(); + $hasRequireArrayFlag = $this->hasFlag($this->getConstant('FILTER_REQUIRE_ARRAY'), $flagsType); + if ($inputIsArray->no() && $hasRequireArrayFlag) { + return $defaultType; + } + + $hasForceArrayFlag = $this->hasFlag($this->getConstant('FILTER_FORCE_ARRAY'), $flagsType); + if ($inputIsArray->yes() && ($hasRequireArrayFlag || $hasForceArrayFlag)) { + $inputArrayKeyType = $inputType->getIterableKeyType(); + $inputType = $inputType->getIterableValueType(); + } + if ($inputType->isScalar()->no() && $inputType->isNull()->no()) { return $defaultType; } @@ -93,14 +105,18 @@ public function getTypeFromFunctionCall(Type $inputType, ?Type $filterType, ?Typ $type = TypeCombinator::intersect($type, $accessory); } + if ($hasRequireArrayFlag) { + $type = new ArrayType($inputArrayKeyType ?? $mixedType, $type); + } + if ($exactType === null || $hasOptions->maybe() || (!$inputType->equals($type) && $inputType->isSuperTypeOf($type)->yes())) { if ($defaultType->isSuperTypeOf($type)->no()) { $type = TypeCombinator::union($type, $defaultType); } } - if ($this->hasFlag($this->getConstant('FILTER_FORCE_ARRAY'), $flagsType)) { - return new ArrayType(new MixedType(), $type); + if (!$hasRequireArrayFlag && $hasForceArrayFlag) { + return new ArrayType($inputArrayKeyType ?? $mixedType, $type); } return $type; diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index e2319e9f3b..2dfae94f2d 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1168,6 +1168,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-8485.php'); } yield from $this->gatherAssertTypes(__DIR__ . '/data/discussion-8447.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/discussion-9134.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7805.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-82.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4565.php'); diff --git a/tests/PHPStan/Analyser/data/discussion-9134.php b/tests/PHPStan/Analyser/data/discussion-9134.php new file mode 100644 index 0000000000..330b51cbaa --- /dev/null +++ b/tests/PHPStan/Analyser/data/discussion-9134.php @@ -0,0 +1,12 @@ +|false', $res); +if (is_array($res) === false) { + throw new \RuntimeException(); +} diff --git a/tests/PHPStan/Analyser/data/filter-var.php b/tests/PHPStan/Analyser/data/filter-var.php index cdef9c09da..12f97e63b5 100644 --- a/tests/PHPStan/Analyser/data/filter-var.php +++ b/tests/PHPStan/Analyser/data/filter-var.php @@ -8,12 +8,44 @@ class FilterVar { - public function doFoo($mixed): void + /** + * @param array $stringMixedMap + */ + public function doFoo($mixed, array $stringMixedMap): void { assertType('int|false', filter_var($mixed, FILTER_VALIDATE_INT)); assertType('int|null', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_NULL_ON_FAILURE])); - assertType('array', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); + + assertType('17', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_SCALAR])); + assertType('false', filter_var([17], FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_SCALAR])); + + assertType('false', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY])); + assertType('null', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('false', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY])); + assertType('null', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array|false', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY])); + assertType('array|null', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array|false', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY])); + assertType('array|null', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_NULL_ON_FAILURE])); + + assertType('array<17>', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); + assertType('array<17>', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); + assertType('array', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); assertType('array', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); + assertType('array', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + + assertType('false', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY])); + assertType('null', filter_var(17, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('false', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY])); + assertType('null', filter_var('foo', FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array|false', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY])); + assertType('array|null', filter_var($mixed, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('array|false', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY])); + assertType('array|null', filter_var($stringMixedMap, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY|FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('0|int<17, 19>', filter_var($mixed, FILTER_VALIDATE_INT, ['options' => ['default' => 0, 'min_range' => 17, 'max_range' => 19]])); assertType('array', filter_var(false, FILTER_VALIDATE_BOOLEAN, FILTER_FORCE_ARRAY | FILTER_NULL_ON_FAILURE)); diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 57087652fb..601c87836b 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -724,6 +724,13 @@ public function testBug8752(): void $this->analyse([__DIR__ . '/../../Analyser/data/bug-8752.php'], []); } + public function testDiscussion9134(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/data/discussion-9134.php'], []); + } + public function testImpossibleMethodExistOnGenericClassString(): void { $this->checkAlwaysTrueCheckTypeFunctionCall = true;