mirror of
https://github.com/danog/psalm.git
synced 2024-12-15 10:57:08 +01:00
398 lines
17 KiB
PHP
398 lines
17 KiB
PHP
<?php
|
|
namespace Psalm\Internal\Scanner;
|
|
|
|
use PhpParser;
|
|
use Psalm\Context;
|
|
use Psalm\Codebase;
|
|
use Psalm\CodeLocation;
|
|
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
|
use Psalm\Type;
|
|
use function strtolower;
|
|
use function implode;
|
|
use function is_string;
|
|
use function strpos;
|
|
use function str_replace;
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
class PhpStormMetaScanner
|
|
{
|
|
/**
|
|
* @param array<PhpParser\Node\Arg> $args
|
|
* @return void
|
|
*/
|
|
public static function handleOverride(array $args, Codebase $codebase)
|
|
{
|
|
$identifier = $args[0]->value;
|
|
|
|
if (!$args[1]->value instanceof PhpParser\Node\Expr\FuncCall
|
|
|| !$args[1]->value->name instanceof PhpParser\Node\Name
|
|
) {
|
|
return;
|
|
}
|
|
|
|
$map = [];
|
|
|
|
if ($args[1]->value->name->parts === ['map']
|
|
&& $args[1]->value->args
|
|
&& $args[1]->value->args[0]->value instanceof PhpParser\Node\Expr\Array_
|
|
) {
|
|
foreach ($args[1]->value->args[0]->value->items as $array_item) {
|
|
if ($array_item
|
|
&& $array_item->key instanceof PhpParser\Node\Scalar\String_
|
|
) {
|
|
if ($array_item->value instanceof PhpParser\Node\Expr\ClassConstFetch
|
|
&& $array_item->value->class instanceof PhpParser\Node\Name\FullyQualified
|
|
&& $array_item->value->name instanceof PhpParser\Node\Identifier
|
|
&& strtolower($array_item->value->name->name)
|
|
) {
|
|
$map[$array_item->key->value] = new Type\Union([
|
|
new Type\Atomic\TNamedObject(implode('\\', $array_item->value->class->parts))
|
|
]);
|
|
} elseif ($array_item->value instanceof PhpParser\Node\Scalar\String_) {
|
|
$map[$array_item->key->value] = $array_item->value->value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$type_offset = null;
|
|
|
|
if ($args[1]->value->name->parts === ['type']
|
|
&& $args[1]->value->args
|
|
&& $args[1]->value->args[0]->value instanceof PhpParser\Node\Scalar\LNumber
|
|
) {
|
|
$type_offset = $args[1]->value->args[0]->value->value;
|
|
}
|
|
|
|
$element_type_offset = null;
|
|
|
|
if ($args[1]->value->name->parts === ['elementType']
|
|
&& $args[1]->value->args
|
|
&& $args[1]->value->args[0]->value instanceof PhpParser\Node\Scalar\LNumber
|
|
) {
|
|
$element_type_offset = $args[1]->value->args[0]->value->value;
|
|
}
|
|
|
|
if ($identifier instanceof PhpParser\Node\Expr\StaticCall
|
|
&& $identifier->class instanceof PhpParser\Node\Name\FullyQualified
|
|
&& $identifier->name instanceof PhpParser\Node\Identifier
|
|
&& $identifier->args
|
|
&& $identifier->args[0]->value instanceof PhpParser\Node\Scalar\LNumber
|
|
) {
|
|
$meta_fq_classlike_name = implode('\\', $identifier->class->parts);
|
|
|
|
$meta_method_name = $identifier->name->name;
|
|
|
|
if ($map) {
|
|
$offset = $identifier->args[0]->value->value;
|
|
|
|
$codebase->methods->return_type_provider->registerClosure(
|
|
$meta_fq_classlike_name,
|
|
/**
|
|
* @param array<PhpParser\Node\Arg> $call_args
|
|
* @return ?Type\Union
|
|
*/
|
|
function (
|
|
\Psalm\StatementsSource $statements_analyzer,
|
|
string $fq_classlike_name,
|
|
string $method_name,
|
|
array $call_args,
|
|
Context $_context,
|
|
CodeLocation $_code_location
|
|
) use (
|
|
$map,
|
|
$offset,
|
|
$meta_fq_classlike_name,
|
|
$meta_method_name
|
|
) {
|
|
if (!$statements_analyzer instanceof \Psalm\Internal\Analyzer\StatementsAnalyzer) {
|
|
return Type::getMixed();
|
|
}
|
|
|
|
if ($meta_method_name !== $method_name
|
|
|| $meta_fq_classlike_name !== $fq_classlike_name
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
if (isset($call_args[$offset]->value)
|
|
&& ($call_arg_type = $statements_analyzer->node_data->getType($call_args[$offset]->value))
|
|
&& $call_arg_type->isSingleStringLiteral()
|
|
) {
|
|
$offset_arg_value = $call_arg_type->getSingleStringLiteral()->value;
|
|
|
|
if ($mapped_type = $map[$offset_arg_value] ?? null) {
|
|
if ($mapped_type instanceof Type\Union) {
|
|
return clone $mapped_type;
|
|
}
|
|
}
|
|
|
|
if (($mapped_type = $map[''] ?? null) && is_string($mapped_type)) {
|
|
if (strpos($mapped_type, '@') !== false) {
|
|
$mapped_type = str_replace('@', $offset_arg_value, $mapped_type);
|
|
|
|
if (strpos($mapped_type, '.') === false) {
|
|
return new Type\Union([
|
|
new Type\Atomic\TNamedObject($mapped_type)
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
);
|
|
} elseif ($type_offset !== null) {
|
|
$codebase->methods->return_type_provider->registerClosure(
|
|
$meta_fq_classlike_name,
|
|
/**
|
|
* @param array<PhpParser\Node\Arg> $call_args
|
|
* @return ?Type\Union
|
|
*/
|
|
function (
|
|
\Psalm\StatementsSource $statements_analyzer,
|
|
string $fq_classlike_name,
|
|
string $method_name,
|
|
array $call_args,
|
|
Context $_context,
|
|
CodeLocation $_code_location
|
|
) use (
|
|
$map,
|
|
$type_offset,
|
|
$meta_fq_classlike_name,
|
|
$meta_method_name
|
|
) {
|
|
if (!$statements_analyzer instanceof \Psalm\Internal\Analyzer\StatementsAnalyzer) {
|
|
return Type::getMixed();
|
|
}
|
|
|
|
if ($meta_method_name !== $method_name
|
|
|| $meta_fq_classlike_name !== $fq_classlike_name
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
if (isset($call_args[$type_offset]->value)
|
|
&& ($call_arg_type
|
|
= $statements_analyzer->node_data->getType($call_args[$type_offset]->value))
|
|
) {
|
|
return clone $call_arg_type;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
);
|
|
} elseif ($element_type_offset !== null) {
|
|
$codebase->methods->return_type_provider->registerClosure(
|
|
$meta_fq_classlike_name,
|
|
/**
|
|
* @param array<PhpParser\Node\Arg> $call_args
|
|
* @return ?Type\Union
|
|
*/
|
|
function (
|
|
\Psalm\StatementsSource $statements_analyzer,
|
|
string $fq_classlike_name,
|
|
string $method_name,
|
|
array $call_args,
|
|
Context $_context,
|
|
CodeLocation $_code_location
|
|
) use (
|
|
$map,
|
|
$element_type_offset,
|
|
$meta_fq_classlike_name,
|
|
$meta_method_name
|
|
) {
|
|
if (!$statements_analyzer instanceof \Psalm\Internal\Analyzer\StatementsAnalyzer) {
|
|
return Type::getMixed();
|
|
}
|
|
|
|
if ($meta_method_name !== $method_name
|
|
|| $meta_fq_classlike_name !== $fq_classlike_name
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
if (isset($call_args[$element_type_offset]->value)
|
|
&& ($call_arg_type
|
|
= $statements_analyzer->node_data->getType($call_args[$element_type_offset]->value))
|
|
&& $call_arg_type->hasArray()
|
|
) {
|
|
/**
|
|
* @psalm-suppress PossiblyUndefinedStringArrayOffset
|
|
* @var Type\Atomic\TArray|Type\Atomic\ObjectLike|Type\Atomic\TList
|
|
*/
|
|
$array_atomic_type = $call_arg_type->getAtomicTypes()['array'];
|
|
|
|
if ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
|
|
return $array_atomic_type->getGenericValueType();
|
|
}
|
|
|
|
if ($array_atomic_type instanceof Type\Atomic\TList) {
|
|
return $array_atomic_type->type_param;
|
|
}
|
|
|
|
return clone $array_atomic_type->type_params[1];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
if ($identifier instanceof PhpParser\Node\Expr\FuncCall
|
|
&& $identifier->name instanceof PhpParser\Node\Name\FullyQualified
|
|
&& $identifier->args
|
|
&& $identifier->args[0]->value instanceof PhpParser\Node\Scalar\LNumber
|
|
) {
|
|
$function_id = implode('\\', $identifier->name->parts);
|
|
|
|
if ($map) {
|
|
$offset = $identifier->args[0]->value->value;
|
|
|
|
$codebase->functions->return_type_provider->registerClosure(
|
|
$function_id,
|
|
/**
|
|
* @param array<PhpParser\Node\Arg> $call_args
|
|
*/
|
|
function (
|
|
\Psalm\StatementsSource $statements_analyzer,
|
|
string $function_id,
|
|
array $call_args,
|
|
Context $_context,
|
|
CodeLocation $_code_location
|
|
) use (
|
|
$map,
|
|
$offset
|
|
) : Type\Union {
|
|
if (!$statements_analyzer instanceof \Psalm\Internal\Analyzer\StatementsAnalyzer) {
|
|
return Type::getMixed();
|
|
}
|
|
|
|
if (isset($call_args[$offset]->value)
|
|
&& ($call_arg_type
|
|
= $statements_analyzer->node_data->getType($call_args[$offset]->value))
|
|
&& $call_arg_type->isSingleStringLiteral()
|
|
) {
|
|
$offset_arg_value = $call_arg_type->getSingleStringLiteral()->value;
|
|
|
|
if ($mapped_type = $map[$offset_arg_value] ?? null) {
|
|
if ($mapped_type instanceof Type\Union) {
|
|
return clone $mapped_type;
|
|
}
|
|
}
|
|
|
|
if (($mapped_type = $map[''] ?? null) && is_string($mapped_type)) {
|
|
if (strpos($mapped_type, '@') !== false) {
|
|
$mapped_type = str_replace('@', $offset_arg_value, $mapped_type);
|
|
|
|
if (strpos($mapped_type, '.') === false) {
|
|
return new Type\Union([
|
|
new Type\Atomic\TNamedObject($mapped_type)
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$storage = $statements_analyzer->getCodebase()->functions->getStorage(
|
|
$statements_analyzer,
|
|
$function_id
|
|
);
|
|
|
|
return $storage->return_type ?: Type::getMixed();
|
|
}
|
|
);
|
|
} elseif ($type_offset !== null) {
|
|
$codebase->functions->return_type_provider->registerClosure(
|
|
$function_id,
|
|
/**
|
|
* @param array<PhpParser\Node\Arg> $call_args
|
|
*/
|
|
function (
|
|
\Psalm\StatementsSource $statements_analyzer,
|
|
string $function_id,
|
|
array $call_args,
|
|
Context $_context,
|
|
CodeLocation $_code_location
|
|
) use (
|
|
$map,
|
|
$type_offset
|
|
) : Type\Union {
|
|
if (!$statements_analyzer instanceof \Psalm\Internal\Analyzer\StatementsAnalyzer) {
|
|
return Type::getMixed();
|
|
}
|
|
|
|
if (isset($call_args[$type_offset]->value)
|
|
&& ($call_arg_type
|
|
= $statements_analyzer->node_data->getType($call_args[$type_offset]->value))
|
|
) {
|
|
return clone $call_arg_type;
|
|
}
|
|
|
|
$storage = $statements_analyzer->getCodebase()->functions->getStorage(
|
|
$statements_analyzer,
|
|
$function_id
|
|
);
|
|
|
|
return $storage->return_type ?: Type::getMixed();
|
|
}
|
|
);
|
|
} elseif ($element_type_offset !== null) {
|
|
$codebase->functions->return_type_provider->registerClosure(
|
|
$function_id,
|
|
/**
|
|
* @param array<PhpParser\Node\Arg> $call_args
|
|
*/
|
|
function (
|
|
\Psalm\StatementsSource $statements_analyzer,
|
|
string $function_id,
|
|
array $call_args,
|
|
Context $_context,
|
|
CodeLocation $_code_location
|
|
) use (
|
|
$map,
|
|
$element_type_offset
|
|
) : Type\Union {
|
|
if (!$statements_analyzer instanceof \Psalm\Internal\Analyzer\StatementsAnalyzer) {
|
|
return Type::getMixed();
|
|
}
|
|
|
|
if (isset($call_args[$element_type_offset]->value)
|
|
&& ($call_arg_type
|
|
= $statements_analyzer->node_data->getType($call_args[$element_type_offset]->value))
|
|
&& $call_arg_type->hasArray()
|
|
) {
|
|
/**
|
|
* @psalm-suppress PossiblyUndefinedStringArrayOffset
|
|
* @var Type\Atomic\TArray|Type\Atomic\ObjectLike|Type\Atomic\TList
|
|
*/
|
|
$array_atomic_type = $call_arg_type->getAtomicTypes()['array'];
|
|
|
|
if ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
|
|
return $array_atomic_type->getGenericValueType();
|
|
}
|
|
|
|
if ($array_atomic_type instanceof Type\Atomic\TList) {
|
|
return $array_atomic_type->type_param;
|
|
}
|
|
|
|
return clone $array_atomic_type->type_params[1];
|
|
}
|
|
|
|
$storage = $statements_analyzer->getCodebase()->functions->getStorage(
|
|
$statements_analyzer,
|
|
$function_id
|
|
);
|
|
|
|
return $storage->return_type ?: Type::getMixed();
|
|
}
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|