AST: new NodeList class for collections of nodes (vs array) to enable effective conversion of libgraphqlparser output to our AST tree

This commit is contained in:
Vladimir Razuvaev 2017-07-21 22:29:59 +07:00
parent e04d3300a7
commit 1af902865b
16 changed files with 1713 additions and 65 deletions

View File

@ -11,6 +11,7 @@ use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\FragmentSpreadNode;
use GraphQL\Language\AST\InlineFragmentNode;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\VariableNode;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Language\Printer;
@ -180,7 +181,7 @@ class Values
*/
public static function getDirectiveValues(Directive $directiveDef, $node, $variableValues = null)
{
if (isset($node->directives) && is_array($node->directives)) {
if (isset($node->directives) && $node->directives instanceof NodeList) {
$directiveNode = Utils::find($node->directives, function(DirectiveNode $directive) use ($directiveDef) {
return $directive->name->value === $directiveDef->name;
});

View File

@ -45,12 +45,28 @@ class Location
*/
public $source;
public function __construct(Token $startToken, Token $endToken, Source $source = null)
/**
* @param $start
* @param $end
* @return static
*/
public static function create($start, $end)
{
$tmp = new static();
$tmp->start = $start;
$tmp->end = $end;
return $tmp;
}
public function __construct(Token $startToken = null, Token $endToken = null, Source $source = null)
{
$this->startToken = $startToken;
$this->endToken = $endToken;
$this->start = $startToken->start;
$this->end = $endToken->end;
$this->source = $source;
if ($startToken && $endToken) {
$this->start = $startToken->start;
$this->end = $endToken->end;
}
}
}

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Language\AST;
use GraphQL\Error\InvariantViolation;
use GraphQL\Utils\Utils;
abstract class Node
@ -37,12 +38,69 @@ abstract class Node
*/
public $loc;
/**
* Converts representation of AST as associative array to Node instance.
*
* For example:
*
* ```php
* Node::fromArray([
* 'kind' => 'ListValue',
* 'values' => [
* ['kind' => 'StringValue', 'value' => 'my str'],
* ['kind' => 'StringValue', 'value' => 'my other str']
* ],
* 'loc' => ['start' => 21, 'end' => 25]
* ]);
* ```
*
* Will produce instance of `ListValueNode` where `values` prop is a lazily-evaluated `NodeList`
* returning instances of `StringValueNode` on access.
*
* This is a reverse operation for $node->toArray(true)
*
* @param array $node
* @return EnumValueDefinitionNode
*/
public static function fromArray(array $node)
{
if (!isset($node['kind']) || !isset(NodeKind::$classMap[$node['kind']])) {
throw new InvariantViolation("Unexpected node structure: " . Utils::printSafeJson($node));
}
$kind = isset($node['kind']) ? $node['kind'] : null;
$class = NodeKind::$classMap[$kind];
$instance = new $class([]);
if (isset($node['loc'], $node['loc']['start'], $node['loc']['end'])) {
$instance->loc = Location::create($node['loc']['start'], $node['loc']['end']);
}
foreach ($node as $key => $value) {
if ('loc' === $key || 'kind' === $key) {
continue ;
}
if (is_array($value)) {
if (isset($value[0]) || empty($value)) {
$value = new NodeList($value);
} else {
$value = self::fromArray($value);
}
}
$instance->{$key} = $value;
}
return $instance;
}
/**
* @param array $vars
*/
public function __construct(array $vars)
{
Utils::assign($this, $vars);
if (!empty($vars)) {
Utils::assign($this, $vars);
}
}
/**
@ -91,34 +149,53 @@ abstract class Node
*/
public function toArray($recursive = false)
{
$tmp = (array) $this;
$tmp['loc'] = [
'start' => $this->loc->start,
'end' => $this->loc->end
];
if ($recursive) {
$this->recursiveToArray($tmp);
}
return $this->recursiveToArray($this);
} else {
$tmp = (array) $this;
return $tmp;
$tmp['loc'] = [
'start' => $this->loc->start,
'end' => $this->loc->end
];
return $tmp;
}
}
/**
* @param $object
* @param Node $node
* @return array
*/
public function recursiveToArray(&$object)
private function recursiveToArray(Node $node)
{
if ($object instanceof Node) {
/** @var Node $object */
$object = $object->toArray(true);
} elseif (is_object($object)) {
$object = (array) $object;
} elseif (is_array($object)) {
foreach ($object as &$o) {
$this->recursiveToArray($o);
$result = [
'kind' => $node->kind,
'loc' => [
'start' => $node->loc->start,
'end' => $node->loc->end
]
];
foreach (get_object_vars($node) as $prop => $propValue) {
if (isset($result[$prop]))
continue;
if (is_array($propValue) || $propValue instanceof NodeList) {
$tmp = [];
foreach ($propValue as $tmp1) {
$tmp[] = $tmp1 instanceof Node ? $this->recursiveToArray($tmp1) : (array) $tmp1;
}
} else if ($propValue instanceof Node) {
$tmp = $this->recursiveToArray($propValue);
} else if (is_scalar($propValue) || null === $propValue) {
$tmp = $propValue;
} else {
$tmp = null;
}
$result[$prop] = $tmp;
}
return $result;
}
}

View File

@ -70,4 +70,66 @@ class NodeKind
// Directive Definitions
const DIRECTIVE_DEFINITION = 'DirectiveDefinition';
/**
* @todo conver to const array when moving to PHP5.6
* @var array
*/
public static $classMap = [
NodeKind::NAME => NameNode::class,
// Document
NodeKind::DOCUMENT => DocumentNode::class,
NodeKind::OPERATION_DEFINITION => OperationDefinitionNode::class,
NodeKind::VARIABLE_DEFINITION => VariableDefinitionNode::class,
NodeKind::VARIABLE => VariableNode::class,
NodeKind::SELECTION_SET => SelectionSetNode::class,
NodeKind::FIELD => FieldNode::class,
NodeKind::ARGUMENT => ArgumentNode::class,
// Fragments
NodeKind::FRAGMENT_SPREAD => FragmentSpreadNode::class,
NodeKind::INLINE_FRAGMENT => InlineFragmentNode::class,
NodeKind::FRAGMENT_DEFINITION => FragmentDefinitionNode::class,
// Values
NodeKind::INT => IntValueNode::class,
NodeKind::FLOAT => FloatValueNode::class,
NodeKind::STRING => StringValueNode::class,
NodeKind::BOOLEAN => BooleanValueNode::class,
NodeKind::ENUM => EnumValueNode::class,
NodeKind::NULL => NullValueNode::class,
NodeKind::LST => ListValueNode::class,
NodeKind::OBJECT => ObjectValueNode::class,
NodeKind::OBJECT_FIELD => ObjectFieldNode::class,
// Directives
NodeKind::DIRECTIVE => DirectiveNode::class,
// Types
NodeKind::NAMED_TYPE => NamedTypeNode::class,
NodeKind::LIST_TYPE => ListTypeNode::class,
NodeKind::NON_NULL_TYPE => NonNullTypeNode::class,
// Type System Definitions
NodeKind::SCHEMA_DEFINITION => SchemaDefinitionNode::class,
NodeKind::OPERATION_TYPE_DEFINITION => OperationTypeDefinitionNode::class,
// Type Definitions
NodeKind::SCALAR_TYPE_DEFINITION => ScalarTypeDefinitionNode::class,
NodeKind::OBJECT_TYPE_DEFINITION => ObjectTypeDefinitionNode::class,
NodeKind::FIELD_DEFINITION => FieldDefinitionNode::class,
NodeKind::INPUT_VALUE_DEFINITION => InputValueDefinitionNode::class,
NodeKind::INTERFACE_TYPE_DEFINITION => InterfaceTypeDefinitionNode::class,
NodeKind::UNION_TYPE_DEFINITION => UnionTypeDefinitionNode::class,
NodeKind::ENUM_TYPE_DEFINITION => EnumTypeDefinitionNode::class,
NodeKind::ENUM_VALUE_DEFINITION => EnumValueDefinitionNode::class,
NodeKind::INPUT_OBJECT_TYPE_DEFINITION =>InputObjectTypeDefinitionNode::class,
// Type Extensions
NodeKind::TYPE_EXTENSION_DEFINITION => TypeExtensionDefinitionNode::class,
// Directive Definitions
NodeKind::DIRECTIVE_DEFINITION => DirectiveDefinitionNode::class
];
}

View File

@ -0,0 +1,119 @@
<?php
namespace GraphQL\Language\AST;
/**
* Class NodeList
*
* @package GraphQL\Utils
*/
class NodeList implements \ArrayAccess, \IteratorAggregate, \Countable
{
/**
* @var array
*/
private $nodes;
/**
* @param array $nodes
* @return static
*/
public static function create(array $nodes)
{
return new static($nodes);
}
/**
* NodeList constructor.
* @param array $nodes
*/
public function __construct(array $nodes)
{
$this->nodes = $nodes;
}
/**
* @param mixed $offset
* @return bool
*/
public function offsetExists($offset)
{
return isset($this->nodes[$offset]);
}
/**
* @param mixed $offset
* @return mixed
*/
public function offsetGet($offset)
{
$item = $this->nodes[$offset];
if (is_array($item) && isset($item['kind'])) {
$this->nodes[$offset] = $item = Node::fromArray($item);
}
return $item;
}
/**
* @param mixed $offset
* @param mixed $value
*/
public function offsetSet($offset, $value)
{
if (is_array($value) && isset($value['kind'])) {
$value = Node::fromArray($value);
}
$this->nodes[$offset] = $value;
}
/**
* @param mixed $offset
*/
public function offsetUnset($offset)
{
unset($this->nodes[$offset]);
}
/**
* @param int $offset
* @param int $length
* @param mixed $replacement
* @return NodeList
*/
public function splice($offset, $length, $replacement = null)
{
return new NodeList(array_splice($this->nodes, $offset, $length, $replacement));
}
/**
* @param $list
* @return NodeList
*/
public function merge($list)
{
if ($list instanceof NodeList) {
$list = $list->nodes;
}
return new NodeList(array_merge($this->nodes, $list));
}
/**
* @return \Generator
*/
public function getIterator()
{
$count = count($this->nodes);
for ($i = 0; $i < $count; $i++) {
yield $this->offsetGet($i);
}
}
/**
* @return int
*/
public function count()
{
return count($this->nodes);
}
}

View File

@ -26,6 +26,7 @@ use GraphQL\Language\AST\ListTypeNode;
use GraphQL\Language\AST\Location;
use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\NamedTypeNode;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\NonNullTypeNode;
use GraphQL\Language\AST\NullValueNode;
use GraphQL\Language\AST\ObjectFieldNode;
@ -248,7 +249,7 @@ class Parser
while (!$this->skip($closeKind)) {
$nodes[] = $parseFn($this);
}
return $nodes;
return new NodeList($nodes);
}
/**
@ -260,7 +261,7 @@ class Parser
* @param $openKind
* @param $parseFn
* @param $closeKind
* @return array
* @return NodeList
* @throws SyntaxError
*/
function many($openKind, $parseFn, $closeKind)
@ -271,7 +272,7 @@ class Parser
while (!$this->skip($closeKind)) {
$nodes[] = $parseFn($this);
}
return $nodes;
return new NodeList($nodes);
}
/**
@ -307,7 +308,7 @@ class Parser
} while (!$this->skip(Token::EOF));
return new DocumentNode([
'definitions' => $definitions,
'definitions' => new NodeList($definitions),
'loc' => $this->loc($start)
]);
}
@ -363,7 +364,7 @@ class Parser
'operation' => 'query',
'name' => null,
'variableDefinitions' => null,
'directives' => [],
'directives' => new NodeList([]),
'selectionSet' => $this->parseSelectionSet(),
'loc' => $this->loc($start)
]);
@ -408,13 +409,13 @@ class Parser
*/
function parseVariableDefinitions()
{
return $this->peek(Token::PAREN_L) ?
$this->many(
Token::PAREN_L,
[$this, 'parseVariableDefinition'],
Token::PAREN_R
) :
[];
return $this->peek(Token::PAREN_L) ?
$this->many(
Token::PAREN_L,
[$this, 'parseVariableDefinition'],
Token::PAREN_R
) :
new NodeList([]);
}
/**
@ -513,7 +514,7 @@ class Parser
{
return $this->peek(Token::PAREN_L) ?
$this->many(Token::PAREN_L, [$this, 'parseArgument'], Token::PAREN_R) :
[];
new NodeList([]);
}
/**
@ -726,7 +727,7 @@ class Parser
$fields[] = $this->parseObjectField($isConst);
}
return new ObjectValueNode([
'fields' => $fields,
'fields' => new NodeList($fields),
'loc' => $this->loc($start)
]);
}
@ -760,7 +761,7 @@ class Parser
while ($this->peek(Token::AT)) {
$directives[] = $this->parseDirective();
}
return $directives;
return new NodeList($directives);
}
/**

View File

@ -38,6 +38,7 @@ use GraphQL\Language\AST\StringValueNode;
use GraphQL\Language\AST\TypeExtensionDefinitionNode;
use GraphQL\Language\AST\UnionTypeDefinitionNode;
use GraphQL\Language\AST\VariableDefinitionNode;
use GraphQL\Utils\Utils;
class Printer
{
@ -280,7 +281,7 @@ class Printer
return $maybeArray
? implode(
$separator,
array_filter(
Utils::filter(
$maybeArray,
function($x) { return !!$x;}
)

View File

@ -3,6 +3,7 @@ namespace GraphQL\Language;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NodeList;
use GraphQL\Utils\TypeInfo;
class VisitorOperation
@ -180,7 +181,7 @@ class Visitor
$visitorKeys = $keyMap ?: self::$visitorKeys;
$stack = null;
$inArray = is_array($root);
$inArray = $root instanceof NodeList || is_array($root);
$keys = [$root];
$index = -1;
$edits = [];
@ -206,6 +207,9 @@ class Visitor
if ($isEdited) {
if ($inArray) {
// $node = $node; // arrays are value types in PHP
if ($node instanceof NodeList) {
$node = clone $node;
}
} else {
$node = clone $node;
}
@ -218,10 +222,14 @@ class Visitor
$editKey -= $editOffset;
}
if ($inArray && $editValue === null) {
array_splice($node, $editKey, 1);
if ($node instanceof NodeList) {
$node->splice($editKey, 1);
} else {
array_splice($node, $editKey, 1);
}
$editOffset++;
} else {
if (is_array($node)) {
if ($node instanceof NodeList || is_array($node)) {
$node[$editKey] = $editValue;
} else {
$node->{$editKey} = $editValue;
@ -236,7 +244,7 @@ class Visitor
$stack = $stack['prev'];
} else {
$key = $parent ? ($inArray ? $index : $keys[$index]) : $UNDEFINED;
$node = $parent ? (is_array($parent) ? $parent[$key] : $parent->{$key}) : $newRoot;
$node = $parent ? (($parent instanceof NodeList || is_array($parent)) ? $parent[$key] : $parent->{$key}) : $newRoot;
if ($node === null || $node === $UNDEFINED) {
continue;
}
@ -246,7 +254,7 @@ class Visitor
}
$result = null;
if (!is_array($node)) {
if (!$node instanceof NodeList && !is_array($node)) {
if (!($node instanceof Node)) {
throw new \Exception('Invalid AST Node: ' . json_encode($node));
}
@ -297,7 +305,7 @@ class Visitor
'edits' => $edits,
'prev' => $stack
];
$inArray = is_array($node);
$inArray = $node instanceof NodeList || is_array($node);
$keys = ($inArray ? $node : $visitorKeys[$node->kind]) ?: [];
$index = -1;

View File

@ -183,12 +183,19 @@ class Utils
return $grouped;
}
/**
* @param array|Traversable $traversable
* @param callable $keyFn
* @param callable $valFn
* @return array
*/
public static function keyValMap($traversable, callable $keyFn, callable $valFn)
{
return array_reduce($traversable, function ($map, $item) use ($keyFn, $valFn) {
$map = [];
foreach ($traversable as $item) {
$map[$keyFn($item)] = $valFn($item);
return $map;
}, []);
}
return $map;
}
/**

View File

@ -379,7 +379,7 @@ class OverlappingFieldsCanBeMerged
*
* @return bool|string
*/
private function sameArguments(array $arguments1, array $arguments2)
private function sameArguments($arguments1, $arguments2)
{
if (count($arguments1) !== count($arguments2)) {
return false;

View File

@ -131,13 +131,13 @@ class ValidationContext
{
$fragments = $this->fragments;
if (!$fragments) {
$this->fragments = $fragments =
array_reduce($this->getDocument()->definitions, function($frags, $statement) {
if ($statement->kind === NodeKind::FRAGMENT_DEFINITION) {
$frags[$statement->name->value] = $statement;
}
return $frags;
}, []);
$fragments = [];
foreach ($this->getDocument()->definitions as $statement) {
if ($statement->kind === NodeKind::FRAGMENT_DEFINITION) {
$fragments[$statement->name->value] = $statement;
}
}
$this->fragments = $fragments;
}
return isset($fragments[$name]) ? $fragments[$name] : null;
}

View File

@ -7,6 +7,7 @@ use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Language\Parser;
@ -150,20 +151,20 @@ HEREDOC;
$result = Parser::parse($query, ['noLocation' => true]);
$expected = new SelectionSetNode([
'selections' => [
'selections' => new NodeList([
new FieldNode([
'name' => new NameNode(['value' => 'field']),
'arguments' => [
'arguments' => new NodeList([
new ArgumentNode([
'name' => new NameNode(['value' => 'arg']),
'value' => new StringValueNode([
'value' => "Has a $char multi-byte character."
])
])
],
'directives' => []
]),
'directives' => new NodeList([])
])
]
])
]);
$this->assertEquals($expected, $result->definitions[0]->selectionSet);

View File

@ -0,0 +1,73 @@
<?php
namespace GraphQL\Tests;
use GraphQL\Language\AST\Location;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\Parser;
class SerializationTest extends \PHPUnit_Framework_TestCase
{
public function testSerializesAst()
{
$kitchenSink = file_get_contents(__DIR__ . '/kitchen-sink.graphql');
$ast = Parser::parse($kitchenSink);
$expectedAst = json_decode(file_get_contents(__DIR__ . '/kitchen-sink.ast'), true);
$this->assertEquals($expectedAst, $ast->toArray(true));
}
public function testUnserializesAst()
{
$kitchenSink = file_get_contents(__DIR__ . '/kitchen-sink.graphql');
$serializedAst = json_decode(file_get_contents(__DIR__ . '/kitchen-sink.ast'), true);
$actualAst = Node::fromArray($serializedAst);
$parsedAst = Parser::parse($kitchenSink);
$this->assertNodesAreEqual($parsedAst, $actualAst);
}
/**
* Compares two nodes by actually iterating over all NodeLists, properly comparing locations (ignoring tokens), etc
*
* @param $expected
* @param $actual
* @param array $path
*/
private function assertNodesAreEqual($expected, $actual, $path = [])
{
$err = "Mismatch at AST path: " . implode(', ', $path);
$this->assertInstanceOf(Node::class, $actual, $err);
$this->assertEquals(get_class($expected), get_class($actual), $err);
$expectedVars = get_object_vars($expected);
$actualVars = get_object_vars($actual);
$this->assertSame(count($expectedVars), count($actualVars), $err);
$this->assertEquals(array_keys($expectedVars), array_keys($actualVars), $err);
foreach ($expectedVars as $name => $expectedValue) {
$actualValue = $actualVars[$name];
$tmpPath = $path;
$tmpPath[] = $name;
$err = "Mismatch at AST path: " . implode(', ', $tmpPath);
if ($expectedValue instanceof Node) {
$this->assertNodesAreEqual($expectedValue, $actualValue, $tmpPath);
} else if ($expectedValue instanceof NodeList) {
$this->assertEquals(count($expectedValue), count($actualValue), $err);
$this->assertInstanceOf(NodeList::class, $actualValue, $err);
foreach ($expectedValue as $index => $listNode) {
$tmpPath2 = $tmpPath;
$tmpPath2 [] = $index;
$this->assertNodesAreEqual($listNode, $actualValue[$index], $tmpPath2);
}
} else if ($expectedValue instanceof Location) {
$this->assertInstanceOf(Location::class, $actualValue, $err);
$this->assertSame($expectedValue->start, $actualValue->start, $err);
$this->assertSame($expectedValue->end, $actualValue->end, $err);
} else {
$this->assertEquals($expectedValue, $actualValue, $err);
}
}
}
}

View File

@ -4,6 +4,7 @@ namespace GraphQL\Tests\Language;
use GraphQL\Language\AST\Location;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeList;
class TestUtils
{
@ -22,7 +23,7 @@ class TestUtils
if (isset($result[$prop]))
continue;
if (is_array($propValue)) {
if (is_array($propValue) || $propValue instanceof NodeList) {
$tmp = [];
foreach ($propValue as $tmp1) {
$tmp[] = $tmp1 instanceof Node ? self::nodeToArray($tmp1) : (array) $tmp1;

View File

@ -6,6 +6,7 @@ use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\NameNode;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\AST\NodeList;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\AST\SelectionSetNode;
use GraphQL\Language\Parser;
@ -157,7 +158,7 @@ class VisitorTest extends \PHPUnit_Framework_TestCase
if ($node instanceof FieldNode && $node->name->value === 'a') {
return new FieldNode([
'selectionSet' => new SelectionSetNode(array(
'selections' => array_merge([$addedField], $node->selectionSet->selections)
'selections' => NodeList::create([$addedField])->merge($node->selectionSet->selections)
))
]);
}

File diff suppressed because it is too large Load Diff