1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-12 17:27:28 +01:00
psalm/src/Psalm/Checker/FunctionChecker.php

1216 lines
49 KiB
PHP
Raw Normal View History

2016-01-08 00:28:27 +01:00
<?php
namespace Psalm\Checker;
2016-01-08 00:28:27 +01:00
2016-02-04 15:22:46 +01:00
use PhpParser;
use Psalm\Checker\Statements\Expression\AssertionFinder;
2018-02-04 00:52:35 +01:00
use Psalm\Codebase\CallMap;
use Psalm\CodeLocation;
use Psalm\Issue\InvalidArgument;
2016-10-14 06:53:43 +02:00
use Psalm\Issue\InvalidReturnType;
2016-11-02 07:29:00 +01:00
use Psalm\IssueBuffer;
use Psalm\StatementsSource;
2016-08-14 05:26:45 +02:00
use Psalm\Type;
use Psalm\Type\Reconciler;
use Psalm\Type\TypeCombination;
2016-01-08 00:28:27 +01:00
2016-08-14 05:26:45 +02:00
class FunctionChecker extends FunctionLikeChecker
2016-01-08 00:28:27 +01:00
{
2016-06-16 02:16:40 +02:00
/**
2016-08-14 05:26:45 +02:00
* @param StatementsSource $source
2016-06-16 02:16:40 +02:00
*/
2018-01-08 06:14:02 +01:00
public function __construct(PhpParser\Node\Stmt\Function_ $function, StatementsSource $source)
2016-05-16 22:12:02 +02:00
{
2016-08-14 05:26:45 +02:00
parent::__construct($function, $source);
2016-01-08 00:28:27 +01:00
}
2016-11-01 05:39:41 +01:00
/**
* @param string $function_id
* @param array<PhpParser\Node\Arg> $call_args
* @param CodeLocation $code_location
2016-11-01 05:39:41 +01:00
* @param array $suppressed_issues
2017-05-27 02:16:18 +02:00
*
2016-11-01 05:39:41 +01:00
* @return Type\Union
*/
public static function getReturnTypeFromCallMapWithArgs(
StatementsChecker $statements_checker,
2016-11-01 05:39:41 +01:00
$function_id,
array $call_args,
CodeLocation $code_location,
2016-11-01 05:39:41 +01:00
array $suppressed_issues
) {
$call_map_key = strtolower($function_id);
2016-08-22 21:00:12 +02:00
2018-02-04 00:52:35 +01:00
$call_map = CallMap::getCallMap();
2016-08-30 06:05:13 +02:00
2016-10-22 23:35:59 +02:00
if (!isset($call_map[$call_map_key])) {
throw new \InvalidArgumentException('Function ' . $function_id . ' was not found in callmap');
}
2018-07-08 02:35:24 +02:00
if (!$call_args) {
switch ($call_map_key) {
case 'getenv':
return new Type\Union([new Type\Atomic\TArray([Type::getMixed(), Type::getString()])]);
case 'gettimeofday':
return new Type\Union([
new Type\Atomic\TArray([
Type::getString(),
Type::getInt()
])
]);
}
} else {
2018-05-24 03:17:14 +02:00
switch ($call_map_key) {
case 'str_replace':
case 'str_ireplace':
case 'substr_replace':
case 'preg_replace':
case 'preg_replace_callback':
if (isset($call_args[2]->value->inferredType)) {
$subject_type = $call_args[2]->value->inferredType;
if (!$subject_type->hasString() && $subject_type->hasArray()) {
return Type::getArray();
}
$return_type = Type::getString();
2016-11-01 05:39:41 +01:00
2018-05-24 03:17:14 +02:00
if (in_array($call_map_key, ['preg_replace', 'preg_replace_callback'], true)) {
$return_type->addType(new Type\Atomic\TNull());
$return_type->ignore_nullable_issues = true;
}
2018-05-24 03:17:14 +02:00
return $return_type;
}
return Type::getMixed();
2016-11-01 05:39:41 +01:00
2018-05-24 03:17:14 +02:00
case 'pathinfo':
if (isset($call_args[1])) {
return Type::getString();
}
2016-11-01 05:39:41 +01:00
2018-05-24 03:17:14 +02:00
return Type::getArray();
2016-11-01 05:39:41 +01:00
2018-05-24 03:17:14 +02:00
case 'count':
if (isset($call_args[0]->value->inferredType)) {
$atomic_types = $call_args[0]->value->inferredType->getTypes();
if (count($atomic_types) === 1 && isset($atomic_types['array'])) {
if ($atomic_types['array'] instanceof Type\Atomic\TArray) {
return new Type\Union([
$atomic_types['array']->count !== null
? new Type\Atomic\TLiteralInt($atomic_types['array']->count)
: new Type\Atomic\TInt
]);
} elseif ($atomic_types['array'] instanceof Type\Atomic\ObjectLike
&& $atomic_types['array']->sealed
) {
return new Type\Union([
new Type\Atomic\TLiteralInt(count($atomic_types['array']->properties))
]);
}
}
}
break;
case 'var_export':
case 'highlight_string':
case 'highlight_file':
if (isset($call_args[1]->value->inferredType)) {
$subject_type = $call_args[1]->value->inferredType;
if ((string) $subject_type === 'true') {
return Type::getString();
}
return new Type\Union([
2018-05-24 03:17:14 +02:00
new Type\Atomic\TString,
$call_map_key === 'var_export' ? new Type\Atomic\TNull : new Type\Atomic\TBool
]);
}
2018-05-24 03:17:14 +02:00
return $call_map_key === 'var_export' ? Type::getVoid() : Type::getBool();
2018-04-03 04:19:58 +02:00
2018-07-08 02:35:24 +02:00
case 'getenv':
return new Type\Union([new Type\Atomic\TString, new Type\Atomic\TFalse]);
case 'gettimeofday':
if (isset($call_args[0]->value->inferredType)) {
$subject_type = $call_args[0]->value->inferredType;
if ((string) $subject_type === 'true') {
return Type::getFloat();
}
if ((string) $subject_type === 'false') {
return new Type\Union([
new Type\Atomic\TArray([
Type::getString(),
Type::getInt()
])
]);
}
}
break;
2018-05-24 03:17:14 +02:00
case 'array_map':
return self::getArrayMapReturnType(
$statements_checker,
$call_args
2018-05-24 03:17:14 +02:00
);
2018-04-03 04:19:58 +02:00
2018-05-24 03:17:14 +02:00
case 'array_filter':
return self::getArrayFilterReturnType(
$statements_checker,
$call_args,
$code_location,
$suppressed_issues
);
2018-04-03 04:19:58 +02:00
case 'array_reduce':
return self::getArrayReduceReturnType(
$statements_checker,
$call_args
);
2018-05-24 03:17:14 +02:00
case 'array_merge':
2018-05-24 03:27:01 +02:00
return self::getArrayMergeReturnType($call_args);
2018-04-03 04:19:58 +02:00
2018-05-24 03:17:14 +02:00
case 'array_rand':
2018-05-24 03:27:01 +02:00
return self::getArrayRandReturnType($call_args);
2016-11-02 14:24:36 +01:00
2018-05-24 03:17:14 +02:00
case 'explode':
if ($call_args[0]->value instanceof PhpParser\Node\Scalar\String_) {
if ($call_args[0]->value->value === '') {
return Type::getFalse();
}
2016-08-30 06:05:13 +02:00
2018-05-24 03:17:14 +02:00
return new Type\Union([
new Type\Atomic\TArray([
Type::getInt(),
Type::getString()
])
]);
}
2018-04-16 22:03:04 +02:00
2018-05-24 03:17:14 +02:00
break;
2018-05-24 03:17:14 +02:00
case 'iterator_to_array':
if (isset($call_args[0]->value->inferredType)
&& $call_args[0]->value->inferredType->hasObjectType()
) {
$value_type = null;
foreach ($call_args[0]->value->inferredType->getTypes() as $call_arg_atomic_type) {
if ($call_arg_atomic_type instanceof Type\Atomic\TGenericObject) {
$type_params = $call_arg_atomic_type->type_params;
$last_param_type = $type_params[count($type_params) - 1];
2018-05-24 03:17:14 +02:00
$value_type = $value_type
? Type::combineUnionTypes($value_type, $last_param_type)
: $last_param_type;
}
}
2018-05-24 03:17:14 +02:00
if ($value_type) {
return new Type\Union([
new Type\Atomic\TArray([
Type::getMixed(),
$value_type
])
]);
}
}
2018-05-24 03:17:14 +02:00
break;
case 'array_column':
$row_shape = null;
// calculate row shape
if (isset($call_args[0]->value->inferredType)
&& $call_args[0]->value->inferredType->isSingle()
&& $call_args[0]->value->inferredType->hasArray()
) {
$input_array = $call_args[0]->value->inferredType->getTypes()['array'];
if ($input_array instanceof Type\Atomic\ObjectLike) {
$row_type = $input_array->getGenericArrayType()->type_params[1];
if ($row_type->isSingle() && $row_type->hasArray()) {
$row_shape = $row_type->getTypes()['array'];
}
} elseif ($input_array instanceof Type\Atomic\TArray) {
$row_type = $input_array->type_params[1];
if ($row_type->isSingle() && $row_type->hasArray()) {
$row_shape = $row_type->getTypes()['array'];
}
}
}
$value_column_name = null;
// calculate value column name
if (isset($call_args[1]->value->inferredType)) {
$value_column_name_arg= $call_args[1]->value->inferredType;
if ($value_column_name_arg->isSingleIntLiteral()) {
$value_column_name = $value_column_name_arg->getSingleIntLiteral();
} elseif ($value_column_name_arg->isSingleStringLiteral()) {
$value_column_name = $value_column_name_arg->getSingleStringLiteral();
}
}
$key_column_name = null;
// calculate key column name
if (isset($call_args[2]->value->inferredType)) {
$key_column_name_arg = $call_args[2]->value->inferredType;
if ($key_column_name_arg->isSingleIntLiteral()) {
$key_column_name = $key_column_name_arg->getSingleIntLiteral();
} elseif ($key_column_name_arg->isSingleStringLiteral()) {
$key_column_name = $key_column_name_arg->getSingleStringLiteral();
}
}
$result_key_type = Type::getMixed();
$result_element_type = null;
// calculate results
if ($row_shape instanceof Type\Atomic\ObjectLike) {
if ((null !== $value_column_name) && isset($row_shape->properties[$value_column_name])) {
$result_element_type = $row_shape->properties[$value_column_name];
} else {
$result_element_type = Type::getMixed();
}
if ((null !== $key_column_name) && isset($row_shape->properties[$key_column_name])) {
$result_key_type = $row_shape->properties[$key_column_name];
}
}
if ($result_element_type) {
return new Type\Union([
new Type\Atomic\TArray([
$result_key_type,
$result_element_type
])
]);
}
break;
2018-05-24 03:17:14 +02:00
case 'abs':
if (isset($call_args[0]->value)) {
$first_arg = $call_args[0]->value;
2018-05-24 03:17:14 +02:00
if (isset($first_arg->inferredType)) {
$numeric_types = [];
foreach ($first_arg->inferredType->getTypes() as $inner_type) {
if ($inner_type->isNumericType()) {
$numeric_types[] = $inner_type;
}
}
2018-05-24 03:17:14 +02:00
if ($numeric_types) {
return new Type\Union($numeric_types);
}
}
}
2018-05-24 03:17:14 +02:00
break;
case 'version_compare':
if (count($call_args) > 2) {
if (isset($call_args[2]->value->inferredType)) {
$operator_type = $call_args[2]->value->inferredType;
if (!$operator_type->isMixed()) {
$project_checker = $statements_checker->getFileChecker()->project_checker;
$codebase = $project_checker->codebase;
$acceptable_operator_type = new Type\Union([
new Type\Atomic\TLiteralString('<'),
new Type\Atomic\TLiteralString('lt'),
new Type\Atomic\TLiteralString('<='),
new Type\Atomic\TLiteralString('le'),
new Type\Atomic\TLiteralString('>'),
new Type\Atomic\TLiteralString('gt'),
new Type\Atomic\TLiteralString('>='),
new Type\Atomic\TLiteralString('ge'),
new Type\Atomic\TLiteralString('=='),
new Type\Atomic\TLiteralString('='),
new Type\Atomic\TLiteralString('eq'),
new Type\Atomic\TLiteralString('!='),
new Type\Atomic\TLiteralString('<>'),
new Type\Atomic\TLiteralString('ne'),
]);
if (TypeChecker::isContainedBy(
$codebase,
$operator_type,
$acceptable_operator_type
)) {
return Type::getBool();
}
}
}
return new Type\Union([
new Type\Atomic\TBool,
new Type\Atomic\TNull
]);
}
return new Type\Union([
new Type\Atomic\TLiteralInt(-1),
new Type\Atomic\TLiteralInt(0),
new Type\Atomic\TLiteralInt(1)
]);
case 'parse_url':
if (count($call_args) > 1) {
if (isset($call_args[1]->value->inferredType)) {
$component_type = $call_args[1]->value->inferredType;
if (!$component_type->isMixed()) {
$project_checker = $statements_checker->getFileChecker()->project_checker;
$codebase = $project_checker->codebase;
$acceptable_string_component_type = new Type\Union([
new Type\Atomic\TLiteralInt(PHP_URL_SCHEME),
new Type\Atomic\TLiteralInt(PHP_URL_USER),
new Type\Atomic\TLiteralInt(PHP_URL_PASS),
new Type\Atomic\TLiteralInt(PHP_URL_HOST),
new Type\Atomic\TLiteralInt(PHP_URL_PATH),
new Type\Atomic\TLiteralInt(PHP_URL_QUERY),
new Type\Atomic\TLiteralInt(PHP_URL_FRAGMENT),
]);
$acceptable_int_component_type = new Type\Union([
new Type\Atomic\TLiteralInt(PHP_URL_PORT)
]);
if (TypeChecker::isContainedBy(
$codebase,
$component_type,
$acceptable_string_component_type
)) {
$nullable_string = new Type\Union([
new Type\Atomic\TString,
new Type\Atomic\TNull
]);
$nullable_string->ignore_nullable_issues = true;
return $nullable_string;
}
if (TypeChecker::isContainedBy(
$codebase,
$component_type,
$acceptable_int_component_type
)) {
$nullable_int = new Type\Union([
new Type\Atomic\TInt,
new Type\Atomic\TNull
]);
$nullable_int->ignore_nullable_issues = true;
return $nullable_int;
}
}
}
$nullable_string_or_int = new Type\Union([
new Type\Atomic\TString,
new Type\Atomic\TInt,
new Type\Atomic\TNull
]);
$nullable_string_or_int->ignore_nullable_issues = true;
return $nullable_string_or_int;
}
$component_key_type = new Type\Union([
new Type\Atomic\TLiteralString('scheme'),
new Type\Atomic\TLiteralString('user'),
new Type\Atomic\TLiteralString('pass'),
new Type\Atomic\TLiteralString('host'),
new Type\Atomic\TLiteralString('port'),
new Type\Atomic\TLiteralString('path'),
new Type\Atomic\TLiteralString('query'),
new Type\Atomic\TLiteralString('fragment'),
]);
$nullable_string_or_int = new Type\Union([
new Type\Atomic\TArray([$component_key_type, Type::getMixed()]),
new Type\Atomic\TFalse
]);
$nullable_string_or_int->ignore_falsable_issues = true;
return $nullable_string_or_int;
2018-05-24 03:17:14 +02:00
case 'min':
case 'max':
if (isset($call_args[0])) {
$first_arg = $call_args[0]->value;
2018-05-24 03:17:14 +02:00
if (isset($first_arg->inferredType)) {
if ($first_arg->inferredType->hasArray()) {
$array_type = $first_arg->inferredType->getTypes()['array'];
if ($array_type instanceof Type\Atomic\ObjectLike) {
return $array_type->getGenericValueType();
}
2018-05-24 03:17:14 +02:00
if ($array_type instanceof Type\Atomic\TArray) {
return clone $array_type->type_params[1];
}
} elseif ($first_arg->inferredType->hasScalarType() &&
($second_arg = $call_args[1]->value) &&
isset($second_arg->inferredType) &&
$second_arg->inferredType->hasScalarType()
) {
return Type::combineUnionTypes($first_arg->inferredType, $second_arg->inferredType);
}
}
}
2018-05-24 03:17:14 +02:00
break;
}
2016-11-02 14:24:36 +01:00
}
2016-11-02 14:24:36 +01:00
if (!$call_map[$call_map_key][0]) {
return Type::getMixed();
}
$call_map_return_type = Type::parseString($call_map[$call_map_key][0]);
2018-05-24 03:17:14 +02:00
switch ($call_map_key) {
case 'mb_strpos':
case 'mb_strrpos':
case 'mb_stripos':
case 'mb_strripos':
case 'strpos':
case 'strrpos':
case 'stripos':
case 'strripos':
break;
default:
if ($call_map_return_type->isFalsable()) {
$call_map_return_type->ignore_falsable_issues = true;
}
}
return $call_map_return_type;
2016-11-02 14:24:36 +01:00
}
2016-11-02 14:24:36 +01:00
/**
* @param array<PhpParser\Node\Arg> $call_args
2017-05-27 02:16:18 +02:00
*
2018-05-24 03:17:14 +02:00
* @return Type\Union
2016-11-02 14:24:36 +01:00
*/
2018-05-24 03:27:01 +02:00
private static function getArrayMergeReturnType(array $call_args)
{
2018-05-24 03:17:14 +02:00
$inner_value_types = [];
$inner_key_types = [];
2016-08-22 21:00:12 +02:00
2018-05-24 03:17:14 +02:00
$generic_properties = [];
2018-05-24 03:17:14 +02:00
foreach ($call_args as $call_arg) {
if (!isset($call_arg->value->inferredType)) {
return Type::getArray();
}
2016-08-22 21:00:12 +02:00
2018-05-24 03:17:14 +02:00
foreach ($call_arg->value->inferredType->getTypes() as $type_part) {
if ($call_arg->unpack) {
if (!$type_part instanceof Type\Atomic\TArray) {
if ($type_part instanceof Type\Atomic\ObjectLike) {
$type_part_value_type = $type_part->getGenericValueType();
} else {
2018-05-24 03:17:14 +02:00
return Type::getArray();
}
2018-05-24 03:17:14 +02:00
} else {
$type_part_value_type = $type_part->type_params[1];
}
2016-08-22 21:00:12 +02:00
2018-05-24 03:17:14 +02:00
$unpacked_type_parts = [];
2018-05-24 03:17:14 +02:00
foreach ($type_part_value_type->getTypes() as $value_type_part) {
$unpacked_type_parts[] = $value_type_part;
2016-08-22 21:00:12 +02:00
}
2018-05-24 03:17:14 +02:00
} else {
$unpacked_type_parts = [$type_part];
}
2016-08-22 21:00:12 +02:00
2018-05-24 03:17:14 +02:00
foreach ($unpacked_type_parts as $unpacked_type_part) {
if (!$unpacked_type_part instanceof Type\Atomic\TArray) {
if ($unpacked_type_part instanceof Type\Atomic\ObjectLike) {
if ($generic_properties !== null) {
$generic_properties = array_merge(
$generic_properties,
$unpacked_type_part->properties
);
}
2018-05-24 03:17:14 +02:00
$unpacked_type_part = $unpacked_type_part->getGenericArrayType();
} else {
if ($unpacked_type_part instanceof Type\Atomic\TMixed
&& $unpacked_type_part->from_isset
) {
$unpacked_type_part = new Type\Atomic\TArray([
Type::getMixed(),
Type::getMixed(true)
]);
} else {
2018-05-24 03:17:14 +02:00
return Type::getArray();
}
}
2018-05-24 03:17:14 +02:00
} elseif (!$unpacked_type_part->type_params[0]->isEmpty()) {
$generic_properties = null;
}
2018-05-24 03:17:14 +02:00
if ($unpacked_type_part->type_params[1]->isEmpty()) {
continue;
}
2016-08-22 21:00:12 +02:00
2018-05-24 03:17:14 +02:00
$inner_key_types = array_merge(
$inner_key_types,
array_values($unpacked_type_part->type_params[0]->getTypes())
);
$inner_value_types = array_merge(
$inner_value_types,
array_values($unpacked_type_part->type_params[1]->getTypes())
);
}
}
2018-05-24 03:17:14 +02:00
}
2018-05-24 03:17:14 +02:00
if ($generic_properties) {
return new Type\Union([
new Type\Atomic\ObjectLike($generic_properties),
]);
}
2016-11-02 14:24:36 +01:00
2018-05-24 03:17:14 +02:00
if ($inner_value_types) {
return new Type\Union([
new Type\Atomic\TArray([
TypeCombination::combineTypes($inner_key_types),
TypeCombination::combineTypes($inner_value_types),
]),
]);
2016-11-02 14:24:36 +01:00
}
2018-05-24 03:17:14 +02:00
return Type::getArray();
}
2018-05-24 03:17:14 +02:00
/**
* @param array<PhpParser\Node\Arg> $call_args
*
* @return Type\Union
*/
2018-05-24 03:27:01 +02:00
private static function getArrayRandReturnType(array $call_args)
{
2018-05-24 03:17:14 +02:00
$first_arg = isset($call_args[0]->value) ? $call_args[0]->value : null;
$second_arg = isset($call_args[1]->value) ? $call_args[1]->value : null;
2018-05-24 03:17:14 +02:00
$first_arg_array = $first_arg
&& isset($first_arg->inferredType)
&& $first_arg->inferredType->hasType('array')
&& ($array_atomic_type = $first_arg->inferredType->getTypes()['array'])
&& ($array_atomic_type instanceof Type\Atomic\TArray ||
$array_atomic_type instanceof Type\Atomic\ObjectLike)
? $array_atomic_type
: null;
2018-05-24 03:17:14 +02:00
if (!$first_arg_array) {
return Type::getMixed();
}
2018-05-24 03:17:14 +02:00
if ($first_arg_array instanceof Type\Atomic\TArray) {
$key_type = clone $first_arg_array->type_params[0];
} else {
$key_type = $first_arg_array->getGenericKeyType();
}
if (!$second_arg
|| ($second_arg instanceof PhpParser\Node\Scalar\LNumber && $second_arg->value === 1)
) {
return $key_type;
}
$arr_type = new Type\Union([
new Type\Atomic\TArray([
Type::getInt(),
$key_type,
]),
]);
2018-05-24 03:17:14 +02:00
if ($second_arg instanceof PhpParser\Node\Scalar\LNumber) {
return $arr_type;
}
2018-05-24 03:17:14 +02:00
return Type::combineUnionTypes($key_type, $arr_type);
2016-11-02 14:24:36 +01:00
}
/**
* @param array<PhpParser\Node\Arg> $call_args
* @param CodeLocation $code_location
2016-11-02 14:24:36 +01:00
* @param array $suppressed_issues
2017-05-27 02:16:18 +02:00
*
2016-11-02 14:24:36 +01:00
* @return Type\Union
*/
2018-05-24 03:17:14 +02:00
private static function getArrayMapReturnType(
StatementsChecker $statements_checker,
$call_args
2016-11-04 01:51:56 +01:00
) {
$array_arg = isset($call_args[1]->value) ? $call_args[1]->value : null;
2016-11-02 14:24:36 +01:00
2018-02-06 19:52:59 +01:00
$array_arg_type = null;
if ($array_arg && isset($array_arg->inferredType)) {
$arg_types = $array_arg->inferredType->getTypes();
if (isset($arg_types['array'])
&& ($arg_types['array'] instanceof Type\Atomic\TArray
|| $arg_types['array'] instanceof Type\Atomic\ObjectLike)
) {
2018-02-06 19:52:59 +01:00
$array_arg_type = $arg_types['array'];
}
}
2016-11-02 14:24:36 +01:00
if (isset($call_args[0])) {
$function_call_arg = $call_args[0];
2016-11-02 14:24:36 +01:00
if (count($call_args) === 2) {
if ($array_arg_type instanceof Type\Atomic\ObjectLike) {
$generic_key_type = $array_arg_type->getGenericKeyType();
} else {
$generic_key_type = $array_arg_type ? clone $array_arg_type->type_params[0] : Type::getMixed();
}
} else {
$generic_key_type = Type::getInt();
}
if (isset($function_call_arg->value->inferredType)
&& ($first_arg_atomic_types = $function_call_arg->value->inferredType->getTypes())
&& ($closure_atomic_type = isset($first_arg_atomic_types['Closure'])
? $first_arg_atomic_types['Closure']
: null)
&& $closure_atomic_type instanceof Type\Atomic\Fn
2016-12-07 20:13:39 +01:00
) {
$closure_return_type = $closure_atomic_type->return_type ?: Type::getMixed();
2016-11-02 14:24:36 +01:00
2016-12-07 20:13:39 +01:00
if ($closure_return_type->isVoid()) {
$closure_return_type = Type::getNull();
2016-10-22 23:35:59 +02:00
}
$inner_type = clone $closure_return_type;
2017-05-25 04:07:49 +02:00
if ($array_arg_type instanceof Type\Atomic\ObjectLike && count($call_args) === 2) {
return new Type\Union([
new Type\Atomic\ObjectLike(
array_map(
/**
* @return Type\Union
*/
function (Type\Union $_) use ($inner_type) {
return clone $inner_type;
},
$array_arg_type->properties
)
),
]);
}
return new Type\Union([
new Type\Atomic\TArray([
$generic_key_type,
$inner_type,
]),
]);
} elseif ($function_call_arg->value instanceof PhpParser\Node\Scalar\String_
|| $function_call_arg->value instanceof PhpParser\Node\Expr\Array_
) {
$mapping_function_ids = Statements\Expression\CallChecker::getFunctionIdsFromCallableArg(
$statements_checker,
$function_call_arg->value
);
2018-02-04 00:52:35 +01:00
$call_map = CallMap::getCallMap();
$mapping_return_type = null;
$project_checker = $statements_checker->getFileChecker()->project_checker;
$codebase = $project_checker->codebase;
foreach ($mapping_function_ids as $mapping_function_id) {
$mapping_function_id = strtolower($mapping_function_id);
$mapping_function_id_parts = explode('&', $mapping_function_id);
$part_match_found = false;
2017-10-20 00:23:18 +02:00
foreach ($mapping_function_id_parts as $mapping_function_id_part) {
if (isset($call_map[$mapping_function_id_part][0])) {
if ($call_map[$mapping_function_id_part][0]) {
$mapped_function_return =
Type::parseString($call_map[$mapping_function_id_part][0]);
2018-01-26 16:59:30 +01:00
if ($mapping_return_type) {
$mapping_return_type = Type::combineUnionTypes(
$mapping_return_type,
$mapped_function_return
);
} else {
$mapping_return_type = $mapped_function_return;
}
$part_match_found = true;
}
} else {
if (strpos($mapping_function_id_part, '::') !== false) {
list($callable_fq_class_name) = explode('::', $mapping_function_id_part);
2017-10-20 00:23:18 +02:00
if (in_array($callable_fq_class_name, ['self', 'static', 'parent'], true)) {
continue;
}
if (!$codebase->methodExists($mapping_function_id_part)) {
continue;
}
$part_match_found = true;
$self_class = 'self';
$return_type = $codebase->methods->getMethodReturnType(
$mapping_function_id_part,
$self_class
) ?: Type::getMixed();
if ($mapping_return_type) {
$mapping_return_type = Type::combineUnionTypes(
$mapping_return_type,
$return_type
);
} else {
$mapping_return_type = $return_type;
}
} else {
if (!$codebase->functions->functionExists(
$statements_checker,
$mapping_function_id_part
)) {
$mapping_return_type = Type::getMixed();
continue;
}
$part_match_found = true;
$function_storage = $codebase->functions->getStorage(
$statements_checker,
$mapping_function_id_part
);
$return_type = $function_storage->return_type ?: Type::getMixed();
if ($mapping_return_type) {
$mapping_return_type = Type::combineUnionTypes(
$mapping_return_type,
$return_type
);
} else {
$mapping_return_type = $return_type;
}
}
}
2016-11-02 14:24:36 +01:00
}
if ($part_match_found === false) {
$mapping_return_type = Type::getMixed();
}
2016-11-02 14:24:36 +01:00
}
if ($mapping_return_type) {
if ($array_arg_type instanceof Type\Atomic\ObjectLike && count($call_args) === 2) {
return new Type\Union([
new Type\Atomic\ObjectLike(
array_map(
/**
* @return Type\Union
*/
function (Type\Union $_) use ($mapping_return_type) {
return clone $mapping_return_type;
},
$array_arg_type->properties
)
),
]);
}
return new Type\Union([
new Type\Atomic\TArray([
$generic_key_type,
$mapping_return_type,
]),
]);
}
2016-10-22 23:35:59 +02:00
}
}
2016-11-02 14:24:36 +01:00
return Type::getArray();
2016-08-22 21:00:12 +02:00
}
/**
* @param array<PhpParser\Node\Arg> $call_args
* @param CodeLocation $code_location
* @param array $suppressed_issues
*
* @return Type\Union
*/
private static function getArrayReduceReturnType(
StatementsChecker $statements_checker,
$call_args
) {
if (!isset($call_args[0]) || !isset($call_args[1])) {
return Type::getMixed();
}
$project_checker = $statements_checker->getFileChecker()->project_checker;
$array_arg = $call_args[0]->value;
$function_call_arg = $call_args[1]->value;
if (!isset($array_arg->inferredType) || !isset($function_call_arg->inferredType)) {
return Type::getMixed();
}
$array_arg_type = null;
$array_arg_types = $array_arg->inferredType->getTypes();
if (isset($array_arg_types['array'])
&& ($array_arg_types['array'] instanceof Type\Atomic\TArray
|| $array_arg_types['array'] instanceof Type\Atomic\ObjectLike)
) {
$array_arg_type = $array_arg_types['array'];
if ($array_arg_type instanceof Type\Atomic\ObjectLike) {
$array_arg_type = $array_arg_type->getGenericArrayType();
}
}
if (!isset($call_args[2])) {
$reduce_return_type = Type::getNull();
$reduce_return_type->ignore_nullable_issues = true;
} else {
if (!isset($call_args[2]->value->inferredType)) {
return Type::getMixed();
}
$reduce_return_type = $call_args[2]->value->inferredType;
if ($reduce_return_type->isMixed()) {
return Type::getMixed();
}
}
$initial_type = $reduce_return_type;
if (($first_arg_atomic_types = $function_call_arg->inferredType->getTypes())
&& ($closure_atomic_type = isset($first_arg_atomic_types['Closure'])
? $first_arg_atomic_types['Closure']
: null)
&& $closure_atomic_type instanceof Type\Atomic\Fn
) {
$closure_return_type = $closure_atomic_type->return_type ?: Type::getMixed();
if ($closure_return_type->isVoid()) {
$closure_return_type = Type::getNull();
}
$reduce_return_type = Type::combineUnionTypes($closure_return_type, $reduce_return_type);
if ($closure_atomic_type->params !== null) {
if (count($closure_atomic_type->params) < 2) {
if (IssueBuffer::accepts(
new InvalidArgument(
'The closure passed to array_reduce needs two params',
new CodeLocation($statements_checker->getSource(), $function_call_arg)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
return Type::getMixed();
}
$carry_param = $closure_atomic_type->params[0];
$item_param = $closure_atomic_type->params[1];
if ($carry_param->type
&& (!TypeChecker::isContainedBy(
$project_checker->codebase,
$initial_type,
$carry_param->type
) || (!$reduce_return_type->isMixed()
&& !TypeChecker::isContainedBy(
$project_checker->codebase,
$reduce_return_type,
$carry_param->type
)
)
)
) {
if (IssueBuffer::accepts(
new InvalidArgument(
'The first param of the closure passed to array_reduce must take '
. $reduce_return_type . ' but only accepts ' . $carry_param->type,
$carry_param->type_location
?: new CodeLocation($statements_checker->getSource(), $function_call_arg)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
return Type::getMixed();
}
if ($item_param->type
&& $array_arg_type
&& !$array_arg_type->type_params[1]->isMixed()
&& !TypeChecker::isContainedBy(
$project_checker->codebase,
$array_arg_type->type_params[1],
$item_param->type
)
) {
if (IssueBuffer::accepts(
new InvalidArgument(
'The second param of the closure passed to array_reduce must take '
. $array_arg_type->type_params[1] . ' but only accepts ' . $item_param->type,
$item_param->type_location
?: new CodeLocation($statements_checker->getSource(), $function_call_arg)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
return Type::getMixed();
}
}
return $reduce_return_type;
}
if ($function_call_arg instanceof PhpParser\Node\Scalar\String_
|| $function_call_arg instanceof PhpParser\Node\Expr\Array_
) {
$mapping_function_ids = Statements\Expression\CallChecker::getFunctionIdsFromCallableArg(
$statements_checker,
$function_call_arg
);
$call_map = CallMap::getCallMap();
$project_checker = $statements_checker->getFileChecker()->project_checker;
$codebase = $project_checker->codebase;
foreach ($mapping_function_ids as $mapping_function_id) {
$mapping_function_id = strtolower($mapping_function_id);
$mapping_function_id_parts = explode('&', $mapping_function_id);
$part_match_found = false;
foreach ($mapping_function_id_parts as $mapping_function_id_part) {
if (isset($call_map[$mapping_function_id_part][0])) {
if ($call_map[$mapping_function_id_part][0]) {
$mapped_function_return =
Type::parseString($call_map[$mapping_function_id_part][0]);
$reduce_return_type = Type::combineUnionTypes(
$reduce_return_type,
$mapped_function_return
);
$part_match_found = true;
}
} else {
if (strpos($mapping_function_id_part, '::') !== false) {
list($callable_fq_class_name) = explode('::', $mapping_function_id_part);
if (in_array($callable_fq_class_name, ['self', 'static', 'parent'], true)) {
continue;
}
if (!$codebase->methodExists($mapping_function_id_part)) {
continue;
}
$part_match_found = true;
$self_class = 'self';
$return_type = $codebase->methods->getMethodReturnType(
$mapping_function_id_part,
$self_class
) ?: Type::getMixed();
$reduce_return_type = Type::combineUnionTypes(
$reduce_return_type,
$return_type
);
} else {
if (!$codebase->functions->functionExists(
$statements_checker,
$mapping_function_id_part
)) {
return Type::getMixed();
}
$part_match_found = true;
$function_storage = $codebase->functions->getStorage(
$statements_checker,
$mapping_function_id_part
);
$return_type = $function_storage->return_type ?: Type::getMixed();
$reduce_return_type = Type::combineUnionTypes(
$reduce_return_type,
$return_type
);
}
}
}
if ($part_match_found === false) {
return Type::getMixed();
}
}
return $reduce_return_type;
}
return Type::getMixed();
}
/**
* @param array<PhpParser\Node\Arg> $call_args
* @param CodeLocation $code_location
* @param array $suppressed_issues
*
* @return Type\Union
*/
2018-05-24 03:17:14 +02:00
private static function getArrayFilterReturnType(
StatementsChecker $statements_checker,
$call_args,
CodeLocation $code_location,
array $suppressed_issues
) {
$array_arg = isset($call_args[0]->value) ? $call_args[0]->value : null;
$first_arg_array = $array_arg
&& isset($array_arg->inferredType)
&& $array_arg->inferredType->hasType('array')
&& ($array_atomic_type = $array_arg->inferredType->getTypes()['array'])
&& ($array_atomic_type instanceof Type\Atomic\TArray ||
$array_atomic_type instanceof Type\Atomic\ObjectLike)
? $array_atomic_type
: null;
if (!$first_arg_array) {
return Type::getArray();
}
if ($first_arg_array instanceof Type\Atomic\TArray) {
$inner_type = $first_arg_array->type_params[1];
$key_type = clone $first_arg_array->type_params[0];
} else {
$inner_type = $first_arg_array->getGenericValueType();
$key_type = $first_arg_array->getGenericKeyType();
}
if (!isset($call_args[1])) {
$inner_type->removeType('null');
$inner_type->removeType('false');
} elseif (!isset($call_args[2])) {
$function_call_arg = $call_args[1];
if ($function_call_arg->value instanceof PhpParser\Node\Expr\Closure
&& isset($function_call_arg->value->inferredType)
&& ($closure_atomic_type = $function_call_arg->value->inferredType->getTypes()['Closure'])
&& $closure_atomic_type instanceof Type\Atomic\Fn
) {
$closure_return_type = $closure_atomic_type->return_type ?: Type::getMixed();
if ($closure_return_type->isVoid()) {
IssueBuffer::accepts(
new InvalidReturnType(
'No return type could be found in the closure passed to array_filter',
$code_location
),
$suppressed_issues
);
return Type::getArray();
}
if (count($function_call_arg->value->stmts) === 1 && count($function_call_arg->value->params)) {
$first_param = $function_call_arg->value->params[0];
$stmt = $function_call_arg->value->stmts[0];
if ($first_param->variadic === false
2018-06-03 16:05:50 +02:00
&& $first_param->var instanceof PhpParser\Node\Expr\Variable
&& is_string($first_param->var->name)
&& $stmt instanceof PhpParser\Node\Stmt\Return_
&& $stmt->expr
) {
AssertionFinder::scrapeAssertions($stmt->expr, null, $statements_checker);
$assertions = isset($stmt->expr->assertions) ? $stmt->expr->assertions : null;
if (isset($assertions['$' . $first_param->var->name])) {
$changed_var_ids = [];
$reconciled_types = Reconciler::reconcileKeyedTypes(
['$inner_type' => $assertions['$' . $first_param->var->name]],
['$inner_type' => $inner_type],
$changed_var_ids,
['$inner_type' => true],
$statements_checker,
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);
if (isset($reconciled_types['$inner_type'])) {
$inner_type = $reconciled_types['$inner_type'];
}
}
}
}
}
return new Type\Union([
new Type\Atomic\TArray([
$key_type,
$inner_type,
]),
]);
}
return new Type\Union([
new Type\Atomic\TArray([
$key_type,
$inner_type,
]),
]);
}
2016-01-08 00:28:27 +01:00
}