diff --git a/conf/config.neon b/conf/config.neon index 5676c7aba3..805e64dbee 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1456,6 +1456,11 @@ services: - class: PHPStan\Type\Php\FilterFunctionReturnTypeHelper + - + class: PHPStan\Type\Php\FilterInputDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\FilterVarDynamicReturnTypeExtension tags: diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index 2fedc486d8..2a9f6bc977 100644 --- a/src/Type/Php/FilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/FilterFunctionReturnTypeHelper.php @@ -51,7 +51,25 @@ public function __construct(private ReflectionProvider $reflectionProvider) $this->flagsString = new ConstantStringType('flags'); } - public function getTypeFromFunctionCall(Type $inputType, ?Type $filterType, ?Type $flagsType): Type + public function getOffsetValueType(Type $inputType, Type $offsetType, ?Type $filterType, ?Type $flagsType): ?Type + { + $inexistentOffsetType = $this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsType) + ? new ConstantBooleanType(false) + : new NullType(); + + $hasOffsetValueType = $inputType->hasOffsetValueType($offsetType); + if ($hasOffsetValueType->no()) { + return $inexistentOffsetType; + } + + $filteredType = $this->getType($inputType->getOffsetValueType($offsetType), $filterType, $flagsType); + + return $hasOffsetValueType->maybe() + ? TypeCombinator::union($filteredType, $inexistentOffsetType) + : $filteredType; + } + + public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): Type { $mixedType = new MixedType(); diff --git a/src/Type/Php/FilterInputDynamicReturnTypeExtension.php b/src/Type/Php/FilterInputDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..cd16600475 --- /dev/null +++ b/src/Type/Php/FilterInputDynamicReturnTypeExtension.php @@ -0,0 +1,60 @@ +getName() === 'filter_input'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + $supportedTypes = TypeCombinator::union( + $this->reflectionProvider->getConstant(new Node\Name('INPUT_GET'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_POST'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_COOKIE'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_SERVER'), null)->getValueType(), + $this->reflectionProvider->getConstant(new Node\Name('INPUT_ENV'), null)->getValueType(), + ); + $typeType = $scope->getType($functionCall->getArgs()[0]->value); + if (!$typeType->isInteger()->yes() || $supportedTypes->isSuperTypeOf($typeType)->no()) { + return null; + } + + // Pragmatical solution since global expressions are not passed through the scope for performance reasons + // See https://github.com/phpstan/phpstan-src/pull/2012 for details + $inputType = new ArrayType(new StringType(), new MixedType()); + + return $this->filterFunctionReturnTypeHelper->getOffsetValueType( + $inputType, + $scope->getType($functionCall->getArgs()[1]->value), + isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null, + isset($functionCall->getArgs()[3]) ? $scope->getType($functionCall->getArgs()[3]->value) : null, + ); + } + +} diff --git a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php index 3042f5b23e..438d6440e9 100644 --- a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php @@ -32,7 +32,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $filterType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : null; $flagsType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null; - return $this->filterFunctionReturnTypeHelper->getTypeFromFunctionCall($inputType, $filterType, $flagsType); + return $this->filterFunctionReturnTypeHelper->getType($inputType, $filterType, $flagsType); } } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index cd934fa83d..bfdf68b759 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -623,6 +623,7 @@ public function dataFileAsserts(): iterable } yield from $this->gatherAssertTypes(__DIR__ . '/data/filesystem-functions.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-input.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-var.php'); if (PHP_VERSION_ID >= 80100) { diff --git a/tests/PHPStan/Analyser/data/filter-input.php b/tests/PHPStan/Analyser/data/filter-input.php new file mode 100644 index 0000000000..0715c2e3e3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/filter-input.php @@ -0,0 +1,43 @@ + FILTER_NULL_ON_FAILURE])); + assertType("'invalid'|int|null", filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['options' => ['default' => 'invalid']])); + assertType('array|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY])); + assertType('array|false', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => FILTER_FORCE_ARRAY|FILTER_NULL_ON_FAILURE])); + assertType('0|int<17, 19>|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['options' => ['default' => 0, 'min_range' => 17, 'max_range' => 19]])); + } + +} diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 54a8d9a56b..7e86059be6 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -1130,6 +1130,12 @@ public function testBug5474(): void ]); } + public function testBug6261(): void + { + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-6261.php'], []); + } + public function testBug6781(): void { $this->analyse([__DIR__ . '/data/bug-6781.php'], []); diff --git a/tests/PHPStan/Rules/Functions/data/bug-6261.php b/tests/PHPStan/Rules/Functions/data/bug-6261.php new file mode 100644 index 0000000000..055f809075 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-6261.php @@ -0,0 +1,15 @@ +