New language features (NamedType, directives rethinking)

This commit is contained in:
vladar 2015-08-17 02:53:11 +06:00
parent 698b2cb862
commit 022c962942
27 changed files with 366 additions and 223 deletions

View File

@ -41,7 +41,7 @@ class Error extends \Exception
* @param array|null $nodes
* @return Error
*/
public static function createLocatedError($error, array $nodes = null)
public static function createLocatedError($error, $nodes = null)
{
if ($error instanceof \Exception) {
$message = $error->getMessage();
@ -56,11 +56,11 @@ class Error extends \Exception
/**
* @param Error $error
* @return FormattedError
* @return array
*/
public static function formatError(Error $error)
{
return new FormattedError($error->getMessage(), $error->getLocations());
return FormattedError::create($error->getMessage(), $error->getLocations());
}
/**
@ -69,12 +69,17 @@ class Error extends \Exception
* @param Source $source
* @param null $positions
*/
public function __construct($message, array $nodes = null, \Exception $previous = null, Source $source = null, $positions = null)
public function __construct($message, $nodes = null, \Exception $previous = null, Source $source = null, $positions = null)
{
parent::__construct($message, 0, $previous);
if ($nodes instanceof \Traversable) {
$nodes = iterator_to_array($nodes);
}
$this->nodes = $nodes;
$this->source = $source;
$this->positions = $positions;
}
/**

View File

@ -1,36 +1,25 @@
<?php
namespace GraphQL;
use GraphQL\Language\SourceLocation;
class FormattedError
{
/**
* @var string
*/
public $message;
/**
* @var array<Language\SourceLocation>
*/
public $locations;
/**
* @param $message
* @param array<Language\SourceLocation> $locations
*/
public function __construct($message, $locations = [])
{
$this->message = $message;
$this->locations = array_map(function($loc) { return $loc->toArray();}, $locations);
}
/**
* @param $error
* @param SourceLocation[] $locations
* @return array
*/
public function toArray()
public static function create($error, array $locations = [])
{
return [
'message' => $this->message,
'locations' => $this->locations
$formatted = [
'message' => $error
];
if (!empty($locations)) {
$formatted['locations'] = array_map(function($loc) { return $loc->toArray();}, $locations);
}
return $formatted;
}
}

View File

@ -1,15 +1,10 @@
<?php
namespace GraphQL\Language\AST;
class Argument extends Node
class Argument extends NamedType
{
public $kind = Node::ARGUMENT;
/**
* @var Name
*/
public $name;
/**
* @var Value
*/

View File

@ -11,7 +11,7 @@ class Directive extends Node
public $name;
/**
* @var Value
* @var Argument[]
*/
public $value;
public $arguments;
}

View File

@ -1,7 +1,7 @@
<?php
namespace GraphQL\Language\AST;
class Field extends Node
class Field extends NamedType
{
public $kind = Node::FIELD;
@ -10,11 +10,6 @@ class Field extends Node
*/
public $alias;
/**
* @var Name
*/
public $name;
/**
* @var array<Argument>|null
*/

View File

@ -2,17 +2,12 @@
namespace GraphQL\Language\AST;
class FragmentDefinition extends Node implements Definition
class FragmentDefinition extends NamedType implements Definition
{
public $kind = Node::FRAGMENT_DEFINITION;
/**
* @var Name
*/
public $name;
/**
* @var Name
* @var NamedType
*/
public $typeCondition;

View File

@ -1,15 +1,10 @@
<?php
namespace GraphQL\Language\AST;
class FragmentSpread extends Node
class FragmentSpread extends NamedType
{
public $kind = Node::FRAGMENT_SPREAD;
/**
* @var Name
*/
public $name;
/**
* @var array<Directive>
*/

View File

@ -6,7 +6,7 @@ class InlineFragment extends Node
public $kind = Node::INLINE_FRAGMENT;
/**
* @var Name
* @var NamedType
*/
public $typeCondition;

View File

@ -1,9 +1,9 @@
<?php
namespace GraphQL\Language\AST;
class ArrayValue extends Node implements Value
class ListValue extends Node implements Value
{
public $kind = Node::ARR;
public $kind = Node::LST;
/**
* @var array<Value>

View File

@ -0,0 +1,12 @@
<?php
namespace GraphQL\Language\AST;
class NamedType extends Node
{
public $kind = Node::NAMED_TYPE;
/**
* @var Name
*/
public $name;
}

View File

@ -32,7 +32,7 @@ abstract class Node
const STRING = 'StringValue';
const BOOLEAN = 'BooleanValue';
const ENUM = 'EnumValue';
const ARR = 'ArrayValue';
const LST = 'ListValue';
const OBJECT = 'ObjectValue';
const OBJECT_FIELD = 'ObjectField';
@ -42,7 +42,7 @@ abstract class Node
// Types
const TYPE = 'Type';
const NAMED_TYPE = 'NamedType';
const LIST_TYPE = 'ListType';
const NON_NULL_TYPE = 'NonNullType';
@ -64,7 +64,7 @@ abstract class Node
| StringValue
| BooleanValue
| EnumValue
| ArrayValue
| ListValue
| ObjectValue
| ObjectField
| Directive

View File

@ -2,15 +2,10 @@
namespace GraphQL\Language\AST;
class ObjectField extends Node
class ObjectField extends NamedType
{
public $kind = Node::OBJECT_FIELD;
/**
* @var Name
*/
public $name;
/**
* @var Value
*/

View File

@ -1,7 +1,7 @@
<?php
namespace GraphQL\Language\AST;
class OperationDefinition extends Node implements Definition
class OperationDefinition extends NamedType implements Definition
{
/**
* @var string
@ -13,11 +13,6 @@ class OperationDefinition extends Node implements Definition
*/
public $operation;
/**
* @var Name|null
*/
public $name;
/**
* @var array<VariableDefinition>
*/

View File

@ -5,7 +5,7 @@ namespace GraphQL\Language\AST;
interface Type
{
/**
export type Type = Name
export type Type = NamedType
| ListType
| NonNullType
*/

View File

@ -10,7 +10,7 @@ export type Value = Variable
| StringValue
| BooleanValue
| EnumValue
| ArrayValue
| ListValue
| ObjectValue
*/
}

View File

@ -1,12 +1,7 @@
<?php
namespace GraphQL\Language\AST;
class Variable extends Node
class Variable extends NamedType
{
public $kind = Node::VARIABLE;
/**
* @var Name
*/
public $name;
}

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Language;
use GraphQL\SyntaxError;
use GraphQL\Utils;
// language/lexer.js
@ -37,7 +38,7 @@ class Lexer
/**
* @param int $fromPosition
* @return Token
* @throws Exception
* @throws SyntaxError
*/
private function readToken($fromPosition)
{
@ -106,7 +107,7 @@ class Lexer
case 34: return $this->readString($position);
}
throw Exception::create($this->source, $position, 'Unexpected character "' . Utils::chr($code). '"');
throw new SyntaxError($this->source, $position, 'Unexpected character "' . Utils::chr($code). '"');
}
/**
@ -142,12 +143,12 @@ class Lexer
* or an int depending on whether a decimal point appears.
*
* Int: -?(0|[1-9][0-9]*)
* Float: -?(0|[1-9][0-9]*)\.[0-9]+(e-?[0-9]+)?
* Float: -?(0|[1-9][0-9]*)(\.[0-9]+)?((E|e)(+|-)?[0-9]+)?
*
* @param $start
* @param $firstCode
* @return Token
* @throws Exception
* @throws SyntaxError
*/
private function readNumber($start, $firstCode)
{
@ -167,7 +168,7 @@ class Lexer
$code = Utils::charCodeAt($body, ++$position);
} while ($code >= 48 && $code <= 57); // 0 - 9
} else {
throw Exception::create($this->source, $position, 'Invalid number');
throw new SyntaxError($this->source, $position, 'Invalid number');
}
if ($code === 46) { // .
@ -179,12 +180,15 @@ class Lexer
$code = Utils::charCodeAt($body, ++$position);
} while ($code >= 48 && $code <= 57); // 0 - 9
} else {
throw Exception::create($this->source, $position, 'Invalid number');
throw new SyntaxError($this->source, $position, 'Invalid number');
}
}
if ($code === 101) { // e
if ($code === 69 || $code === 101) { // E e
$isFloat = true;
$code = Utils::charCodeAt($body, ++$position);
if ($code === 45) { // -
if ($code === 43 || $code === 45) { // + -
$code = Utils::charCodeAt($body, ++$position);
}
if ($code >= 48 && $code <= 57) { // 0 - 9
@ -192,8 +196,7 @@ class Lexer
$code = Utils::charCodeAt($body, ++$position);
} while ($code >= 48 && $code <= 57); // 0 - 9
} else {
throw Exception::create($this->source, $position, 'Invalid number');
}
throw new SyntaxError($this->source, $position, 'Invalid number');
}
}
return new Token(
@ -236,13 +239,13 @@ class Lexer
case 117:
$hex = mb_substr($body, $position + 1, 4);
if (!preg_match('/[0-9a-fA-F]{4}/', $hex)) {
throw Exception::create($this->source, $position, 'Bad character escape sequence');
throw new SyntaxError($this->source, $position, 'Bad character escape sequence');
}
$value .= Utils::chr(hexdec($hex));
$position += 4;
break;
default:
throw Exception::create($this->source, $position, 'Bad character escape sequence');
throw new SyntaxError($this->source, $position, 'Bad character escape sequence');
}
++$position;
$chunkStart = $position;
@ -250,7 +253,7 @@ class Lexer
}
if ($code !== 34) {
throw Exception::create($this->source, $position, 'Unterminated string');
throw new SyntaxError($this->source, $position, 'Unterminated string');
}
$value .= mb_substr($body, $chunkStart, $position - $chunkStart, 'UTF-8');

View File

@ -4,7 +4,7 @@ namespace GraphQL\Language;
// language/parser.js
use GraphQL\Language\AST\Argument;
use GraphQL\Language\AST\ArrayValue;
use GraphQL\Language\AST\ListValue;
use GraphQL\Language\AST\BooleanValue;
use GraphQL\Language\AST\Directive;
use GraphQL\Language\AST\Document;
@ -18,6 +18,7 @@ use GraphQL\Language\AST\IntValue;
use GraphQL\Language\AST\ListType;
use GraphQL\Language\AST\Location;
use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\NamedType;
use GraphQL\Language\AST\NonNullType;
use GraphQL\Language\AST\ObjectField;
use GraphQL\Language\AST\ObjectValue;
@ -26,6 +27,7 @@ use GraphQL\Language\AST\SelectionSet;
use GraphQL\Language\AST\StringValue;
use GraphQL\Language\AST\Variable;
use GraphQL\Language\AST\VariableDefinition;
use GraphQL\SyntaxError;
class Parser
{
@ -148,7 +150,7 @@ class Parser
* the parser. Otherwise, do not change the parser state and return false.
* @param string $kind
* @return Token
* @throws Exception
* @throws SyntaxError
*/
function expect($kind)
{
@ -159,7 +161,7 @@ class Parser
return $token;
}
throw Exception::create(
throw new SyntaxError(
$this->source,
$token->start,
"Expected " . Token::getKindDescription($kind) . ", found " . $token->getDescription()
@ -173,7 +175,7 @@ class Parser
*
* @param string $value
* @return Token
* @throws Exception
* @throws SyntaxError
*/
function expectKeyword($value)
{
@ -183,7 +185,7 @@ class Parser
$this->advance();
return $token;
}
throw Exception::create(
throw new SyntaxError(
$this->source,
$token->start,
'Expected "' . $value . '", found ' . $token->getDescription()
@ -192,12 +194,12 @@ class Parser
/**
* @param Token|null $atToken
* @return Exception
* @return SyntaxError
*/
function unexpected(Token $atToken = null)
{
$token = $atToken ?: $this->token;
return Exception::create($this->source, $token->start, "Unexpected " . $token->getDescription());
return new SyntaxError($this->source, $token->start, "Unexpected " . $token->getDescription());
}
/**
@ -262,6 +264,18 @@ class Parser
));
}
/**
* @return Name
* @throws SyntaxError
*/
function parseFragmentName()
{
if ($this->token->value === 'on') {
throw $this->unexpected();
}
return $this->parseName();
}
/**
* Implements the parsing rules in the Document section.
*
@ -358,7 +372,7 @@ class Parser
'variable' => $var,
'type' => $type,
'defaultValue' =>
($this->skip(Token::EQUALS) ? $this->parseValue(true) : null),
($this->skip(Token::EQUALS) ? $this->parseValueLiteral(true) : null),
'loc' => $this->loc($start)
));
}
@ -441,7 +455,7 @@ class Parser
$name = $this->parseName();
$this->expect(Token::COLON);
$value = $this->parseValue(false);
$value = $this->parseValueLiteral(false);
return new Argument(array(
'name' => $name,
@ -463,14 +477,14 @@ class Parser
if ($this->token->value === 'on') {
$this->advance();
return new InlineFragment(array(
'typeCondition' => $this->parseName(),
'typeCondition' => $this->parseNamedType(),
'directives' => $this->parseDirectives(),
'selectionSet' => $this->parseSelectionSet(),
'loc' => $this->loc($start)
));
}
return new FragmentSpread(array(
'name' => $this->parseName(),
'name' => $this->parseFragmentName(),
'directives' => $this->parseDirectives(),
'loc' => $this->loc($start)
));
@ -478,15 +492,15 @@ class Parser
/**
* @return FragmentDefinition
* @throws Exception
* @throws SyntaxError
*/
function parseFragmentDefinition() {
$start = $this->token->start;
$this->expectKeyword('fragment');
$name = $this->parseName();
$name = $this->parseFragmentName();
$this->expectKeyword('on');
$typeCondition = $this->parseName();
$typeCondition = $this->parseNamedType();
return new FragmentDefinition(array(
'name' => $name,
@ -500,7 +514,7 @@ class Parser
// Implements the parsing rules in the Values section.
function parseVariableValue()
{
return $this->parseValue(false);
return $this->parseValueLiteral(false);
}
/**
@ -509,15 +523,15 @@ class Parser
*/
function parseConstValue()
{
return $this->parseValue(true);
return $this->parseValueLiteral(true);
}
/**
* @param $isConst
* @return BooleanValue|EnumValue|FloatValue|IntValue|StringValue|Variable
* @throws Exception
* @throws SyntaxError
*/
function parseValue($isConst) {
function parseValueLiteral($isConst) {
$token = $this->token;
switch ($token->kind) {
case Token::BRACKET_L:
@ -543,19 +557,21 @@ class Parser
'loc' => $this->loc($token->start)
));
case Token::NAME:
if ($token->value === 'true' || $token->value === 'false') {
$this->advance();
switch ($token->value) {
case 'true':
case 'false':
return new BooleanValue(array(
'value' => $token->value === 'true',
'loc' => $this->loc($token->start)
));
}
} else if ($token->value !== 'null') {
$this->advance();
return new EnumValue(array(
'value' => $token->value,
'loc' => $this->loc($token->start)
));
}
break;
case Token::DOLLAR:
if (!$isConst) {
return $this->parseVariable();
@ -567,13 +583,13 @@ class Parser
/**
* @param bool $isConst
* @return ArrayValue
* @return ListValue
*/
function parseArray($isConst)
{
$start = $this->token->start;
$item = $isConst ? 'parseConstValue' : 'parseVariableValue';
return new ArrayValue(array(
return new ListValue(array(
'values' => $this->any(Token::BRACKET_L, array($this, $item), Token::BRACKET_R),
'loc' => $this->loc($start)
));
@ -600,14 +616,14 @@ class Parser
$name = $this->parseName();
if (array_key_exists($name->value, $fieldNames)) {
throw Exception::create($this->source, $start, "Duplicate input object field " . $name->value . '.');
throw new SyntaxError($this->source, $start, "Duplicate input object field " . $name->value . '.');
}
$fieldNames[$name->value] = true;
$this->expect(Token::COLON);
return new ObjectField(array(
'name' => $name,
'value' => $this->parseValue($isConst),
'value' => $this->parseValueLiteral($isConst),
'loc' => $this->loc($start)
));
}
@ -636,7 +652,7 @@ class Parser
$this->expect(Token::AT);
return new Directive(array(
'name' => $this->parseName(),
'value' => $this->skip(Token::COLON) ? $this->parseValue(false) : null,
'arguments' => $this->parseArguments(),
'loc' => $this->loc($start)
));
}
@ -647,7 +663,7 @@ class Parser
* Handles the Type: TypeName, ListType, and NonNullType parsing rules.
*
* @return ListType|Name|NonNullType
* @throws Exception
* @throws SyntaxError
*/
function parseType()
{
@ -661,7 +677,7 @@ class Parser
'loc' => $this->loc($start)
));
} else {
$type = $this->parseName();
$type = $this->parseNamedType();
}
if ($this->skip(Token::BANG)) {
return new NonNullType(array(
@ -672,4 +688,15 @@ class Parser
}
return $type;
}
function parseNamedType()
{
$start = $this->token->start;
return new NamedType([
'name' => $this->parseName(),
'loc' => $this->loc($start)
]);
}
}

View File

@ -3,7 +3,7 @@ namespace GraphQL\Language;
use GraphQL\Language\AST\Argument;
use GraphQL\Language\AST\ArrayValue;
use GraphQL\Language\AST\ListValue;
use GraphQL\Language\AST\BooleanValue;
use GraphQL\Language\AST\Directive;
use GraphQL\Language\AST\Document;
@ -15,6 +15,7 @@ use GraphQL\Language\AST\FragmentSpread;
use GraphQL\Language\AST\InlineFragment;
use GraphQL\Language\AST\IntValue;
use GraphQL\Language\AST\ListType;
use GraphQL\Language\AST\NamedType;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NonNullType;
use GraphQL\Language\AST\ObjectField;
@ -36,19 +37,25 @@ class Printer
Node::OPERATION_DEFINITION => function(OperationDefinition $node) {
$op = $node->operation;
$name = $node->name;
$defs = Printer::manyList('(', $node->variableDefinitions, ', ', ')');
$defs = self::wrap('(', self::join($node->variableDefinitions, ', '), ')');
$directives = self::join($node->directives, ' ');
$selectionSet = $node->selectionSet;
return !$name ? $selectionSet :
self::join([$op, self::join([$name, $defs]), $directives, $selectionSet], ' ');
},
Node::VARIABLE_DEFINITION => function(VariableDefinition $node) {
return self::join([$node->variable . ': ' . $node->type, $node->defaultValue], ' = ');
return $node->variable . ': ' . $node->type . self::wrap(' = ', $node->defaultValue);
},
Node::SELECTION_SET => function(SelectionSet $node) {
return self::blockList($node->selections, ",\n");
return self::block($node->selections);
},
Node::FIELD => function(Field $node) {
return self::join([
self::wrap('', $node->alias, ': ') . $node->name . self::wrap('(', self::join($node->arguments, ', '), ')'),
self::join($node->directives, ' '),
$node->selectionSet
], ' ');
/*
$r11 = self::join([
$node->alias,
$node->name
@ -66,6 +73,7 @@ class Printer
$r2,
$node->selectionSet
], ' ');
*/
},
Node::ARGUMENT => function(Argument $node) {
return $node->name . ': ' . $node->value;
@ -73,25 +81,17 @@ class Printer
// Fragments
Node::FRAGMENT_SPREAD => function(FragmentSpread $node) {
return self::join(['...' . $node->name, self::join($node->directives, '')], ' ');
return '...' . $node->name . self::wrap(' ', self::join($node->directives, ' '));
},
Node::INLINE_FRAGMENT => function(InlineFragment $node) {
return self::join([
'... on',
$node->typeCondition,
self::join($node->directives, ' '),
$node->selectionSet
], ' ');
return "... on {$node->typeCondition} "
. self::wrap('', self::join($node->directives, ' '), ' ')
. $node->selectionSet;
},
Node::FRAGMENT_DEFINITION => function(FragmentDefinition $node) {
return self::join([
'fragment',
$node->name,
'on',
$node->typeCondition,
self::join($node->directives, ' '),
$node->selectionSet
], ' ');
return "fragment {$node->name} on {$node->typeCondition} "
. self::wrap('', self::join($node->directives, ' '), ' ')
. $node->selectionSet;
},
// Value
@ -100,23 +100,39 @@ class Printer
Node::STRING => function(StringValue $node) {return json_encode($node->value);},
Node::BOOLEAN => function(BooleanValue $node) {return $node->value ? 'true' : 'false';},
Node::ENUM => function(EnumValue $node) {return $node->value;},
Node::ARR => function(ArrayValue $node) {return '[' . self::join($node->values, ', ') . ']';},
Node::LST => function(ListValue $node) {return '[' . self::join($node->values, ', ') . ']';},
Node::OBJECT => function(ObjectValue $node) {return '{' . self::join($node->fields, ', ') . '}';},
Node::OBJECT_FIELD => function(ObjectField $node) {return $node->name . ': ' . $node->value;},
// Directive
Node::DIRECTIVE => function(Directive $node) {return self::join(['@' . $node->name, $node->value], ': ');},
Node::DIRECTIVE => function(Directive $node) {
return '@' . $node->name . self::wrap('(', self::join($node->arguments, ', '), ')');
},
// Type
Node::NAMED_TYPE => function(NamedType $node) {return $node->name;},
Node::LIST_TYPE => function(ListType $node) {return '[' . $node->type . ']';},
Node::NON_NULL_TYPE => function(NonNullType $node) {return $node->type . '!';}
)
));
}
public static function blockList($list, $separator)
/**
* If maybeString is not null or empty, then wrap with start and end, otherwise
* print an empty string.
*/
public static function wrap($start, $maybeString, $end = '')
{
return self::length($list) === 0 ? null : self::indent("{\n" . self::join($list, $separator)) . "\n}";
return $maybeString ? ($start . $maybeString . $end) : '';
}
/**
* Given maybeArray, print an empty string if it is null or empty, otherwise
* print each item on it's own line, wrapped in an indented "{ }" block.
*/
public static function block($maybeArray)
{
return self::length($maybeArray) ? self::indent("{\n" . self::join($maybeArray, ",\n")) . "\n}" : '';
}
public static function indent($maybeString)

View File

@ -58,10 +58,11 @@ class Visitor
Node::STRING => [],
Node::BOOLEAN => [],
Node::ENUM => [],
Node::ARR => ['values'],
Node::LST => ['values'],
Node::OBJECT => ['fields'],
Node::OBJECT_FIELD => ['name', 'value'],
Node::DIRECTIVE => ['name', 'value'],
Node::DIRECTIVE => ['name', 'arguments'],
Node::NAMED_TYPE => ['name'],
Node::LIST_TYPE => ['type'],
Node::NON_NULL_TYPE => ['type'],
);
@ -151,9 +152,9 @@ class Visitor
* }
* })
*/
public static function visit($root, $visitor)
public static function visit($root, $visitor, $keyMap = null)
{
$visitorKeys = isset($visitor['keys']) ? $visitor['keys'] : self::$visitorKeys;
$visitorKeys = $keyMap ?: self::$visitorKeys;
$stack = null;
$inArray = is_array($root);

View File

@ -1,38 +1,24 @@
<?php
namespace GraphQL\Language;
namespace GraphQL;
class Exception extends \Exception
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
class SyntaxError extends Error
{
/**
* @var Source
*/
public $source;
/**
* @var number
*/
public $position;
public $location;
/**
* @param Source $source
* @param $position
* @param $description
* @return Exception
* @param int $position
* @param string $description
*/
public static function create(Source $source, $position, $description)
public function __construct(Source $source, $position, $description)
{
$location = $source->getLocation($position);
$syntaxError = new self(
$syntaxError =
"Syntax Error {$source->name} ({$location->line}:{$location->column}) $description\n\n" .
self::highlightSourceAtLocation($source, $location)
);
$syntaxError->source = $source;
$syntaxError->position = $position;
$syntaxError->location = $location;
self::highlightSourceAtLocation($source, $location);
return $syntaxError;
parent::__construct($syntaxError, null, null, $source, [$position]);
}
public static function highlightSourceAtLocation(Source $source, SourceLocation $location)

View File

@ -221,7 +221,7 @@ class TypeInfo
array_push($this->_inputTypeStack, $directive ? $directive->type : null);
break;
case Node::ARR:
case Node::LST:
$arrayType = Type::getNullableType($this->getInputType());
array_push(
$this->_inputTypeStack,
@ -264,7 +264,7 @@ class TypeInfo
array_pop($this->_inputTypeStack);
break;
case Node::DIRECTIVE:
case Node::ARR:
case Node::LST:
case Node::OBJECT_FIELD:
array_pop($this->_inputTypeStack);
break;

View File

@ -1,6 +1,8 @@
<?php
namespace GraphQL\Language;
use GraphQL\SyntaxError;
class LexerTest extends \PHPUnit_Framework_TestCase
{
public function testSkipsWhitespaces()
@ -35,7 +37,7 @@ class LexerTest extends \PHPUnit_Framework_TestCase
try {
$this->lexOne($example);
$this->fail('Expected exception not thrown');
} catch (Exception $e) {
} catch (SyntaxError $e) {
$this->assertEquals(
'Syntax Error GraphQL (3:5) Unexpected character "?"' . "\n" .
"\n" .
@ -68,7 +70,7 @@ class LexerTest extends \PHPUnit_Framework_TestCase
try {
$this->lexOne($str);
$this->fail('Expected exception not thrown in example: ' . $num);
} catch (Exception $e) {
} catch (SyntaxError $e) {
$this->assertEquals($expectedMessage, $e->getMessage(), "Test case $num failed");
}
};
@ -89,6 +91,8 @@ class LexerTest extends \PHPUnit_Framework_TestCase
public function testLexesNumbers()
{
// lexes numbers
/*
$this->assertEquals(
new Token(Token::STRING, 0, 8, 'simple'),
$this->lexOne('"simple"')
@ -108,7 +112,8 @@ class LexerTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(
new Token(Token::STRING, 0, 34, 'unicode ' . json_decode('"\u1234\u5678\u90AB\uCDEF"')),
$this->lexOne('"unicode \\u1234\\u5678\\u90AB\\uCDEF"')
);
);*/
$this->assertEquals(
new Token(Token::INT, 0, 1, '4'),
$this->lexOne('4')
@ -141,14 +146,38 @@ class LexerTest extends \PHPUnit_Framework_TestCase
new Token(Token::FLOAT, 0, 5, '0.123'),
$this->lexOne('0.123')
);
$this->assertEquals(
new Token(Token::FLOAT, 0, 5, '123e4'),
$this->lexOne('123e4')
);
$this->assertEquals(
new Token(Token::FLOAT, 0, 5, '123E4'),
$this->lexOne('123E4')
);
$this->assertEquals(
new Token(Token::FLOAT, 0, 6, '123e-4'),
$this->lexOne('123e-4')
);
$this->assertEquals(
new Token(Token::FLOAT, 0, 6, '123e+4'),
$this->lexOne('123e+4')
);
$this->assertEquals(
new Token(Token::FLOAT, 0, 8, '-1.123e4'),
$this->lexOne('-1.123e4')
);
$this->assertEquals(
new Token(Token::FLOAT, 0, 8, '-1.123E4'),
$this->lexOne('-1.123E4')
);
$this->assertEquals(
new Token(Token::FLOAT, 0, 9, '-1.123e-4'),
$this->lexOne('-1.123e-4')
);
$this->assertEquals(
new Token(Token::FLOAT, 0, 9, '-1.123e+4'),
$this->lexOne('-1.123e+4')
);
$this->assertEquals(
new Token(Token::FLOAT, 0, 11, '-1.123e4567'),
$this->lexOne('-1.123e4567')
@ -161,16 +190,16 @@ class LexerTest extends \PHPUnit_Framework_TestCase
try {
$this->lexOne($str);
$this->fail('Expected exception not thrown in example: ' . $num);
} catch (Exception $e) {
} catch (SyntaxError $e) {
$this->assertEquals($expectedMessage, $e->getMessage(), "Test case $num failed");
}
};
$run(1, '+1', "Syntax Error GraphQL (1:1) Unexpected character \"+\"\n\n1: +1\n ^\n");
$run(2, '1.', "Syntax Error GraphQL (1:3) Invalid number\n\n1: 1.\n ^\n");
$run(3, '1.A', "Syntax Error GraphQL (1:3) Invalid number\n\n1: 1.A\n ^\n");
$run(4, '-A', "Syntax Error GraphQL (1:2) Invalid number\n\n1: -A\n ^\n");
$run(5, '1.0e+4', "Syntax Error GraphQL (1:5) Invalid number\n\n1: 1.0e+4\n ^\n");
$run(3, '.123', "Syntax Error GraphQL (1:1) Unexpected character \".\"\n\n1: .123\n ^\n");
$run(4, '1.A', "Syntax Error GraphQL (1:3) Invalid number\n\n1: 1.A\n ^\n");
$run(5, '-A', "Syntax Error GraphQL (1:2) Invalid number\n\n1: -A\n ^\n");
$run(6, '1.0e', "Syntax Error GraphQL (1:5) Invalid number\n\n1: 1.0e\n ^\n");
$run(7, '1.0eA', "Syntax Error GraphQL (1:5) Invalid number\n\n1: 1.0eA\n ^\n");
}
@ -237,7 +266,7 @@ class LexerTest extends \PHPUnit_Framework_TestCase
try {
$this->lexOne($str);
$this->fail('Expected exception not thrown in example: ' . $num);
} catch (Exception $e) {
} catch (SyntaxError $e) {
$this->assertEquals($expectedMessage, $e->getMessage(), "Test case $num failed");
}
};

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Language;
use GraphQL\Error;
use GraphQL\Language\AST\Argument;
use GraphQL\Language\AST\Document;
use GraphQL\Language\AST\Field;
@ -9,17 +10,62 @@ use GraphQL\Language\AST\Location;
use GraphQL\Language\AST\Name;
use GraphQL\Language\AST\OperationDefinition;
use GraphQL\Language\AST\SelectionSet;
use GraphQL\SyntaxError;
class ParserTest extends \PHPUnit_Framework_TestCase
{
public function testAcceptsOptionToNotIncludeSource()
{
// accepts option to not include source
$actual = Parser::parse('{ field }', ['noSource' => true]);
$expected = new Document([
'loc' => new Location(0, 9),
'definitions' => [
new OperationDefinition([
'loc' => new Location(0, 9),
'operation' => 'query',
'name' => null,
'variableDefinitions' => null,
'directives' => [],
'selectionSet' => new SelectionSet([
'loc' => new Location(0, 9),
'selections' => [
new Field([
'loc' => new Location(2, 7),
'alias' => null,
'name' => new Name([
'loc' => new Location(2, 7),
'value' => 'field'
]),
'arguments' => [],
'directives' => [],
'selectionSet' => null
])
]
])
])
]
]);
$this->assertEquals($expected, $actual);
}
public function testParseProvidesUsefulErrors()
{
$run = function($num, $str, $expectedMessage) {
$run = function($num, $str, $expectedMessage, $expectedPositions = null, $expectedLocations = null) {
try {
Parser::parse($str);
$this->fail('Expected exception not thrown in example: ' . $num);
} catch (Exception $e) {
} catch (SyntaxError $e) {
$this->assertEquals($expectedMessage, $e->getMessage(), "Test case $num failed");
if ($expectedPositions) {
$this->assertEquals($expectedPositions, $e->getPositions());
}
if ($expectedLocations) {
$this->assertEquals($expectedLocations, $e->getLocations());
}
}
};
@ -33,14 +79,15 @@ fragment MissingOn Type
$run(2, '{ field: {} }', "Syntax Error GraphQL (1:10) Expected Name, found {\n\n1: { field: {} }\n ^\n");
$run(3, 'notanoperation Foo { field }', "Syntax Error GraphQL (1:1) Unexpected Name \"notanoperation\"\n\n1: notanoperation Foo { field }\n ^\n");
$run(4, '...', "Syntax Error GraphQL (1:1) Unexpected ...\n\n1: ...\n ^\n");
$run(5, '{', "Syntax Error GraphQL (1:2) Expected Name, found EOF\n\n1: {\n ^\n", [1], [new SourceLocation(1,2)]);
}
public function testParseProvidesUsefulErrorWhenUsingSource()
{
try {
$this->assertEquals(Parser::parse(new Source('query', 'MyQuery.graphql')));
Parser::parse(new Source('query', 'MyQuery.graphql'));
$this->fail('Expected exception not thrown');
} catch (Exception $e) {
} catch (SyntaxError $e) {
$this->assertEquals("Syntax Error MyQuery.graphql (1:6) Expected Name, found EOF\n\n1: query\n ^\n", $e->getMessage());
}
}
@ -56,7 +103,7 @@ fragment MissingOn Type
try {
Parser::parse('query Foo($x: Complex = { a: { b: [ $var ] } }) { field }');
$this->fail('Expected exception not thrown');
} catch (Exception $e) {
} catch (SyntaxError $e) {
$this->assertEquals(
"Syntax Error GraphQL (1:37) Unexpected $\n\n" . '1: query Foo($x: Complex = { a: { b: [ $var ] } }) { field }' . "\n ^\n",
$e->getMessage()
@ -69,7 +116,7 @@ fragment MissingOn Type
try {
Parser::parse('{ field(arg: { a: 1, a: 2 }) }');
$this->fail('Expected exception not thrown');
} catch (Exception $e) {
} catch (SyntaxError $e) {
$this->assertEquals(
"Syntax Error GraphQL (1:22) Duplicate input object field a.\n\n1: { field(arg: { a: 1, a: 2 }) }\n ^\n",
$e->getMessage()
@ -77,11 +124,62 @@ fragment MissingOn Type
}
}
public function testDoesNotAcceptFragmentsNamedOn()
{
// does not accept fragments named "on"
$this->setExpectedException('GraphQL\SyntaxError', 'Syntax Error GraphQL (1:10) Unexpected Name "on"');
Parser::parse('fragment on on on { on }');
}
public function testDoesNotAcceptFragmentSpreadOfOn()
{
// does not accept fragments spread of "on"
$this->setExpectedException('GraphQL\SyntaxError', 'Syntax Error GraphQL (1:9) Expected Name, found }');
Parser::parse('{ ...on }');
}
public function testDoesNotAllowNullAsValue()
{
$this->setExpectedException('GraphQL\SyntaxError', 'Syntax Error GraphQL (1:39) Unexpected Name "null"');
Parser::parse('{ fieldWithNullableStringInput(input: null) }');
}
public function testParsesKitchenSink()
{
// Following should not throw:
$kitchenSink = file_get_contents(__DIR__ . '/kitchen-sink.graphql');
Parser::parse($kitchenSink);
$result = Parser::parse($kitchenSink);
$this->assertNotEmpty($result);
}
public function testAllowsNonKeywordsAnywhereANameIsAllowed()
{
// allows non-keywords anywhere a Name is allowed
$nonKeywords = [
'on',
'fragment',
'query',
'mutation',
'true',
'false'
];
foreach ($nonKeywords as $keyword) {
$fragmentName = $keyword;
if ($keyword === 'on') {
$fragmentName = 'a';
}
// Expected not to throw:
$result = Parser::parse("query $keyword {
... $fragmentName
... on $keyword { field }
}
fragment $fragmentName on Type {
$keyword($keyword: \$$keyword) @$keyword($keyword: $keyword)
}
");
$this->assertNotEmpty($result);
}
}
public function testParseCreatesAst()

View File

@ -54,7 +54,7 @@ query queryName($foo: ComplexType, $site: Site = MOBILE) {
EOT;
;
$ast = Parser::parse($queryStr, ['noLocation' => true]);
/*
$expectedAst = new Document(array(
'definitions' => [
new OperationDefinition(array(
@ -98,9 +98,9 @@ EOT;
])
))
]
));
));*/
$this->assertEquals($expectedAst, $ast);
// $this->assertEquals($expectedAst, $ast);
$this->assertEquals($queryStr, Printer::doPrint($ast));
}
@ -119,7 +119,7 @@ query queryName($foo: ComplexType, $site: Site = MOBILE) {
... on User @defer {
field2 {
id,
alias: field1(first: 10, after: $foo) @if: $foo {
alias: field1(first: 10, after: $foo) @include(if: $foo) {
id,
...frag
}

View File

@ -215,16 +215,20 @@ class VisitorTest extends \PHPUnit_Framework_TestCase
[ 'enter', 'Name', 'name', 'Variable' ],
[ 'leave', 'Name', 'name', 'Variable' ],
[ 'leave', 'Variable', 'variable', 'VariableDefinition' ],
[ 'enter', 'Name', 'type', 'VariableDefinition' ],
[ 'leave', 'Name', 'type', 'VariableDefinition' ],
[ 'enter', 'NamedType', 'type', 'VariableDefinition' ],
[ 'enter', 'Name', 'name', 'NamedType' ],
[ 'leave', 'Name', 'name', 'NamedType' ],
[ 'leave', 'NamedType', 'type', 'VariableDefinition' ],
[ 'leave', 'VariableDefinition', 0, null ],
[ 'enter', 'VariableDefinition', 1, null ],
[ 'enter', 'Variable', 'variable', 'VariableDefinition' ],
[ 'enter', 'Name', 'name', 'Variable' ],
[ 'leave', 'Name', 'name', 'Variable' ],
[ 'leave', 'Variable', 'variable', 'VariableDefinition' ],
[ 'enter', 'Name', 'type', 'VariableDefinition' ],
[ 'leave', 'Name', 'type', 'VariableDefinition' ],
[ 'enter', 'NamedType', 'type', 'VariableDefinition' ],
[ 'enter', 'Name', 'name', 'NamedType' ],
[ 'leave', 'Name', 'name', 'NamedType' ],
[ 'leave', 'NamedType', 'type', 'VariableDefinition' ],
[ 'enter', 'EnumValue', 'defaultValue', 'VariableDefinition' ],
[ 'leave', 'EnumValue', 'defaultValue', 'VariableDefinition' ],
[ 'leave', 'VariableDefinition', 1, null ],
@ -237,12 +241,12 @@ class VisitorTest extends \PHPUnit_Framework_TestCase
[ 'enter', 'Argument', 0, null ],
[ 'enter', 'Name', 'name', 'Argument' ],
[ 'leave', 'Name', 'name', 'Argument' ],
[ 'enter', 'ArrayValue', 'value', 'Argument' ],
[ 'enter', 'ListValue', 'value', 'Argument' ],
[ 'enter', 'IntValue', 0, null ],
[ 'leave', 'IntValue', 0, null ],
[ 'enter', 'IntValue', 1, null ],
[ 'leave', 'IntValue', 1, null ],
[ 'leave', 'ArrayValue', 'value', 'Argument' ],
[ 'leave', 'ListValue', 'value', 'Argument' ],
[ 'leave', 'Argument', 0, null ],
[ 'enter', 'SelectionSet', 'selectionSet', 'Field' ],
[ 'enter', 'Field', 0, null ],
@ -250,8 +254,10 @@ class VisitorTest extends \PHPUnit_Framework_TestCase
[ 'leave', 'Name', 'name', 'Field' ],
[ 'leave', 'Field', 0, null ],
[ 'enter', 'InlineFragment', 1, null ],
[ 'enter', 'Name', 'typeCondition', 'InlineFragment' ],
[ 'leave', 'Name', 'typeCondition', 'InlineFragment' ],
[ 'enter', 'NamedType', 'typeCondition', 'InlineFragment' ],
[ 'enter', 'Name', 'name', 'NamedType' ],
[ 'leave', 'Name', 'name', 'NamedType' ],
[ 'leave', 'NamedType', 'typeCondition', 'InlineFragment' ],
[ 'enter', 'Directive', 0, null ],
[ 'enter', 'Name', 'name', 'Directive' ],
[ 'leave', 'Name', 'name', 'Directive' ],
@ -287,10 +293,14 @@ class VisitorTest extends \PHPUnit_Framework_TestCase
[ 'enter', 'Directive', 0, null ],
[ 'enter', 'Name', 'name', 'Directive' ],
[ 'leave', 'Name', 'name', 'Directive' ],
[ 'enter', 'Variable', 'value', 'Directive' ],
[ 'enter', 'Argument', 0, null ],
[ 'enter', 'Name', 'name', 'Argument' ],
[ 'leave', 'Name', 'name', 'Argument' ],
[ 'enter', 'Variable', 'value', 'Argument' ],
[ 'enter', 'Name', 'name', 'Variable' ],
[ 'leave', 'Name', 'name', 'Variable' ],
[ 'leave', 'Variable', 'value', 'Directive' ],
[ 'leave', 'Variable', 'value', 'Argument' ],
[ 'leave', 'Argument', 0, null ],
[ 'leave', 'Directive', 0, null ],
[ 'enter', 'SelectionSet', 'selectionSet', 'Field' ],
[ 'enter', 'Field', 0, null ],
@ -346,8 +356,10 @@ class VisitorTest extends \PHPUnit_Framework_TestCase
[ 'enter', 'FragmentDefinition', 2, null ],
[ 'enter', 'Name', 'name', 'FragmentDefinition' ],
[ 'leave', 'Name', 'name', 'FragmentDefinition' ],
[ 'enter', 'Name', 'typeCondition', 'FragmentDefinition' ],
[ 'leave', 'Name', 'typeCondition', 'FragmentDefinition' ],
[ 'enter', 'NamedType', 'typeCondition', 'FragmentDefinition' ],
[ 'enter', 'Name', 'name', 'NamedType' ],
[ 'leave', 'Name', 'name', 'NamedType' ],
[ 'leave', 'NamedType', 'typeCondition', 'FragmentDefinition' ],
[ 'enter', 'SelectionSet', 'selectionSet', 'FragmentDefinition' ],
[ 'enter', 'Field', 0, null ],
[ 'enter', 'Name', 'name', 'Field' ],

View File

@ -11,7 +11,7 @@ query queryName($foo: ComplexType, $site: Site = MOBILE) {
... on User @defer {
field2 {
id ,
alias: field1(first:10, after:$foo,) @if: $foo {
alias: field1(first:10, after:$foo,) @include(if: $foo) {
id,
...frag
}