From 30070ebff1dd5b9ab034664406638491a4db3aed Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Sat, 25 Feb 2023 22:09:33 +0100 Subject: [PATCH] fix, improve and simplify --- .../Php/FilterFunctionReturnTypeHelper.php | 25 +++++-- .../FilterInputDynamicReturnTypeExtension.php | 69 +++++++------------ .../FilterVarDynamicReturnTypeExtension.php | 2 +- tests/PHPStan/Analyser/data/filter-input.php | 7 +- 4 files changed, 47 insertions(+), 56 deletions(-) diff --git a/src/Type/Php/FilterFunctionReturnTypeHelper.php b/src/Type/Php/FilterFunctionReturnTypeHelper.php index f0fc287e076..d22620cafad 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 getArrayOffsetValueType(Type $arrayType, Type $offsetType, ?Type $filterType, ?Type $flagsType): ?Type + { + $inexistentOffsetType = $this->hasFlag($this->getConstant('FILTER_NULL_ON_FAILURE'), $flagsType) + ? new ConstantBooleanType(false) + : new NullType(); + + $arrayTypeHasOffsetValueType = $arrayType->hasOffsetValueType($offsetType); + if ($arrayTypeHasOffsetValueType->no()) { + return $inexistentOffsetType; + } + + $filteredType = $this->getType($arrayType->getOffsetValueType($offsetType), $filterType, $flagsType); + + return $arrayTypeHasOffsetValueType->maybe() + ? TypeCombinator::union($filteredType, $inexistentOffsetType) + : $filteredType; + } + + public function getType(Type $inputType, ?Type $filterType, ?Type $flagsType): Type { $mixedType = new MixedType(); @@ -106,11 +124,6 @@ public function getTypeFromFunctionCall(Type $inputType, ?Type $filterType, ?Typ return $type; } - public function hasConstantFlag(string $constant, ?Type $flagsType): bool - { - return $this->hasFlag($this->getConstant($constant), $flagsType); - } - /** * @return array */ diff --git a/src/Type/Php/FilterInputDynamicReturnTypeExtension.php b/src/Type/Php/FilterInputDynamicReturnTypeExtension.php index 07b81a138c7..6db2718d0aa 100644 --- a/src/Type/Php/FilterInputDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterInputDynamicReturnTypeExtension.php @@ -2,30 +2,22 @@ namespace PHPStan\Type\Php; -use PhpParser\Node\Arg; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\ArrayType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\NullType; +use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; +use PHPStan\Type\StringType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use function count; +use function in_array; class FilterInputDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - private const INPUT_TYPE_VARIABLE_NAME_MAP = [ - 'INPUT_GET' => '_GET', - 'INPUT_POST' => '_POST', - 'INPUT_COOKIE' => '_COOKIE', - 'INPUT_SERVER' => '_SERVER', - 'INPUT_ENV' => '_ENV', - ]; - public function __construct(private FilterFunctionReturnTypeHelper $filterFunctionReturnTypeHelper) { } @@ -41,46 +33,31 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return null; } - $inputVariable = $this->determineInputVariable($functionCall->getArgs()[0]); + $typeArgExpr = $functionCall->getArgs()[0]->value; $varNameType = $scope->getType($functionCall->getArgs()[1]->value); - if ($inputVariable === null || !$varNameType->isString()->yes()) { - return null; - } - - $inputVariableType = $scope->getType($inputVariable); - $inputHasOffsetValue = $inputVariableType->hasOffsetValueType($varNameType); - $flagsType = isset($functionCall->getArgs()[3]) ? $scope->getType($functionCall->getArgs()[3]->value) : null; - if ($inputHasOffsetValue->no()) { - return $this->determineNonExistentOffsetReturnType($flagsType); + $varNameTypeIsString = $varNameType->isString(); + if ( + $varNameTypeIsString->no() + || !($typeArgExpr instanceof ConstFetch) + || !in_array((string) $typeArgExpr->name, ['INPUT_GET', 'INPUT_POST', 'INPUT_COOKIE', 'INPUT_SERVER', 'INPUT_ENV'], true) + ) { + return new NeverType(); } - $filterType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null; - $filteredType = $this->filterFunctionReturnTypeHelper->getTypeFromFunctionCall( - $inputVariableType->getOffsetValueType($varNameType), - $filterType, - $flagsType, - ); - - return !$inputVariableType->hasOffsetValueType($varNameType)->yes() - ? TypeCombinator::union($filteredType, $this->determineNonExistentOffsetReturnType($flagsType)) - : $filteredType; - } - - private function determineInputVariable(Arg $type): ?Variable - { - if (!$type->value instanceof ConstFetch) { + if ($varNameTypeIsString->maybe()) { return null; } - $variableName = self::INPUT_TYPE_VARIABLE_NAME_MAP[(string) $type->value->name] ?? null; - return $variableName === null ? null : new Variable($variableName); - } + // 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()); - private function determineNonExistentOffsetReturnType(?Type $flagsType): Type - { - return $flagsType === null || !$this->filterFunctionReturnTypeHelper->hasConstantFlag('FILTER_NULL_ON_FAILURE', $flagsType) - ? new NullType() - : new ConstantBooleanType(false); + return $this->filterFunctionReturnTypeHelper->getArrayOffsetValueType( + $inputType, + $varNameType, + 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 3042f5b23e0..438d6440e97 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/data/filter-input.php b/tests/PHPStan/Analyser/data/filter-input.php index 3ba7716b684..c44c3364299 100644 --- a/tests/PHPStan/Analyser/data/filter-input.php +++ b/tests/PHPStan/Analyser/data/filter-input.php @@ -7,11 +7,11 @@ class FilterInput { - public function invalidTypesOrVarNamesAreIgnored($mixed): void + public function invalidTypesOrVarNames($mixed): void { assertType('mixed', filter_input(INPUT_GET, $mixed, FILTER_VALIDATE_INT)); - assertType('mixed', filter_input(INPUT_GET, 17, FILTER_VALIDATE_INT)); - assertType('mixed', filter_input(-1, 'foo', FILTER_VALIDATE_INT)); + assertType('*NEVER*', filter_input(INPUT_GET, 17, FILTER_VALIDATE_INT)); + assertType('*NEVER*', filter_input(-1, 'foo', FILTER_VALIDATE_INT)); } public function supportedSuperGlobals(): void @@ -28,6 +28,7 @@ public function doFoo(string $foo): void assertType('int|false|null', filter_input(INPUT_GET, $foo, FILTER_VALIDATE_INT)); assertType('int|false|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT)); assertType('int|false|null', filter_input(INPUT_GET, 'foo', FILTER_VALIDATE_INT, ['flags' => 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]]));