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;
|
2016-08-13 20:20:46 +02:00
|
|
|
use Psalm\StatementsSource;
|
2016-08-14 01:44:24 +02:00
|
|
|
use Psalm\Config;
|
2016-08-14 05:26:45 +02:00
|
|
|
use Psalm\Type;
|
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-08-14 00:54:49 +02:00
|
|
|
protected static $function_return_types = [];
|
|
|
|
protected static $function_namespaces = [];
|
|
|
|
protected static $existing_functions = [];
|
|
|
|
protected static $deprecated_functions = [];
|
|
|
|
protected static $have_registered_function = [];
|
2016-08-14 05:26:45 +02:00
|
|
|
protected static $file_function_params = [];
|
|
|
|
protected static $builtin_function_params = [];
|
|
|
|
protected static $builtin_functions = [];
|
2016-08-22 21:00:12 +02:00
|
|
|
protected static $call_map = null;
|
2016-05-16 22:12:02 +02:00
|
|
|
|
2016-06-16 02:16:40 +02:00
|
|
|
/**
|
2016-08-14 05:26:45 +02:00
|
|
|
* @param PhpParser\Node\Stmt\Function_ $function
|
|
|
|
* @param StatementsSource $source
|
|
|
|
* @param string $base_file_name
|
2016-06-16 02:16:40 +02:00
|
|
|
*/
|
2016-08-14 05:26:45 +02:00
|
|
|
public function __construct(PhpParser\Node\Stmt\Function_ $function, StatementsSource $source, $base_file_name)
|
2016-05-16 22:12:02 +02:00
|
|
|
{
|
2016-08-14 05:26:45 +02:00
|
|
|
parent::__construct($function, $source);
|
2016-05-16 22:12:02 +02:00
|
|
|
|
2016-08-14 05:26:45 +02:00
|
|
|
$this->registerFunction($function, $base_file_name);
|
2016-01-08 00:28:27 +01:00
|
|
|
}
|
|
|
|
|
2016-08-14 05:26:45 +02:00
|
|
|
public static function functionExists($function_id, $file_name)
|
2016-02-27 01:11:11 +01:00
|
|
|
{
|
2016-08-14 05:26:45 +02:00
|
|
|
if (isset(self::$existing_functions[$file_name][$function_id])) {
|
|
|
|
return true;
|
2016-05-09 14:56:07 +02:00
|
|
|
}
|
|
|
|
|
2016-08-14 05:26:45 +02:00
|
|
|
if (strpos($function_id, '::') !== false) {
|
2016-08-15 19:37:21 +02:00
|
|
|
$function_id = strtolower(preg_replace('/^[^:]+::/', '', $function_id));
|
2016-08-14 05:26:45 +02:00
|
|
|
}
|
2016-05-16 05:06:03 +02:00
|
|
|
|
2016-08-14 05:26:45 +02:00
|
|
|
if (!isset(self::$builtin_functions[$function_id])) {
|
|
|
|
self::extractReflectionInfo($function_id);
|
|
|
|
}
|
2016-07-22 19:29:46 +02:00
|
|
|
|
2016-08-14 05:26:45 +02:00
|
|
|
return self::$builtin_functions[$function_id];
|
2016-07-22 19:29:46 +02:00
|
|
|
}
|
2016-08-11 01:21:03 +02:00
|
|
|
|
2016-08-14 05:26:45 +02:00
|
|
|
public static function getParams($function_id, $file_name)
|
2016-08-11 01:21:03 +02:00
|
|
|
{
|
2016-08-14 05:26:45 +02:00
|
|
|
if (isset(self::$builtin_functions[$function_id]) && self::$builtin_functions[$function_id]) {
|
|
|
|
return self::$builtin_function_params[$function_id];
|
2016-08-11 01:21:03 +02:00
|
|
|
}
|
|
|
|
|
2016-08-14 05:26:45 +02:00
|
|
|
return self::$file_function_params[$file_name][$function_id];
|
2016-08-11 01:21:03 +02:00
|
|
|
}
|
|
|
|
|
2016-08-14 05:26:45 +02:00
|
|
|
protected static function extractReflectionInfo($function_id)
|
2016-08-11 01:21:03 +02:00
|
|
|
{
|
2016-08-14 05:26:45 +02:00
|
|
|
try {
|
|
|
|
$reflection_function = new \ReflectionFunction($function_id);
|
2016-08-11 01:21:03 +02:00
|
|
|
|
2016-08-14 05:26:45 +02:00
|
|
|
$reflection_params = $reflection_function->getParameters();
|
2016-08-11 01:21:03 +02:00
|
|
|
|
2016-08-14 05:26:45 +02:00
|
|
|
self::$builtin_function_params[$function_id] = [];
|
2016-08-11 01:21:03 +02:00
|
|
|
|
2016-08-14 05:26:45 +02:00
|
|
|
foreach ($reflection_params as $param) {
|
|
|
|
self::$builtin_function_params[$function_id][] = self::getReflectionParamArray($param);
|
2016-08-11 01:21:03 +02:00
|
|
|
}
|
|
|
|
|
2016-08-14 05:26:45 +02:00
|
|
|
self::$builtin_functions[$function_id] = true;
|
|
|
|
}
|
|
|
|
catch (\ReflectionException $e) {
|
|
|
|
self::$builtin_functions[$function_id] = false;
|
2016-08-11 01:21:03 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-14 19:13:53 +02:00
|
|
|
public static function getFunctionReturnTypes($function_id, $file_name)
|
|
|
|
{
|
|
|
|
if (!isset(self::$function_return_types[$file_name][$function_id])) {
|
|
|
|
throw new \InvalidArgumentException('Do not know function');
|
|
|
|
}
|
|
|
|
|
|
|
|
return self::$function_return_types[$file_name][$function_id]
|
|
|
|
? clone self::$function_return_types[$file_name][$function_id]
|
|
|
|
: null;
|
|
|
|
}
|
|
|
|
|
2016-08-14 03:14:32 +02:00
|
|
|
protected function registerFunction(PhpParser\Node\Stmt\Function_ $function, $file_name)
|
2016-08-11 01:21:03 +02:00
|
|
|
{
|
2016-08-15 19:37:21 +02:00
|
|
|
$function_id = strtolower($function->name);
|
2016-08-11 01:21:03 +02:00
|
|
|
|
2016-08-14 03:14:32 +02:00
|
|
|
if (isset(self::$have_registered_function[$file_name][$function_id])) {
|
2016-08-15 05:24:16 +02:00
|
|
|
return;
|
2016-08-11 01:21:03 +02:00
|
|
|
}
|
|
|
|
|
2016-08-14 03:14:32 +02:00
|
|
|
self::$have_registered_function[$file_name][$function_id] = true;
|
2016-08-11 01:21:03 +02:00
|
|
|
|
2016-08-14 03:14:32 +02:00
|
|
|
self::$function_namespaces[$file_name][$function_id] = $this->namespace;
|
2016-08-14 05:26:45 +02:00
|
|
|
self::$existing_functions[$file_name][$function_id] = true;
|
2016-08-11 01:21:03 +02:00
|
|
|
|
2016-08-14 05:26:45 +02:00
|
|
|
self::$file_function_params[$file_name][$function_id] = [];
|
2016-08-11 01:21:03 +02:00
|
|
|
|
|
|
|
$function_param_names = [];
|
|
|
|
|
|
|
|
foreach ($function->getParams() as $param) {
|
|
|
|
$param_array = $this->getParamArray($param);
|
2016-08-14 05:26:45 +02:00
|
|
|
self::$file_function_params[$file_name][$function_id][] = $param_array;
|
2016-08-11 01:21:03 +02:00
|
|
|
$function_param_names[$param->name] = $param_array['type'];
|
|
|
|
}
|
|
|
|
|
|
|
|
$config = Config::getInstance();
|
|
|
|
$return_type = null;
|
|
|
|
|
|
|
|
$docblock_info = CommentChecker::extractDocblockInfo($function->getDocComment());
|
|
|
|
|
|
|
|
if ($docblock_info['deprecated']) {
|
2016-08-14 03:14:32 +02:00
|
|
|
self::$deprecated_functions[$file_name][$function_id] = true;
|
2016-08-11 01:21:03 +02:00
|
|
|
}
|
|
|
|
|
2016-08-14 00:54:49 +02:00
|
|
|
$this->suppressed_issues = $docblock_info['suppress'];
|
2016-08-11 01:21:03 +02:00
|
|
|
|
|
|
|
if ($config->use_docblock_types) {
|
|
|
|
if ($docblock_info['return_type']) {
|
|
|
|
$return_type =
|
|
|
|
Type::parseString(
|
|
|
|
self::fixUpLocalType(
|
|
|
|
$docblock_info['return_type'],
|
|
|
|
null,
|
2016-08-14 00:54:49 +02:00
|
|
|
$this->namespace,
|
2016-08-15 07:21:50 +02:00
|
|
|
$this->getAliasedClasses()
|
2016-08-11 01:21:03 +02:00
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($docblock_info['params']) {
|
|
|
|
$this->improveParamsFromDocblock(
|
|
|
|
$docblock_info['params'],
|
|
|
|
$function_param_names,
|
2016-08-14 05:26:45 +02:00
|
|
|
self::$file_function_params[$file_name][$function_id],
|
2016-08-11 01:21:03 +02:00
|
|
|
$function->getLine()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-14 03:14:32 +02:00
|
|
|
self::$function_return_types[$file_name][$function_id] = $return_type;
|
2016-08-11 01:21:03 +02:00
|
|
|
}
|
2016-08-22 21:00:12 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $function_id
|
|
|
|
* @return array<array<Type\Union>>|null
|
|
|
|
*/
|
|
|
|
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];
|
|
|
|
|
|
|
|
for ($i = 1; $i < 10; $i++) {
|
|
|
|
if (isset($call_map[$call_map_key . '\'' . $i])) {
|
|
|
|
$call_map_functions[] = $call_map[$call_map_key . '\'' . $i];
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$function_type_options = [];
|
|
|
|
|
|
|
|
foreach ($call_map_functions as $call_map_function_args) {
|
|
|
|
array_shift($call_map_function_args);
|
|
|
|
|
|
|
|
$function_types = [];
|
|
|
|
|
|
|
|
foreach ($call_map_function_args as $arg_name => $arg_type) {
|
|
|
|
$by_reference = false;
|
|
|
|
|
|
|
|
if ($arg_name[0] === '&') {
|
|
|
|
$arg_name = substr($arg_name, 1);
|
|
|
|
$by_reference = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
$function_types[] = [
|
|
|
|
'name' => $arg_name,
|
|
|
|
'by_ref' => $by_reference,
|
|
|
|
'type' => $arg_type ? Type::parseString($arg_type) : Type::getMixed(),
|
|
|
|
'is_optional' => true, // @todo - need to have non-optional parameters
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
$function_type_options[] = $function_types;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $function_type_options;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function getReturnTypeFromCallMap($function_id, array $call_args)
|
|
|
|
{
|
|
|
|
$call_map_key = strtolower($function_id);
|
|
|
|
|
|
|
|
if (in_array($call_map_key, ['str_replace', 'preg_replace', 'preg_replace_callback'])) {
|
|
|
|
if (isset($call_args[2]->value->inferredType)) {
|
|
|
|
|
|
|
|
$subject_type = $call_args[2]->value->inferredType;
|
|
|
|
|
|
|
|
if (!$subject_type->isString() && $subject_type->isArray()) {
|
|
|
|
return Type::getArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
return Type::getString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (in_array($call_map_key, ['pathinfo'])) {
|
|
|
|
if (isset($call_args[1])) {
|
|
|
|
return Type::getString();
|
|
|
|
}
|
|
|
|
|
|
|
|
return Type::getArray();
|
|
|
|
}
|
|
|
|
|
2016-08-30 06:05:13 +02:00
|
|
|
$call_map = self::getCallMap();
|
|
|
|
|
2016-08-22 21:00:12 +02:00
|
|
|
if ($call_map_key === 'array_map') {
|
2016-08-30 06:05:13 +02:00
|
|
|
if (isset($call_args[0])) {
|
|
|
|
if ($call_args[0]->value instanceof PhpParser\Node\Expr\Closure) {
|
|
|
|
$closure_return_types = \Psalm\EffectsAnalyser::getReturnTypes($call_args[0]->value->stmts, true);
|
2016-08-22 21:00:12 +02:00
|
|
|
|
2016-08-30 06:05:13 +02:00
|
|
|
if (!$closure_return_types) {
|
|
|
|
// @todo report issue
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$inner_type = new Type\Union($closure_return_types);
|
2016-08-22 21:00:12 +02:00
|
|
|
|
2016-08-30 06:05:13 +02:00
|
|
|
return new Type\Union([new Type\Generic('array', [$inner_type])]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elseif ($call_args[0]->value instanceof PhpParser\Node\Scalar\String_) {
|
|
|
|
$mapped_function_id = strtolower($call_args[0]->value->value);
|
|
|
|
|
|
|
|
if (isset($call_map[$mapped_function_id][0])) {
|
|
|
|
if ($call_map[$mapped_function_id][0]) {
|
|
|
|
$mapped_function_return = Type::parseString($call_map[$mapped_function_id][0]);
|
|
|
|
return new Type\Union([new Type\Generic('array', [$mapped_function_return])]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// @todo handle array_map('some_custom_function', $arr)
|
|
|
|
}
|
2016-08-22 21:00:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Type::getArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (in_array($call_map_key, ['array_filter', 'array_values'])) {
|
|
|
|
if (isset($call_args[0]->value->inferredType) && $call_args[0]->value->inferredType->isArray()) {
|
|
|
|
return clone $call_args[0]->value->inferredType;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($call_map_key === 'array_merge') {
|
|
|
|
$inner_types = [];
|
|
|
|
|
|
|
|
foreach ($call_args as $call_arg) {
|
|
|
|
if (!isset($call_arg->value->inferredType)) {
|
|
|
|
return Type::getArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($call_arg->value->inferredType->types as $type_part) {
|
|
|
|
if (!$type_part instanceof Type\Generic) {
|
|
|
|
return Type::getArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($type_part->type_params[0]->isEmpty()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$inner_types = array_merge(array_values($type_part->type_params[0]->types), $inner_types);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($inner_types) {
|
|
|
|
return new Type\Union([
|
|
|
|
new Type\Generic('array',
|
|
|
|
[Type::combineTypes($inner_types)]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Type::getArray();
|
|
|
|
}
|
|
|
|
|
2016-08-23 00:09:52 +02:00
|
|
|
if ($call_map_key === 'explode') {
|
|
|
|
return Type::parseString('array<string>');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isset($call_map[$call_map_key]) || !$call_map[$call_map_key][0]) {
|
|
|
|
return Type::getMixed();
|
|
|
|
}
|
|
|
|
|
2016-08-22 21:00:12 +02:00
|
|
|
return Type::parseString($call_map[$call_map_key][0]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the method/function call map
|
|
|
|
*
|
|
|
|
* @return array<array<string>>
|
|
|
|
*/
|
|
|
|
protected static function getCallMap()
|
|
|
|
{
|
|
|
|
if (self::$call_map !== null) {
|
|
|
|
return self::$call_map;
|
|
|
|
}
|
|
|
|
|
|
|
|
$call_map = require_once(__DIR__.'/../CallMap.php');
|
|
|
|
|
|
|
|
self::$call_map = [];
|
|
|
|
|
|
|
|
foreach ($call_map as $key => $value) {
|
|
|
|
self::$call_map[strtolower($key)] = $value;
|
|
|
|
}
|
|
|
|
|
|
|
|
return self::$call_map;
|
|
|
|
}
|
2016-01-08 00:28:27 +01:00
|
|
|
}
|