diff --git a/build/composer-require-checker.json b/build/composer-require-checker.json index bff20de50f..879f93aa40 100644 --- a/build/composer-require-checker.json +++ b/build/composer-require-checker.json @@ -4,7 +4,11 @@ "static", "self", "parent", "array", "string", "int", "float", "bool", "iterable", "callable", "void", "object", "PHPUnit\\Framework\\TestCase", "PHPUnit\\Framework\\AssertionFailedError", - "JSON_THROW_ON_ERROR", "SimpleXMLElement", "PHPStan\\ExtensionInstaller\\GeneratedConfig", "Nette\\DI\\InvalidConfigurationException" + "JSON_THROW_ON_ERROR", "SimpleXMLElement", "PHPStan\\ExtensionInstaller\\GeneratedConfig", "Nette\\DI\\InvalidConfigurationException", + "FILTER_SANITIZE_EMAIL", "FILTER_SANITIZE_EMAIL", "FILTER_SANITIZE_ENCODED", "FILTER_SANITIZE_MAGIC_QUOTES", "FILTER_SANITIZE_NUMBER_FLOAT", + "FILTER_SANITIZE_NUMBER_INT", "FILTER_SANITIZE_SPECIAL_CHARS", "FILTER_SANITIZE_STRING", "FILTER_SANITIZE_URL", "FILTER_VALIDATE_BOOLEAN", + "FILTER_VALIDATE_EMAIL", "FILTER_VALIDATE_FLOAT", "FILTER_VALIDATE_INT", "FILTER_VALIDATE_IP", "FILTER_VALIDATE_MAC", "FILTER_VALIDATE_REGEXP", + "FILTER_VALIDATE_URL", "FILTER_NULL_ON_FAILURE", "FILTER_FORCE_ARRAY" ], "php-core-extensions" : [ "Core", diff --git a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php index b6b14ccee4..e58ed9f7da 100644 --- a/src/Type/Php/FilterVarDynamicReturnTypeExtension.php +++ b/src/Type/Php/FilterVarDynamicReturnTypeExtension.php @@ -2,16 +2,21 @@ namespace PHPStan\Type\Php; -use PhpParser\Node\Expr\ConstFetch; +use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; +use PHPStan\Type\NullType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; @@ -19,39 +24,73 @@ class FilterVarDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { - /** @var array */ + /** @var array */ private $filterTypesHashMaps; + /** @var array */ + private $nullableTypes; + + /** @var ConstantStringType */ + private $flagsString; + public function __construct() { + if (!defined('FILTER_SANITIZE_EMAIL')) { + return; + } + $booleanType = new BooleanType(); - $floatOrFalseType = new UnionType([new FloatType(), new ConstantBooleanType(false)]); - $intOrFalseType = new UnionType([new IntegerType(), new ConstantBooleanType(false)]); - $stringOrFalseType = new UnionType([new StringType(), new ConstantBooleanType(false)]); + $floatType = new FloatType(); + $intType = new IntegerType(); + $stringType = new StringType(); + + $nullType = new NullType(); + $falseType = new ConstantBooleanType(false); + + $nullableBooleanType = new UnionType([$booleanType, $nullType]); + $floatOrFalseType = new UnionType([$floatType, $falseType]); + $nullableFloatType = new UnionType([$floatType, $nullType]); + $intOrFalseType = new UnionType([$intType, $falseType]); + $nullableIntType = new UnionType([$intType, $nullType]); + $stringOrFalseType = new UnionType([$stringType, $falseType]); + $nullableStringType = new UnionType([$stringType, $nullType]); $this->filterTypesHashMaps = [ - 'FILTER_SANITIZE_EMAIL' => $stringOrFalseType, - 'FILTER_SANITIZE_ENCODED' => $stringOrFalseType, - 'FILTER_SANITIZE_MAGIC_QUOTES' => $stringOrFalseType, - 'FILTER_SANITIZE_NUMBER_FLOAT' => $stringOrFalseType, - 'FILTER_SANITIZE_NUMBER_INT' => $stringOrFalseType, - 'FILTER_SANITIZE_SPECIAL_CHARS' => $stringOrFalseType, - 'FILTER_SANITIZE_STRING' => $stringOrFalseType, - 'FILTER_SANITIZE_URL' => $stringOrFalseType, - 'FILTER_VALIDATE_BOOLEAN' => $booleanType, - 'FILTER_VALIDATE_EMAIL' => $stringOrFalseType, - 'FILTER_VALIDATE_FLOAT' => $floatOrFalseType, - 'FILTER_VALIDATE_INT' => $intOrFalseType, - 'FILTER_VALIDATE_IP' => $stringOrFalseType, - 'FILTER_VALIDATE_MAC' => $stringOrFalseType, - 'FILTER_VALIDATE_REGEXP' => $stringOrFalseType, - 'FILTER_VALIDATE_URL' => $stringOrFalseType, + FILTER_SANITIZE_EMAIL => $stringOrFalseType, + FILTER_SANITIZE_ENCODED => $stringOrFalseType, + FILTER_SANITIZE_MAGIC_QUOTES => $stringOrFalseType, + FILTER_SANITIZE_NUMBER_FLOAT => $stringOrFalseType, + FILTER_SANITIZE_NUMBER_INT => $stringOrFalseType, + FILTER_SANITIZE_SPECIAL_CHARS => $stringOrFalseType, + FILTER_SANITIZE_STRING => $stringOrFalseType, + FILTER_SANITIZE_URL => $stringOrFalseType, + FILTER_VALIDATE_BOOLEAN => $booleanType, + FILTER_VALIDATE_EMAIL => $stringOrFalseType, + FILTER_VALIDATE_FLOAT => $floatOrFalseType, + FILTER_VALIDATE_INT => $intOrFalseType, + FILTER_VALIDATE_IP => $stringOrFalseType, + FILTER_VALIDATE_MAC => $stringOrFalseType, + FILTER_VALIDATE_REGEXP => $stringOrFalseType, + FILTER_VALIDATE_URL => $stringOrFalseType, + ]; + + $this->nullableTypes = [ + FILTER_VALIDATE_BOOLEAN => $nullableBooleanType, + FILTER_VALIDATE_EMAIL => $nullableStringType, + FILTER_VALIDATE_FLOAT => $nullableFloatType, + FILTER_VALIDATE_INT => $nullableIntType, + FILTER_VALIDATE_IP => $nullableStringType, + FILTER_VALIDATE_MAC => $nullableStringType, + FILTER_VALIDATE_REGEXP => $nullableStringType, + FILTER_VALIDATE_URL => $nullableStringType, ]; + + $this->flagsString = new ConstantStringType('flags'); } public function isFunctionSupported(FunctionReflection $functionReflection): bool { - return strtolower($functionReflection->getName()) === 'filter_var'; + return defined('FILTER_SANITIZE_EMAIL') && strtolower($functionReflection->getName()) === 'filter_var'; } public function getTypeFromFunctionCall( @@ -67,14 +106,59 @@ public function getTypeFromFunctionCall( return $mixedType; } - $filterExpr = $filterArg->value; - if (!$filterExpr instanceof ConstFetch) { + $filterType = $scope->getType($filterArg->value); + if (!$filterType instanceof ConstantIntegerType) { return $mixedType; } - $filterName = (string) $filterExpr->name; + $filterValue = $filterType->getValue(); + + $flagsArg = $functionCall->args[2] ?? null; + if ($this->isNullableType($filterValue, $flagsArg, $scope)) { + $type = $this->nullableTypes[$filterValue]; + } else { + $type = $this->filterTypesHashMaps[$filterValue] ?? $mixedType; + } + + if ($this->isForcedArrayType($flagsArg, $scope)) { + return new ArrayType(new MixedType(), $type); + } + + return $type; + } + + private function isNullableType(int $filterValue, ?Node\Arg $flagsArg, Scope $scope): bool + { + if ($flagsArg === null || !array_key_exists($filterValue, $this->nullableTypes)) { + return false; + } + + return $this->hasFlag(FILTER_NULL_ON_FAILURE, $flagsArg, $scope); + } + + private function isForcedArrayType(?Node\Arg $flagsArg, Scope $scope): bool + { + if ($flagsArg === null) { + return false; + } + + return $this->hasFlag(FILTER_FORCE_ARRAY, $flagsArg, $scope); + } + + private function hasFlag(int $flag, Node\Arg $expression, Scope $scope): bool + { + $type = $this->getFlagsValue($scope->getType($expression->value)); + + return $type instanceof ConstantIntegerType && ($type->getValue() & $flag) === $flag; + } + + private function getFlagsValue(Type $exprType): Type + { + if (!$exprType instanceof ConstantArrayType) { + return $exprType; + } - return $this->filterTypesHashMaps[$filterName] ?? $mixedType; + return $exprType->getOffsetValueType($this->flagsString); } } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 0126a9bd01..3e21f8c29e 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -8006,6 +8006,10 @@ public function dataFilterVar(): array 'string|false', 'filter_var($mixed, FILTER_SANITIZE_EMAIL)', ], + [ + 'array', + 'filter_var($mixed, FILTER_SANITIZE_EMAIL, FILTER_FORCE_ARRAY)', + ], [ 'string|false', 'filter_var($mixed, FILTER_SANITIZE_ENCODED)', @@ -8038,34 +8042,151 @@ public function dataFilterVar(): array 'bool', 'filter_var($mixed, FILTER_VALIDATE_BOOLEAN)', ], + [ + 'bool|null', + 'filter_var($mixed, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)', + ], + [ + 'bool|null', + 'filter_var($mixed, FILTER_VALIDATE_BOOLEAN ,["flags" => FILTER_NULL_ON_FAILURE])', + ], [ 'string|false', 'filter_var($mixed, FILTER_VALIDATE_EMAIL)', ], + [ + 'string|null', + 'filter_var($mixed, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE)', + ], + [ + 'string|null', + 'filter_var($mixed, FILTER_VALIDATE_EMAIL ,["flags" => FILTER_NULL_ON_FAILURE])', + ], [ 'float|false', 'filter_var($mixed, FILTER_VALIDATE_FLOAT)', ], + [ + 'float|null', + 'filter_var($mixed, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE)', + ], + + [ + 'array', + 'filter_var($mixed, FILTER_VALIDATE_FLOAT, FILTER_FORCE_ARRAY)', + ], + [ + 'array', + 'filter_var($mixed, FILTER_VALIDATE_FLOAT, FILTER_FORCE_ARRAY | FILTER_NULL_ON_FAILURE)', + ], + [ + 'array', + 'filter_var($mixed, FILTER_VALIDATE_FLOAT, $forceArrayFilter | $nullFilter)', + ], + [ + 'float|null', + 'filter_var($mixed, FILTER_VALIDATE_FLOAT ,["flags" => FILTER_NULL_ON_FAILURE])', + ], [ 'int|false', 'filter_var($mixed, FILTER_VALIDATE_INT)', ], + [ + 'int|null', + 'filter_var($mixed, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE)', + ], + [ + 'int|null', + 'filter_var($mixed, FILTER_VALIDATE_INT ,["flags" => FILTER_NULL_ON_FAILURE])', + ], [ 'string|false', 'filter_var($mixed, FILTER_VALIDATE_IP)', ], + [ + 'string|false', + 'filter_var($mixed, $filterIp)', + ], + [ + 'string|false', + 'filter_var($mixed, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)', + ], + [ + 'string|null', + 'filter_var($mixed, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE)', + ], + [ + 'string|null', + 'filter_var($mixed, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4)', + ], + [ + 'array', + 'filter_var($mixed, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4 | FILTER_FORCE_ARRAY)', + ], + [ + 'string|null', + 'filter_var($mixed, FILTER_VALIDATE_IP ,["flags" => FILTER_NULL_ON_FAILURE])', + ], [ 'string|false', 'filter_var($mixed, FILTER_VALIDATE_MAC)', ], + [ + 'string|null', + 'filter_var($mixed, FILTER_VALIDATE_MAC, FILTER_NULL_ON_FAILURE)', + ], + [ + 'string|null', + 'filter_var($mixed, FILTER_VALIDATE_MAC ,["flags" => FILTER_NULL_ON_FAILURE])', + ], [ 'string|false', 'filter_var($mixed, FILTER_VALIDATE_REGEXP)', ], + [ + 'string|false', + 'filter_var($mixed, FILTER_VALIDATE_REGEXP, ["options" => ["regexp" => "/match/"]])', + ], + [ + 'string|null', + 'filter_var($mixed, FILTER_VALIDATE_REGEXP, FILTER_NULL_ON_FAILURE)', + ], + [ + 'string|null', + 'filter_var($mixed, FILTER_VALIDATE_REGEXP ,["flags" => FILTER_NULL_ON_FAILURE])', + ], + [ + 'string|null', + 'filter_var($mixed, FILTER_VALIDATE_REGEXP ,["flags" => FILTER_NULL_ON_FAILURE, "options" => ["regexp" => "/match/"]])', + ], [ 'string|false', 'filter_var($mixed, FILTER_VALIDATE_URL)', ], + [ + 'string|false', + 'filter_var($mixed, FILTER_VALIDATE_URL, $mixed)', + ], + [ + 'string|false', + 'filter_var($mixed, FILTER_VALIDATE_URL ,["flags" => $mixed])', + ], + [ + 'string|null', + 'filter_var($mixed, FILTER_VALIDATE_URL, FILTER_NULL_ON_FAILURE)', + ], + [ + 'string|null', + 'filter_var($mixed, FILTER_VALIDATE_URL, $nullFilter)', + ], + [ + 'string|null', + 'filter_var($mixed, FILTER_VALIDATE_URL ,["flags" => FILTER_NULL_ON_FAILURE])', + ], + [ + 'string|null', + 'filter_var($mixed, FILTER_VALIDATE_URL ,["flags" => $nullFilter])', + ], ]; } diff --git a/tests/PHPStan/Analyser/data/filterVar.php b/tests/PHPStan/Analyser/data/filterVar.php index 70317ea60a..a66c0fe1f5 100644 --- a/tests/PHPStan/Analyser/data/filterVar.php +++ b/tests/PHPStan/Analyser/data/filterVar.php @@ -5,6 +5,9 @@ function () { /** @var mixed $mixed */ $mixed = null; + $nullFilter = \FILTER_NULL_ON_FAILURE; + $forceArrayFilter = \FILTER_FORCE_ARRAY; + $filterIp = FILTER_VALIDATE_IP; die; };