New: printError()

Lifted from / inspired by a similar change in graphql/graphql-js#722, this creates a new function `printError()` (and uses it as the implementation for `GraphQLError#toString()`) which prints location information in the context of an error.

This is moved from the syntax error where it used to be hard-coded, so it may now be used to format validation errors, value coercion errors, or any other error which may be associated with a location.

ref: graphql/graphql-js

BREAKING CHANGE: The SyntaxError message does not contain the codeframe anymore and only the message, (string) $error will print the codeframe.
This commit is contained in:
Daniel Tschinder 2018-02-12 12:23:39 +01:00
parent f661f38215
commit 15374a31dd
9 changed files with 370 additions and 196 deletions

View File

@ -324,4 +324,12 @@ class Error extends \Exception implements \JsonSerializable, ClientAware
{
return $this->toSerializableArray();
}
/**
* @return string
*/
public function __toString()
{
return FormattedError::printError($this);
}
}

View File

@ -1,6 +1,7 @@
<?php
namespace GraphQL\Error;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\WrappingType;
@ -27,6 +28,97 @@ class FormattedError
self::$internalErrorMessage = $msg;
}
/**
* Prints a GraphQLError to a string, representing useful location information
* about the error's position in the source.
*
* @param Error $error
* @return string
*/
public static function printError(Error $error)
{
$source = $error->getSource();
$locations = $error->getLocations();
$message = $error->getMessage();
foreach($locations as $location) {
$message .= $source
? self::highlightSourceAtLocation($source, $location)
: " ({$location->line}:{$location->column})";
}
return $message;
}
/**
* Render a helpful description of the location of the error in the GraphQL
* Source document.
*
* @param Source $source
* @param SourceLocation $location
* @return string
*/
private static function highlightSourceAtLocation(Source $source, SourceLocation $location)
{
$line = $location->line;
$lineOffset = $source->locationOffset->line - 1;
$columnOffset = self::getColumnOffset($source, $location);
$contextLine = $line + $lineOffset;
$contextColumn = $location->column + $columnOffset;
$prevLineNum = (string) ($contextLine - 1);
$lineNum = (string) $contextLine;
$nextLineNum = (string) ($contextLine + 1);
$padLen = strlen($nextLineNum);
$lines = preg_split('/\r\n|[\n\r]/', $source->body);
$lines[0] = self::whitespace($source->locationOffset->column - 1) . $lines[0];
return (
"\n\n{$source->name} ($contextLine:$contextColumn)\n" .
($line >= 2
? (self::lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2] . "\n")
: ''
) .
self::lpad($padLen, $lineNum) .
': ' .
$lines[$line - 1] .
"\n" .
self::whitespace(2 + $padLen + $contextColumn - 1) .
"^\n" .
($line < count($lines)
? (self::lpad($padLen, $nextLineNum) . ': ' . $lines[$line] . "\n")
: ''
)
);
}
/**
* @param Source $source
* @param SourceLocation $location
* @return int
*/
private static function getColumnOffset(Source $source, SourceLocation $location)
{
return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
}
/**
* @param int $len
* @return string
*/
private static function whitespace($len) {
return str_repeat(' ', $len);
}
/**
* @param int $len
* @return string
*/
private static function lpad($len, $str) {
return self::whitespace($len - mb_strlen($str)) . $str;
}
/**
* Standard GraphQL error formatter. Converts any exception to array
* conforming to GraphQL spec.

View File

@ -2,7 +2,6 @@
namespace GraphQL\Error;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
class SyntaxError extends Error
{
@ -13,59 +12,11 @@ class SyntaxError extends Error
*/
public function __construct(Source $source, $position, $description)
{
$location = $source->getLocation($position);
$line = $location->line + $source->locationOffset->line - 1;
$columnOffset = self::getColumnOffset($source, $location);
$column = $location->column + $columnOffset;
$syntaxError =
"Syntax Error {$source->name} ({$line}:{$column}) $description\n" .
"\n".
self::highlightSourceAtLocation($source, $location);
parent::__construct($syntaxError, null, $source, [$position]);
parent::__construct(
"Syntax Error: $description",
null,
$source,
[$position]
);
}
/**
* @param Source $source
* @param SourceLocation $location
* @return string
*/
public static function highlightSourceAtLocation(Source $source, SourceLocation $location)
{
$line = $location->line;
$lineOffset = $source->locationOffset->line - 1;
$columnOffset = self::getColumnOffset($source, $location);
$contextLine = $line + $lineOffset;
$prevLineNum = (string) ($contextLine - 1);
$lineNum = (string) $contextLine;
$nextLineNum = (string) ($contextLine + 1);
$padLen = mb_strlen($nextLineNum, 'UTF-8');
$unicodeChars = json_decode('"\u2028\u2029"'); // Quick hack to get js-compatible representation of these chars
$lines = preg_split('/\r\n|[\n\r' . $unicodeChars . ']/su', $source->body);
$whitespace = function ($len) {
return str_repeat(' ', $len);
};
$lpad = function ($len, $str) {
return str_pad($str, $len - mb_strlen($str, 'UTF-8') + 1, ' ', STR_PAD_LEFT);
};
$lines[0] = $whitespace($source->locationOffset->column - 1) . $lines[0];
return
($line >= 2 ? $lpad($padLen, $prevLineNum) . ': ' . $lines[$line - 2] . "\n" : '') .
($lpad($padLen, $lineNum) . ': ' . $lines[$line - 1] . "\n") .
($whitespace(2 + $padLen + $location->column - 1 + $columnOffset) . "^\n") .
($line < count($lines) ? $lpad($padLen, $nextLineNum) . ': ' . $lines[$line] . "\n" : '');
}
public static function getColumnOffset(Source $source, SourceLocation $location)
{
return $location->line === 1 ? $source->locationOffset->column - 1 : 0;
}
}

View File

@ -39,8 +39,8 @@ class Source
* be "Foo.graphql" and location to be `{ line: 40, column: 0 }`.
* line and column in locationOffset are 1-indexed
*
* @param $body
* @param null $name
* @param string $body
* @param string|null $name
* @param SourceLocation|null $location
*/
public function __construct($body, $name = null, SourceLocation $location = null)
@ -52,7 +52,7 @@ class Source
$this->body = $body;
$this->length = mb_strlen($body, 'UTF-8');
$this->name = $name ?: 'GraphQL';
$this->name = $name ?: 'GraphQL request';
$this->locationOffset = $location ?: new SourceLocation(1, 1);
Utils::invariant(

View File

@ -15,10 +15,11 @@ class LexerTest extends \PHPUnit_Framework_TestCase
*/
public function testDissallowsUncommonControlCharacters()
{
$char = Utils::chr(0x0007);
$this->setExpectedExceptionRegExp(SyntaxError::class, '/' . preg_quote('Syntax Error GraphQL (1:1) Cannot contain the invalid character "\u0007"', '/') . '/');
$this->lexOne($char);
$this->expectSyntaxError(
Utils::chr(0x0007),
'Cannot contain the invalid character "\u0007"',
$this->loc(1, 1)
);
}
/**
@ -107,14 +108,21 @@ class LexerTest extends \PHPUnit_Framework_TestCase
" ?\n" .
"\n";
$this->setExpectedException(SyntaxError::class,
'Syntax Error GraphQL (3:5) Cannot parse the unexpected character "?".' . "\n" .
"\n" .
"2: \n" .
"3: ?\n" .
" ^\n" .
"4: \n");
$this->lexOne($str);
try {
$this->lexOne($str);
$this->fail('Expected exception not thrown');
} catch (SyntaxError $error) {
$this->assertEquals(
'Syntax Error: Cannot parse the unexpected character "?".' . "\n" .
"\n" .
"GraphQL request (3:5)\n" .
"2: \n" .
"3: ?\n" .
" ^\n" .
"4: \n",
(string) $error
);
}
}
/**
@ -129,34 +137,42 @@ class LexerTest extends \PHPUnit_Framework_TestCase
"\n";
$source = new Source($str, 'foo.js', new SourceLocation(11, 12));
$this->setExpectedException(
SyntaxError::class,
'Syntax Error foo.js (13:6) ' .
'Cannot parse the unexpected character "?".' . "\n" .
"\n" .
'12: ' . "\n" .
'13: ?' . "\n" .
' ^' . "\n" .
'14: ' . "\n"
);
$lexer = new Lexer($source);
$lexer->advance();
try {
$lexer = new Lexer($source);
$lexer->advance();
$this->fail('Expected exception not thrown');
} catch (SyntaxError $error) {
$this->assertEquals(
'Syntax Error: Cannot parse the unexpected character "?".' . "\n" .
"\n" .
"foo.js (13:6)\n" .
"12: \n" .
"13: ?\n" .
" ^\n" .
"14: \n",
(string) $error
);
}
}
public function testUpdatesColumnNumbersInErrorForFileContext()
{
$source = new Source('?', 'foo.js', new SourceLocation(1, 5));
$this->setExpectedException(
SyntaxError::class,
'Syntax Error foo.js (1:5) ' .
'Cannot parse the unexpected character "?".' . "\n" .
"\n" .
'1: ?' . "\n" .
' ^' . "\n"
);
$lexer = new Lexer($source);
$lexer->advance();
try {
$lexer = new Lexer($source);
$lexer->advance();
$this->fail('Expected exception not thrown');
} catch (SyntaxError $error) {
$this->assertEquals(
'Syntax Error: Cannot parse the unexpected character "?".' . "\n" .
"\n" .
"foo.js (1:5)\n" .
'1: ?' . "\n" .
' ^' . "\n",
(string) $error
);
}
}
/**
@ -298,41 +314,22 @@ class LexerTest extends \PHPUnit_Framework_TestCase
\"\"\""));
}
public function reportsUsefulBlockStringErrors() {
return [
['"""', "Syntax Error GraphQL (1:4) Unterminated string.\n\n1: \"\"\"\n ^\n"],
['"""no end quote', "Syntax Error GraphQL (1:16) Unterminated string.\n\n1: \"\"\"no end quote\n ^\n"],
['"""contains unescaped ' . json_decode('"\u0007"') . ' control char"""', "Syntax Error GraphQL (1:23) Invalid character within String: \"\\u0007\""],
['"""null-byte is not ' . json_decode('"\u0000"') . ' end of file"""', "Syntax Error GraphQL (1:21) Invalid character within String: \"\\u0000\""],
];
}
/**
* @dataProvider reportsUsefulBlockStringErrors
* @it lex reports useful block string errors
*/
public function testReportsUsefulBlockStringErrors($str, $expectedMessage)
{
$this->setExpectedException(SyntaxError::class, $expectedMessage);
$this->lexOne($str);
}
public function reportsUsefulStringErrors() {
return [
['"', "Syntax Error GraphQL (1:2) Unterminated string.\n\n1: \"\n ^\n"],
['"no end quote', "Syntax Error GraphQL (1:14) Unterminated string.\n\n1: \"no end quote\n ^\n"],
["'single quotes'", "Syntax Error GraphQL (1:1) Unexpected single quote character ('), did you mean to use a double quote (\")?\n\n1: 'single quotes'\n ^\n"],
['"contains unescaped \u0007 control char"', "Syntax Error GraphQL (1:21) Invalid character within String: \"\\u0007\"\n\n1: \"contains unescaped \\u0007 control char\"\n ^\n"],
['"null-byte is not \u0000 end of file"', 'Syntax Error GraphQL (1:19) Invalid character within String: "\\u0000"' . "\n\n1: \"null-byte is not \\u0000 end of file\"\n ^\n"],
['"multi' . "\n" . 'line"', "Syntax Error GraphQL (1:7) Unterminated string.\n\n1: \"multi\n ^\n2: line\"\n"],
['"multi' . "\r" . 'line"', "Syntax Error GraphQL (1:7) Unterminated string.\n\n1: \"multi\n ^\n2: line\"\n"],
['"bad \\z esc"', "Syntax Error GraphQL (1:7) Invalid character escape sequence: \\z\n\n1: \"bad \\z esc\"\n ^\n"],
['"bad \\x esc"', "Syntax Error GraphQL (1:7) Invalid character escape sequence: \\x\n\n1: \"bad \\x esc\"\n ^\n"],
['"bad \\u1 esc"', "Syntax Error GraphQL (1:7) Invalid character escape sequence: \\u1 es\n\n1: \"bad \\u1 esc\"\n ^\n"],
['"bad \\u0XX1 esc"', "Syntax Error GraphQL (1:7) Invalid character escape sequence: \\u0XX1\n\n1: \"bad \\u0XX1 esc\"\n ^\n"],
['"bad \\uXXXX esc"', "Syntax Error GraphQL (1:7) Invalid character escape sequence: \\uXXXX\n\n1: \"bad \\uXXXX esc\"\n ^\n"],
['"bad \\uFXXX esc"', "Syntax Error GraphQL (1:7) Invalid character escape sequence: \\uFXXX\n\n1: \"bad \\uFXXX esc\"\n ^\n"],
['"bad \\uXXXF esc"', "Syntax Error GraphQL (1:7) Invalid character escape sequence: \\uXXXF\n\n1: \"bad \\uXXXF esc\"\n ^\n"],
['"', "Unterminated string.", $this->loc(1, 2)],
['"no end quote', "Unterminated string.", $this->loc(1, 14)],
["'single quotes'", "Unexpected single quote character ('), did you mean to use a double quote (\")?", $this->loc(1, 1)],
['"contains unescaped \u0007 control char"', "Invalid character within String: \"\\u0007\"", $this->loc(1, 21)],
['"null-byte is not \u0000 end of file"', 'Invalid character within String: "\\u0000"', $this->loc(1, 19)],
['"multi' . "\n" . 'line"', "Unterminated string.", $this->loc(1, 7)],
['"multi' . "\r" . 'line"', "Unterminated string.", $this->loc(1, 7)],
['"bad \\z esc"', "Invalid character escape sequence: \\z", $this->loc(1, 7)],
['"bad \\x esc"', "Invalid character escape sequence: \\x", $this->loc(1, 7)],
['"bad \\u1 esc"', "Invalid character escape sequence: \\u1 es", $this->loc(1, 7)],
['"bad \\u0XX1 esc"', "Invalid character escape sequence: \\u0XX1", $this->loc(1, 7)],
['"bad \\uXXXX esc"', "Invalid character escape sequence: \\uXXXX", $this->loc(1, 7)],
['"bad \\uFXXX esc"', "Invalid character escape sequence: \\uFXXX", $this->loc(1, 7)],
['"bad \\uXXXF esc"', "Invalid character escape sequence: \\uXXXF", $this->loc(1, 7)],
];
}
@ -340,10 +337,27 @@ class LexerTest extends \PHPUnit_Framework_TestCase
* @dataProvider reportsUsefulStringErrors
* @it lex reports useful string errors
*/
public function testLexReportsUsefulStringErrors($str, $expectedMessage)
public function testLexReportsUsefulStringErrors($str, $expectedMessage, $location)
{
$this->setExpectedException(SyntaxError::class, $expectedMessage);
$this->lexOne($str);
$this->expectSyntaxError($str, $expectedMessage, $location);
}
public function reportsUsefulBlockStringErrors() {
return [
['"""', "Unterminated string.", $this->loc(1, 4)],
['"""no end quote', "Unterminated string.", $this->loc(1, 16)],
['"""contains unescaped ' . json_decode('"\u0007"') . ' control char"""', "Invalid character within String: \"\\u0007\"", $this->loc(1, 23)],
['"""null-byte is not ' . json_decode('"\u0000"') . ' end of file"""', "Invalid character within String: \"\\u0000\"", $this->loc(1, 21)],
];
}
/**
* @dataProvider reportsUsefulBlockStringErrors
* @it lex reports useful block string errors
*/
public function testReportsUsefulBlockStringErrors($str, $expectedMessage, $location)
{
$this->expectSyntaxError($str, $expectedMessage, $location);
}
/**
@ -420,15 +434,15 @@ class LexerTest extends \PHPUnit_Framework_TestCase
public function reportsUsefulNumberErrors()
{
return [
[ '00', "Syntax Error GraphQL (1:2) Invalid number, unexpected digit after 0: \"0\"\n\n1: 00\n ^\n"],
[ '+1', "Syntax Error GraphQL (1:1) Cannot parse the unexpected character \"+\".\n\n1: +1\n ^\n"],
[ '1.', "Syntax Error GraphQL (1:3) Invalid number, expected digit but got: <EOF>\n\n1: 1.\n ^\n"],
[ '1.e1', "Syntax Error GraphQL (1:3) Invalid number, expected digit but got: \"e\"\n\n1: 1.e1\n ^\n"],
[ '.123', "Syntax Error GraphQL (1:1) Cannot parse the unexpected character \".\".\n\n1: .123\n ^\n"],
[ '1.A', "Syntax Error GraphQL (1:3) Invalid number, expected digit but got: \"A\"\n\n1: 1.A\n ^\n"],
[ '-A', "Syntax Error GraphQL (1:2) Invalid number, expected digit but got: \"A\"\n\n1: -A\n ^\n"],
[ '1.0e', "Syntax Error GraphQL (1:5) Invalid number, expected digit but got: <EOF>\n\n1: 1.0e\n ^\n"],
[ '1.0eA', "Syntax Error GraphQL (1:5) Invalid number, expected digit but got: \"A\"\n\n1: 1.0eA\n ^\n"],
[ '00', "Invalid number, unexpected digit after 0: \"0\"", $this->loc(1, 2)],
[ '+1', "Cannot parse the unexpected character \"+\".", $this->loc(1, 1)],
[ '1.', "Invalid number, expected digit but got: <EOF>", $this->loc(1, 3)],
[ '1.e1', "Invalid number, expected digit but got: \"e\"", $this->loc(1, 3)],
[ '.123', "Cannot parse the unexpected character \".\".", $this->loc(1, 1)],
[ '1.A', "Invalid number, expected digit but got: \"A\"", $this->loc(1, 3)],
[ '-A', "Invalid number, expected digit but got: \"A\"", $this->loc(1, 2)],
[ '1.0e', "Invalid number, expected digit but got: <EOF>", $this->loc(1, 5)],
[ '1.0eA', "Invalid number, expected digit but got: \"A\"", $this->loc(1, 5)],
];
}
@ -436,10 +450,9 @@ class LexerTest extends \PHPUnit_Framework_TestCase
* @dataProvider reportsUsefulNumberErrors
* @it lex reports useful number errors
*/
public function testReportsUsefulNumberErrors($str, $expectedMessage)
public function testReportsUsefulNumberErrors($str, $expectedMessage, $location)
{
$this->setExpectedException(SyntaxError::class, $expectedMessage);
$this->lexOne($str);
$this->expectSyntaxError($str, $expectedMessage, $location);
}
/**
@ -507,10 +520,10 @@ class LexerTest extends \PHPUnit_Framework_TestCase
$unicode2 = json_decode('"\u200b"');
return [
['..', "Syntax Error GraphQL (1:1) Cannot parse the unexpected character \".\".\n\n1: ..\n ^\n"],
['?', "Syntax Error GraphQL (1:1) Cannot parse the unexpected character \"?\".\n\n1: ?\n ^\n"],
[$unicode1, "Syntax Error GraphQL (1:1) Cannot parse the unexpected character \"\\u203b\".\n\n1: $unicode1\n ^\n"],
[$unicode2, "Syntax Error GraphQL (1:1) Cannot parse the unexpected character \"\\u200b\".\n\n1: $unicode2\n ^\n"],
['..', "Cannot parse the unexpected character \".\".", $this->loc(1, 1)],
['?', "Cannot parse the unexpected character \"?\".", $this->loc(1, 1)],
[$unicode1, "Cannot parse the unexpected character \"\\u203b\".", $this->loc(1, 1)],
[$unicode2, "Cannot parse the unexpected character \"\\u200b\".", $this->loc(1, 1)],
];
}
@ -518,10 +531,9 @@ class LexerTest extends \PHPUnit_Framework_TestCase
* @dataProvider reportsUsefulUnknownCharErrors
* @it lex reports useful unknown character error
*/
public function testReportsUsefulUnknownCharErrors($str, $expectedMessage)
public function testReportsUsefulUnknownCharErrors($str, $expectedMessage, $location)
{
$this->setExpectedException(SyntaxError::class, $expectedMessage);
$this->lexOne($str);
$this->expectSyntaxError($str, $expectedMessage, $location);
}
/**
@ -533,8 +545,14 @@ class LexerTest extends \PHPUnit_Framework_TestCase
$lexer = new Lexer(new Source($q));
$this->assertArraySubset(['kind' => Token::NAME, 'start' => 0, 'end' => 1, 'value' => 'a'], (array) $lexer->advance());
$this->setExpectedException(SyntaxError::class, 'Syntax Error GraphQL (1:3) Invalid number, expected digit but got: "b"' . "\n\n1: a-b\n ^\n");
$lexer->advance();
$this->setExpectedException(SyntaxError::class, 'Syntax Error: Invalid number, expected digit but got: "b"');
try {
$lexer->advance();
$this->fail('Expected exception not thrown');
} catch(SyntaxError $error) {
$this->assertEquals([$this->loc(1,3)], $error->getLocations());
throw $error;
}
}
/**
@ -588,4 +606,20 @@ class LexerTest extends \PHPUnit_Framework_TestCase
$lexer = new Lexer(new Source($body));
return $lexer->advance();
}
private function loc($line, $column)
{
return new SourceLocation($line, $column);
}
private function expectSyntaxError($text, $message, $location)
{
$this->setExpectedException(SyntaxError::class, $message);
try {
$this->lexOne($text);
} catch (SyntaxError $error) {
$this->assertEquals([$location], $error->getLocations());
throw $error;
}
}
}

View File

@ -39,13 +39,13 @@ class ParserTest extends \PHPUnit_Framework_TestCase
public function parseProvidesUsefulErrors()
{
return [
['{', "Syntax Error GraphQL (1:2) Expected Name, found <EOF>\n\n1: {\n ^\n", [1], [new SourceLocation(1, 2)]],
['{', "Syntax Error: Expected Name, found <EOF>", "Syntax Error: Expected Name, found <EOF>\n\nGraphQL request (1:2)\n1: {\n ^\n", [1], [new SourceLocation(1, 2)]],
['{ ...MissingOn }
fragment MissingOn Type
', "Syntax Error GraphQL (2:20) Expected \"on\", found Name \"Type\"\n\n1: { ...MissingOn }\n2: fragment MissingOn Type\n ^\n3: \n",],
['{ field: {} }', "Syntax Error GraphQL (1:10) Expected Name, found {\n\n1: { field: {} }\n ^\n"],
['notanoperation Foo { field }', "Syntax Error GraphQL (1:1) Unexpected Name \"notanoperation\"\n\n1: notanoperation Foo { field }\n ^\n"],
['...', "Syntax Error GraphQL (1:1) Unexpected ...\n\n1: ...\n ^\n"],
', "Syntax Error: Expected \"on\", found Name \"Type\"", "Syntax Error: Expected \"on\", found Name \"Type\"\n\nGraphQL request (2:20)\n1: { ...MissingOn }\n2: fragment MissingOn Type\n ^\n3: \n",],
['{ field: {} }', "Syntax Error: Expected Name, found {", "Syntax Error: Expected Name, found {\n\nGraphQL request (1:10)\n1: { field: {} }\n ^\n"],
['notanoperation Foo { field }', "Syntax Error: Unexpected Name \"notanoperation\"", "Syntax Error: Unexpected Name \"notanoperation\"\n\nGraphQL request (1:1)\n1: notanoperation Foo { field }\n ^\n"],
['...', "Syntax Error: Unexpected ...", "Syntax Error: Unexpected ...\n\nGraphQL request (1:1)\n1: ...\n ^\n"],
];
}
@ -53,13 +53,14 @@ fragment MissingOn Type
* @dataProvider parseProvidesUsefulErrors
* @it parse provides useful errors
*/
public function testParseProvidesUsefulErrors($str, $expectedMessage, $expectedPositions = null, $expectedLocations = null)
public function testParseProvidesUsefulErrors($str, $expectedMessage, $stringRepresentation, $expectedPositions = null, $expectedLocations = null)
{
try {
Parser::parse($str);
$this->fail('Expected exception not thrown');
} catch (SyntaxError $e) {
$this->assertEquals($expectedMessage, $e->getMessage());
$this->assertEquals($stringRepresentation, (string) $e);
if ($expectedPositions) {
$this->assertEquals($expectedPositions, $e->getPositions());
@ -76,8 +77,15 @@ fragment MissingOn Type
*/
public function testParseProvidesUsefulErrorWhenUsingSource()
{
$this->setExpectedException(SyntaxError::class, "Syntax Error MyQuery.graphql (1:6) Expected {, found <EOF>\n\n1: query\n ^\n");
Parser::parse(new Source('query', 'MyQuery.graphql'));
try {
Parser::parse(new Source('query', 'MyQuery.graphql'));
$this->fail('Expected exception not thrown');
} catch (SyntaxError $error) {
$this->assertEquals(
"Syntax Error: Expected {, found <EOF>\n\nMyQuery.graphql (1:6)\n1: query\n ^\n",
(string) $error
);
}
}
/**
@ -94,8 +102,11 @@ fragment MissingOn Type
*/
public function testParsesConstantDefaultValues()
{
$this->setExpectedException(SyntaxError::class, "Syntax Error GraphQL (1:37) Unexpected $\n\n" . '1: query Foo($x: Complex = { a: { b: [ $var ] } }) { field }' . "\n ^\n");
Parser::parse('query Foo($x: Complex = { a: { b: [ $var ] } }) { field }');
$this->expectSyntaxError(
'query Foo($x: Complex = { a: { b: [ $var ] } }) { field }',
'Unexpected $',
$this->loc(1,37)
);
}
/**
@ -103,8 +114,11 @@ fragment MissingOn Type
*/
public function testDoesNotAcceptFragmentsNamedOn()
{
$this->setExpectedException('GraphQL\Error\SyntaxError', 'Syntax Error GraphQL (1:10) Unexpected Name "on"');
Parser::parse('fragment on on on { on }');
$this->expectSyntaxError(
'fragment on on on { on }',
'Unexpected Name "on"',
$this->loc(1,10)
);
}
/**
@ -112,8 +126,11 @@ fragment MissingOn Type
*/
public function testDoesNotAcceptFragmentSpreadOfOn()
{
$this->setExpectedException('GraphQL\Error\SyntaxError', 'Syntax Error GraphQL (1:9) Expected Name, found }');
Parser::parse('{ ...on }');
$this->expectSyntaxError(
'{ ...on }',
'Expected Name, found }',
$this->loc(1,9)
);
}
/**
@ -610,4 +627,20 @@ fragment $fragmentName on Type {
{
return TestUtils::nodeToArray($node);
}
private function loc($line, $column)
{
return new SourceLocation($line, $column);
}
private function expectSyntaxError($text, $message, $location)
{
$this->setExpectedException(SyntaxError::class, $message);
try {
Parser::parse($text);
} catch (SyntaxError $error) {
$this->assertEquals([$location], $error->getLocations());
throw $error;
}
}
}

View File

@ -4,6 +4,7 @@ namespace GraphQL\Tests\Language;
use GraphQL\Error\SyntaxError;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation;
class SchemaParserTest extends \PHPUnit_Framework_TestCase
{
@ -199,31 +200,49 @@ extend type Hello {
}
/**
* @it Extension do not include descriptions
* @expectedException \GraphQL\Error\SyntaxError
* @expectedExceptionMessage Syntax Error GraphQL (3:7)
* @it Extension without anything throws
*/
public function testExtensionDoNotIncludeDescriptions() {
public function testExtensionWithoutAnythingThrows()
{
$this->expectSyntaxError(
'extend type Hello',
'Unexpected <EOF>',
$this->loc(1, 18)
);
}
/**
* @it Extension do not include descriptions
*/
public function testExtensionDoNotIncludeDescriptions()
{
$body = '
"Description"
extend type Hello {
world: String
}';
Parser::parse($body);
$this->expectSyntaxError(
$body,
'Unexpected Name "extend"',
$this->loc(3, 7)
);
}
/**
* @it Extension do not include descriptions
* @expectedException \GraphQL\Error\SyntaxError
* @expectedExceptionMessage Syntax Error GraphQL (2:14)
*/
public function testExtensionDoNotIncludeDescriptions2() {
public function testExtensionDoNotIncludeDescriptions2()
{
$body = '
extend "Description" type Hello {
world: String
}
}';
Parser::parse($body);
$this->expectSyntaxError(
$body,
'Unexpected String "Description"',
$this->loc(2, 14)
);
}
/**
@ -707,9 +726,11 @@ type Hello {
*/
public function testUnionFailsWithNoTypes()
{
$body = 'union Hello = |';
$this->setExpectedExceptionRegExp(SyntaxError::class, '/' . preg_quote('Syntax Error GraphQL (1:16) Expected Name, found <EOF>', '/') . '/');
Parser::parse($body);
$this->expectSyntaxError(
'union Hello = |',
'Expected Name, found <EOF>',
$this->loc(1, 16)
);
}
/**
@ -717,9 +738,11 @@ type Hello {
*/
public function testUnionFailsWithLeadingDoublePipe()
{
$body = 'union Hello = || Wo | Rld';
$this->setExpectedExceptionRegExp(SyntaxError::class, '/' . preg_quote('Syntax Error GraphQL (1:16) Expected Name, found |', '/') . '/');
Parser::parse($body);
$this->expectSyntaxError(
'union Hello = || Wo | Rld',
'Expected Name, found |',
$this->loc(1, 16)
);
}
/**
@ -727,9 +750,11 @@ type Hello {
*/
public function testUnionFailsWithDoublePipe()
{
$body = 'union Hello = Wo || Rld';
$this->setExpectedExceptionRegExp(SyntaxError::class, '/' . preg_quote('Syntax Error GraphQL (1:19) Expected Name, found |', '/') . '/');
Parser::parse($body);
$this->expectSyntaxError(
'union Hello = Wo || Rld',
'Expected Name, found |',
$this->loc(1, 19)
);
}
/**
@ -737,9 +762,11 @@ type Hello {
*/
public function testUnionFailsWithTrailingPipe()
{
$body = 'union Hello = | Wo | Rld |';
$this->setExpectedExceptionRegExp(SyntaxError::class, '/' . preg_quote('Syntax Error GraphQL (1:27) Expected Name, found <EOF>', '/') . '/');
Parser::parse($body);
$this->expectSyntaxError(
'union Hello = | Wo | Rld |',
'Expected Name, found <EOF>',
$this->loc(1, 27)
);
}
/**
@ -804,28 +831,33 @@ input Hello {
/**
* @it Simple input object with args should fail
* @expectedException \GraphQL\Error\SyntaxError
*/
public function testSimpleInputObjectWithArgsShouldFail()
{
$body = '
input Hello {
world(foo: Int): String
}';
Parser::parse($body);
input Hello {
world(foo: Int): String
}';
$this->expectSyntaxError(
$body,
'Expected :, found (',
$this->loc(3, 14)
);
}
/**
* @it Directive with incorrect locations
* @expectedException \GraphQL\Error\SyntaxError
* @expectedExceptionMessage Syntax Error GraphQL (2:33) Unexpected Name "INCORRECT_LOCATION"
*/
public function testDirectiveWithIncorrectLocationShouldFail()
{
$body = '
directive @foo on FIELD | INCORRECT_LOCATION
';
Parser::parse($body);
$this->expectSyntaxError(
$body,
'Unexpected Name "INCORRECT_LOCATION"',
$this->loc(2, 33)
);
}
private function typeNode($name, $loc)
@ -887,4 +919,20 @@ input Hello {
'description' => null
];
}
private function loc($line, $column)
{
return new SourceLocation($line, $column);
}
private function expectSyntaxError($text, $message, $location)
{
$this->setExpectedException(SyntaxError::class, $message);
try {
Parser::parse($text);
} catch (SyntaxError $error) {
$this->assertEquals([$location], $error->getLocations());
throw $error;
}
}
}

View File

@ -52,7 +52,7 @@ class QueryExecutionTest extends TestCase
$this->assertSame(null, $result->data);
$this->assertCount(1, $result->errors);
$this->assertContains(
'Syntax Error GraphQL (1:4) Expected Name, found <EOF>',
'Syntax Error: Expected Name, found <EOF>',
$result->errors[0]->getMessage()
);
}

View File

@ -303,10 +303,18 @@ class ServerTest extends \PHPUnit_Framework_TestCase
$server = Server::create();
$ast = $server->parse('{q}');
$this->assertInstanceOf('GraphQL\Language\AST\DocumentNode', $ast);
}
$this->setExpectedExceptionRegExp(SyntaxError::class, '/' . preg_quote('{q', '/') . '/');
$server->parse('{q');
$this->fail('Expected exception not thrown');
public function testParseFailure()
{
$server = Server::create();
try {
$server->parse('{q');
$this->fail('Expected exception not thrown');
} catch (SyntaxError $error) {
$this->assertContains('{q', (string) $error);
$this->assertEquals('Syntax Error: Expected Name, found <EOF>', $error->getMessage());
}
}
public function testValidate()