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 $this->toSerializableArray();
} }
/**
* @return string
*/
public function __toString()
{
return FormattedError::printError($this);
}
} }

View File

@ -1,6 +1,7 @@
<?php <?php
namespace GraphQL\Error; namespace GraphQL\Error;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation; use GraphQL\Language\SourceLocation;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\Definition\WrappingType; use GraphQL\Type\Definition\WrappingType;
@ -27,6 +28,97 @@ class FormattedError
self::$internalErrorMessage = $msg; 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 * Standard GraphQL error formatter. Converts any exception to array
* conforming to GraphQL spec. * conforming to GraphQL spec.

View File

@ -2,7 +2,6 @@
namespace GraphQL\Error; namespace GraphQL\Error;
use GraphQL\Language\Source; use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
class SyntaxError extends Error class SyntaxError extends Error
{ {
@ -13,59 +12,11 @@ class SyntaxError extends Error
*/ */
public function __construct(Source $source, $position, $description) public function __construct(Source $source, $position, $description)
{ {
$location = $source->getLocation($position); parent::__construct(
$line = $location->line + $source->locationOffset->line - 1; "Syntax Error: $description",
$columnOffset = self::getColumnOffset($source, $location); null,
$column = $location->column + $columnOffset; $source,
[$position]
$syntaxError = );
"Syntax Error {$source->name} ({$line}:{$column}) $description\n" .
"\n".
self::highlightSourceAtLocation($source, $location);
parent::__construct($syntaxError, 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 }`. * be "Foo.graphql" and location to be `{ line: 40, column: 0 }`.
* line and column in locationOffset are 1-indexed * line and column in locationOffset are 1-indexed
* *
* @param $body * @param string $body
* @param null $name * @param string|null $name
* @param SourceLocation|null $location * @param SourceLocation|null $location
*/ */
public function __construct($body, $name = null, SourceLocation $location = null) public function __construct($body, $name = null, SourceLocation $location = null)
@ -52,7 +52,7 @@ class Source
$this->body = $body; $this->body = $body;
$this->length = mb_strlen($body, 'UTF-8'); $this->length = mb_strlen($body, 'UTF-8');
$this->name = $name ?: 'GraphQL'; $this->name = $name ?: 'GraphQL request';
$this->locationOffset = $location ?: new SourceLocation(1, 1); $this->locationOffset = $location ?: new SourceLocation(1, 1);
Utils::invariant( Utils::invariant(

View File

@ -15,10 +15,11 @@ class LexerTest extends \PHPUnit_Framework_TestCase
*/ */
public function testDissallowsUncommonControlCharacters() public function testDissallowsUncommonControlCharacters()
{ {
$char = Utils::chr(0x0007); $this->expectSyntaxError(
Utils::chr(0x0007),
$this->setExpectedExceptionRegExp(SyntaxError::class, '/' . preg_quote('Syntax Error GraphQL (1:1) Cannot contain the invalid character "\u0007"', '/') . '/'); 'Cannot contain the invalid character "\u0007"',
$this->lexOne($char); $this->loc(1, 1)
);
} }
/** /**
@ -107,14 +108,21 @@ class LexerTest extends \PHPUnit_Framework_TestCase
" ?\n" . " ?\n" .
"\n"; "\n";
$this->setExpectedException(SyntaxError::class, try {
'Syntax Error GraphQL (3:5) Cannot parse the unexpected character "?".' . "\n" . $this->lexOne($str);
$this->fail('Expected exception not thrown');
} catch (SyntaxError $error) {
$this->assertEquals(
'Syntax Error: Cannot parse the unexpected character "?".' . "\n" .
"\n" . "\n" .
"GraphQL request (3:5)\n" .
"2: \n" . "2: \n" .
"3: ?\n" . "3: ?\n" .
" ^\n" . " ^\n" .
"4: \n"); "4: \n",
$this->lexOne($str); (string) $error
);
}
} }
/** /**
@ -129,34 +137,42 @@ class LexerTest extends \PHPUnit_Framework_TestCase
"\n"; "\n";
$source = new Source($str, 'foo.js', new SourceLocation(11, 12)); $source = new Source($str, 'foo.js', new SourceLocation(11, 12));
$this->setExpectedException( try {
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 = new Lexer($source);
$lexer->advance(); $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() public function testUpdatesColumnNumbersInErrorForFileContext()
{ {
$source = new Source('?', 'foo.js', new SourceLocation(1, 5)); $source = new Source('?', 'foo.js', new SourceLocation(1, 5));
$this->setExpectedException( try {
SyntaxError::class,
'Syntax Error foo.js (1:5) ' .
'Cannot parse the unexpected character "?".' . "\n" .
"\n" .
'1: ?' . "\n" .
' ^' . "\n"
);
$lexer = new Lexer($source); $lexer = new Lexer($source);
$lexer->advance(); $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() { public function reportsUsefulStringErrors() {
return [ return [
['"', "Syntax Error GraphQL (1:2) Unterminated string.\n\n1: \"\n ^\n"], ['"', "Unterminated string.", $this->loc(1, 2)],
['"no end quote', "Syntax Error GraphQL (1:14) Unterminated string.\n\n1: \"no end quote\n ^\n"], ['"no end quote', "Unterminated string.", $this->loc(1, 14)],
["'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"], ["'single quotes'", "Unexpected single quote character ('), did you mean to use a double quote (\")?", $this->loc(1, 1)],
['"contains unescaped \u0007 control char"', "Syntax Error GraphQL (1:21) Invalid character within String: \"\\u0007\"\n\n1: \"contains unescaped \\u0007 control char\"\n ^\n"], ['"contains unescaped \u0007 control char"', "Invalid character within String: \"\\u0007\"", $this->loc(1, 21)],
['"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"], ['"null-byte is not \u0000 end of file"', 'Invalid character within String: "\\u0000"', $this->loc(1, 19)],
['"multi' . "\n" . 'line"', "Syntax Error GraphQL (1:7) Unterminated string.\n\n1: \"multi\n ^\n2: line\"\n"], ['"multi' . "\n" . 'line"', "Unterminated string.", $this->loc(1, 7)],
['"multi' . "\r" . 'line"', "Syntax Error GraphQL (1:7) Unterminated string.\n\n1: \"multi\n ^\n2: line\"\n"], ['"multi' . "\r" . 'line"', "Unterminated string.", $this->loc(1, 7)],
['"bad \\z esc"', "Syntax Error GraphQL (1:7) Invalid character escape sequence: \\z\n\n1: \"bad \\z esc\"\n ^\n"], ['"bad \\z esc"', "Invalid character escape sequence: \\z", $this->loc(1, 7)],
['"bad \\x esc"', "Syntax Error GraphQL (1:7) Invalid character escape sequence: \\x\n\n1: \"bad \\x esc\"\n ^\n"], ['"bad \\x esc"', "Invalid character escape sequence: \\x", $this->loc(1, 7)],
['"bad \\u1 esc"', "Syntax Error GraphQL (1:7) Invalid character escape sequence: \\u1 es\n\n1: \"bad \\u1 esc\"\n ^\n"], ['"bad \\u1 esc"', "Invalid character escape sequence: \\u1 es", $this->loc(1, 7)],
['"bad \\u0XX1 esc"', "Syntax Error GraphQL (1:7) Invalid character escape sequence: \\u0XX1\n\n1: \"bad \\u0XX1 esc\"\n ^\n"], ['"bad \\u0XX1 esc"', "Invalid character escape sequence: \\u0XX1", $this->loc(1, 7)],
['"bad \\uXXXX esc"', "Syntax Error GraphQL (1:7) Invalid character escape sequence: \\uXXXX\n\n1: \"bad \\uXXXX esc\"\n ^\n"], ['"bad \\uXXXX esc"', "Invalid character escape sequence: \\uXXXX", $this->loc(1, 7)],
['"bad \\uFXXX esc"', "Syntax Error GraphQL (1:7) Invalid character escape sequence: \\uFXXX\n\n1: \"bad \\uFXXX esc\"\n ^\n"], ['"bad \\uFXXX esc"', "Invalid character escape sequence: \\uFXXX", $this->loc(1, 7)],
['"bad \\uXXXF esc"', "Syntax Error GraphQL (1:7) Invalid character escape sequence: \\uXXXF\n\n1: \"bad \\uXXXF esc\"\n ^\n"], ['"bad \\uXXXF esc"', "Invalid character escape sequence: \\uXXXF", $this->loc(1, 7)],
]; ];
} }
@ -340,10 +337,27 @@ class LexerTest extends \PHPUnit_Framework_TestCase
* @dataProvider reportsUsefulStringErrors * @dataProvider reportsUsefulStringErrors
* @it lex reports useful string errors * @it lex reports useful string errors
*/ */
public function testLexReportsUsefulStringErrors($str, $expectedMessage) public function testLexReportsUsefulStringErrors($str, $expectedMessage, $location)
{ {
$this->setExpectedException(SyntaxError::class, $expectedMessage); $this->expectSyntaxError($str, $expectedMessage, $location);
$this->lexOne($str); }
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() public function reportsUsefulNumberErrors()
{ {
return [ return [
[ '00', "Syntax Error GraphQL (1:2) Invalid number, unexpected digit after 0: \"0\"\n\n1: 00\n ^\n"], [ '00', "Invalid number, unexpected digit after 0: \"0\"", $this->loc(1, 2)],
[ '+1', "Syntax Error GraphQL (1:1) Cannot parse the unexpected character \"+\".\n\n1: +1\n ^\n"], [ '+1', "Cannot parse the unexpected character \"+\".", $this->loc(1, 1)],
[ '1.', "Syntax Error GraphQL (1:3) Invalid number, expected digit but got: <EOF>\n\n1: 1.\n ^\n"], [ '1.', "Invalid number, expected digit but got: <EOF>", $this->loc(1, 3)],
[ '1.e1', "Syntax Error GraphQL (1:3) Invalid number, expected digit but got: \"e\"\n\n1: 1.e1\n ^\n"], [ '1.e1', "Invalid number, expected digit but got: \"e\"", $this->loc(1, 3)],
[ '.123', "Syntax Error GraphQL (1:1) Cannot parse the unexpected character \".\".\n\n1: .123\n ^\n"], [ '.123', "Cannot parse the unexpected character \".\".", $this->loc(1, 1)],
[ '1.A', "Syntax Error GraphQL (1:3) Invalid number, expected digit but got: \"A\"\n\n1: 1.A\n ^\n"], [ '1.A', "Invalid number, expected digit but got: \"A\"", $this->loc(1, 3)],
[ '-A', "Syntax Error GraphQL (1:2) Invalid number, expected digit but got: \"A\"\n\n1: -A\n ^\n"], [ '-A', "Invalid number, expected digit but got: \"A\"", $this->loc(1, 2)],
[ '1.0e', "Syntax Error GraphQL (1:5) Invalid number, expected digit but got: <EOF>\n\n1: 1.0e\n ^\n"], [ '1.0e', "Invalid number, expected digit but got: <EOF>", $this->loc(1, 5)],
[ '1.0eA', "Syntax Error GraphQL (1:5) Invalid number, expected digit but got: \"A\"\n\n1: 1.0eA\n ^\n"], [ '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 * @dataProvider reportsUsefulNumberErrors
* @it lex reports useful number errors * @it lex reports useful number errors
*/ */
public function testReportsUsefulNumberErrors($str, $expectedMessage) public function testReportsUsefulNumberErrors($str, $expectedMessage, $location)
{ {
$this->setExpectedException(SyntaxError::class, $expectedMessage); $this->expectSyntaxError($str, $expectedMessage, $location);
$this->lexOne($str);
} }
/** /**
@ -507,10 +520,10 @@ class LexerTest extends \PHPUnit_Framework_TestCase
$unicode2 = json_decode('"\u200b"'); $unicode2 = json_decode('"\u200b"');
return [ return [
['..', "Syntax Error GraphQL (1:1) Cannot parse the unexpected character \".\".\n\n1: ..\n ^\n"], ['..', "Cannot parse the unexpected character \".\".", $this->loc(1, 1)],
['?', "Syntax Error GraphQL (1:1) Cannot parse the unexpected character \"?\".\n\n1: ?\n ^\n"], ['?', "Cannot parse the unexpected character \"?\".", $this->loc(1, 1)],
[$unicode1, "Syntax Error GraphQL (1:1) Cannot parse the unexpected character \"\\u203b\".\n\n1: $unicode1\n ^\n"], [$unicode1, "Cannot parse the unexpected character \"\\u203b\".", $this->loc(1, 1)],
[$unicode2, "Syntax Error GraphQL (1:1) Cannot parse the unexpected character \"\\u200b\".\n\n1: $unicode2\n ^\n"], [$unicode2, "Cannot parse the unexpected character \"\\u200b\".", $this->loc(1, 1)],
]; ];
} }
@ -518,10 +531,9 @@ class LexerTest extends \PHPUnit_Framework_TestCase
* @dataProvider reportsUsefulUnknownCharErrors * @dataProvider reportsUsefulUnknownCharErrors
* @it lex reports useful unknown character error * @it lex reports useful unknown character error
*/ */
public function testReportsUsefulUnknownCharErrors($str, $expectedMessage) public function testReportsUsefulUnknownCharErrors($str, $expectedMessage, $location)
{ {
$this->setExpectedException(SyntaxError::class, $expectedMessage); $this->expectSyntaxError($str, $expectedMessage, $location);
$this->lexOne($str);
} }
/** /**
@ -533,8 +545,14 @@ class LexerTest extends \PHPUnit_Framework_TestCase
$lexer = new Lexer(new Source($q)); $lexer = new Lexer(new Source($q));
$this->assertArraySubset(['kind' => Token::NAME, 'start' => 0, 'end' => 1, 'value' => 'a'], (array) $lexer->advance()); $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"); $this->setExpectedException(SyntaxError::class, 'Syntax Error: Invalid number, expected digit but got: "b"');
try {
$lexer->advance(); $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)); $lexer = new Lexer(new Source($body));
return $lexer->advance(); 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() public function parseProvidesUsefulErrors()
{ {
return [ 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 } ['{ ...MissingOn }
fragment MissingOn Type fragment MissingOn Type
', "Syntax Error GraphQL (2:20) Expected \"on\", found Name \"Type\"\n\n1: { ...MissingOn }\n2: fragment MissingOn Type\n ^\n3: \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 GraphQL (1:10) Expected Name, found {\n\n1: { field: {} }\n ^\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 GraphQL (1:1) Unexpected Name \"notanoperation\"\n\n1: notanoperation Foo { 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 GraphQL (1:1) Unexpected ...\n\n1: ...\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 * @dataProvider parseProvidesUsefulErrors
* @it parse provides useful errors * @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 { try {
Parser::parse($str); Parser::parse($str);
$this->fail('Expected exception not thrown'); $this->fail('Expected exception not thrown');
} catch (SyntaxError $e) { } catch (SyntaxError $e) {
$this->assertEquals($expectedMessage, $e->getMessage()); $this->assertEquals($expectedMessage, $e->getMessage());
$this->assertEquals($stringRepresentation, (string) $e);
if ($expectedPositions) { if ($expectedPositions) {
$this->assertEquals($expectedPositions, $e->getPositions()); $this->assertEquals($expectedPositions, $e->getPositions());
@ -76,8 +77,15 @@ fragment MissingOn Type
*/ */
public function testParseProvidesUsefulErrorWhenUsingSource() public function testParseProvidesUsefulErrorWhenUsingSource()
{ {
$this->setExpectedException(SyntaxError::class, "Syntax Error MyQuery.graphql (1:6) Expected {, found <EOF>\n\n1: query\n ^\n"); try {
Parser::parse(new Source('query', 'MyQuery.graphql')); 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() 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"); $this->expectSyntaxError(
Parser::parse('query Foo($x: Complex = { a: { b: [ $var ] } }) { field }'); 'query Foo($x: Complex = { a: { b: [ $var ] } }) { field }',
'Unexpected $',
$this->loc(1,37)
);
} }
/** /**
@ -103,8 +114,11 @@ fragment MissingOn Type
*/ */
public function testDoesNotAcceptFragmentsNamedOn() public function testDoesNotAcceptFragmentsNamedOn()
{ {
$this->setExpectedException('GraphQL\Error\SyntaxError', 'Syntax Error GraphQL (1:10) Unexpected Name "on"'); $this->expectSyntaxError(
Parser::parse('fragment on on on { on }'); 'fragment on on on { on }',
'Unexpected Name "on"',
$this->loc(1,10)
);
} }
/** /**
@ -112,8 +126,11 @@ fragment MissingOn Type
*/ */
public function testDoesNotAcceptFragmentSpreadOfOn() public function testDoesNotAcceptFragmentSpreadOfOn()
{ {
$this->setExpectedException('GraphQL\Error\SyntaxError', 'Syntax Error GraphQL (1:9) Expected Name, found }'); $this->expectSyntaxError(
Parser::parse('{ ...on }'); '{ ...on }',
'Expected Name, found }',
$this->loc(1,9)
);
} }
/** /**
@ -610,4 +627,20 @@ fragment $fragmentName on Type {
{ {
return TestUtils::nodeToArray($node); 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\Error\SyntaxError;
use GraphQL\Language\AST\NodeKind; use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Parser; use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation;
class SchemaParserTest extends \PHPUnit_Framework_TestCase class SchemaParserTest extends \PHPUnit_Framework_TestCase
{ {
@ -199,31 +200,49 @@ extend type Hello {
} }
/** /**
* @it Extension do not include descriptions * @it Extension without anything throws
* @expectedException \GraphQL\Error\SyntaxError
* @expectedExceptionMessage Syntax Error GraphQL (3:7)
*/ */
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 = ' $body = '
"Description" "Description"
extend type Hello { extend type Hello {
world: String world: String
}'; }';
Parser::parse($body); $this->expectSyntaxError(
$body,
'Unexpected Name "extend"',
$this->loc(3, 7)
);
} }
/** /**
* @it Extension do not include descriptions * @it Extension do not include descriptions
* @expectedException \GraphQL\Error\SyntaxError
* @expectedExceptionMessage Syntax Error GraphQL (2:14)
*/ */
public function testExtensionDoNotIncludeDescriptions2() { public function testExtensionDoNotIncludeDescriptions2()
{
$body = ' $body = '
extend "Description" type Hello { extend "Description" type Hello {
world: String world: String
} }
}'; }';
Parser::parse($body); $this->expectSyntaxError(
$body,
'Unexpected String "Description"',
$this->loc(2, 14)
);
} }
/** /**
@ -707,9 +726,11 @@ type Hello {
*/ */
public function testUnionFailsWithNoTypes() public function testUnionFailsWithNoTypes()
{ {
$body = 'union Hello = |'; $this->expectSyntaxError(
$this->setExpectedExceptionRegExp(SyntaxError::class, '/' . preg_quote('Syntax Error GraphQL (1:16) Expected Name, found <EOF>', '/') . '/'); 'union Hello = |',
Parser::parse($body); 'Expected Name, found <EOF>',
$this->loc(1, 16)
);
} }
/** /**
@ -717,9 +738,11 @@ type Hello {
*/ */
public function testUnionFailsWithLeadingDoublePipe() public function testUnionFailsWithLeadingDoublePipe()
{ {
$body = 'union Hello = || Wo | Rld'; $this->expectSyntaxError(
$this->setExpectedExceptionRegExp(SyntaxError::class, '/' . preg_quote('Syntax Error GraphQL (1:16) Expected Name, found |', '/') . '/'); 'union Hello = || Wo | Rld',
Parser::parse($body); 'Expected Name, found |',
$this->loc(1, 16)
);
} }
/** /**
@ -727,9 +750,11 @@ type Hello {
*/ */
public function testUnionFailsWithDoublePipe() public function testUnionFailsWithDoublePipe()
{ {
$body = 'union Hello = Wo || Rld'; $this->expectSyntaxError(
$this->setExpectedExceptionRegExp(SyntaxError::class, '/' . preg_quote('Syntax Error GraphQL (1:19) Expected Name, found |', '/') . '/'); 'union Hello = Wo || Rld',
Parser::parse($body); 'Expected Name, found |',
$this->loc(1, 19)
);
} }
/** /**
@ -737,9 +762,11 @@ type Hello {
*/ */
public function testUnionFailsWithTrailingPipe() public function testUnionFailsWithTrailingPipe()
{ {
$body = 'union Hello = | Wo | Rld |'; $this->expectSyntaxError(
$this->setExpectedExceptionRegExp(SyntaxError::class, '/' . preg_quote('Syntax Error GraphQL (1:27) Expected Name, found <EOF>', '/') . '/'); 'union Hello = | Wo | Rld |',
Parser::parse($body); 'Expected Name, found <EOF>',
$this->loc(1, 27)
);
} }
/** /**
@ -804,7 +831,6 @@ input Hello {
/** /**
* @it Simple input object with args should fail * @it Simple input object with args should fail
* @expectedException \GraphQL\Error\SyntaxError
*/ */
public function testSimpleInputObjectWithArgsShouldFail() public function testSimpleInputObjectWithArgsShouldFail()
{ {
@ -812,20 +838,26 @@ input Hello {
input Hello { input Hello {
world(foo: Int): String world(foo: Int): String
}'; }';
Parser::parse($body); $this->expectSyntaxError(
$body,
'Expected :, found (',
$this->loc(3, 14)
);
} }
/** /**
* @it Directive with incorrect locations * @it Directive with incorrect locations
* @expectedException \GraphQL\Error\SyntaxError
* @expectedExceptionMessage Syntax Error GraphQL (2:33) Unexpected Name "INCORRECT_LOCATION"
*/ */
public function testDirectiveWithIncorrectLocationShouldFail() public function testDirectiveWithIncorrectLocationShouldFail()
{ {
$body = ' $body = '
directive @foo on FIELD | INCORRECT_LOCATION 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) private function typeNode($name, $loc)
@ -887,4 +919,20 @@ input Hello {
'description' => null '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->assertSame(null, $result->data);
$this->assertCount(1, $result->errors); $this->assertCount(1, $result->errors);
$this->assertContains( $this->assertContains(
'Syntax Error GraphQL (1:4) Expected Name, found <EOF>', 'Syntax Error: Expected Name, found <EOF>',
$result->errors[0]->getMessage() $result->errors[0]->getMessage()
); );
} }

View File

@ -303,10 +303,18 @@ class ServerTest extends \PHPUnit_Framework_TestCase
$server = Server::create(); $server = Server::create();
$ast = $server->parse('{q}'); $ast = $server->parse('{q}');
$this->assertInstanceOf('GraphQL\Language\AST\DocumentNode', $ast); $this->assertInstanceOf('GraphQL\Language\AST\DocumentNode', $ast);
}
$this->setExpectedExceptionRegExp(SyntaxError::class, '/' . preg_quote('{q', '/') . '/'); public function testParseFailure()
{
$server = Server::create();
try {
$server->parse('{q'); $server->parse('{q');
$this->fail('Expected exception not thrown'); $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() public function testValidate()