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

487 lines
17 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\StatementsSource;
use Psalm\Config;
use Psalm\FunctionLikeParameter;
use Psalm\IssueBuffer;
2016-10-14 06:53:43 +02:00
use Psalm\Issue\InvalidReturnType;
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
{
protected static $function_return_types = [];
protected static $function_namespaces = [];
protected static $existing_functions = [];
protected static $deprecated_functions = [];
protected static $have_registered_function = [];
2016-10-14 06:53:43 +02:00
/**
* @var array<string,array<string,array<FunctionLikeParameter>>>
*/
2016-08-14 05:26:45 +02:00
protected static $file_function_params = [];
/**
* @var array<string,bool>
*/
protected static $variadic_functions = [];
2016-08-14 05:26:45 +02:00
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-10-14 06:53:43 +02:00
/**
* @param string $function_id
* @param string $file_name
* @return boolean
*/
2016-08-14 05:26:45 +02:00
public static function functionExists($function_id, $file_name)
{
2016-08-14 05:26:45 +02:00
if (isset(self::$existing_functions[$file_name][$function_id])) {
return true;
}
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-08-14 05:26:45 +02:00
if (!isset(self::$builtin_functions[$function_id])) {
self::extractReflectionInfo($function_id);
}
2016-08-14 05:26:45 +02:00
return self::$builtin_functions[$function_id];
}
2016-08-11 01:21:03 +02:00
2016-10-15 06:12:57 +02:00
/**
* @param string $function_id
* @param string $file_name
* @return array<FunctionLikeParameter>
*/
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
}
/**
* @param string $function_id
* @param string $file_name
* @return boolean
*/
public static function isVariadic($function_id, $file_name)
{
return isset(self::$variadic_functions[$file_name][$function_id]);
}
2016-10-14 06:53:43 +02:00
/**
* @param string $function_id
* @return void
*/
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-10-14 06:53:43 +02:00
/** @var \ReflectionParameter $param */
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-10-15 06:12:57 +02:00
/**
* @param string $function_id
* @param string $file_name
* @return Type\Union|null
*/
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])) {
2016-10-20 20:26:03 +02:00
throw new \InvalidArgumentException('Do not know function ' . $function_id . ' in file ' . $file_name);
2016-08-14 19:13:53 +02:00
}
return self::$function_return_types[$file_name][$function_id]
? clone self::$function_return_types[$file_name][$function_id]
: null;
}
2016-10-14 06:53:43 +02:00
/**
* @param PhpParser\Node\Stmt\Function_ $function
* @param string $file_name
* @return void
*/
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
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
}
self::$have_registered_function[$file_name][$function_id] = true;
2016-08-11 01:21:03 +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 = [];
2016-10-14 06:53:43 +02:00
/** @var PhpParser\Node\Param $param */
2016-08-11 01:21:03 +02:00
foreach ($function->getParams() as $param) {
$param_array = self::getParamArray($param, $this->absolute_class, $this->namespace, $this->getAliasedClasses());
2016-08-14 05:26:45 +02:00
self::$file_function_params[$file_name][$function_id][] = $param_array;
$function_param_names[$param->name] = $param_array->type;
2016-08-11 01:21:03 +02:00
}
$config = Config::getInstance();
$return_type = null;
2016-10-14 06:53:43 +02:00
$docblock_info = CommentChecker::extractDocblockInfo((string)$function->getDocComment());
2016-08-11 01:21:03 +02:00
if ($docblock_info['deprecated']) {
self::$deprecated_functions[$file_name][$function_id] = true;
2016-08-11 01:21:03 +02:00
}
if ($docblock_info['variadic']) {
self::$variadic_functions[$file_name][$function_id] = true;
}
$this->suppressed_issues = $docblock_info['suppress'];
2016-08-11 01:21:03 +02:00
2016-10-19 03:54:08 +02:00
if (isset($function->returnType)) {
$return_type = Type::parseString($function->returnType);
}
2016-08-11 01:21:03 +02:00
if ($config->use_docblock_types) {
if ($docblock_info['return_type']) {
$return_type =
Type::parseString(
self::fixUpLocalType(
2016-10-15 06:12:57 +02:00
(string)$docblock_info['return_type'],
2016-08-11 01:21:03 +02:00
null,
$this->namespace,
$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-10-20 20:36:15 +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
* @psalm-return array<array<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];
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[] = new FunctionLikeParameter(
$arg_name,
$by_reference,
$arg_type ? Type::parseString($arg_type) : Type::getMixed(),
true // @todo - need to have non-optional parameters
);
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
/**
* @param string $function_id
* @param array<PhpParser\Node\Arg> $call_args
* @param string $file_name
* @param int $line_number
* @param array $suppressed_issues
* @return Type\Union
*/
public static function getReturnTypeFromCallMap($function_id, array $call_args, $file_name, $line_number, array $suppressed_issues)
2016-08-22 21:00:12 +02:00
{
$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)) {
2016-10-14 06:53:43 +02:00
/** @var Type\Union */
2016-08-22 21:00:12 +02:00
$subject_type = $call_args[2]->value->inferredType;
2016-09-17 17:57:44 +02:00
if (!$subject_type->hasString() && $subject_type->hasArray()) {
2016-08-22 21:00:12 +02:00
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-10-14 06:53:43 +02:00
if ($call_map_key === 'array_map' || $call_map_key === 'array_filter') {
$function_index = $call_map_key === 'array_map' ? 0 : 1;
if (isset($call_args[$function_index])) {
$function_call_arg = $call_args[$function_index];
if ($function_call_arg->value instanceof PhpParser\Node\Expr\Closure) {
2016-10-21 00:05:28 +02:00
$closure_yield_types = [];
$closure_return_types = \Psalm\EffectsAnalyser::getReturnTypes($function_call_arg->value->stmts, $closure_yield_types, true);
2016-08-22 21:00:12 +02:00
2016-08-30 06:05:13 +02:00
if (!$closure_return_types) {
if (IssueBuffer::accepts(
new InvalidReturnType(
2016-10-14 06:53:43 +02:00
'No return type could be found in the closure passed to ' . $call_map_key,
$file_name,
$line_number
),
$suppressed_issues
)) {
return false;
}
2016-08-30 06:05:13 +02:00
}
else {
2016-10-14 06:53:43 +02:00
if ($call_map_key === 'array_map') {
$inner_type = new Type\Union($closure_return_types);
return new Type\Union([new Type\Generic('array', [Type::getInt(), $inner_type])]);
}
2016-10-15 16:36:19 +02:00
elseif (isset($call_args[0]->value->inferredType->types['array'])) {
2016-10-14 06:53:43 +02:00
$inner_type = clone $call_args[0]->value->inferredType->types['array']->type_params[1];
return new Type\Union([new Type\Generic('array', [Type::getInt(), $inner_type])]);
}
2016-08-22 21:00:12 +02:00
2016-08-30 06:05:13 +02:00
}
}
2016-10-14 06:53:43 +02:00
elseif ($function_call_arg->value instanceof PhpParser\Node\Scalar\String_) {
$mapped_function_id = strtolower($function_call_arg->value->value);
2016-08-30 06:05:13 +02:00
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]);
2016-09-10 00:36:35 +02:00
return new Type\Union([new Type\Generic('array', [Type::getInt(), $mapped_function_return])]);
2016-08-30 06:05:13 +02:00
}
}
else {
// @todo handle array_map('some_custom_function', $arr)
}
2016-08-22 21:00:12 +02:00
}
}
2016-10-14 06:53:43 +02:00
// where there's no function passed to array_filter
if ($call_map_key === 'array_filter' && isset($call_args[0]->value->inferredType) && $call_args[0]->value->inferredType->hasArray()) {
$inner_type = clone $call_args[0]->value->inferredType->types['array']->type_params[1];
return new Type\Union([new Type\Generic('array', [Type::getInt(), $inner_type])]);
}
2016-08-22 21:00:12 +02:00
return Type::getArray();
}
2016-10-15 06:12:57 +02:00
if ($call_map_key === 'array_values' || $call_map_key === 'array_unique') {
2016-10-14 06:53:43 +02:00
if (isset($call_args[0]->value->inferredType) && $call_args[0]->value->inferredType->hasArray()) {
$inner_type = clone $call_args[0]->value->inferredType->types['array']->type_params[1];
return new Type\Union([new Type\Generic('array', [Type::getInt(), $inner_type])]);
}
}
if ($call_map_key === 'array_keys') {
2016-09-17 17:57:44 +02:00
if (isset($call_args[0]->value->inferredType) && $call_args[0]->value->inferredType->hasArray()) {
2016-10-14 06:53:43 +02:00
$inner_type = clone $call_args[0]->value->inferredType->types['array']->type_params[0];
return new Type\Union([new Type\Generic('array', [Type::getInt(), $inner_type])]);
2016-08-22 21:00:12 +02:00
}
}
if ($call_map_key === 'array_merge') {
2016-09-09 22:21:49 +02:00
$inner_value_types = [];
$inner_key_types = [];
2016-08-22 21:00:12 +02:00
2016-09-10 00:36:35 +02:00
foreach ($call_args as $offset => $call_arg) {
2016-08-22 21:00:12 +02:00
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();
}
2016-09-09 22:21:49 +02:00
if ($type_part->type_params[1]->isEmpty()) {
2016-08-22 21:00:12 +02:00
continue;
}
2016-09-09 22:21:49 +02:00
$inner_key_types = array_merge(array_values($type_part->type_params[0]->types), $inner_key_types);
$inner_value_types = array_merge(array_values($type_part->type_params[1]->types), $inner_value_types);
2016-08-22 21:00:12 +02:00
}
2016-09-10 00:36:35 +02:00
if ($inner_value_types) {
2016-08-22 21:00:12 +02:00
return new Type\Union([
new Type\Generic('array',
2016-09-09 22:21:49 +02:00
[
Type::combineTypes($inner_key_types),
Type::combineTypes($inner_value_types)
]
2016-08-22 21:00:12 +02:00
)
]);
}
}
return Type::getArray();
}
if ($call_map_key === 'array_diff') {
if (!isset($call_args[0]->value->inferredType) || !$call_args[0]->value->inferredType->hasArray()) {
return Type::getArray();
}
return new Type\Union([
new Type\Generic('array',
[
Type::getInt(),
2016-10-10 07:35:12 +02:00
clone $call_args[0]->value->inferredType->types['array']->type_params[1]
]
)
]);
}
if ($call_map_key === 'array_diff_key') {
if (!isset($call_args[0]->value->inferredType) || !$call_args[0]->value->inferredType->hasArray()) {
return Type::getArray();
}
return clone $call_args[0]->value->inferredType;
}
if ($call_map_key === 'array_shift' || $call_map_key === 'array_pop') {
if (!isset($call_args[0]->value->inferredType) || !$call_args[0]->value->inferredType->hasArray()) {
return Type::getMixed();
}
return clone $call_args[0]->value->inferredType->types['array']->type_params[1];
}
if ($call_map_key === 'explode' || $call_map_key === 'preg_split') {
2016-09-10 00:36:35 +02:00
return Type::parseString('array<int, 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
*
2016-10-14 06:53:43 +02:00
* @return array<array<string,string>>
2016-08-22 21:00:12 +02:00
*/
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-10-21 00:12:13 +02:00
public static function clearCache()
{
self::$function_return_types = [];
self::$function_namespaces = [];
self::$existing_functions = [];
self::$deprecated_functions = [];
self::$have_registered_function = [];
self::$file_function_params = [];
self::$variadic_functions = [];
self::$builtin_function_params = [];
self::$builtin_functions = [];
}
2016-01-08 00:28:27 +01:00
}