1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-22 13:51:54 +01:00

filter_input & filter_var return type more specific

This commit is contained in:
kkmuffme 2023-09-18 14:48:31 +02:00
parent b38530ed0d
commit dee555daaf
12 changed files with 2162 additions and 140 deletions

View File

@ -418,6 +418,7 @@
<xs:element name="RedundantCastGivenDocblockType" type="IssueHandlerType" minOccurs="0" />
<xs:element name="RedundantCondition" type="IssueHandlerType" minOccurs="0" />
<xs:element name="RedundantConditionGivenDocblockType" type="IssueHandlerType" minOccurs="0" />
<xs:element name="RedundantFlag" type="IssueHandlerType" minOccurs="0" />
<xs:element name="RedundantFunctionCall" type="IssueHandlerType" minOccurs="0" />
<xs:element name="RedundantFunctionCallGivenDocblockType" type="IssueHandlerType" minOccurs="0" />
<xs:element name="RedundantIdentityWithTrue" type="IssueHandlerType" minOccurs="0" />

View File

@ -2847,11 +2847,11 @@ return [
'FilesystemIterator::setInfoClass' => ['void', 'class='=>'class-string'],
'FilesystemIterator::valid' => ['bool'],
'filetype' => ['string|false', 'filename'=>'string'],
'filter_has_var' => ['bool', 'input_type'=>'int', 'var_name'=>'string'],
'filter_has_var' => ['bool', 'input_type'=>'0|1|2|4|5', 'var_name'=>'string'],
'filter_id' => ['int|false', 'name'=>'string'],
'filter_input' => ['mixed|false', 'type'=>'int', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'],
'filter_input_array' => ['array|false|null', 'type'=>'int', 'options='=>'int|array', 'add_empty='=>'bool'],
'filter_list' => ['array'],
'filter_input' => ['mixed|false|null', 'type'=>'0|1|2|4|5', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'],
'filter_input_array' => ['array|false|null', 'type'=>'0|1|2|4|5', 'options='=>'int|array', 'add_empty='=>'bool'],
'filter_list' => ['non-empty-list<non-falsy-string>'],
'filter_var' => ['mixed|false', 'value'=>'mixed', 'filter='=>'int', 'options='=>'array|int'],
'filter_var_array' => ['array|false|null', 'array'=>'array', 'options='=>'array|int', 'add_empty='=>'bool'],
'FilterIterator::__construct' => ['void', 'iterator'=>'Iterator'],

View File

@ -10434,11 +10434,11 @@ return [
'filepro_rowcount' => ['int'],
'filesize' => ['int|false', 'filename'=>'string'],
'filetype' => ['string|false', 'filename'=>'string'],
'filter_has_var' => ['bool', 'input_type'=>'int', 'var_name'=>'string'],
'filter_has_var' => ['bool', 'input_type'=>'0|1|2|4|5', 'var_name'=>'string'],
'filter_id' => ['int|false', 'name'=>'string'],
'filter_input' => ['mixed|false', 'type'=>'int', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'],
'filter_input_array' => ['array|false|null', 'type'=>'int', 'options='=>'int|array', 'add_empty='=>'bool'],
'filter_list' => ['array'],
'filter_input' => ['mixed|false|null', 'type'=>'0|1|2|4|5', 'var_name'=>'string', 'filter='=>'int', 'options='=>'array|int'],
'filter_input_array' => ['array|false|null', 'type'=>'0|1|2|4|5', 'options='=>'int|array', 'add_empty='=>'bool'],
'filter_list' => ['non-empty-list<non-falsy-string>'],
'filter_var' => ['mixed|false', 'value'=>'mixed', 'filter='=>'int', 'options='=>'array|int'],
'filter_var_array' => ['array|false|null', 'array'=>'array', 'options='=>'array|int', 'add_empty='=>'bool'],
'finfo::__construct' => ['void', 'flags='=>'int', 'magic_database='=>'string'],

View File

@ -220,6 +220,7 @@
- [RedundantCastGivenDocblockType](issues/RedundantCastGivenDocblockType.md)
- [RedundantCondition](issues/RedundantCondition.md)
- [RedundantConditionGivenDocblockType](issues/RedundantConditionGivenDocblockType.md)
- [RedundantFlag](issues/RedundantFlag.md)
- [RedundantFunctionCall](issues/RedundantFunctionCall.md)
- [RedundantFunctionCallGivenDocblockType](issues/RedundantFunctionCallGivenDocblockType.md)
- [RedundantIdentityWithTrue](issues/RedundantIdentityWithTrue.md)

View File

@ -0,0 +1,8 @@
# RedundantFlag
Emitted when a flag is redundant. e.g. FILTER_NULL_ON_FAILURE won't do anything when the default option is specified
```php
<?php
$x = filter_input(INPUT_GET, 'hello', FILTER_VALIDATE_DOMAIN, array('options' => array('default' => 'world.com'), 'flags' => FILTER_NULL_ON_FAILURE));
```

View File

@ -25,6 +25,7 @@ use Psalm\Internal\Provider\ReturnTypeProvider\ArraySpliceReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\BasenameReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\DateReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\DirnameReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\FilterInputReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\FilterVarReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\FirstArgStringReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\GetClassMethodsReturnTypeProvider;
@ -85,6 +86,7 @@ final class FunctionReturnTypeProvider
$this->registerClass(ArrayReverseReturnTypeProvider::class);
$this->registerClass(ArrayFillReturnTypeProvider::class);
$this->registerClass(ArrayFillKeysReturnTypeProvider::class);
$this->registerClass(FilterInputReturnTypeProvider::class);
$this->registerClass(FilterVarReturnTypeProvider::class);
$this->registerClass(IteratorToArrayReturnTypeProvider::class);
$this->registerClass(ParseUrlReturnTypeProvider::class);

View File

@ -0,0 +1,251 @@
<?php
namespace Psalm\Internal\Provider\ReturnTypeProvider;
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\VariableFetchAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Union;
use UnexpectedValueException;
use function array_search;
use function in_array;
use function is_array;
use function is_int;
use const FILTER_CALLBACK;
use const FILTER_DEFAULT;
use const FILTER_FLAG_NONE;
use const FILTER_REQUIRE_ARRAY;
use const FILTER_VALIDATE_REGEXP;
use const INPUT_COOKIE;
use const INPUT_ENV;
use const INPUT_GET;
use const INPUT_POST;
use const INPUT_SERVER;
/**
* @internal
*/
class FilterInputReturnTypeProvider implements FunctionReturnTypeProviderInterface
{
/**
* @return array<lowercase-string>
*/
public static function getFunctionIds(): array
{
return ['filter_input'];
}
public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union
{
$statements_analyzer = $event->getStatementsSource();
if (! $statements_analyzer instanceof StatementsAnalyzer) {
throw new UnexpectedValueException('Expected StatementsAnalyzer not StatementsSource');
}
$call_args = $event->getCallArgs();
$function_id = $event->getFunctionId();
$code_location = $event->getCodeLocation();
$codebase = $statements_analyzer->getCodebase();
if (! isset($call_args[0]) || ! isset($call_args[1])) {
return FilterUtils::missingFirstArg($codebase);
}
$first_arg_type = $statements_analyzer->node_data->getType($call_args[0]->value);
if ($first_arg_type && ! $first_arg_type->isInt()) {
if ($codebase->analysis_php_version_id >= 8_00_00) {
// throws
return Type::getNever();
}
// default option won't be used in this case
return Type::getNull();
}
$filter_int_used = FILTER_DEFAULT;
if (isset($call_args[2])) {
$filter_int_used = FilterUtils::getFilterArgValueOrError(
$call_args[2],
$statements_analyzer,
$codebase,
);
if (!is_int($filter_int_used)) {
return $filter_int_used;
}
}
$options = null;
$flags_int_used = FILTER_FLAG_NONE;
if (isset($call_args[3])) {
$helper = FilterUtils::getOptionsArgValueOrError(
$call_args[3],
$statements_analyzer,
$codebase,
$code_location,
$function_id,
$filter_int_used,
);
if (!is_array($helper)) {
return $helper;
}
$flags_int_used = $helper['flags_int_used'];
$options = $helper['options'];
}
// if we reach this point with callback, the callback is missing
if ($filter_int_used === FILTER_CALLBACK) {
return FilterUtils::missingFilterCallbackCallable(
$function_id,
$code_location,
$statements_analyzer,
$codebase,
);
}
[$default, $min_range, $max_range, $has_range, $regexp] = FilterUtils::getOptions(
$filter_int_used,
$flags_int_used,
$options,
$statements_analyzer,
$code_location,
$codebase,
$function_id,
);
// only return now, as we still want to report errors above
if (!$first_arg_type) {
return null;
}
if (! $first_arg_type->isSingleIntLiteral()) {
// eventually complex cases can be handled too, however practically this is irrelevant
return null;
}
if (!$default) {
[$fails_type, $not_set_type, $fails_or_not_set_type] = FilterUtils::getFailsNotSetType($flags_int_used);
} else {
$fails_type = $default;
$not_set_type = $default;
$fails_or_not_set_type = $default;
}
if ($filter_int_used === FILTER_VALIDATE_REGEXP && $regexp === null) {
if ($codebase->analysis_php_version_id >= 8_00_00) {
// throws
return Type::getNever();
}
// any "array" flags are ignored by this filter!
return $fails_or_not_set_type;
}
$possible_types = array(
'$_GET' => INPUT_GET,
'$_POST' => INPUT_POST,
'$_COOKIE' => INPUT_COOKIE,
'$_SERVER' => INPUT_SERVER,
'$_ENV' => INPUT_ENV,
);
$first_arg_type_type = $first_arg_type->getSingleIntLiteral();
$global_name = array_search($first_arg_type_type->value, $possible_types);
if (!$global_name) {
// invalid
if ($codebase->analysis_php_version_id >= 8_00_00) {
// throws
return Type::getNever();
}
// the "not set type" is never in an array, even if FILTER_FORCE_ARRAY is set!
return $not_set_type;
}
$second_arg_type = $statements_analyzer->node_data->getType($call_args[1]->value);
if (!$second_arg_type) {
return null;
}
if (! $second_arg_type->hasString()) {
// for filter_input there can only be string array keys
return $not_set_type;
}
if (! $second_arg_type->isString()) {
// already reports an error by default
return null;
}
// in all these cases it can fail or be not set, depending on whether the variable is set or not
$redundant_error_return_type = FilterUtils::checkRedundantFlags(
$filter_int_used,
$flags_int_used,
$fails_or_not_set_type,
$statements_analyzer,
$code_location,
$codebase,
);
if ($redundant_error_return_type !== null) {
return $redundant_error_return_type;
}
if (FilterUtils::hasFlag($flags_int_used, FILTER_REQUIRE_ARRAY)
&& in_array($first_arg_type_type->value, array(INPUT_COOKIE, INPUT_SERVER, INPUT_ENV), true)) {
// these globals can never be an array
return $fails_or_not_set_type;
}
// @todo eventually this needs to be changed when we fully support filter_has_var
$global_type = VariableFetchAnalyzer::getGlobalType($global_name, $codebase->analysis_php_version_id);
$input_type = null;
if ($global_type->isArray() && $global_type->getArray() instanceof TKeyedArray) {
$array_instance = $global_type->getArray();
if ($second_arg_type->isSingleStringLiteral()) {
$key = $second_arg_type->getSingleStringLiteral()->value;
if (isset($array_instance->properties[ $key ])) {
$input_type = $array_instance->properties[ $key ];
}
}
if ($input_type === null) {
$input_type = $array_instance->getGenericValueType();
$input_type = $input_type->setPossiblyUndefined(true);
}
} elseif ($global_type->isArray()
&& ($array_atomic = $global_type->getArray())
&& $array_atomic instanceof TArray) {
[$_, $input_type] = $array_atomic->type_params;
$input_type = $input_type->setPossiblyUndefined(true);
} else {
// this is impossible
throw new UnexpectedValueException('This should not happen');
}
return FilterUtils::getReturnType(
$filter_int_used,
$flags_int_used,
$input_type,
$fails_type,
$not_set_type,
$statements_analyzer,
$code_location,
$codebase,
$function_id,
$has_range,
$min_range,
$max_range,
$regexp,
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -3,30 +3,19 @@
namespace Psalm\Internal\Provider\ReturnTypeProvider;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Union;
use UnexpectedValueException;
use function in_array;
use function is_array;
use function is_int;
use const FILTER_NULL_ON_FAILURE;
use const FILTER_SANITIZE_URL;
use const FILTER_VALIDATE_BOOLEAN;
use const FILTER_VALIDATE_DOMAIN;
use const FILTER_VALIDATE_EMAIL;
use const FILTER_VALIDATE_FLOAT;
use const FILTER_VALIDATE_INT;
use const FILTER_VALIDATE_IP;
use const FILTER_VALIDATE_MAC;
use const FILTER_CALLBACK;
use const FILTER_DEFAULT;
use const FILTER_FLAG_NONE;
use const FILTER_VALIDATE_REGEXP;
use const FILTER_VALIDATE_URL;
/**
* @internal
@ -41,135 +30,124 @@ final class FilterVarReturnTypeProvider implements FunctionReturnTypeProviderInt
return ['filter_var'];
}
public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): Union
public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union
{
$statements_source = $event->getStatementsSource();
$call_args = $event->getCallArgs();
$function_id = $event->getFunctionId();
$code_location = $event->getCodeLocation();
if (!$statements_source instanceof StatementsAnalyzer) {
$statements_analyzer = $event->getStatementsSource();
if (!$statements_analyzer instanceof StatementsAnalyzer) {
throw new UnexpectedValueException();
}
$filter_type = null;
$call_args = $event->getCallArgs();
$function_id = $event->getFunctionId();
$code_location = $event->getCodeLocation();
$codebase = $statements_analyzer->getCodebase();
if (isset($call_args[1])
&& ($second_arg_type = $statements_source->node_data->getType($call_args[1]->value))
&& $second_arg_type->isSingleIntLiteral()
) {
$filter_type_type = $second_arg_type->getSingleIntLiteral();
if (! isset($call_args[0])) {
return FilterUtils::missingFirstArg($codebase);
}
switch ($filter_type_type->value) {
case FILTER_VALIDATE_INT:
$filter_type = Type::getInt();
break;
$filter_int_used = FILTER_DEFAULT;
if (isset($call_args[1])) {
$filter_int_used = FilterUtils::getFilterArgValueOrError(
$call_args[1],
$statements_analyzer,
$codebase,
);
case FILTER_VALIDATE_FLOAT:
$filter_type = Type::getFloat();
break;
case FILTER_VALIDATE_BOOLEAN:
$filter_type = Type::getBool();
break;
case FILTER_VALIDATE_IP:
case FILTER_VALIDATE_MAC:
case FILTER_VALIDATE_REGEXP:
case FILTER_VALIDATE_URL:
case FILTER_VALIDATE_EMAIL:
case FILTER_VALIDATE_DOMAIN:
case FILTER_SANITIZE_URL:
$filter_type = Type::getString();
break;
}
$has_object_like = false;
$filter_null = false;
if (isset($call_args[2])
&& ($third_arg_type = $statements_source->node_data->getType($call_args[2]->value))
&& $filter_type
) {
foreach ($third_arg_type->getAtomicTypes() as $atomic_type) {
if ($atomic_type instanceof TKeyedArray) {
$has_object_like = true;
if (isset($atomic_type->properties['options'])
&& $atomic_type->properties['options']->hasArray()
&& ($options_array = $atomic_type->properties['options']->getArray())
&& $options_array instanceof TKeyedArray
&& isset($options_array->properties['default'])
) {
$filter_type = Type::combineUnionTypes(
$filter_type,
$options_array->properties['default'],
);
} else {
$filter_type = $filter_type->getBuilder()->addType(new TFalse)->freeze();
}
if (isset($atomic_type->properties['flags'])
&& $atomic_type->properties['flags']->isSingleIntLiteral()
) {
$filter_flag_type =
$atomic_type->properties['flags']->getSingleIntLiteral();
if ($filter_type->hasBool()
&& $filter_flag_type->value === FILTER_NULL_ON_FAILURE
) {
$filter_type = $filter_type->getBuilder()->addType(new TNull)->freeze();
}
}
} elseif ($atomic_type instanceof TLiteralInt) {
if ($atomic_type->value === FILTER_NULL_ON_FAILURE) {
$filter_null = true;
$filter_type = $filter_type->getBuilder()->addType(new TNull)->freeze();
}
}
}
}
if (!$has_object_like && !$filter_null && $filter_type) {
$filter_type = $filter_type->getBuilder()->addType(new TFalse)->freeze();
if (!is_int($filter_int_used)) {
return $filter_int_used;
}
}
if (!$filter_type) {
$filter_type = Type::getMixed();
}
if ($statements_source->data_flow_graph
&& !in_array('TaintedInput', $statements_source->getSuppressedIssues())
) {
$function_return_sink = DataFlowNode::getForMethodReturn(
$function_id,
$function_id,
null,
$options = null;
$flags_int_used = FILTER_FLAG_NONE;
if (isset($call_args[2])) {
$helper = FilterUtils::getOptionsArgValueOrError(
$call_args[2],
$statements_analyzer,
$codebase,
$code_location,
);
$statements_source->data_flow_graph->addNode($function_return_sink);
$function_param_sink = DataFlowNode::getForMethodArgument(
$function_id,
$function_id,
0,
null,
$code_location,
$filter_int_used,
);
$statements_source->data_flow_graph->addNode($function_param_sink);
if (!is_array($helper)) {
return $helper;
}
$statements_source->data_flow_graph->addPath(
$function_param_sink,
$function_return_sink,
'arg',
);
return $filter_type->setParentNodes([$function_return_sink->id => $function_return_sink]);
$flags_int_used = $helper['flags_int_used'];
$options = $helper['options'];
}
return $filter_type;
// if we reach this point with callback, the callback is missing
if ($filter_int_used === FILTER_CALLBACK) {
return FilterUtils::missingFilterCallbackCallable(
$function_id,
$code_location,
$statements_analyzer,
$codebase,
);
}
[$default, $min_range, $max_range, $has_range, $regexp] = FilterUtils::getOptions(
$filter_int_used,
$flags_int_used,
$options,
$statements_analyzer,
$code_location,
$codebase,
$function_id,
);
if (!$default) {
[$fails_type] = FilterUtils::getFailsNotSetType($flags_int_used);
} else {
$fails_type = $default;
}
if ($filter_int_used === FILTER_VALIDATE_REGEXP && $regexp === null) {
if ($codebase->analysis_php_version_id >= 8_00_00) {
// throws
return Type::getNever();
}
// any "array" flags are ignored by this filter!
return $fails_type;
}
$input_type = $statements_analyzer->node_data->getType($call_args[0]->value);
// only return now, as we still want to report errors above
if (!$input_type) {
return null;
}
$redundant_error_return_type = FilterUtils::checkRedundantFlags(
$filter_int_used,
$flags_int_used,
$fails_type,
$statements_analyzer,
$code_location,
$codebase,
);
if ($redundant_error_return_type !== null) {
return $redundant_error_return_type;
}
return FilterUtils::getReturnType(
$filter_int_used,
$flags_int_used,
$input_type,
$fails_type,
null,
$statements_analyzer,
$code_location,
$codebase,
$function_id,
$has_range,
$min_range,
$max_range,
$regexp,
);
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Psalm\Issue;
final class RedundantFlag extends CodeIssue
{
public const ERROR_LEVEL = 1;
public const SHORTCODE = 322;
}

View File

@ -1028,6 +1028,41 @@ class FunctionCallTest extends TestCase
'$d' => 'string',
],
],
'filterInput' => [
'code' => '<?php
function filterInt(string $s) : int {
$filtered = filter_var($s, FILTER_VALIDATE_INT);
if ($filtered === false) {
return 0;
}
return $filtered;
}
function filterNullableInt(string $s) : ?int {
return filter_var($s, FILTER_VALIDATE_INT, ["options" => ["default" => null]]);
}
function filterIntWithDefault(string $s) : int {
return filter_var($s, FILTER_VALIDATE_INT, ["options" => ["default" => 5]]);
}
function filterBool(string $s) : bool {
return filter_var($s, FILTER_VALIDATE_BOOLEAN);
}
function filterNullableBool(string $s) : ?bool {
return filter_var($s, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
}
function filterNullableBoolWithFlagsArray(string $s) : ?bool {
return filter_var($s, FILTER_VALIDATE_BOOLEAN, ["flags" => FILTER_NULL_ON_FAILURE]);
}
function filterFloat(string $s) : float {
$filtered = filter_var($s, FILTER_VALIDATE_FLOAT);
if ($filtered === false) {
return 0.0;
}
return $filtered;
}
function filterFloatWithDefault(string $s) : float {
return filter_var($s, FILTER_VALIDATE_FLOAT, ["options" => ["default" => 5.0]]);
}',
],
'filterVar' => [
'code' => '<?php
function filterInt(string $s) : int {

View File

@ -1418,10 +1418,9 @@ class ConditionalTest extends TestCase
'nullCoalescePossibleMixed' => [
'code' => '<?php
/**
* @psalm-suppress MixedReturnStatement
* @psalm-suppress MixedInferredReturnType
* @return array<never, never>|false|string
*/
function foo() : array {
function foo() {
return filter_input(INPUT_POST, "some_var") ?? [];
}',
],