1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-13 09:47:29 +01:00
psalm/src/Psalm/Type.php

550 lines
16 KiB
PHP
Raw Normal View History

2016-06-14 07:23:57 +02:00
<?php
2016-07-26 00:37:44 +02:00
namespace Psalm;
2016-06-14 07:23:57 +02:00
2016-07-26 00:37:44 +02:00
use Psalm\Type\Atomic;
use Psalm\Type\Generic;
use Psalm\Type\Union;
2016-10-03 00:59:16 +02:00
use Psalm\Type\ObjectLike;
2016-07-26 00:37:44 +02:00
use Psalm\Type\ParseTree;
2016-06-14 07:23:57 +02:00
abstract class Type
{
/**
* Parses a string type representation
* @param string $type_string
2016-06-28 20:28:45 +02:00
* @return Union
2016-06-14 07:23:57 +02:00
*/
2016-06-28 20:28:45 +02:00
public static function parseString($type_string)
2016-06-14 07:23:57 +02:00
{
2016-06-27 04:02:23 +02:00
if (strpos($type_string, '[') !== false) {
$type_string = self::convertSquareBrackets($type_string);
2016-06-27 04:02:23 +02:00
}
$type_string = str_replace('?', 'null|', $type_string);
$type_tokens = self::tokenize($type_string);
2016-06-14 07:23:57 +02:00
if (count($type_tokens) === 1) {
$type_tokens[0] = self::fixScalarTerms($type_tokens[0]);
2016-06-24 00:45:46 +02:00
2016-09-10 00:36:35 +02:00
if ($type_tokens[0] === 'array') {
return Type::getArray();
}
2016-06-28 20:28:45 +02:00
return new Union([new Atomic($type_tokens[0])]);
2016-06-14 07:23:57 +02:00
}
2016-09-01 06:07:29 +02:00
$parse_tree = ParseTree::createFromTokens($type_tokens);
2016-06-14 07:23:57 +02:00
2016-06-15 01:22:29 +02:00
$parsed_type = self::getTypeFromTree($parse_tree);
2016-06-28 20:28:45 +02:00
if (!($parsed_type instanceof Union)) {
2016-06-15 01:22:29 +02:00
$parsed_type = new Union([$parsed_type]);
}
return $parsed_type;
2016-06-14 07:23:57 +02:00
}
public static function fixScalarTerms($type_string)
{
2016-08-14 18:07:19 +02:00
if (in_array(
strtolower($type_string),
['numeric', 'int', 'float', 'string', 'bool', 'true', 'false', 'null', 'array', 'object', 'mixed', 'resource']
)) {
return strtolower($type_string);
}
elseif ($type_string === 'boolean') {
return 'bool';
}
elseif ($type_string === 'integer') {
return 'int';
}
2016-08-14 18:07:19 +02:00
elseif ($type_string === 'double' || $type_string === 'real') {
return 'float';
}
return $type_string;
}
2016-06-14 07:23:57 +02:00
private static function getTypeFromTree(ParseTree $parse_tree)
{
if ($parse_tree->value === ParseTree::GENERIC) {
$generic_type = array_shift($parse_tree->children);
$generic_params = array_map(
function (ParseTree $child_tree) {
$tree_type = self::getTypeFromTree($child_tree);
return $tree_type instanceof Union ? $tree_type : new Union([$tree_type]);
2016-06-14 07:23:57 +02:00
},
$parse_tree->children
);
2016-09-09 22:21:49 +02:00
$generic_type_value = self::fixScalarTerms($generic_type->value);
if ($generic_type_value === 'array' && count($generic_params) === 1) {
2016-09-15 22:42:50 +02:00
array_unshift($generic_params, Type::getMixed());
2016-09-09 22:21:49 +02:00
}
2016-06-14 07:23:57 +02:00
if (!$generic_params) {
throw new \InvalidArgumentException('No generic params provided for type');
}
2016-09-22 04:15:46 +02:00
if ($generic_type_value === 'array') {
return new Type\GenericArray($generic_type_value, $generic_params);
}
2016-09-09 22:21:49 +02:00
return new Generic($generic_type_value, $generic_params);
2016-06-14 07:23:57 +02:00
}
if ($parse_tree->value === ParseTree::UNION) {
$union_types = array_map(
function (ParseTree $child_tree) {
return self::getTypeFromTree($child_tree);
},
$parse_tree->children
);
return new Union($union_types);
}
if ($parse_tree->value === ParseTree::OBJECT_LIKE) {
$properties = [];
$type = array_shift($parse_tree->children);
foreach ($parse_tree->children as $property_branch) {
2016-10-03 00:59:16 +02:00
$property_type = self::getTypeFromTree($property_branch->children[1]);
if (!$property_type instanceof Union) {
$property_type = new Union([$property_type]);
}
$properties[$property_branch->children[0]->value] = $property_type;
}
2016-10-03 00:59:16 +02:00
return new ObjectLike($type, $properties);
}
2016-09-09 22:21:49 +02:00
$atomic_type = self::fixScalarTerms($parse_tree->value);
if ($atomic_type === 'array') {
return self::getArray()->types['array'];
}
return new Atomic($atomic_type);
}
/**
2016-09-09 22:21:49 +02:00
* @return array<int, string>
*/
public static function tokenize($return_type)
{
$return_type_tokens = [''];
$was_char = false;
2016-09-09 22:21:49 +02:00
$return_type = str_replace(' ', '', $return_type);
foreach (str_split($return_type) as $char) {
if ($was_char) {
$return_type_tokens[] = '';
}
if ($char === '<' || $char === '>' || $char === '|' || $char === '?' || $char === ',' || $char === '{' || $char === '}' || $char === ':') {
if ($return_type_tokens[count($return_type_tokens) - 1] === '') {
$return_type_tokens[count($return_type_tokens) - 1] = $char;
}
else {
$return_type_tokens[] = $char;
}
$was_char = true;
}
else {
$return_type_tokens[count($return_type_tokens) - 1] .= $char;
$was_char = false;
}
}
return $return_type_tokens;
}
public static function convertSquareBrackets($type)
{
return preg_replace_callback(
2016-08-11 01:21:03 +02:00
'/([a-zA-Z\<\>\\\\_]+)((\[\])+)/',
function ($matches) {
$inner_type = $matches[1];
$dimensionality = strlen($matches[2]) / 2;
for ($i = 0; $i < $dimensionality; $i++) {
2016-09-09 22:21:49 +02:00
$inner_type = 'array<int, ' . $inner_type . '>';
}
return $inner_type;
},
$type
);
2016-06-14 07:23:57 +02:00
}
2016-06-15 01:22:29 +02:00
2016-09-09 22:21:49 +02:00
public static function getInt()
2016-06-15 01:22:29 +02:00
{
$type = new Atomic('int');
2016-09-09 22:21:49 +02:00
return new Union([$type]);
2016-06-15 01:22:29 +02:00
}
2016-09-09 22:21:49 +02:00
public static function getString()
2016-06-15 01:22:29 +02:00
{
$type = new Atomic('string');
2016-09-09 22:21:49 +02:00
return new Union([$type]);
2016-06-15 01:22:29 +02:00
}
2016-09-09 22:21:49 +02:00
public static function getNull()
2016-06-16 02:16:40 +02:00
{
$type = new Atomic('null');
2016-09-09 22:21:49 +02:00
return new Union([$type]);
2016-06-16 02:16:40 +02:00
}
2016-09-09 22:21:49 +02:00
public static function getMixed()
2016-06-15 01:22:29 +02:00
{
$type = new Atomic('mixed');
2016-09-09 22:21:49 +02:00
return new Union([$type]);
2016-06-15 01:22:29 +02:00
}
2016-09-09 22:21:49 +02:00
public static function getBool()
2016-06-15 01:22:29 +02:00
{
$type = new Atomic('bool');
2016-09-09 22:21:49 +02:00
return new Union([$type]);
2016-06-15 01:22:29 +02:00
}
2016-09-09 22:21:49 +02:00
public static function getFloat()
2016-06-15 01:22:29 +02:00
{
$type = new Atomic('float');
2016-09-09 22:21:49 +02:00
return new Union([$type]);
2016-06-15 01:22:29 +02:00
}
2016-09-09 22:21:49 +02:00
public static function getObject()
2016-06-15 01:22:29 +02:00
{
$type = new Atomic('object');
2016-09-09 22:21:49 +02:00
return new Union([$type]);
2016-06-15 01:22:29 +02:00
}
public static function getObjectLike()
{
$type = new Atomic('object-like');
return new Union([$type]);
}
2016-09-09 22:21:49 +02:00
public static function getArray()
2016-06-15 01:22:29 +02:00
{
2016-09-22 04:15:46 +02:00
$type = new Type\GenericArray(
2016-09-09 22:21:49 +02:00
'array',
[
2016-09-15 22:42:50 +02:00
Type::getMixed(),
2016-09-09 22:21:49 +02:00
Type::getMixed()
]
);
2016-06-15 01:22:29 +02:00
2016-09-09 22:21:49 +02:00
return new Union([$type]);
2016-06-15 01:22:29 +02:00
}
2016-09-12 06:02:26 +02:00
public static function getEmptyArray()
{
return new Type\Union([
2016-09-22 04:15:46 +02:00
new Type\GenericArray(
2016-09-12 06:02:26 +02:00
'array',
[
new Type\Union([new Type\Atomic('empty')]),
new Type\Union([new Type\Atomic('empty')])
]
)
]);
}
2016-09-09 22:21:49 +02:00
public static function getVoid()
2016-06-16 02:16:40 +02:00
{
$type = new Atomic('void');
2016-09-09 22:21:49 +02:00
return new Union([$type]);
2016-06-16 02:16:40 +02:00
}
2016-09-09 22:21:49 +02:00
public static function getFalse()
2016-06-16 02:16:40 +02:00
{
$type = new Atomic('false');
2016-09-09 22:21:49 +02:00
return new Union([$type]);
2016-06-16 02:16:40 +02:00
}
2016-06-15 01:22:29 +02:00
public function isMixed()
{
if ($this instanceof Atomic) {
return $this->value === 'mixed';
}
if ($this instanceof Union) {
return isset($this->types['mixed']);
2016-06-15 01:22:29 +02:00
}
}
2016-06-16 02:16:40 +02:00
public function isNull()
{
if ($this instanceof Atomic) {
return $this->value === 'null';
}
if ($this instanceof Union) {
return count($this->types) === 1 && isset($this->types['null']);
2016-06-16 02:16:40 +02:00
}
}
2016-06-16 07:19:52 +02:00
public function isVoid()
{
if ($this instanceof Atomic) {
return $this->value === 'void';
}
if ($this instanceof Union) {
return isset($this->types['void']);
2016-06-16 07:19:52 +02:00
}
}
public function isEmpty()
{
if ($this instanceof Atomic) {
return $this->value === 'empty';
}
2016-07-25 00:02:03 +02:00
if ($this instanceof Union) {
return isset($this->types['empty']);
}
}
/**
* @param array<Union> $redefined_vars
* @param Context $context
* @return void
*/
public static function redefineGenericUnionTypes(array $redefined_vars, Context $context)
{
foreach ($redefined_vars as $var_name => $redefined_union_type) {
foreach ($redefined_union_type->types as $redefined_atomic_type) {
foreach ($context->vars_in_scope[$var_name]->types as $context_type) {
if ($context_type instanceof Type\Generic &&
$redefined_atomic_type instanceof Type\Generic &&
$context_type->value === $redefined_atomic_type->value
) {
2016-09-09 22:21:49 +02:00
// index of last param
$i = count($context_type->type_params) - 1;
if ($context_type->type_params[$i]->isEmpty()) {
$context_type->type_params[$i] = $redefined_atomic_type->type_params[$i];
}
else {
2016-09-09 22:21:49 +02:00
$context_type->type_params[$i] = Type::combineUnionTypes(
$redefined_atomic_type->type_params[$i],
$context_type->type_params[$i]
);
}
2016-09-09 22:21:49 +02:00
if ($i) {
if ($context_type->type_params[0]->isEmpty()) {
$context_type->type_params[0] = $redefined_atomic_type->type_params[0];
}
else {
$context_type->type_params[0] = Type::combineUnionTypes(
$redefined_atomic_type->type_params[0],
$context_type->type_params[0]
);
}
}
}
}
}
}
}
2016-06-16 02:16:40 +02:00
/**
* Combines two union types into one
* @param Union $type_1
* @param Union $type_2
* @return Union
*/
public static function combineUnionTypes(Union $type_1, Union $type_2)
2016-06-15 01:22:29 +02:00
{
return self::combineTypes(array_merge(array_values($type_1->types), array_values($type_2->types)));
2016-06-16 02:16:40 +02:00
}
2016-06-15 01:22:29 +02:00
2016-06-16 02:16:40 +02:00
/**
* Combines types together
* so int + string = int|string
* so array<int> + array<string> = array<int|string>
* and array<int> + string = array<int>|string
* and array<empty> + array<empty> = array<empty>
* and array<string> + array<empty> = array<string>
* and array + array<string> = array<mixed>
*
* @param array<Atomic> $types
* @return Union
*/
public static function combineTypes(array $types)
2016-06-16 02:16:40 +02:00
{
if (in_array(null, $types)) {
return Type::getMixed();
}
if (count($types) === 1) {
if ($types[0]->value === 'false') {
$types[0]->value = 'bool';
2016-06-15 01:22:29 +02:00
}
2016-06-16 02:16:40 +02:00
return new Union([$types[0]]);
}
if (!$types) {
throw new \InvalidArgumentException('You must pass at least one type to combineTypes');
}
2016-09-09 22:21:49 +02:00
$key_types = [];
2016-06-16 02:16:40 +02:00
$value_types = [];
foreach ($types as $type) {
if ($type instanceof Union) {
throw new \InvalidArgumentException('Union type not expected here');
}
// if we see the magic empty value and there's more than one type, ignore it
if ($type->value === 'empty') {
continue;
}
if ($type->value === 'mixed') {
return Type::getMixed();
}
if ($type->value === 'void') {
$type->value = 'null';
}
// deal with false|bool => bool
if ($type->value === 'false' && isset($value_types['bool'])) {
continue;
}
elseif ($type->value === 'bool' && isset($value_types['false'])) {
unset($value_types['false']);
}
if (!isset($value_types[$type->value])) {
$value_types[$type->value] = [];
}
2016-09-09 22:21:49 +02:00
if ($type instanceof Generic) {
$value_type_param_index = count($type->type_params) - 1;
$value_types[$type->value][(string) $type->type_params[$value_type_param_index]] = $type->type_params[$value_type_param_index];
if ($value_type_param_index) {
$key_types[$type->value][(string) $type->type_params[0]] = $type->type_params[0];
}
}
2016-10-03 00:59:16 +02:00
elseif ($type instanceof ObjectLike) {
foreach ($type->properties as $candidate_property_name => $candidate_property_type) {
if (!isset($value_types[$type->value][$candidate_property_name])) {
$value_types[$type->value][$candidate_property_name] = $candidate_property_type;
}
else {
$value_types[$type->value][$candidate_property_name] = Type::combineUnionTypes(
$value_types[$type->value][$candidate_property_name],
$candidate_property_type
);
}
}
}
2016-09-09 22:21:49 +02:00
else {
2016-09-10 00:36:35 +02:00
if ($type->value === 'array') {
throw new \InvalidArgumentException('Cannot have a non-generic array');
}
2016-09-09 22:21:49 +02:00
$value_types[$type->value][(string) $type] = null;
}
2016-06-16 02:16:40 +02:00
}
2016-06-16 07:19:52 +02:00
if (count($value_types) === 1) {
if (isset($value_types['false'])) {
return self::getBool();
}
}
2016-06-16 02:16:40 +02:00
$new_types = [];
2016-09-09 22:21:49 +02:00
foreach ($value_types as $generic_type => $value_type) {
2016-10-03 00:59:16 +02:00
// special case for ObjectLike where $value_type is actually an array of properties
if ($generic_type === 'object-like') {
$new_types[] = new ObjectLike('object-like', $value_type);
continue;
}
2016-09-10 00:36:35 +02:00
$key_type = isset($key_types[$generic_type]) ? $key_types[$generic_type] : [];
// if we're merging an empty array with an object-like, clobber empty array
if ($generic_type === 'array'
&& count($value_type) === 1
&& isset($value_type['empty'])
&& count($key_type) === 1
&& isset($key_type['empty'])
) {
continue;
}
2016-09-10 00:36:35 +02:00
$expanded_key_types = [];
foreach ($key_type as $expandable_key_type) {
$expanded_key_types = array_merge($expanded_key_types, array_values($expandable_key_type->types));
}
2016-09-09 22:21:49 +02:00
2016-06-16 02:16:40 +02:00
if (count($value_type) === 1) {
$value_type_param = array_values($value_type)[0];
2016-09-09 22:21:49 +02:00
$generic_type_params = [$value_type_param];
// if we're continuing, also add the correspoinding key type param if it exists
2016-09-10 00:36:35 +02:00
if ($expanded_key_types) {
array_unshift($generic_type_params, self::combineTypes($expanded_key_types));
2016-09-09 22:21:49 +02:00
}
$new_types[] = $value_type_param ? new Generic($generic_type, $generic_type_params) : new Atomic($generic_type);
2016-06-16 02:16:40 +02:00
continue;
}
$expanded_value_types = [];
$has_null = false;
2016-09-09 22:21:49 +02:00
foreach ($value_type as $expandable_value_type) {
if ($expandable_value_type) {
$has_null = $has_null || $expandable_value_type->isNullable();
$expanded_value_types = array_merge($expanded_value_types, array_values($expandable_value_type->types));
}
else {
$expanded_value_types = [Type::getMixed()->types['mixed']];
}
2016-09-09 22:21:49 +02:00
}
$generic_type_params = [self::combineTypes($expanded_value_types)];
2016-06-16 02:16:40 +02:00
2016-09-09 22:21:49 +02:00
if ($expanded_key_types) {
array_unshift($generic_type_params, self::combineTypes($expanded_key_types));
2016-06-15 01:22:29 +02:00
}
2016-06-16 02:16:40 +02:00
// we have a generic type with
2016-09-09 22:21:49 +02:00
$new_types[] = new Generic($generic_type, $generic_type_params);
2016-06-15 01:22:29 +02:00
}
2016-06-16 02:16:40 +02:00
$new_types = array_values($new_types);
return new Union($new_types);
2016-06-15 01:22:29 +02:00
}
2016-06-14 07:23:57 +02:00
}