2016-01-08 00:28:27 +01:00
|
|
|
<?php
|
2016-08-13 20:20:46 +02:00
|
|
|
namespace Psalm\Checker;
|
2016-01-08 00:28:27 +01:00
|
|
|
|
2016-02-04 15:22:46 +01:00
|
|
|
use PhpParser;
|
2018-01-17 22:07:46 +01:00
|
|
|
use Psalm\Checker\Statements\Expression\AssertionFinder;
|
2016-12-04 01:11:30 +01:00
|
|
|
use Psalm\CodeLocation;
|
2016-10-09 23:54:58 +02:00
|
|
|
use Psalm\FunctionLikeParameter;
|
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;
|
2018-01-17 22:07:46 +01:00
|
|
|
use Psalm\Type\Reconciler;
|
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-11-01 05:39:41 +01:00
|
|
|
/**
|
2016-12-31 00:08:07 +01:00
|
|
|
* @var array<array<string,string>>|null
|
2016-11-01 05:39:41 +01:00
|
|
|
*/
|
2016-12-31 00:08:07 +01:00
|
|
|
protected static $call_map = null;
|
2016-11-01 05:39:41 +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-10-18 23:55:07 +02:00
|
|
|
/**
|
|
|
|
* @param string $function_id
|
2016-12-31 00:08:07 +01:00
|
|
|
* @param string $file_path
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2017-05-25 04:07:49 +02:00
|
|
|
* @return bool
|
2016-10-18 23:55:07 +02:00
|
|
|
*/
|
2017-07-29 21:05:06 +02:00
|
|
|
public static function isVariadic(ProjectChecker $project_checker, $function_id, $file_path)
|
2016-10-18 23:55:07 +02:00
|
|
|
{
|
2017-07-29 21:05:06 +02:00
|
|
|
$file_storage = $project_checker->file_storage_provider->get($file_path);
|
2016-12-31 00:08:07 +01:00
|
|
|
|
|
|
|
return isset($file_storage->functions[$function_id]) && $file_storage->functions[$function_id]->variadic;
|
2016-10-18 23:55:07 +02:00
|
|
|
}
|
|
|
|
|
2016-08-22 21:00:12 +02:00
|
|
|
/**
|
|
|
|
* @param string $function_id
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-11-21 05:57:37 +01:00
|
|
|
* @return array|null
|
2016-12-23 21:10:20 +01:00
|
|
|
* @psalm-return array<int, array<int, FunctionLikeParameter>>|null
|
2016-08-22 21:00:12 +02:00
|
|
|
*/
|
|
|
|
public static function getParamsFromCallMap($function_id)
|
|
|
|
{
|
|
|
|
$call_map = self::getCallMap();
|
|
|
|
|
|
|
|
$call_map_key = strtolower($function_id);
|
|
|
|
|
|
|
|
if (!isset($call_map[$call_map_key])) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$call_map_functions = [];
|
|
|
|
$call_map_functions[] = $call_map[$call_map_key];
|
|
|
|
|
2017-05-27 02:05:57 +02:00
|
|
|
for ($i = 1; $i < 10; ++$i) {
|
2016-11-02 07:29:00 +01:00
|
|
|
if (!isset($call_map[$call_map_key . '\'' . $i])) {
|
2016-08-22 21:00:12 +02:00
|
|
|
break;
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
|
|
|
|
$call_map_functions[] = $call_map[$call_map_key . '\'' . $i];
|
2016-08-22 21:00:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$function_type_options = [];
|
|
|
|
|
|
|
|
foreach ($call_map_functions as $call_map_function_args) {
|
|
|
|
array_shift($call_map_function_args);
|
|
|
|
|
|
|
|
$function_types = [];
|
|
|
|
|
2017-11-16 02:45:53 +01:00
|
|
|
/** @var string $arg_name - key type changed with above array_shift */
|
2016-08-22 21:00:12 +02:00
|
|
|
foreach ($call_map_function_args as $arg_name => $arg_type) {
|
|
|
|
$by_reference = false;
|
2016-10-26 17:51:59 +02:00
|
|
|
$optional = false;
|
2017-01-14 00:01:07 +01:00
|
|
|
$variadic = false;
|
2016-08-22 21:00:12 +02:00
|
|
|
|
|
|
|
if ($arg_name[0] === '&') {
|
|
|
|
$arg_name = substr($arg_name, 1);
|
|
|
|
$by_reference = true;
|
|
|
|
}
|
|
|
|
|
2016-10-26 17:51:59 +02:00
|
|
|
if (substr($arg_name, -1) === '=') {
|
|
|
|
$arg_name = substr($arg_name, 0, -1);
|
|
|
|
$optional = true;
|
|
|
|
}
|
|
|
|
|
2017-01-14 00:01:07 +01:00
|
|
|
if (substr($arg_name, 0, 3) === '...') {
|
|
|
|
$arg_name = substr($arg_name, 3);
|
|
|
|
$variadic = true;
|
|
|
|
}
|
|
|
|
|
2017-03-30 17:44:38 +02:00
|
|
|
$param_type = $arg_type
|
2017-07-25 22:11:02 +02:00
|
|
|
? Type::parseString($arg_type)
|
2017-03-30 17:44:38 +02:00
|
|
|
: Type::getMixed();
|
|
|
|
|
2018-01-05 00:58:03 +01:00
|
|
|
if ($param_type->hasScalarType() || $param_type->hasObject()) {
|
2017-12-15 22:48:06 +01:00
|
|
|
$param_type->from_docblock = true;
|
|
|
|
}
|
|
|
|
|
2016-10-09 23:54:58 +02:00
|
|
|
$function_types[] = new FunctionLikeParameter(
|
|
|
|
$arg_name,
|
|
|
|
$by_reference,
|
2017-03-30 17:44:38 +02:00
|
|
|
$param_type,
|
2016-12-04 01:11:30 +01:00
|
|
|
null,
|
2017-12-30 03:28:21 +01:00
|
|
|
null,
|
2016-10-30 17:46:18 +01:00
|
|
|
$optional,
|
|
|
|
false,
|
2017-01-14 00:01:07 +01:00
|
|
|
$variadic
|
2016-10-09 23:54:58 +02:00
|
|
|
);
|
2016-08-22 21:00:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$function_type_options[] = $function_types;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $function_type_options;
|
|
|
|
}
|
|
|
|
|
2016-10-14 06:53:43 +02:00
|
|
|
/**
|
2016-11-01 05:39:41 +01:00
|
|
|
* @param string $function_id
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-10-14 06:53:43 +02:00
|
|
|
* @return Type\Union
|
|
|
|
*/
|
2016-11-01 05:39:41 +01:00
|
|
|
public static function getReturnTypeFromCallMap($function_id)
|
|
|
|
{
|
2016-08-22 21:00:12 +02:00
|
|
|
$call_map_key = strtolower($function_id);
|
|
|
|
|
2016-11-01 05:39:41 +01:00
|
|
|
$call_map = self::getCallMap();
|
2016-08-22 21:00:12 +02:00
|
|
|
|
2016-11-01 05:39:41 +01:00
|
|
|
if (!isset($call_map[$call_map_key])) {
|
|
|
|
throw new \InvalidArgumentException('Function ' . $function_id . ' was not found in callmap');
|
|
|
|
}
|
2016-08-22 21:00:12 +02:00
|
|
|
|
2016-11-01 05:39:41 +01:00
|
|
|
if (!$call_map[$call_map_key][0]) {
|
|
|
|
return Type::getMixed();
|
2016-08-22 21:00:12 +02:00
|
|
|
}
|
|
|
|
|
2016-11-01 05:39:41 +01:00
|
|
|
return Type::parseString($call_map[$call_map_key][0]);
|
|
|
|
}
|
2016-08-22 21:00:12 +02:00
|
|
|
|
2016-11-01 05:39:41 +01:00
|
|
|
/**
|
|
|
|
* @param string $function_id
|
|
|
|
* @param array<PhpParser\Node\Arg> $call_args
|
2016-12-04 01:11:30 +01:00
|
|
|
* @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(
|
2017-08-15 01:30:11 +02:00
|
|
|
StatementsChecker $statements_checker,
|
2016-11-01 05:39:41 +01:00
|
|
|
$function_id,
|
|
|
|
array $call_args,
|
2016-12-04 01:11:30 +01:00
|
|
|
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
|
|
|
|
2016-08-30 06:05:13 +02:00
|
|
|
$call_map = self::getCallMap();
|
|
|
|
|
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-01-08 06:09:22 +01:00
|
|
|
if ($call_map_key === 'getenv') {
|
2018-01-21 03:22:33 +01:00
|
|
|
if (!empty($call_args)) {
|
2018-01-08 06:09:22 +01:00
|
|
|
return new Type\Union([new Type\Atomic\TString, new Type\Atomic\TFalse]);
|
|
|
|
}
|
|
|
|
|
2018-01-08 06:14:02 +01:00
|
|
|
return new Type\Union([new Type\Atomic\TArray([Type::getMixed(), Type::getString()])]);
|
2018-01-08 06:09:22 +01:00
|
|
|
}
|
|
|
|
|
2016-11-01 05:39:41 +01:00
|
|
|
if ($call_args) {
|
2017-05-27 02:05:57 +02:00
|
|
|
if (in_array($call_map_key, ['str_replace', 'preg_replace', 'preg_replace_callback'], true)) {
|
2016-11-01 05:39:41 +01:00
|
|
|
if (isset($call_args[2]->value->inferredType)) {
|
|
|
|
|
|
|
|
/** @var Type\Union */
|
|
|
|
$subject_type = $call_args[2]->value->inferredType;
|
|
|
|
|
|
|
|
if (!$subject_type->hasString() && $subject_type->hasArray()) {
|
|
|
|
return Type::getArray();
|
|
|
|
}
|
|
|
|
|
2018-01-24 23:07:03 +01:00
|
|
|
$return_type = Type::getString();
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $return_type;
|
2016-11-01 05:39:41 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-27 02:05:57 +02:00
|
|
|
if (in_array($call_map_key, ['pathinfo'], true)) {
|
2016-11-01 05:39:41 +01:00
|
|
|
if (isset($call_args[1])) {
|
|
|
|
return Type::getString();
|
|
|
|
}
|
|
|
|
|
|
|
|
return Type::getArray();
|
|
|
|
}
|
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
if (substr($call_map_key, 0, 6) === 'array_') {
|
2016-11-04 01:51:56 +01:00
|
|
|
$array_return_type = self::getArrayReturnType(
|
2017-08-15 01:30:11 +02:00
|
|
|
$statements_checker,
|
2016-11-04 01:51:56 +01:00
|
|
|
$call_map_key,
|
|
|
|
$call_args,
|
2016-12-04 01:11:30 +01:00
|
|
|
$code_location,
|
2016-11-04 01:51:56 +01:00
|
|
|
$suppressed_issues
|
|
|
|
);
|
2016-11-02 14:24:36 +01:00
|
|
|
|
|
|
|
if ($array_return_type) {
|
|
|
|
return $array_return_type;
|
2016-08-30 06:05:13 +02:00
|
|
|
}
|
2016-11-02 14:24:36 +01:00
|
|
|
}
|
2016-08-30 06:05:13 +02:00
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
if ($call_map_key === 'explode' || $call_map_key === 'preg_split') {
|
|
|
|
return Type::parseString('array<int, string>');
|
|
|
|
}
|
2016-12-17 00:56:23 +01:00
|
|
|
|
|
|
|
if ($call_map_key === 'min' || $call_map_key === 'max') {
|
|
|
|
if (isset($call_args[0])) {
|
|
|
|
$first_arg = $call_args[0]->value;
|
|
|
|
|
2016-12-31 02:05:32 +01:00
|
|
|
if (isset($first_arg->inferredType)) {
|
2016-12-17 00:56:23 +01:00
|
|
|
if ($first_arg->inferredType->hasArray()) {
|
2018-01-09 21:05:48 +01:00
|
|
|
$array_type = $first_arg->inferredType->getTypes()['array'];
|
2017-01-15 01:06:58 +01:00
|
|
|
if ($array_type instanceof Type\Atomic\ObjectLike) {
|
2017-12-19 00:47:17 +01:00
|
|
|
return $array_type->getGenericValueType();
|
2016-12-17 00:56:23 +01:00
|
|
|
}
|
|
|
|
|
2017-01-15 01:06:58 +01:00
|
|
|
if ($array_type instanceof Type\Atomic\TArray) {
|
2016-12-17 00:56:23 +01:00
|
|
|
return clone $array_type->type_params[1];
|
|
|
|
}
|
|
|
|
} elseif ($first_arg->inferredType->hasScalarType() &&
|
|
|
|
($second_arg = $call_args[1]->value) &&
|
2016-12-31 02:39:12 +01:00
|
|
|
isset($second_arg->inferredType) &&
|
2016-12-17 00:56:23 +01:00
|
|
|
$second_arg->inferredType->hasScalarType()
|
|
|
|
) {
|
|
|
|
return Type::combineUnionTypes($first_arg->inferredType, $second_arg->inferredType);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-11-02 14:24:36 +01:00
|
|
|
}
|
2016-10-26 23:51:34 +02:00
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
if (!$call_map[$call_map_key][0]) {
|
|
|
|
return Type::getMixed();
|
|
|
|
}
|
2016-10-26 23:51:34 +02:00
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
return Type::parseString($call_map[$call_map_key][0]);
|
|
|
|
}
|
2016-10-26 23:51:34 +02:00
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
/**
|
|
|
|
* @param string $call_map_key
|
|
|
|
* @param array<PhpParser\Node\Arg> $call_args
|
2016-12-04 01:11:30 +01:00
|
|
|
* @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|null
|
|
|
|
*/
|
2016-11-04 01:51:56 +01:00
|
|
|
protected static function getArrayReturnType(
|
2017-08-15 01:30:11 +02:00
|
|
|
StatementsChecker $statements_checker,
|
2016-11-04 01:51:56 +01:00
|
|
|
$call_map_key,
|
|
|
|
$call_args,
|
2016-12-04 01:11:30 +01:00
|
|
|
CodeLocation $code_location,
|
2016-11-04 01:51:56 +01:00
|
|
|
array $suppressed_issues
|
|
|
|
) {
|
2016-12-25 02:33:14 +01:00
|
|
|
if ($call_map_key === 'array_map') {
|
2017-08-15 01:30:11 +02:00
|
|
|
return self::getArrayMapReturnType(
|
|
|
|
$statements_checker,
|
2018-01-17 22:07:46 +01:00
|
|
|
$call_args,
|
|
|
|
$code_location,
|
|
|
|
$suppressed_issues
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($call_map_key === 'array_filter') {
|
|
|
|
return self::getArrayFilterReturnType(
|
|
|
|
$statements_checker,
|
2017-08-15 01:30:11 +02:00
|
|
|
$call_args,
|
|
|
|
$code_location,
|
|
|
|
$suppressed_issues
|
|
|
|
);
|
2016-11-02 14:24:36 +01:00
|
|
|
}
|
2016-08-22 21:00:12 +02:00
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
$first_arg = isset($call_args[0]->value) ? $call_args[0]->value : null;
|
2017-01-17 17:17:49 +01:00
|
|
|
$second_arg = isset($call_args[1]->value) ? $call_args[1]->value : null;
|
2016-10-14 06:53:43 +02:00
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
if ($call_map_key === 'array_merge') {
|
|
|
|
$inner_value_types = [];
|
|
|
|
$inner_key_types = [];
|
2016-08-22 21:00:12 +02:00
|
|
|
|
2017-11-11 20:19:45 +01:00
|
|
|
$generic_properties = [];
|
|
|
|
|
2017-11-09 05:14:27 +01:00
|
|
|
foreach ($call_args as $call_arg) {
|
2016-11-02 14:24:36 +01:00
|
|
|
if (!isset($call_arg->value->inferredType)) {
|
|
|
|
return Type::getArray();
|
|
|
|
}
|
2016-08-22 21:00:12 +02:00
|
|
|
|
2018-01-09 21:05:48 +01:00
|
|
|
foreach ($call_arg->value->inferredType->getTypes() as $type_part) {
|
2017-12-23 02:09:58 +01:00
|
|
|
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 {
|
|
|
|
return Type::getArray();
|
2017-11-11 20:19:45 +01:00
|
|
|
}
|
2017-05-06 00:53:45 +02:00
|
|
|
} else {
|
2017-12-23 02:09:58 +01:00
|
|
|
$type_part_value_type = $type_part->type_params[0];
|
2017-05-06 00:53:45 +02:00
|
|
|
}
|
2016-08-22 21:00:12 +02:00
|
|
|
|
2017-12-23 02:09:58 +01:00
|
|
|
$unpacked_type_parts = [];
|
|
|
|
|
2018-01-09 21:05:48 +01:00
|
|
|
foreach ($type_part_value_type->getTypes() as $value_type_part) {
|
2017-12-23 02:09:58 +01:00
|
|
|
$unpacked_type_parts[] = $value_type_part;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$unpacked_type_parts = [$type_part];
|
2016-08-22 21:00:12 +02:00
|
|
|
}
|
|
|
|
|
2017-12-23 02:09:58 +01: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
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$unpacked_type_part = $unpacked_type_part->getGenericArrayType();
|
|
|
|
} else {
|
|
|
|
return Type::getArray();
|
|
|
|
}
|
|
|
|
} elseif (!$unpacked_type_part->type_params[0]->isEmpty()) {
|
|
|
|
$generic_properties = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($unpacked_type_part->type_params[1]->isEmpty()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$inner_key_types = array_merge(
|
|
|
|
$inner_key_types,
|
2018-01-09 21:05:48 +01:00
|
|
|
array_values($unpacked_type_part->type_params[0]->getTypes())
|
2017-12-23 02:09:58 +01:00
|
|
|
);
|
|
|
|
$inner_value_types = array_merge(
|
|
|
|
$inner_value_types,
|
2018-01-09 21:05:48 +01:00
|
|
|
array_values($unpacked_type_part->type_params[1]->getTypes())
|
2017-12-23 02:09:58 +01:00
|
|
|
);
|
|
|
|
}
|
2016-08-22 21:00:12 +02:00
|
|
|
}
|
2017-01-19 07:32:35 +01:00
|
|
|
}
|
2016-08-22 21:00:12 +02:00
|
|
|
|
2017-11-11 20:19:45 +01:00
|
|
|
if ($generic_properties) {
|
|
|
|
return new Type\Union([
|
|
|
|
new Type\Atomic\ObjectLike($generic_properties),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2017-01-19 07:32:35 +01:00
|
|
|
if ($inner_value_types) {
|
|
|
|
return new Type\Union([
|
|
|
|
new Type\Atomic\TArray([
|
|
|
|
Type::combineTypes($inner_key_types),
|
2017-05-27 02:05:57 +02:00
|
|
|
Type::combineTypes($inner_value_types),
|
|
|
|
]),
|
2017-01-19 07:32:35 +01:00
|
|
|
]);
|
2016-11-02 14:24:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return Type::getArray();
|
|
|
|
}
|
|
|
|
|
2017-11-09 03:56:54 +01:00
|
|
|
if ($call_map_key === 'array_rand') {
|
|
|
|
$first_arg_array = $first_arg
|
|
|
|
&& isset($first_arg->inferredType)
|
2018-01-09 21:05:48 +01:00
|
|
|
&& $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
|
2017-11-09 03:56:54 +01:00
|
|
|
: null;
|
|
|
|
|
|
|
|
if (!$first_arg_array) {
|
|
|
|
return Type::getMixed();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($first_arg_array instanceof Type\Atomic\TArray) {
|
|
|
|
$key_type = clone $first_arg_array->type_params[0];
|
|
|
|
} else {
|
2017-12-19 00:47:17 +01:00
|
|
|
$key_type = $first_arg_array->getGenericKeyType();
|
2017-11-09 03:56:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
]),
|
|
|
|
]);
|
|
|
|
|
|
|
|
if ($second_arg instanceof PhpParser\Node\Scalar\LNumber) {
|
|
|
|
return $arr_type;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Type::combineUnionTypes($key_type, $arr_type);
|
|
|
|
}
|
|
|
|
|
2016-11-04 01:51:56 +01:00
|
|
|
return null;
|
2016-11-02 14:24:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array<PhpParser\Node\Arg> $call_args
|
2016-12-04 01:11:30 +01:00
|
|
|
* @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
|
|
|
|
*/
|
2016-11-04 01:51:56 +01:00
|
|
|
protected static function getArrayMapReturnType(
|
2017-08-15 01:30:11 +02:00
|
|
|
StatementsChecker $statements_checker,
|
2016-11-04 01:51:56 +01:00
|
|
|
$call_args,
|
2016-12-04 01:11:30 +01:00
|
|
|
CodeLocation $code_location,
|
2016-11-04 01:51:56 +01:00
|
|
|
array $suppressed_issues
|
|
|
|
) {
|
2018-01-17 22:07:46 +01:00
|
|
|
$array_arg = isset($call_args[1]->value) ? $call_args[1]->value : null;
|
2016-11-02 14:24:36 +01:00
|
|
|
|
|
|
|
$array_arg_type = $array_arg
|
2016-11-02 07:29:00 +01:00
|
|
|
&& isset($array_arg->inferredType)
|
2018-01-09 21:05:48 +01:00
|
|
|
&& isset($array_arg->inferredType->getTypes()['array'])
|
|
|
|
&& ($array_atomic_type = $array_arg->inferredType->getTypes()['array'])
|
|
|
|
&& $array_atomic_type instanceof Type\Atomic\TArray
|
|
|
|
? $array_atomic_type
|
2016-11-02 07:29:00 +01:00
|
|
|
: null;
|
2016-11-02 14:24:36 +01:00
|
|
|
|
2018-01-17 22:07:46 +01:00
|
|
|
if (isset($call_args[0])) {
|
|
|
|
$function_call_arg = $call_args[0];
|
2016-11-02 14:24:36 +01:00
|
|
|
|
2018-01-09 21:05:48 +01:00
|
|
|
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
|
2016-12-07 20:13:39 +01:00
|
|
|
) {
|
2018-01-09 21:05:48 +01:00
|
|
|
$closure_return_type = $closure_atomic_type->return_type;
|
2016-11-02 14:24:36 +01:00
|
|
|
|
2016-12-07 20:13:39 +01:00
|
|
|
if ($closure_return_type->isVoid()) {
|
2016-11-02 14:24:36 +01:00
|
|
|
IssueBuffer::accepts(
|
|
|
|
new InvalidReturnType(
|
2018-01-17 22:07:46 +01:00
|
|
|
'No return type could be found in the closure passed to array_map',
|
2016-12-04 01:11:30 +01:00
|
|
|
$code_location
|
2016-11-02 14:24:36 +01:00
|
|
|
),
|
|
|
|
$suppressed_issues
|
|
|
|
);
|
|
|
|
|
2016-10-22 23:35:59 +02:00
|
|
|
return Type::getArray();
|
|
|
|
}
|
2016-10-09 23:54:58 +02:00
|
|
|
|
2016-12-24 19:23:22 +01:00
|
|
|
$key_type = $array_arg_type ? clone $array_arg_type->type_params[0] : Type::getMixed();
|
|
|
|
|
2018-01-17 22:07:46 +01:00
|
|
|
$inner_type = clone $closure_return_type;
|
2017-05-25 04:07:49 +02:00
|
|
|
|
2018-01-17 22:07:46 +01:00
|
|
|
return new Type\Union([
|
|
|
|
new Type\Atomic\TArray([
|
|
|
|
$key_type,
|
|
|
|
$inner_type,
|
|
|
|
]),
|
|
|
|
]);
|
2017-08-15 01:30:11 +02:00
|
|
|
} 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
|
|
|
|
);
|
2016-10-09 23:54:58 +02:00
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
$call_map = self::getCallMap();
|
2016-10-12 07:37:32 +02:00
|
|
|
|
2017-08-15 01:30:11 +02:00
|
|
|
$mapping_return_type = null;
|
|
|
|
|
|
|
|
$project_checker = $statements_checker->getFileChecker()->project_checker;
|
2018-01-21 19:38:51 +01:00
|
|
|
$codebase = $project_checker->codebase;
|
2017-08-15 01:30:11 +02:00
|
|
|
|
|
|
|
foreach ($mapping_function_ids as $mapping_function_id) {
|
|
|
|
if (isset($call_map[$mapping_function_id][0])) {
|
|
|
|
if ($call_map[$mapping_function_id][0]) {
|
|
|
|
$mapped_function_return = Type::parseString($call_map[$mapping_function_id][0]);
|
2017-05-25 04:07:49 +02:00
|
|
|
|
2017-08-15 01:30:11 +02:00
|
|
|
if ($mapping_return_type) {
|
|
|
|
$mapping_return_type = Type::combineUnionTypes(
|
|
|
|
$mapping_return_type,
|
|
|
|
$mapped_function_return
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$mapping_return_type = $mapped_function_return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (strpos($mapping_function_id, '::') !== false) {
|
2017-08-18 23:02:15 +02:00
|
|
|
list($callable_fq_class_name) = explode('::', $mapping_function_id);
|
|
|
|
|
2017-08-22 18:38:38 +02:00
|
|
|
if (in_array($callable_fq_class_name, ['self', 'static', 'parent'], true)) {
|
2017-08-18 23:02:15 +02:00
|
|
|
$mapping_return_type = Type::getMixed();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2017-10-20 00:23:18 +02:00
|
|
|
if (!MethodChecker::methodExists($project_checker, $mapping_function_id)) {
|
|
|
|
$mapping_return_type = Type::getMixed();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2017-08-15 01:30:11 +02:00
|
|
|
$return_type = MethodChecker::getMethodReturnType(
|
|
|
|
$project_checker,
|
|
|
|
$mapping_function_id
|
|
|
|
) ?: Type::getMixed();
|
|
|
|
|
|
|
|
if ($mapping_return_type) {
|
|
|
|
$mapping_return_type = Type::combineUnionTypes(
|
|
|
|
$mapping_return_type,
|
|
|
|
$return_type
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$mapping_return_type = $return_type;
|
|
|
|
}
|
|
|
|
} else {
|
2018-01-21 19:38:51 +01:00
|
|
|
if (!$codebase->functionExists($statements_checker, $mapping_function_id)) {
|
2017-10-20 00:23:18 +02:00
|
|
|
$mapping_return_type = Type::getMixed();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
$function_storage = $codebase->getFunctionStorage(
|
2017-08-15 01:30:11 +02:00
|
|
|
$statements_checker,
|
|
|
|
$mapping_function_id
|
|
|
|
);
|
|
|
|
|
|
|
|
$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
|
|
|
}
|
|
|
|
}
|
2017-08-15 01:30:11 +02:00
|
|
|
|
|
|
|
if ($mapping_return_type) {
|
|
|
|
return new Type\Union([
|
|
|
|
new Type\Atomic\TArray([
|
|
|
|
Type::getInt(),
|
|
|
|
$mapping_return_type,
|
|
|
|
]),
|
|
|
|
]);
|
|
|
|
}
|
2016-10-22 23:35:59 +02:00
|
|
|
}
|
2016-10-12 07:37:32 +02:00
|
|
|
}
|
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
return Type::getArray();
|
2016-08-22 21:00:12 +02:00
|
|
|
}
|
|
|
|
|
2018-01-17 22:07:46 +01:00
|
|
|
/**
|
|
|
|
* @param array<PhpParser\Node\Arg> $call_args
|
|
|
|
* @param CodeLocation $code_location
|
|
|
|
* @param array $suppressed_issues
|
|
|
|
*
|
|
|
|
* @return Type\Union
|
|
|
|
*/
|
|
|
|
protected 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;
|
|
|
|
|
|
|
|
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])
|
|
|
|
&& $first_param->variadic === false
|
|
|
|
&& ($stmt = $function_call_arg->value->stmts[0])
|
|
|
|
&& $stmt instanceof PhpParser\Node\Stmt\Return_
|
|
|
|
&& $stmt->expr
|
|
|
|
) {
|
|
|
|
$assertions = AssertionFinder::getAssertions($stmt->expr, null, $statements_checker);
|
|
|
|
|
|
|
|
if (isset($assertions['$' . $first_param->name])) {
|
|
|
|
$changed_var_ids = [];
|
|
|
|
|
|
|
|
$reconciled_types = Reconciler::reconcileKeyedTypes(
|
|
|
|
['$inner_type' => $assertions['$' . $first_param->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-08-22 21:00:12 +02:00
|
|
|
/**
|
|
|
|
* Gets the method/function call map
|
|
|
|
*
|
2017-11-16 02:45:53 +01:00
|
|
|
* @return array<string, array<int|string, string>>
|
2016-11-05 23:46:17 +01:00
|
|
|
* @psalm-suppress MixedInferredReturnType as the use of require buggers things up
|
2017-01-09 05:58:06 +01:00
|
|
|
* @psalm-suppress MixedAssignment
|
2016-08-22 21:00:12 +02:00
|
|
|
*/
|
|
|
|
protected static function getCallMap()
|
|
|
|
{
|
|
|
|
if (self::$call_map !== null) {
|
|
|
|
return self::$call_map;
|
|
|
|
}
|
|
|
|
|
2017-01-17 02:00:51 +01:00
|
|
|
/** @var array<string, array<string, string>> */
|
2017-05-25 04:07:49 +02:00
|
|
|
$call_map = require_once(__DIR__ . '/../CallMap.php');
|
2016-08-22 21:00:12 +02:00
|
|
|
|
|
|
|
self::$call_map = [];
|
|
|
|
|
|
|
|
foreach ($call_map as $key => $value) {
|
2016-10-22 23:35:59 +02:00
|
|
|
$cased_key = strtolower($key);
|
|
|
|
self::$call_map[$cased_key] = $value;
|
2016-08-22 21:00:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return self::$call_map;
|
|
|
|
}
|
2016-10-21 00:12:13 +02:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
|
|
|
* @param string $key
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-11-02 07:29:00 +01:00
|
|
|
* @return bool
|
|
|
|
*/
|
2016-10-22 23:35:59 +02:00
|
|
|
public static function inCallMap($key)
|
|
|
|
{
|
|
|
|
return isset(self::getCallMap()[strtolower($key)]);
|
|
|
|
}
|
|
|
|
|
2017-01-12 15:42:24 +01:00
|
|
|
/**
|
|
|
|
* @param string $function_name
|
|
|
|
* @param StatementsSource $source
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2017-01-12 15:42:24 +01:00
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function getFQFunctionNameFromString($function_name, StatementsSource $source)
|
|
|
|
{
|
|
|
|
if (empty($function_name)) {
|
|
|
|
throw new \InvalidArgumentException('$function_name cannot be empty');
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($function_name[0] === '\\') {
|
|
|
|
return substr($function_name, 1);
|
|
|
|
}
|
|
|
|
|
2017-04-28 06:31:55 +02:00
|
|
|
$function_name_lcase = strtolower($function_name);
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
$aliases = $source->getAliases();
|
|
|
|
|
|
|
|
$imported_function_namespaces = $aliases->functions;
|
|
|
|
$imported_namespaces = $aliases->uses;
|
2017-01-12 15:42:24 +01:00
|
|
|
|
|
|
|
if (strpos($function_name, '\\') !== false) {
|
|
|
|
$function_name_parts = explode('\\', $function_name);
|
|
|
|
$first_namespace = array_shift($function_name_parts);
|
2017-04-28 06:31:55 +02:00
|
|
|
$first_namespace_lcase = strtolower($first_namespace);
|
2017-01-12 15:42:24 +01:00
|
|
|
|
2017-04-28 06:31:55 +02:00
|
|
|
if (isset($imported_namespaces[$first_namespace_lcase])) {
|
|
|
|
return $imported_namespaces[$first_namespace_lcase] . '\\' . implode('\\', $function_name_parts);
|
2017-01-12 15:42:24 +01:00
|
|
|
}
|
2017-01-16 06:49:12 +01:00
|
|
|
|
2017-04-28 06:31:55 +02:00
|
|
|
if (isset($imported_function_namespaces[$first_namespace_lcase])) {
|
|
|
|
return $imported_function_namespaces[$first_namespace_lcase] . '\\' .
|
|
|
|
implode('\\', $function_name_parts);
|
2017-01-16 06:49:12 +01:00
|
|
|
}
|
2017-04-28 06:31:55 +02:00
|
|
|
} elseif (isset($imported_namespaces[$function_name_lcase])) {
|
|
|
|
return $imported_namespaces[$function_name_lcase];
|
|
|
|
} elseif (isset($imported_function_namespaces[$function_name_lcase])) {
|
|
|
|
return $imported_function_namespaces[$function_name_lcase];
|
2017-01-12 15:42:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$namespace = $source->getNamespace();
|
|
|
|
|
|
|
|
return ($namespace ? $namespace . '\\' : '') . $function_name;
|
|
|
|
}
|
2016-01-08 00:28:27 +01:00
|
|
|
}
|