value) { throw new \InvalidArgumentException('Parse tree must have a value'); } 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]); }, $parse_tree->children ); if (!$generic_type->value) { throw new \InvalidArgumentException('Generic type must have a value'); } $generic_type_value = self::fixScalarTerms($generic_type->value); if (($generic_type_value === 'array' || $generic_type_value === 'Generator') && count($generic_params) === 1 ) { array_unshift($generic_params, Type::getMixed()); } if (!$generic_params) { throw new \InvalidArgumentException('No generic params provided for type'); } if ($generic_type_value === 'array') { return new Type\GenericArray($generic_type_value, $generic_params); } return new Generic($generic_type_value, $generic_params); } if ($parse_tree->value === ParseTree::UNION) { $union_types = array_map( function (ParseTree $child_tree) { return self::getTypeFromTree($child_tree); }, $parse_tree->children ); return self::combineTypes($union_types); } if ($parse_tree->value === ParseTree::OBJECT_LIKE) { $properties = []; $type = array_shift($parse_tree->children); foreach ($parse_tree->children as $property_branch) { $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; } if (!$type->value) { throw new \InvalidArgumentException('Object-like type must have a value'); } return new ObjectLike($type->value, $properties); } $atomic_type = self::fixScalarTerms($parse_tree->value); if ($atomic_type === 'array') { return self::getArray()->types['array']; } return new Atomic($atomic_type); } /** * @param string $return_type * @return array */ public static function tokenize($return_type) { $return_type_tokens = ['']; $was_char = false; $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; } /** * @param string $type * @return string */ public static function convertSquareBrackets($type) { $class_chars = '[a-zA-Z\<\>\\\\_]+'; return preg_replace_callback( '/(' . $class_chars . '|' . '\((' . $class_chars . '(\|' . $class_chars . ')*' . ')\))((\[\])+)/', function ($matches) { $inner_type = str_replace(['(', ')'], '', (string)$matches[1]); $dimensionality = strlen((string)$matches[4]) / 2; for ($i = 0; $i < $dimensionality; $i++) { $inner_type = 'array'; } return $inner_type; }, $type ); } /** * @return Type\Union */ public static function getInt() { $type = new Atomic('int'); return new Union([$type]); } /** * @return Type\Union */ public static function getString() { $type = new Atomic('string'); return new Union([$type]); } /** * @return Type\Union */ public static function getNull() { $type = new Atomic('null'); return new Union([$type]); } /** * @return Type\Union */ public static function getMixed() { $type = new Atomic('mixed'); return new Union([$type]); } /** * @return Type\Union */ public static function getBool() { $type = new Atomic('bool'); return new Union([$type]); } /** * @return Type\Union */ public static function getFloat() { $type = new Atomic('float'); return new Union([$type]); } /** * @return Type\Union */ public static function getObject() { $type = new Atomic('object'); return new Union([$type]); } /** * @return Type\Union */ public static function getClosure() { $type = new Atomic('Closure'); return new Union([$type]); } /** * @return Type\Union */ public static function getArray() { $type = new Type\GenericArray( 'array', [ Type::getMixed(), Type::getMixed() ] ); return new Union([$type]); } /** * @return Type\Union */ public static function getEmptyArray() { return new Type\Union([ new Type\GenericArray( 'array', [ new Type\Union([new Type\Atomic('empty')]), new Type\Union([new Type\Atomic('empty')]) ] ) ]); } /** * @return Type\Union */ public static function getVoid() { $type = new Atomic('void'); return new Union([$type]); } /** * @return Type\Union */ public static function getFalse() { $type = new Atomic('false'); return new Union([$type]); } /** * @param array $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 ) { // 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 { $context_type->type_params[$i] = Type::combineUnionTypes( $redefined_atomic_type->type_params[$i], $context_type->type_params[$i] ); } 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] ); } } } } } } } /** * 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) { return self::combineTypes(array_merge(array_values($type_1->types), array_values($type_2->types))); } /** * Combines types together * - so `int + string = int|string` * - so `array + array = array` * - and `array + string = array|string` * - and `array + array = array` * - and `array + array = array` * - and `array + array = array` * * @param array $types * @return Union */ public static function combineTypes(array $types) { if (in_array(null, $types)) { return Type::getMixed(); } if (count($types) === 1) { if ($types[0]->value === 'false') { $types[0]->value = 'bool'; } return new Union([$types[0]]); } $bar = rand(1000, 9999); if (!$types) { throw new \InvalidArgumentException('You must pass at least one type to combineTypes'); } /** @var array> */ $key_types = []; /** @var array> */ $value_types = []; foreach ($types as $type) { $result = self::scrapeTypeProperties($type, $key_types, $value_types); if ($result) { return $result; } } if (count($value_types) === 1) { if (isset($value_types['false'])) { return self::getBool(); } } elseif (isset($value_types['void'])) { unset($value_types['void']); if (!isset($value_types['null'])) { $value_types['null'] = ['null' => null]; } } $new_types = []; foreach ($value_types as $generic_type => $value_type) { // special case for ObjectLike where $value_type is actually an array of properties if ($generic_type === 'object-like') { if (!isset($value_types['array']) || isset($value_types['array']['empty'])) { $new_types[] = new ObjectLike('array', $value_type); } continue; } $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' && isset($value_types['object-like']) && count($value_type) === 1 && isset($value_type['empty']) && count($key_type) === 1 && (isset($key_type['empty']) || isset($key_type['string'])) ) { continue; } $expanded_key_types = []; foreach ($key_type as $expandable_key_type) { $expanded_key_types = array_merge($expanded_key_types, array_values($expandable_key_type->types)); } if (count($value_type) === 1) { $value_type_param = array_values($value_type)[0]; $generic_type_params = [$value_type_param]; // if we're continuing, also add the correspoinding key type param if it exists if ($expanded_key_types) { array_unshift($generic_type_params, self::combineTypes($expanded_key_types)); } $new_types[] = $value_type_param ? new Generic($generic_type, $generic_type_params) : new Atomic($generic_type); continue; } $expanded_value_types = []; $has_null = false; 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']]; } } $generic_type_params = [self::combineTypes($expanded_value_types)]; if ($expanded_key_types) { array_unshift($generic_type_params, self::combineTypes($expanded_key_types)); } // we have a generic type with $new_types[] = new Generic($generic_type, $generic_type_params); } $new_types = array_values($new_types); return new Union($new_types); } /** * @param Atomic $type * @param array> &$key_types * @param array> &$value_types * @return null|Union */ public static function scrapeTypeProperties(Atomic $type, array &$key_types, array &$value_types) { // if we see the magic empty value and there's more than one type, ignore it if ($type->value === 'empty') { return null; } if ($type->value === 'mixed') { return Type::getMixed(); } // deal with false|bool => bool if ($type->value === 'false' && isset($value_types['bool'])) { return null; } elseif ($type->value === 'bool' && isset($value_types['false'])) { unset($value_types['false']); } if ($type instanceof Generic) { if (!isset($value_types[$type->value])) { $value_types[$type->value] = []; } $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]; } } elseif ($type instanceof ObjectLike) { foreach ($type->properties as $candidate_property_name => $candidate_property_type) { $value_type = isset($value_types['object-like'][$candidate_property_name]) ? $value_types['object-like'][$candidate_property_name] : null; if (!$value_type) { $value_types['object-like'][$candidate_property_name] = $candidate_property_type; } else { $value_types['object-like'][$candidate_property_name] = Type::combineUnionTypes( $value_type, $candidate_property_type ); } } } else { if (!isset($value_types[$type->value])) { $value_types[$type->value] = []; } if ($type->value === 'array') { throw new \InvalidArgumentException('Cannot have a non-generic array'); } $value_types[$type->value][(string) $type] = null; } } }