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-12-04 01:11:30 +01:00
|
|
|
use Psalm\CodeLocation;
|
2016-08-14 01:44:24 +02:00
|
|
|
use Psalm\Config;
|
2016-11-02 07:29:00 +01:00
|
|
|
use Psalm\EffectsAnalyser;
|
|
|
|
use Psalm\Exception\DocblockParseException;
|
2016-10-09 23:54:58 +02:00
|
|
|
use Psalm\FunctionLikeParameter;
|
2016-10-28 19:24:06 +02:00
|
|
|
use Psalm\Issue\InvalidDocblock;
|
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-12-31 00:08:07 +01:00
|
|
|
use Psalm\Storage\FunctionLikeStorage;
|
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-12-25 12:32:21 +01:00
|
|
|
/**
|
|
|
|
* @var PhpParser\Node\Stmt\Function_
|
|
|
|
*/
|
|
|
|
protected $function;
|
|
|
|
|
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-12-31 00:08:07 +01:00
|
|
|
* @var array<string, FunctionLikeStorage>
|
2016-11-01 05:39:41 +01:00
|
|
|
*/
|
2016-08-14 05:26:45 +02:00
|
|
|
protected static $builtin_functions = [];
|
2016-11-01 05:39:41 +01:00
|
|
|
|
2016-06-16 02:16:40 +02:00
|
|
|
/**
|
2016-11-05 02:14:04 +01:00
|
|
|
* @param mixed $function
|
2016-08-14 05:26:45 +02:00
|
|
|
* @param StatementsSource $source
|
2016-12-31 01:06:45 +01:00
|
|
|
* @param string $base_file_path
|
2016-06-16 02:16:40 +02:00
|
|
|
*/
|
2016-12-31 01:06:45 +01:00
|
|
|
public function __construct($function, StatementsSource $source, $base_file_path)
|
2016-05-16 22:12:02 +02:00
|
|
|
{
|
2016-10-23 18:24:53 +02:00
|
|
|
if (!$function instanceof PhpParser\Node\Stmt\Function_) {
|
2016-10-23 07:57:11 +02:00
|
|
|
throw new \InvalidArgumentException('Bad');
|
|
|
|
}
|
|
|
|
|
2016-08-14 05:26:45 +02:00
|
|
|
parent::__construct($function, $source);
|
2016-05-16 22:12:02 +02:00
|
|
|
|
2016-12-31 01:06:45 +01:00
|
|
|
$this->registerFunction($function, $base_file_path);
|
2016-01-08 00:28:27 +01:00
|
|
|
}
|
|
|
|
|
2016-10-14 06:53:43 +02:00
|
|
|
/**
|
|
|
|
* @param string $function_id
|
2016-12-31 00:08:07 +01:00
|
|
|
* @param string $file_path
|
2016-10-14 06:53:43 +02:00
|
|
|
* @return boolean
|
|
|
|
*/
|
2016-12-31 00:08:07 +01:00
|
|
|
public static function functionExists($function_id, $file_path)
|
2016-02-27 01:11:11 +01:00
|
|
|
{
|
2016-12-31 00:08:07 +01:00
|
|
|
if (isset(FileChecker::$storage[$file_path]->functions[$function_id])) {
|
2016-08-14 05:26:45 +02:00
|
|
|
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-12-31 00:08:07 +01:00
|
|
|
if (isset(self::$builtin_functions[$function_id])) {
|
|
|
|
return true;
|
2016-08-14 05:26:45 +02:00
|
|
|
}
|
2016-07-22 19:29:46 +02:00
|
|
|
|
2016-12-31 00:08:07 +01:00
|
|
|
if (self::extractReflectionInfo($function_id) === false) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
2016-07-22 19:29:46 +02:00
|
|
|
}
|
2016-08-11 01:21:03 +02:00
|
|
|
|
2016-10-15 06:12:57 +02:00
|
|
|
/**
|
|
|
|
* @param string $function_id
|
2016-12-31 00:08:07 +01:00
|
|
|
* @param string $file_path
|
2016-12-31 05:40:32 +01:00
|
|
|
* @return array<int, FunctionLikeParameter>
|
2016-10-15 06:12:57 +02:00
|
|
|
*/
|
2016-12-31 00:08:07 +01:00
|
|
|
public static function getParams($function_id, $file_path)
|
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]) {
|
2016-12-31 00:08:07 +01:00
|
|
|
return self::$builtin_functions[$function_id]->params;
|
2016-08-11 01:21:03 +02:00
|
|
|
}
|
|
|
|
|
2016-12-31 00:08:07 +01:00
|
|
|
$file_storage = FileChecker::$storage[$file_path];
|
|
|
|
|
|
|
|
return $file_storage->functions[$function_id]->params;
|
2016-08-11 01:21:03 +02: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
|
2016-10-18 23:55:07 +02:00
|
|
|
* @return boolean
|
|
|
|
*/
|
2016-12-31 00:08:07 +01:00
|
|
|
public static function isVariadic($function_id, $file_path)
|
2016-10-18 23:55:07 +02:00
|
|
|
{
|
2016-12-31 00:08:07 +01:00
|
|
|
$file_storage = FileChecker::$storage[$file_path];
|
|
|
|
|
|
|
|
return isset($file_storage->functions[$function_id]) && $file_storage->functions[$function_id]->variadic;
|
2016-10-18 23:55:07 +02:00
|
|
|
}
|
|
|
|
|
2016-10-14 06:53:43 +02:00
|
|
|
/**
|
|
|
|
* @param string $function_id
|
2016-12-31 00:08:07 +01:00
|
|
|
* @return false|null
|
2016-10-14 06:53:43 +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-12-31 00:08:07 +01:00
|
|
|
$storage = self::$builtin_functions[$function_id] = new FunctionLikeStorage();
|
2016-08-11 01:21:03 +02:00
|
|
|
|
2016-12-31 00:08:07 +01:00
|
|
|
$reflection_params = $reflection_function->getParameters();
|
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) {
|
2016-12-31 00:08:07 +01:00
|
|
|
$storage->params[] = self::getReflectionParamArray($param);
|
2016-08-11 01:21:03 +02:00
|
|
|
}
|
|
|
|
|
2016-12-31 00:08:07 +01:00
|
|
|
$storage->cased_name = $reflection_function->getName();
|
2016-11-02 07:29:00 +01:00
|
|
|
} catch (\ReflectionException $e) {
|
2016-12-31 00:08:07 +01:00
|
|
|
return false;
|
2016-08-11 01:21:03 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-15 06:12:57 +02:00
|
|
|
/**
|
|
|
|
* @param string $function_id
|
2016-12-31 00:08:07 +01:00
|
|
|
* @param string $file_path
|
2016-10-15 06:12:57 +02:00
|
|
|
* @return Type\Union|null
|
|
|
|
*/
|
2016-12-31 00:08:07 +01:00
|
|
|
public static function getFunctionReturnType($function_id, $file_path)
|
2016-08-14 19:13:53 +02:00
|
|
|
{
|
2016-12-31 02:05:32 +01:00
|
|
|
if (!isset(FileChecker::$storage[$file_path])) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2016-12-31 00:08:07 +01:00
|
|
|
$file_storage = FileChecker::$storage[$file_path];
|
|
|
|
|
|
|
|
if (!isset($file_storage->functions[$function_id])) {
|
|
|
|
throw new \InvalidArgumentException('Do not know function ' . $function_id . ' in file ' . $file_path);
|
2016-08-14 19:13:53 +02:00
|
|
|
}
|
|
|
|
|
2016-12-31 00:08:07 +01:00
|
|
|
$function_return_type = $file_storage->functions[$function_id]->return_type;
|
2016-11-01 05:39:41 +01:00
|
|
|
|
2016-12-31 00:08:07 +01:00
|
|
|
return $function_return_type ? clone $function_return_type : null;
|
2016-08-14 19:13:53 +02:00
|
|
|
}
|
|
|
|
|
2016-12-06 22:33:47 +01:00
|
|
|
/**
|
|
|
|
* @param string $function_id
|
2016-12-31 00:08:07 +01:00
|
|
|
* @param string $file_path
|
2016-12-06 22:33:47 +01:00
|
|
|
* @return CodeLocation|null
|
|
|
|
*/
|
2016-12-31 00:08:07 +01:00
|
|
|
public static function getFunctionReturnTypeLocation($function_id, $file_path)
|
2016-12-06 22:33:47 +01:00
|
|
|
{
|
2016-12-31 00:08:07 +01:00
|
|
|
$file_storage = FileChecker::$storage[$file_path];
|
|
|
|
|
|
|
|
if (!isset($file_storage->functions[$function_id])) {
|
|
|
|
throw new \InvalidArgumentException('Do not know function ' . $function_id . ' in file ' . $file_path);
|
2016-12-06 22:33:47 +01:00
|
|
|
}
|
|
|
|
|
2016-12-31 00:08:07 +01:00
|
|
|
return $file_storage->functions[$function_id]->return_type_location;
|
2016-12-06 22:33:47 +01:00
|
|
|
}
|
|
|
|
|
2016-12-19 23:02:00 +01:00
|
|
|
/**
|
|
|
|
* @param string $function_id
|
2016-12-31 00:08:07 +01:00
|
|
|
* @param string $file_path
|
2016-12-19 23:02:00 +01:00
|
|
|
* @return string
|
|
|
|
*/
|
2016-12-31 00:08:07 +01:00
|
|
|
public static function getCasedFunctionId($function_id, $file_path)
|
2016-12-19 23:02:00 +01:00
|
|
|
{
|
2016-12-31 00:08:07 +01:00
|
|
|
$file_storage = FileChecker::$storage[$file_path];
|
|
|
|
|
|
|
|
if (!isset($file_storage->functions[$function_id])) {
|
|
|
|
throw new \InvalidArgumentException('Do not know function ' . $function_id . ' in file ' . $file_path);
|
2016-12-19 23:02:00 +01:00
|
|
|
}
|
|
|
|
|
2016-12-31 00:08:07 +01:00
|
|
|
return $file_storage->functions[$function_id]->cased_name;
|
2016-12-19 23:02:00 +01:00
|
|
|
}
|
|
|
|
|
2016-10-14 06:53:43 +02:00
|
|
|
/**
|
|
|
|
* @param PhpParser\Node\Stmt\Function_ $function
|
2016-12-31 00:08:07 +01:00
|
|
|
* @param string $file_path
|
2016-10-30 17:46:18 +01:00
|
|
|
* @return null|false
|
2016-10-14 06:53:43 +02:00
|
|
|
*/
|
2016-12-31 00:08:07 +01:00
|
|
|
protected function registerFunction(PhpParser\Node\Stmt\Function_ $function, $file_path)
|
2016-08-11 01:21:03 +02:00
|
|
|
{
|
2016-12-19 23:02:00 +01:00
|
|
|
$cased_function_id = ($this->namespace ? $this->namespace . '\\' : '') . $function->name;
|
|
|
|
$function_id = strtolower($cased_function_id);
|
2016-08-11 01:21:03 +02:00
|
|
|
|
2016-12-31 00:08:07 +01:00
|
|
|
$file_storage = FileChecker::$storage[$file_path];
|
|
|
|
|
|
|
|
if (isset($file_storage->functions[$function_id])) {
|
2016-11-02 07:29:00 +01:00
|
|
|
return null;
|
2016-08-11 01:21:03 +02:00
|
|
|
}
|
|
|
|
|
2016-12-31 00:08:07 +01:00
|
|
|
$storage = $file_storage->functions[$function_id] = new FunctionLikeStorage();
|
2016-08-11 01:21:03 +02:00
|
|
|
|
2016-12-31 00:08:07 +01:00
|
|
|
$storage->namespace = $this->namespace;
|
|
|
|
$storage->cased_name = $cased_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) {
|
2016-11-02 07:29:00 +01:00
|
|
|
$param_array = self::getTranslatedParam(
|
|
|
|
$param,
|
2016-12-04 01:11:30 +01:00
|
|
|
$this
|
2016-11-02 07:29:00 +01:00
|
|
|
);
|
|
|
|
|
2016-12-31 00:08:07 +01:00
|
|
|
$storage->params[] = $param_array;
|
2016-10-09 23:54:58 +02:00
|
|
|
$function_param_names[$param->name] = $param_array->type;
|
2016-08-11 01:21:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$config = Config::getInstance();
|
|
|
|
$return_type = null;
|
|
|
|
|
2016-12-06 22:33:47 +01:00
|
|
|
$return_type_location = null;
|
|
|
|
|
2016-10-28 16:54:20 +02:00
|
|
|
$docblock_info = null;
|
2016-08-11 01:21:03 +02:00
|
|
|
|
2016-10-28 16:54:20 +02:00
|
|
|
$this->suppressed_issues = [];
|
2016-08-11 01:21:03 +02:00
|
|
|
|
2016-12-04 01:11:30 +01:00
|
|
|
$doc_comment = $function->getDocComment();
|
2016-10-19 03:54:08 +02:00
|
|
|
|
2016-12-04 05:03:18 +01:00
|
|
|
if ($function->returnType) {
|
|
|
|
$parser_return_type = $function->returnType;
|
|
|
|
|
|
|
|
$suffix = '';
|
|
|
|
|
|
|
|
if ($parser_return_type instanceof PhpParser\Node\NullableType) {
|
|
|
|
$suffix = '|null';
|
|
|
|
$parser_return_type = $parser_return_type->type;
|
|
|
|
}
|
|
|
|
|
|
|
|
$return_type = Type::parseString(
|
|
|
|
(is_string($parser_return_type)
|
|
|
|
? $parser_return_type
|
|
|
|
: ClassLikeChecker::getFQCLNFromNameObject(
|
|
|
|
$parser_return_type,
|
|
|
|
$this->namespace,
|
|
|
|
$this->getAliasedClasses()
|
|
|
|
)
|
|
|
|
) . $suffix
|
|
|
|
);
|
2016-12-06 22:33:47 +01:00
|
|
|
|
|
|
|
$return_type_location = new CodeLocation($this->getSource(), $function, false, self::RETURN_TYPE_REGEX);
|
2016-12-04 05:03:18 +01:00
|
|
|
}
|
|
|
|
|
2016-12-31 00:08:07 +01:00
|
|
|
$storage->return_type = $return_type;
|
|
|
|
$storage->return_type_location = $return_type_location;
|
|
|
|
|
2016-12-04 01:11:30 +01:00
|
|
|
if ($doc_comment) {
|
|
|
|
try {
|
|
|
|
$docblock_info = CommentChecker::extractDocblockInfo(
|
|
|
|
(string)$doc_comment,
|
|
|
|
$doc_comment->getLine()
|
|
|
|
);
|
|
|
|
} catch (DocblockParseException $e) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new InvalidDocblock(
|
2016-12-25 12:32:21 +01:00
|
|
|
$e->getMessage() . ' in docblock for ' . $this->function->name,
|
2016-12-04 01:11:30 +01:00
|
|
|
new CodeLocation($this, $function, true)
|
|
|
|
)
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-08-11 01:21:03 +02:00
|
|
|
}
|
|
|
|
|
2016-12-04 01:11:30 +01:00
|
|
|
if ($docblock_info) {
|
|
|
|
if ($docblock_info->deprecated) {
|
2016-12-31 00:08:07 +01:00
|
|
|
$storage->deprecated = true;
|
2016-12-04 01:11:30 +01:00
|
|
|
}
|
2016-10-28 16:54:20 +02:00
|
|
|
|
2016-12-04 01:11:30 +01:00
|
|
|
if ($docblock_info->variadic) {
|
2016-12-31 00:08:07 +01:00
|
|
|
$storage->variadic = true;
|
2016-12-04 01:11:30 +01:00
|
|
|
}
|
2016-10-28 16:54:20 +02:00
|
|
|
|
2016-12-04 01:11:30 +01:00
|
|
|
$this->suppressed_issues = $docblock_info->suppress;
|
2016-10-28 16:54:20 +02:00
|
|
|
|
2016-12-04 01:11:30 +01:00
|
|
|
if ($config->use_docblock_types) {
|
|
|
|
if ($docblock_info->return_type) {
|
2016-12-12 05:40:46 +01:00
|
|
|
$docblock_return_type =
|
2016-12-04 01:11:30 +01:00
|
|
|
Type::parseString(
|
|
|
|
self::fixUpLocalType(
|
|
|
|
(string)$docblock_info->return_type,
|
|
|
|
null,
|
|
|
|
$this->namespace,
|
|
|
|
$this->getAliasedClasses()
|
|
|
|
)
|
|
|
|
);
|
2016-12-06 22:33:47 +01:00
|
|
|
|
|
|
|
if (!$return_type_location) {
|
|
|
|
$return_type_location = new CodeLocation($this->getSource(), $function, true);
|
|
|
|
}
|
|
|
|
|
2016-12-24 19:23:22 +01:00
|
|
|
if ($return_type && !TypeChecker::isContainedBy($docblock_return_type, $return_type)) {
|
2016-12-12 05:40:46 +01:00
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new InvalidDocblock(
|
2016-12-24 19:23:22 +01:00
|
|
|
'Docblock return type does not match function return type for ' .
|
|
|
|
$this->getMethodId(),
|
2016-12-12 05:40:46 +01:00
|
|
|
new CodeLocation($this, $function, true)
|
|
|
|
)
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$return_type = $docblock_return_type;
|
|
|
|
}
|
|
|
|
|
2016-12-06 22:33:47 +01:00
|
|
|
$return_type_location->setCommentLine($docblock_info->return_type_line_number);
|
2016-12-31 00:08:07 +01:00
|
|
|
|
|
|
|
$storage->return_type = $return_type;
|
|
|
|
$storage->return_type_location = $return_type_location;
|
2016-12-04 01:11:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($docblock_info->params) {
|
|
|
|
$this->improveParamsFromDocblock(
|
|
|
|
$docblock_info->params,
|
|
|
|
$function_param_names,
|
2016-12-31 00:08:07 +01:00
|
|
|
$storage->params,
|
2016-12-04 01:11:30 +01:00
|
|
|
new CodeLocation($this, $function, false)
|
|
|
|
);
|
|
|
|
}
|
2016-10-28 16:54:20 +02:00
|
|
|
}
|
|
|
|
}
|
2016-08-11 01:21:03 +02:00
|
|
|
}
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
return null;
|
2016-08-11 01:21:03 +02:00
|
|
|
}
|
2016-08-22 21:00:12 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $function_id
|
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];
|
|
|
|
|
|
|
|
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 = [];
|
|
|
|
|
|
|
|
foreach ($call_map_function_args as $arg_name => $arg_type) {
|
|
|
|
$by_reference = false;
|
2016-10-26 17:51:59 +02:00
|
|
|
$optional = 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;
|
|
|
|
}
|
|
|
|
|
2016-10-09 23:54:58 +02:00
|
|
|
$function_types[] = new FunctionLikeParameter(
|
|
|
|
$arg_name,
|
|
|
|
$by_reference,
|
|
|
|
$arg_type ? Type::parseString($arg_type) : Type::getMixed(),
|
2016-12-04 01:11:30 +01:00
|
|
|
null,
|
2016-10-30 17:46:18 +01:00
|
|
|
$optional,
|
|
|
|
false,
|
|
|
|
$arg_name === '...'
|
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
|
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
|
|
|
|
* @return Type\Union
|
|
|
|
*/
|
|
|
|
public static function getReturnTypeFromCallMapWithArgs(
|
|
|
|
$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');
|
|
|
|
}
|
|
|
|
|
2016-11-01 05:39:41 +01:00
|
|
|
if ($call_args) {
|
|
|
|
if (in_array($call_map_key, ['str_replace', 'preg_replace', 'preg_replace_callback'])) {
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
|
|
|
return Type::getString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (in_array($call_map_key, ['pathinfo'])) {
|
|
|
|
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(
|
|
|
|
$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') {
|
|
|
|
$minmax_return_type = null;
|
|
|
|
|
|
|
|
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()) {
|
|
|
|
$array_type = $first_arg->inferredType->types['array'];
|
|
|
|
if ($array_type instanceof Type\ObjectLike) {
|
|
|
|
return $array_type->getGenericTypeParam();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($array_type instanceof Type\Generic) {
|
|
|
|
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
|
|
|
|
* @return Type\Union|null
|
|
|
|
*/
|
2016-11-04 01:51:56 +01:00
|
|
|
protected static function getArrayReturnType(
|
|
|
|
$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') {
|
2016-12-04 01:11:30 +01:00
|
|
|
return self::getArrayMapReturnType($call_map_key, $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;
|
2016-10-14 06:53:43 +02:00
|
|
|
|
2016-12-10 00:46:08 +01:00
|
|
|
$first_arg_array_generic = $first_arg
|
2016-11-02 07:29:00 +01:00
|
|
|
&& isset($first_arg->inferredType)
|
|
|
|
&& isset($first_arg->inferredType->types['array'])
|
|
|
|
&& $first_arg->inferredType->types['array'] instanceof Type\Generic
|
|
|
|
? $first_arg->inferredType->types['array']
|
|
|
|
: null;
|
2016-11-02 14:24:36 +01:00
|
|
|
|
2016-12-10 00:46:08 +01:00
|
|
|
$first_arg_array_objectlike = $first_arg
|
|
|
|
&& isset($first_arg->inferredType)
|
|
|
|
&& isset($first_arg->inferredType->types['array'])
|
|
|
|
&& $first_arg->inferredType->types['array'] instanceof Type\ObjectLike
|
|
|
|
? $first_arg->inferredType->types['array']
|
|
|
|
: null;
|
|
|
|
|
2016-12-17 00:56:23 +01:00
|
|
|
if ($call_map_key === 'array_values' || $call_map_key === 'array_unique' || $call_map_key === 'array_intersect') {
|
2016-12-10 00:46:08 +01:00
|
|
|
if ($first_arg_array_generic || $first_arg_array_objectlike) {
|
|
|
|
if ($first_arg_array_generic) {
|
|
|
|
$inner_type = clone $first_arg_array_generic->type_params[1];
|
|
|
|
} else {
|
|
|
|
/** @var Type\ObjectLike $first_arg_array_objectlike */
|
|
|
|
$inner_type = $first_arg_array_objectlike->getGenericTypeParam();
|
|
|
|
}
|
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
return new Type\Union([new Type\Generic('array', [Type::getInt(), $inner_type])]);
|
2016-10-14 06:53:43 +02:00
|
|
|
}
|
2016-11-02 14:24:36 +01:00
|
|
|
}
|
2016-10-14 06:53:43 +02:00
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
if ($call_map_key === 'array_keys') {
|
2016-12-10 00:46:08 +01:00
|
|
|
if ($first_arg_array_generic || $first_arg_array_objectlike) {
|
|
|
|
if ($first_arg_array_generic) {
|
|
|
|
$inner_type = clone $first_arg_array_generic->type_params[0];
|
|
|
|
} else {
|
|
|
|
$inner_type = Type::getString();
|
|
|
|
}
|
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
return new Type\Union([new Type\Generic('array', [Type::getInt(), $inner_type])]);
|
2016-08-22 21:00:12 +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
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
foreach ($call_args as $offset => $call_arg) {
|
|
|
|
if (!isset($call_arg->value->inferredType)) {
|
|
|
|
return Type::getArray();
|
|
|
|
}
|
2016-08-22 21:00:12 +02:00
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
foreach ($call_arg->value->inferredType->types as $type_part) {
|
|
|
|
if (!$type_part instanceof Type\Generic) {
|
2016-08-22 21:00:12 +02:00
|
|
|
return Type::getArray();
|
|
|
|
}
|
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
if ($type_part->type_params[1]->isEmpty()) {
|
|
|
|
continue;
|
2016-08-22 21:00:12 +02:00
|
|
|
}
|
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
$inner_key_types = array_merge(array_values($type_part->type_params[0]->types), $inner_key_types);
|
2016-11-04 01:51:56 +01:00
|
|
|
$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-11-02 14:24:36 +01:00
|
|
|
if ($inner_value_types) {
|
|
|
|
return new Type\Union([
|
|
|
|
new Type\Generic(
|
|
|
|
'array',
|
|
|
|
[
|
|
|
|
Type::combineTypes($inner_key_types),
|
|
|
|
Type::combineTypes($inner_value_types)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Type::getArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($call_map_key === 'array_diff') {
|
2016-12-10 00:46:08 +01:00
|
|
|
if (!$first_arg_array_generic) {
|
2016-10-22 23:35:59 +02:00
|
|
|
return Type::getArray();
|
2016-08-22 21:00:12 +02:00
|
|
|
}
|
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
return new Type\Union([
|
|
|
|
new Type\Generic(
|
|
|
|
'array',
|
|
|
|
[
|
|
|
|
Type::getInt(),
|
2016-12-10 00:46:08 +01:00
|
|
|
clone $first_arg_array_generic->type_params[1]
|
2016-11-02 14:24:36 +01:00
|
|
|
]
|
|
|
|
)
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2016-12-25 02:33:14 +01:00
|
|
|
if ($call_map_key === 'array_filter') {
|
|
|
|
if (!$first_arg_array_generic) {
|
|
|
|
return Type::getArray();
|
|
|
|
}
|
|
|
|
|
2016-12-26 15:40:04 +01:00
|
|
|
$second_arg = isset($call_args[1]->value) ? $call_args[1]->value : null;
|
|
|
|
|
|
|
|
$inner_type = clone $first_arg_array_generic->type_params[1];
|
|
|
|
|
|
|
|
if (!$second_arg) {
|
|
|
|
$inner_type->removeType('null');
|
|
|
|
$inner_type->removeType('false');
|
|
|
|
}
|
|
|
|
|
2016-12-25 02:34:47 +01:00
|
|
|
return new Type\Union([
|
|
|
|
new Type\Generic(
|
|
|
|
'array',
|
|
|
|
[
|
|
|
|
clone $first_arg_array_generic->type_params[0],
|
2016-12-26 15:40:04 +01:00
|
|
|
$inner_type
|
2016-12-25 02:34:47 +01:00
|
|
|
]
|
|
|
|
)
|
|
|
|
]);
|
2016-12-25 02:33:14 +01:00
|
|
|
}
|
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
if ($call_map_key === 'array_diff_key') {
|
2016-12-10 00:46:08 +01:00
|
|
|
if (!$first_arg_array_generic) {
|
2016-11-02 14:24:36 +01:00
|
|
|
return Type::getArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
return clone $first_arg->inferredType;
|
|
|
|
}
|
2016-08-22 21:00:12 +02:00
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
if ($call_map_key === 'array_shift' || $call_map_key === 'array_pop') {
|
2016-12-10 00:46:08 +01:00
|
|
|
if (!$first_arg_array_generic) {
|
2016-11-02 14:24:36 +01:00
|
|
|
return Type::getMixed();
|
2016-10-09 23:54:58 +02:00
|
|
|
}
|
|
|
|
|
2016-12-10 00:46:08 +01:00
|
|
|
return clone $first_arg_array_generic->type_params[1];
|
2016-11-02 14:24:36 +01:00
|
|
|
}
|
2016-11-04 01:51:56 +01:00
|
|
|
|
|
|
|
return null;
|
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
|
|
|
|
* @return Type\Union
|
|
|
|
*/
|
2016-11-04 01:51:56 +01:00
|
|
|
protected static function getArrayMapReturnType(
|
|
|
|
$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-11-02 14:24:36 +01:00
|
|
|
$function_index = $call_map_key === 'array_map' ? 0 : 1;
|
|
|
|
$array_index = $call_map_key === 'array_map' ? 1 : 0;
|
|
|
|
|
|
|
|
$array_arg = isset($call_args[$array_index]->value) ? $call_args[$array_index]->value : null;
|
|
|
|
|
|
|
|
$array_arg_type = $array_arg
|
2016-11-02 07:29:00 +01:00
|
|
|
&& isset($array_arg->inferredType)
|
|
|
|
&& isset($array_arg->inferredType->types['array'])
|
|
|
|
&& $array_arg->inferredType->types['array'] instanceof Type\Generic
|
|
|
|
? $array_arg->inferredType->types['array']
|
|
|
|
: null;
|
2016-11-02 14:24:36 +01:00
|
|
|
|
|
|
|
if (isset($call_args[$function_index])) {
|
|
|
|
$function_call_arg = $call_args[$function_index];
|
|
|
|
|
2016-12-07 20:13:39 +01:00
|
|
|
if ($function_call_arg->value instanceof PhpParser\Node\Expr\Closure &&
|
|
|
|
isset($function_call_arg->value->inferredType) &&
|
|
|
|
$function_call_arg->value->inferredType->types['Closure'] instanceof Type\Fn
|
|
|
|
) {
|
|
|
|
$closure_return_type = $function_call_arg->value->inferredType->types['Closure']->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(
|
|
|
|
'No return type could be found in the closure passed to ' . $call_map_key,
|
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();
|
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
if ($call_map_key === 'array_map') {
|
2016-12-07 20:13:39 +01:00
|
|
|
$inner_type = clone $closure_return_type;
|
2016-12-24 19:23:22 +01:00
|
|
|
return new Type\Union([new Type\Generic('array', [$key_type, $inner_type])]);
|
2016-11-02 14:24:36 +01:00
|
|
|
}
|
2016-10-09 23:54:58 +02:00
|
|
|
|
2016-11-02 14:24:36 +01:00
|
|
|
if ($array_arg_type) {
|
|
|
|
$inner_type = clone $array_arg_type->type_params[1];
|
2016-12-24 19:23:22 +01:00
|
|
|
return new Type\Union([new Type\Generic('array', [$key_type, $inner_type])]);
|
2016-10-22 23:35:59 +02:00
|
|
|
}
|
2016-11-02 14:24:36 +01:00
|
|
|
} elseif ($function_call_arg->value instanceof PhpParser\Node\Scalar\String_) {
|
|
|
|
$mapped_function_id = strtolower($function_call_arg->value->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
|
|
|
|
2016-11-02 14:24:36 +01: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]);
|
|
|
|
return new Type\Union([new Type\Generic('array', [Type::getInt(), $mapped_function_return])]);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// @todo handle array_map('some_custom_function', $arr)
|
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the method/function call map
|
|
|
|
*
|
2016-11-05 22:53:30 +01:00
|
|
|
* @return array<array<string, string>>
|
2016-11-05 23:46:17 +01:00
|
|
|
* @psalm-suppress MixedInferredReturnType as the use of require buggers things up
|
2016-08-22 21:00:12 +02:00
|
|
|
*/
|
|
|
|
protected static function getCallMap()
|
|
|
|
{
|
|
|
|
if (self::$call_map !== null) {
|
|
|
|
return self::$call_map;
|
|
|
|
}
|
|
|
|
|
2016-12-07 07:12:19 +01:00
|
|
|
/** @var array<string, array> */
|
2016-08-22 21:00:12 +02:00
|
|
|
$call_map = require_once(__DIR__.'/../CallMap.php');
|
|
|
|
|
|
|
|
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
|
|
|
|
* @return bool
|
|
|
|
*/
|
2016-10-22 23:35:59 +02:00
|
|
|
public static function inCallMap($key)
|
|
|
|
{
|
|
|
|
return isset(self::getCallMap()[strtolower($key)]);
|
|
|
|
}
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
|
|
|
* @return void
|
|
|
|
*/
|
2016-10-21 00:12:13 +02:00
|
|
|
public static function clearCache()
|
|
|
|
{
|
|
|
|
self::$builtin_functions = [];
|
|
|
|
}
|
2016-01-08 00:28:27 +01:00
|
|
|
}
|