1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Use objects for type parsing

This commit is contained in:
Matthew Brown 2018-03-20 20:19:26 -04:00
parent 24490aac0e
commit 3f90bceabf
10 changed files with 283 additions and 106 deletions

View File

@ -51,10 +51,6 @@ abstract class Type
$type_string = preg_replace('/\?(?=[a-zA-Z])/', 'null|', $type_string);
if (preg_match('/[\[\]()]/', $type_string)) {
throw new TypeParseTreeException('Invalid characters in type');
}
$type_tokens = self::tokenize($type_string);
if (count($type_tokens) === 1) {
@ -68,8 +64,6 @@ abstract class Type
$parsed_type = self::getTypeFromTree($parse_tree, $php_compatible);
} catch (TypeParseTreeException $e) {
throw $e;
} catch (\Exception $e) {
throw new TypeParseTreeException($e->getMessage());
}
if (!($parsed_type instanceof Union)) {
@ -131,12 +125,8 @@ abstract class Type
*/
private static function getTypeFromTree(ParseTree $parse_tree, $php_compatible)
{
if (!$parse_tree->value) {
throw new \InvalidArgumentException('Parse tree must have a value');
}
if ($parse_tree->value === ParseTree::GENERIC) {
$generic_type = array_shift($parse_tree->children);
if ($parse_tree instanceof ParseTree\GenericTree) {
$generic_type = $parse_tree->value;
$generic_params = array_map(
/**
@ -150,11 +140,7 @@ abstract class 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, false);
$generic_type_value = self::fixScalarTerms($generic_type, false);
if (($generic_type_value === 'array' || $generic_type_value === 'Generator') &&
count($generic_params) === 1
@ -173,7 +159,7 @@ abstract class Type
return new TGenericObject($generic_type_value, $generic_params);
}
if ($parse_tree->value === ParseTree::UNION) {
if ($parse_tree instanceof ParseTree\UnionTree) {
$union_types = array_map(
/**
* @return Atomic
@ -195,7 +181,7 @@ abstract class Type
return self::combineTypes($union_types);
}
if ($parse_tree->value === ParseTree::INTERSECTION) {
if ($parse_tree instanceof ParseTree\IntersectionTree) {
$intersection_types = array_map(
/**
* @return Atomic
@ -228,22 +214,24 @@ abstract class Type
return new Type\Union([$first_type]);
}
if ($parse_tree->value === ParseTree::OBJECT_LIKE) {
if ($parse_tree instanceof ParseTree\ObjectLikeTree) {
$properties = [];
$type = array_shift($parse_tree->children);
$type = $parse_tree->value;
foreach ($parse_tree->children as $i => $property_branch) {
if ($property_branch->value !== ParseTree::OBJECT_PROPERTY) {
if (!$property_branch instanceof ParseTree\ObjectLikePropertyTree) {
$property_type = self::getTypeFromTree($property_branch, false);
$property_maybe_undefined = false;
$property_key = (string)$i;
} elseif (count($property_branch->children) === 2) {
$property_type = self::getTypeFromTree($property_branch->children[1], false);
} elseif (count($property_branch->children) === 1) {
$property_type = self::getTypeFromTree($property_branch->children[0], false);
$property_maybe_undefined = $property_branch->possibly_undefined;
$property_key = (string)($property_branch->children[0]->value);
$property_key = $property_branch->value;
} else {
throw new \InvalidArgumentException('Unexpected number of property parts');
throw new \InvalidArgumentException(
'Unexpected number of property parts (' . count($property_branch->children) . ')'
);
}
if (!$property_type instanceof Union) {
@ -257,7 +245,7 @@ abstract class Type
$properties[$property_key] = $property_type;
}
if ($type->value !== 'array') {
if ($type !== 'array') {
throw new \InvalidArgumentException('Object-like type must be array');
}
@ -268,6 +256,10 @@ abstract class Type
return new ObjectLike($properties);
}
if (!$parse_tree instanceof ParseTree\Value) {
throw new \InvalidArgumentException('Unrecognised parse tree type');
}
$atomic_type = self::fixScalarTerms($parse_tree->value, $php_compatible);
return Atomic::create($atomic_type, $php_compatible);
@ -309,6 +301,8 @@ abstract class Type
$char === '}' ||
$char === '[' ||
$char === ']' ||
$char === '(' ||
$char === ')' ||
$char === ' ' ||
$char === '&' ||
$char === ':'

View File

@ -5,22 +5,11 @@ use Psalm\Exception\TypeParseTreeException;
class ParseTree
{
const GENERIC = '<>';
const OBJECT_LIKE = '{}';
const OBJECT_PROPERTY = ':';
const UNION = '|';
const INTERSECTION = '&';
/**
* @var array<int, ParseTree>
*/
public $children = [];
/**
* @var string|null
*/
public $value;
/**
* @var null|ParseTree
*/
@ -32,12 +21,10 @@ class ParseTree
public $possibly_undefined = false;
/**
* @param string|null $value
* @param ParseTree|null $parent
*/
public function __construct($value, ParseTree $parent = null)
public function __construct(ParseTree $parent = null)
{
$this->value = $value;
$this->parent = $parent;
}
@ -51,35 +38,19 @@ class ParseTree
public static function createFromTokens(array $type_tokens)
{
// We construct a parse tree corresponding to the type
$parse_tree = new self(null, null);
$parse_tree = new ParseTree\Root();
$current_leaf = $parse_tree;
$last_token = null;
while ($type_tokens) {
$type_token = array_shift($type_tokens);
for ($i = 0, $c = count($type_tokens); $i < $c; ++$i) {
$last_token = $i > 0 ? $type_tokens[$i - 1] : null;
$type_token = $type_tokens[$i];
$next_token = $i + 1 < $c ? $type_tokens[$i + 1] : null;
switch ($type_token) {
case '<':
case '{':
$current_parent = $current_leaf->parent;
$new_parent_leaf = new self(
$type_token === '<' ? ParseTree::GENERIC : ParseTree::OBJECT_LIKE,
$current_parent
);
$new_parent_leaf->children = [$current_leaf];
$current_leaf->parent = $new_parent_leaf;
if ($current_parent) {
array_pop($current_parent->children);
$current_parent->children[] = $new_parent_leaf;
} else {
$parse_tree = $new_parent_leaf;
}
break;
throw new TypeParseTreeException('Unexpected token');
case '>':
do {
@ -88,7 +59,7 @@ class ParseTree
}
$current_leaf = $current_leaf->parent;
} while ($current_leaf->value !== self::GENERIC);
} while (!$current_leaf instanceof ParseTree\GenericTree);
break;
@ -99,7 +70,7 @@ class ParseTree
}
$current_leaf = $current_leaf->parent;
} while ($current_leaf->value !== self::OBJECT_LIKE);
} while (!$current_leaf instanceof ParseTree\ObjectLikeTree);
break;
@ -112,9 +83,15 @@ class ParseTree
$context_node = $current_leaf;
while ($context_node &&
$context_node->value !== self::GENERIC &&
$context_node->value !== self::OBJECT_LIKE
if ($context_node instanceof ParseTree\GenericTree
|| $context_node instanceof ParseTree\ObjectLikeTree
) {
$context_node = $context_node->parent;
}
while ($context_node
&& !$context_node instanceof ParseTree\GenericTree
&& !$context_node instanceof ParseTree\ObjectLikeTree
) {
$context_node = $context_node->parent;
}
@ -123,26 +100,14 @@ class ParseTree
throw new TypeParseTreeException('Cannot parse comma in non-generic/array type');
}
if ($context_node->value === self::GENERIC && $current_parent->value !== self::GENERIC) {
if (!isset($current_parent->parent) || !$current_parent->parent->value) {
throw new TypeParseTreeException('Cannot parse comma in non-generic/array type');
}
$current_leaf = $current_leaf->parent;
} elseif ($context_node->value === self::OBJECT_LIKE
&& $current_parent->value !== self::OBJECT_LIKE
) {
do {
$current_leaf = $current_leaf->parent;
} while ($current_leaf->parent && $current_leaf->parent->value !== self::OBJECT_LIKE);
}
$current_leaf = $context_node;
break;
case ':':
$current_parent = $current_leaf->parent;
if ($current_parent && $current_parent->value === ParseTree::OBJECT_PROPERTY) {
if ($current_parent && $current_parent instanceof ParseTree\ObjectLikePropertyTree) {
continue;
}
@ -150,14 +115,19 @@ class ParseTree
throw new TypeParseTreeException('Cannot process colon without parent');
}
$new_parent_leaf = new self(self::OBJECT_PROPERTY, $current_parent);
$new_parent_leaf->children = [$current_leaf];
if (!$current_leaf instanceof ParseTree\Value) {
throw new TypeParseTreeException('Unexpected LHS of property');
}
$new_parent_leaf = new ParseTree\ObjectLikePropertyTree($current_leaf->value, $current_parent);
$new_parent_leaf->possibly_undefined = $last_token === '?';
$current_leaf->parent = $new_parent_leaf;
array_pop($current_parent->children);
$current_parent->children[] = $new_parent_leaf;
$current_leaf = $new_parent_leaf;
break;
case '?':
@ -166,11 +136,12 @@ class ParseTree
case '|':
$current_parent = $current_leaf->parent;
if ($current_parent && $current_parent->value === ParseTree::UNION) {
if ($current_parent && $current_parent instanceof ParseTree\UnionTree) {
$current_leaf = $current_parent;
continue;
}
$new_parent_leaf = new self(self::UNION, $current_parent);
$new_parent_leaf = new ParseTree\UnionTree($current_parent);
$new_parent_leaf->children = [$current_leaf];
$current_leaf->parent = $new_parent_leaf;
@ -181,16 +152,18 @@ class ParseTree
$parse_tree = $new_parent_leaf;
}
$current_leaf = $new_parent_leaf;
break;
case '&':
$current_parent = $current_leaf->parent;
if ($current_parent && $current_parent->value === ParseTree::INTERSECTION) {
if ($current_parent && $current_parent instanceof ParseTree\IntersectionTree) {
continue;
}
$new_parent_leaf = new self(self::INTERSECTION, $current_parent);
$new_parent_leaf = new ParseTree\IntersectionTree($current_parent);
$new_parent_leaf->children = [$current_leaf];
$current_leaf->parent = $new_parent_leaf;
@ -201,26 +174,52 @@ class ParseTree
$parse_tree = $new_parent_leaf;
}
$current_leaf = $new_parent_leaf;
break;
default:
if ($current_leaf->value === null) {
$current_leaf->value = $type_token;
$new_parent = !$current_leaf instanceof ParseTree\Root ? $current_leaf : null;
switch ($next_token) {
case '<':
$new_leaf = new ParseTree\GenericTree(
$type_token,
$new_parent
);
++$i;
break;
case '{':
$new_leaf = new ParseTree\ObjectLikeTree(
$type_token,
$new_parent
);
++$i;
break;
case '(':
throw new TypeParseTreeException('Cannot process bracket yet');
default:
$new_leaf = new ParseTree\Value(
$type_token,
$new_parent
);
break;
}
if ($current_leaf instanceof ParseTree\Root) {
$current_leaf = $parse_tree = $new_leaf;
continue;
}
$new_leaf = new self($type_token, $current_leaf->parent);
if (!isset($current_leaf->parent)) {
throw new TypeParseTreeException('Current leaf must have a parent');
if ($new_leaf->parent) {
$new_leaf->parent->children[] = $new_leaf;
}
$current_leaf->parent->children[] = $new_leaf;
$current_leaf = $new_leaf;
}
$last_token = $type_token;
}
return $parse_tree;

View File

@ -0,0 +1,20 @@
<?php
namespace Psalm\Type\ParseTree;
class GenericTree extends \Psalm\Type\ParseTree
{
/**
* @var string
*/
public $value;
/**
* @param string $value
* @param \Psalm\Type\ParseTree|null $parent
*/
public function __construct($value, \Psalm\Type\ParseTree $parent = null)
{
$this->value = $value;
$this->parent = $parent;
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Type\ParseTree;
class IntersectionTree extends \Psalm\Type\ParseTree
{
}

View File

@ -0,0 +1,20 @@
<?php
namespace Psalm\Type\ParseTree;
class ObjectLikePropertyTree extends \Psalm\Type\ParseTree
{
/**
* @var string
*/
public $value;
/**
* @param string $value
* @param \Psalm\Type\ParseTree|null $parent
*/
public function __construct($value, \Psalm\Type\ParseTree $parent = null)
{
$this->value = $value;
$this->parent = $parent;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace Psalm\Type\ParseTree;
class ObjectLikeTree extends \Psalm\Type\ParseTree
{
/**
* @var string
*/
public $value;
/**
* @param string $value
* @param \Psalm\Type\ParseTree|null $parent
*/
public function __construct($value, \Psalm\Type\ParseTree $parent = null)
{
$this->value = $value;
$this->parent = $parent;
}
}

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Type\ParseTree;
class Root extends \Psalm\Type\ParseTree
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Type\ParseTree;
class UnionTree extends \Psalm\Type\ParseTree
{
}

View File

@ -0,0 +1,20 @@
<?php
namespace Psalm\Type\ParseTree;
class Value extends \Psalm\Type\ParseTree
{
/**
* @var string
*/
public $value;
/**
* @param string $value
* @param \Psalm\Type\ParseTree|null $parent
*/
public function __construct($value, \Psalm\Type\ParseTree $parent = null)
{
$this->value = $value;
$this->parent = $parent;
}
}

View File

@ -10,7 +10,7 @@ class TypeParseTest extends TestCase
*/
public function setUp()
{
parent::setUp();
//parent::setUp();
}
/**
@ -21,6 +21,14 @@ class TypeParseTest extends TestCase
$this->assertSame('int|string', (string) Type::parseString('int|string'));
}
/**
* @return void
*/
public function testBoolOrIntOrString()
{
$this->assertSame('bool|int|string', (string) Type::parseString('bool|int|string'));
}
/**
* @return void
*/
@ -37,6 +45,29 @@ class TypeParseTest extends TestCase
$this->assertSame('array<int, int>', (string) Type::parseString('array<int, int>'));
$this->assertSame('array<int, string>', (string) Type::parseString('array<int, string>'));
$this->assertSame('array<int, static>', (string) Type::parseString('array<int, static>'));
}
/**
* @return void
*/
public function testArrayWithSingleArg()
{
$this->assertSame('array<mixed, int>', (string) Type::parseString('array<int>'));
}
/**
* @return void
*/
public function testArrayWithNestedSingleArg()
{
$this->assertSame('array<mixed, array<mixed, int>>', (string) Type::parseString('array<array<int>>'));
}
/**
* @return void
*/
public function testArrayWithUnion()
{
$this->assertSame('array<int|string, string>', (string) Type::parseString('array<int|string, string>'));
}
@ -59,13 +90,48 @@ class TypeParseTest extends TestCase
/**
* @return void
*/
public function testPhpDocStyle()
public function testPhpDocSimpleArray()
{
$this->assertSame('array<mixed, A>', (string) Type::parseString('A[]'));
}
/**
* @return void
*/
public function testPhpDocUnionArray()
{
$this->assertSame('array<mixed, A|B>', (string) Type::parseString('(A|B)[]'));
}
/**
* @return void
*/
public function testPhpDocMultiDimensionalArray()
{
$this->assertSame('array<mixed, array<mixed, A>>', (string) Type::parseString('A[][]'));
}
/**
* @return void
*/
public function testPhpDocMultidimensionalUnionArray()
{
$this->assertSame('array<mixed, array<mixed, A|B>>', (string) Type::parseString('(A|B)[][]'));
}
/**
* @return void
*/
public function testPhpDocUnionOfArrays()
{
$this->assertSame('array<mixed, A|B>', (string) Type::parseString('A[]|B[]'));
}
/**
* @return void
*/
public function testPhpDocUnionOfArraysOrObject()
{
$this->assertSame('array<mixed, A|B>|C', (string) Type::parseString('A[]|B[]|C'));
}
@ -90,24 +156,49 @@ class TypeParseTest extends TestCase
/**
* @return void
*/
public function testObjectLike()
public function testObjectLikeWithSimpleArgs()
{
$this->assertSame('array{a:int, b:string}', (string) Type::parseString('array{a:int, b:string}'));
}
/**
* @return void
*/
public function testObjectLikeWithUnionArgs()
{
$this->assertSame(
'array{a:int|string, b:string}',
(string) Type::parseString('array{a:int|string, b:string}')
);
}
/**
* @return void
*/
public function testObjectLikeWithGenericArgs()
{
$this->assertSame(
'array{a:array<int, string|int>, b:string}',
(string) Type::parseString('array{a:array<int, string|int>, b:string}')
);
}
/**
* @return void
*/
public function testObjectLikeWithIntKeysAndUnionArgs()
{
$this->assertSame(
'array{0:stdClass|null}',
(string)Type::parseString('array{stdClass|null}')
);
}
/**
* @return void
*/
public function testObjectLikeWithIntKeysAndGenericArgs()
{
$this->assertSame(
'array{0:array<mixed, mixed>}',
(string)Type::parseString('array{array}')
@ -117,11 +208,6 @@ class TypeParseTest extends TestCase
'array{0:array<int, string>}',
(string)Type::parseString('array{array<int, string>}')
);
$this->assertSame(
'array{a:int, b?:int}',
(string)Type::parseString('array{a:int, b?:int}')
);
}
/**