2016-01-08 00:28:27 +01:00
|
|
|
|
<?php
|
2018-11-06 03:57:36 +01:00
|
|
|
|
namespace Psalm\Internal\Analyzer;
|
2016-01-08 00:28:27 +01:00
|
|
|
|
|
2016-02-04 15:22:46 +01:00
|
|
|
|
use PhpParser;
|
2020-05-25 19:10:06 +02:00
|
|
|
|
use Psalm\Internal\Codebase\InternalCallMapHandler;
|
2018-09-26 00:37:24 +02:00
|
|
|
|
use Psalm\Context;
|
2016-08-14 05:26:45 +02:00
|
|
|
|
use Psalm\Type;
|
2019-06-26 22:52:29 +02:00
|
|
|
|
use function strtolower;
|
|
|
|
|
use function array_values;
|
|
|
|
|
use function count;
|
2020-05-19 18:56:23 +02:00
|
|
|
|
use function is_string;
|
2016-01-08 00:28:27 +01:00
|
|
|
|
|
2018-12-02 00:37:49 +01:00
|
|
|
|
/**
|
|
|
|
|
* @internal
|
|
|
|
|
*/
|
2018-11-06 03:57:36 +01:00
|
|
|
|
class FunctionAnalyzer extends FunctionLikeAnalyzer
|
2016-01-08 00:28:27 +01:00
|
|
|
|
{
|
2020-02-15 02:54:26 +01:00
|
|
|
|
/**
|
|
|
|
|
* @var PhpParser\Node\Stmt\Function_
|
|
|
|
|
*/
|
|
|
|
|
protected $function;
|
|
|
|
|
|
2018-11-06 03:57:36 +01:00
|
|
|
|
public function __construct(PhpParser\Node\Stmt\Function_ $function, SourceAnalyzer $source)
|
2016-05-16 22:12:02 +02:00
|
|
|
|
{
|
2018-12-18 05:29:27 +01:00
|
|
|
|
$codebase = $source->getCodebase();
|
|
|
|
|
|
|
|
|
|
$file_storage_provider = $codebase->file_storage_provider;
|
|
|
|
|
|
|
|
|
|
$file_storage = $file_storage_provider->get($source->getFilePath());
|
|
|
|
|
|
|
|
|
|
$namespace = $source->getNamespace();
|
|
|
|
|
|
|
|
|
|
$function_id = ($namespace ? strtolower($namespace) . '\\' : '') . strtolower($function->name->name);
|
|
|
|
|
|
|
|
|
|
if (!isset($file_storage->functions[$function_id])) {
|
|
|
|
|
throw new \UnexpectedValueException(
|
|
|
|
|
'Function ' . $function_id . ' should be defined in ' . $source->getFilePath()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$storage = $file_storage->functions[$function_id];
|
|
|
|
|
|
|
|
|
|
parent::__construct($function, $source, $storage);
|
2016-01-08 00:28:27 +01:00
|
|
|
|
}
|
|
|
|
|
|
2016-11-01 05:39:41 +01:00
|
|
|
|
/**
|
|
|
|
|
* @param string $function_id
|
|
|
|
|
* @param array<PhpParser\Node\Arg> $call_args
|
2017-05-27 02:16:18 +02:00
|
|
|
|
*
|
2016-11-01 05:39:41 +01:00
|
|
|
|
* @return Type\Union
|
|
|
|
|
*/
|
|
|
|
|
public static function getReturnTypeFromCallMapWithArgs(
|
2018-11-11 18:01:14 +01:00
|
|
|
|
StatementsAnalyzer $statements_analyzer,
|
2016-11-01 05:39:41 +01:00
|
|
|
|
$function_id,
|
|
|
|
|
array $call_args,
|
2019-02-16 00:00:40 +01:00
|
|
|
|
Context $context
|
2016-11-01 05:39:41 +01:00
|
|
|
|
) {
|
|
|
|
|
$call_map_key = strtolower($function_id);
|
2016-08-22 21:00:12 +02:00
|
|
|
|
|
2020-05-25 19:10:06 +02:00
|
|
|
|
$call_map = InternalCallMapHandler::getCallMap();
|
2016-08-30 06:05:13 +02:00
|
|
|
|
|
2019-08-13 05:16:05 +02:00
|
|
|
|
$codebase = $statements_analyzer->getCodebase();
|
|
|
|
|
|
2016-10-22 23:35:59 +02:00
|
|
|
|
if (!isset($call_map[$call_map_key])) {
|
|
|
|
|
throw new \InvalidArgumentException('Function ' . $function_id . ' was not found in callmap');
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-08 02:35:24 +02:00
|
|
|
|
if (!$call_args) {
|
|
|
|
|
switch ($call_map_key) {
|
2019-04-03 23:08:37 +02:00
|
|
|
|
case 'hrtime':
|
|
|
|
|
return new Type\Union([
|
2020-08-30 17:44:14 +02:00
|
|
|
|
new Type\Atomic\TKeyedArray([
|
2019-04-03 23:08:37 +02:00
|
|
|
|
Type::getInt(),
|
|
|
|
|
Type::getInt()
|
|
|
|
|
])
|
|
|
|
|
]);
|
|
|
|
|
|
2019-01-05 21:42:56 +01:00
|
|
|
|
case 'get_called_class':
|
2020-03-27 14:51:53 +01:00
|
|
|
|
return new Type\Union([
|
|
|
|
|
new Type\Atomic\TClassString(
|
|
|
|
|
$context->self ?: 'object',
|
|
|
|
|
$context->self ? new Type\Atomic\TNamedObject($context->self, true) : null
|
|
|
|
|
)
|
|
|
|
|
]);
|
2019-01-05 21:42:56 +01:00
|
|
|
|
|
|
|
|
|
case 'get_parent_class':
|
|
|
|
|
if ($context->self && $codebase->classExists($context->self)) {
|
|
|
|
|
$classlike_storage = $codebase->classlike_storage_provider->get($context->self);
|
|
|
|
|
|
|
|
|
|
if ($classlike_storage->parent_classes) {
|
|
|
|
|
return new Type\Union([
|
|
|
|
|
new Type\Atomic\TClassString(
|
|
|
|
|
array_values($classlike_storage->parent_classes)[0]
|
|
|
|
|
)
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-07-08 02:35:24 +02:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2018-05-24 03:17:14 +02:00
|
|
|
|
switch ($call_map_key) {
|
|
|
|
|
case 'count':
|
2019-11-25 17:44:54 +01:00
|
|
|
|
if (($first_arg_type = $statements_analyzer->node_data->getType($call_args[0]->value))) {
|
2020-01-04 18:20:26 +01:00
|
|
|
|
$atomic_types = $first_arg_type->getAtomicTypes();
|
2018-05-24 03:17:14 +02:00
|
|
|
|
|
2019-08-12 22:17:55 +02:00
|
|
|
|
if (count($atomic_types) === 1) {
|
|
|
|
|
if (isset($atomic_types['array'])) {
|
2020-03-09 15:56:37 +01:00
|
|
|
|
if ($atomic_types['array'] instanceof Type\Atomic\TCallableArray
|
|
|
|
|
|| $atomic_types['array'] instanceof Type\Atomic\TCallableList
|
2020-08-30 17:44:14 +02:00
|
|
|
|
|| $atomic_types['array'] instanceof Type\Atomic\TCallableKeyedArray
|
2020-03-09 15:56:37 +01:00
|
|
|
|
) {
|
|
|
|
|
return Type::getInt(false, 2);
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-12 22:17:55 +02:00
|
|
|
|
if ($atomic_types['array'] instanceof Type\Atomic\TNonEmptyArray) {
|
|
|
|
|
return new Type\Union([
|
|
|
|
|
$atomic_types['array']->count !== null
|
|
|
|
|
? new Type\Atomic\TLiteralInt($atomic_types['array']->count)
|
|
|
|
|
: new Type\Atomic\TInt
|
|
|
|
|
]);
|
2020-03-09 15:56:37 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($atomic_types['array'] instanceof Type\Atomic\TNonEmptyList) {
|
2019-10-11 02:16:43 +02:00
|
|
|
|
return new Type\Union([
|
|
|
|
|
$atomic_types['array']->count !== null
|
|
|
|
|
? new Type\Atomic\TLiteralInt($atomic_types['array']->count)
|
|
|
|
|
: new Type\Atomic\TInt
|
|
|
|
|
]);
|
2020-03-09 15:56:37 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-30 17:44:14 +02:00
|
|
|
|
if ($atomic_types['array'] instanceof Type\Atomic\TKeyedArray
|
2019-08-12 22:17:55 +02:00
|
|
|
|
&& $atomic_types['array']->sealed
|
|
|
|
|
) {
|
|
|
|
|
return new Type\Union([
|
|
|
|
|
new Type\Atomic\TLiteralInt(count($atomic_types['array']->properties))
|
|
|
|
|
]);
|
|
|
|
|
}
|
2018-05-24 03:17:14 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
2019-04-03 23:08:37 +02:00
|
|
|
|
case 'hrtime':
|
2019-11-25 17:44:54 +01:00
|
|
|
|
if (($first_arg_type = $statements_analyzer->node_data->getType($call_args[0]->value))) {
|
|
|
|
|
if ((string) $first_arg_type === 'true') {
|
2019-04-03 23:08:37 +02:00
|
|
|
|
$int = Type::getInt();
|
|
|
|
|
$int->from_calculation = true;
|
|
|
|
|
return $int;
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-25 17:44:54 +01:00
|
|
|
|
if ((string) $first_arg_type === 'false') {
|
2019-04-03 23:08:37 +02:00
|
|
|
|
return new Type\Union([
|
2020-08-30 17:44:14 +02:00
|
|
|
|
new Type\Atomic\TKeyedArray([
|
2019-04-03 23:08:37 +02:00
|
|
|
|
Type::getInt(),
|
|
|
|
|
Type::getInt()
|
|
|
|
|
])
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new Type\Union([
|
2020-08-30 17:44:14 +02:00
|
|
|
|
new Type\Atomic\TKeyedArray([
|
2019-04-03 23:08:37 +02:00
|
|
|
|
Type::getInt(),
|
|
|
|
|
Type::getInt()
|
|
|
|
|
]),
|
|
|
|
|
new Type\Atomic\TInt()
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$int = Type::getInt();
|
|
|
|
|
$int->from_calculation = true;
|
|
|
|
|
return $int;
|
|
|
|
|
|
2020-05-26 18:28:56 +02:00
|
|
|
|
case 'min':
|
|
|
|
|
case 'max':
|
|
|
|
|
if (isset($call_args[0])) {
|
|
|
|
|
$first_arg = $call_args[0]->value;
|
|
|
|
|
|
|
|
|
|
if ($first_arg_type = $statements_analyzer->node_data->getType($first_arg)) {
|
|
|
|
|
if ($first_arg_type->hasArray()) {
|
|
|
|
|
/** @psalm-suppress PossiblyUndefinedStringArrayOffset */
|
|
|
|
|
$array_type = $first_arg_type->getAtomicTypes()['array'];
|
2020-08-30 17:44:14 +02:00
|
|
|
|
if ($array_type instanceof Type\Atomic\TKeyedArray) {
|
2020-05-26 18:28:56 +02:00
|
|
|
|
return $array_type->getGenericValueType();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($array_type instanceof Type\Atomic\TArray) {
|
|
|
|
|
return clone $array_type->type_params[1];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($array_type instanceof Type\Atomic\TList) {
|
|
|
|
|
return clone $array_type->type_param;
|
|
|
|
|
}
|
|
|
|
|
} elseif ($first_arg_type->hasScalarType()
|
2020-06-24 19:16:52 +02:00
|
|
|
|
&& ($second_arg = ($call_args[1]->value ?? null))
|
2020-05-26 18:28:56 +02:00
|
|
|
|
&& ($second_arg_type = $statements_analyzer->node_data->getType($second_arg))
|
|
|
|
|
&& $second_arg_type->hasScalarType()
|
|
|
|
|
) {
|
|
|
|
|
return Type::combineUnionTypes($first_arg_type, $second_arg_type);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
2019-01-05 21:49:50 +01:00
|
|
|
|
case 'get_parent_class':
|
2019-01-05 23:10:29 +01:00
|
|
|
|
// this is unreliable, as it's hard to know exactly what's wanted - attempted this in
|
|
|
|
|
// https://github.com/vimeo/psalm/commit/355ed831e1c69c96bbf9bf2654ef64786cbe9fd7
|
|
|
|
|
// but caused problems where it didn’t know exactly what level of child we
|
|
|
|
|
// were receiving.
|
|
|
|
|
//
|
|
|
|
|
// Really this should only work on instances we've created with new Foo(),
|
|
|
|
|
// but that requires more work
|
|
|
|
|
break;
|
2020-07-30 20:21:42 +02:00
|
|
|
|
|
|
|
|
|
case 'fgetcsv':
|
|
|
|
|
$string_type = Type::getString();
|
|
|
|
|
$string_type->addType(new Type\Atomic\TNull);
|
|
|
|
|
$string_type->ignore_nullable_issues = true;
|
|
|
|
|
|
|
|
|
|
$call_map_return_type = new Type\Union([
|
|
|
|
|
new Type\Atomic\TNonEmptyList(
|
|
|
|
|
$string_type
|
|
|
|
|
),
|
|
|
|
|
new Type\Atomic\TFalse,
|
|
|
|
|
new Type\Atomic\TNull
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if ($codebase->config->ignore_internal_nullable_issues) {
|
|
|
|
|
$call_map_return_type->ignore_nullable_issues = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($codebase->config->ignore_internal_falsable_issues) {
|
|
|
|
|
$call_map_return_type->ignore_falsable_issues = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $call_map_return_type;
|
2016-12-17 00:56:23 +01:00
|
|
|
|
}
|
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
|
|
|
|
|
2018-01-25 19:07:36 +01:00
|
|
|
|
$call_map_return_type = Type::parseString($call_map[$call_map_key][0]);
|
|
|
|
|
|
2018-05-24 03:17:14 +02:00
|
|
|
|
switch ($call_map_key) {
|
|
|
|
|
case 'mb_strpos':
|
|
|
|
|
case 'mb_strrpos':
|
|
|
|
|
case 'mb_stripos':
|
|
|
|
|
case 'mb_strripos':
|
|
|
|
|
case 'strpos':
|
|
|
|
|
case 'strrpos':
|
|
|
|
|
case 'stripos':
|
|
|
|
|
case 'strripos':
|
2018-08-29 19:58:07 +02:00
|
|
|
|
case 'strstr':
|
|
|
|
|
case 'stristr':
|
|
|
|
|
case 'strrchr':
|
|
|
|
|
case 'strpbrk':
|
|
|
|
|
case 'array_search':
|
2018-05-24 03:17:14 +02:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
2018-12-19 22:15:19 +01:00
|
|
|
|
if ($call_map_return_type->isFalsable()
|
|
|
|
|
&& $codebase->config->ignore_internal_falsable_issues
|
|
|
|
|
) {
|
2018-05-24 03:17:14 +02:00
|
|
|
|
$call_map_return_type->ignore_falsable_issues = true;
|
|
|
|
|
}
|
2018-01-25 19:07:36 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-03-15 19:07:55 +01:00
|
|
|
|
switch ($call_map_key) {
|
|
|
|
|
case 'array_replace':
|
|
|
|
|
case 'array_replace_recursive':
|
|
|
|
|
if ($codebase->config->ignore_internal_nullable_issues) {
|
|
|
|
|
$call_map_return_type->ignore_nullable_issues = true;
|
|
|
|
|
}
|
2020-03-15 19:14:53 +01:00
|
|
|
|
break;
|
2020-03-15 19:07:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-25 19:07:36 +01:00
|
|
|
|
return $call_map_return_type;
|
2016-11-02 14:24:36 +01:00
|
|
|
|
}
|
2019-08-13 05:16:05 +02:00
|
|
|
|
|
2020-02-15 02:54:26 +01:00
|
|
|
|
/**
|
2020-05-15 16:18:05 +02:00
|
|
|
|
* @return non-empty-lowercase-string
|
2020-02-15 02:54:26 +01:00
|
|
|
|
*/
|
|
|
|
|
public function getFunctionId()
|
|
|
|
|
{
|
|
|
|
|
$namespace = $this->source->getNamespace();
|
|
|
|
|
|
2020-05-15 16:18:05 +02:00
|
|
|
|
/** @var non-empty-lowercase-string */
|
2020-02-15 02:54:26 +01:00
|
|
|
|
return ($namespace ? strtolower($namespace) . '\\' : '') . strtolower($this->function->name->name);
|
|
|
|
|
}
|
2020-05-19 18:56:23 +02:00
|
|
|
|
|
|
|
|
|
public static function analyzeStatement(
|
|
|
|
|
StatementsAnalyzer $statements_analyzer,
|
|
|
|
|
PhpParser\Node\Stmt\Function_ $stmt,
|
|
|
|
|
Context $context
|
|
|
|
|
) : void {
|
|
|
|
|
foreach ($stmt->stmts as $function_stmt) {
|
|
|
|
|
if ($function_stmt instanceof PhpParser\Node\Stmt\Global_) {
|
|
|
|
|
foreach ($function_stmt->vars as $var) {
|
|
|
|
|
if ($var instanceof PhpParser\Node\Expr\Variable) {
|
|
|
|
|
if (is_string($var->name)) {
|
|
|
|
|
$var_id = '$' . $var->name;
|
|
|
|
|
|
|
|
|
|
// registers variable in global context
|
|
|
|
|
$context->hasVariable($var_id, $statements_analyzer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} elseif (!$function_stmt instanceof PhpParser\Node\Stmt\Nop) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$codebase = $statements_analyzer->getCodebase();
|
|
|
|
|
|
|
|
|
|
if (!$codebase->register_stub_files
|
|
|
|
|
&& !$codebase->register_autoload_files
|
|
|
|
|
) {
|
|
|
|
|
$function_name = strtolower($stmt->name->name);
|
|
|
|
|
|
|
|
|
|
if ($ns = $statements_analyzer->getNamespace()) {
|
|
|
|
|
$fq_function_name = strtolower($ns) . '\\' . $function_name;
|
|
|
|
|
} else {
|
|
|
|
|
$fq_function_name = $function_name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$function_context = new Context($context->self);
|
|
|
|
|
$function_context->strict_types = $context->strict_types;
|
|
|
|
|
$config = \Psalm\Config::getInstance();
|
|
|
|
|
$function_context->collect_exceptions = $config->check_for_throws_docblock;
|
|
|
|
|
|
|
|
|
|
if ($function_analyzer = $statements_analyzer->getFunctionAnalyzer($fq_function_name)) {
|
|
|
|
|
$function_analyzer->analyze(
|
|
|
|
|
$function_context,
|
|
|
|
|
$statements_analyzer->node_data,
|
|
|
|
|
$context
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ($config->reportIssueInFile('InvalidReturnType', $statements_analyzer->getFilePath())) {
|
|
|
|
|
$method_id = $function_analyzer->getId();
|
|
|
|
|
|
|
|
|
|
$function_storage = $codebase->functions->getStorage(
|
|
|
|
|
$statements_analyzer,
|
|
|
|
|
strtolower($method_id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$return_type = $function_storage->return_type;
|
|
|
|
|
$return_type_location = $function_storage->return_type_location;
|
|
|
|
|
|
|
|
|
|
$function_analyzer->verifyReturnType(
|
|
|
|
|
$stmt->getStmts(),
|
|
|
|
|
$statements_analyzer,
|
|
|
|
|
$return_type,
|
|
|
|
|
$statements_analyzer->getFQCLN(),
|
|
|
|
|
$return_type_location,
|
|
|
|
|
$function_context->has_returned
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-01-08 00:28:27 +01:00
|
|
|
|
}
|