diff --git a/src/Error/Error.php b/src/Error/Error.php index 06a1d12..a96a4c7 100644 --- a/src/Error/Error.php +++ b/src/Error/Error.php @@ -324,4 +324,12 @@ class Error extends \Exception implements \JsonSerializable, ClientAware { return $this->toSerializableArray(); } + + /** + * @return string + */ + public function __toString() + { + return FormattedError::printError($this); + } } diff --git a/src/Error/FormattedError.php b/src/Error/FormattedError.php index 5ed4adb..16ed5e7 100644 --- a/src/Error/FormattedError.php +++ b/src/Error/FormattedError.php @@ -1,6 +1,7 @@ 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. diff --git a/src/Error/SyntaxError.php b/src/Error/SyntaxError.php index 7f9bd5e..ee17ab5 100644 --- a/src/Error/SyntaxError.php +++ b/src/Error/SyntaxError.php @@ -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; - } - } diff --git a/src/Language/Source.php b/src/Language/Source.php index 29fd0fe..899d450 100644 --- a/src/Language/Source.php +++ b/src/Language/Source.php @@ -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( diff --git a/tests/Language/LexerTest.php b/tests/Language/LexerTest.php index 946c39a..4d62b5e 100644 --- a/tests/Language/LexerTest.php +++ b/tests/Language/LexerTest.php @@ -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: \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: \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: ", $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: ", $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; + } + } } diff --git a/tests/Language/ParserTest.php b/tests/Language/ParserTest.php index 71fd7e4..f4032e6 100644 --- a/tests/Language/ParserTest.php +++ b/tests/Language/ParserTest.php @@ -39,13 +39,13 @@ class ParserTest extends \PHPUnit_Framework_TestCase public function parseProvidesUsefulErrors() { return [ - ['{', "Syntax Error GraphQL (1:2) Expected Name, found \n\n1: {\n ^\n", [1], [new SourceLocation(1, 2)]], + ['{', "Syntax Error: Expected Name, found ", "Syntax Error: Expected Name, found \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 \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 \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; + } + } } diff --git a/tests/Language/SchemaParserTest.php b/tests/Language/SchemaParserTest.php index 77e2e92..b7d018a 100644 --- a/tests/Language/SchemaParserTest.php +++ b/tests/Language/SchemaParserTest.php @@ -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 ', + $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 ', '/') . '/'); - Parser::parse($body); + $this->expectSyntaxError( + 'union Hello = |', + 'Expected Name, found ', + $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 ', '/') . '/'); - Parser::parse($body); + $this->expectSyntaxError( + 'union Hello = | Wo | Rld |', + 'Expected Name, found ', + $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; + } + } } diff --git a/tests/Server/QueryExecutionTest.php b/tests/Server/QueryExecutionTest.php index 75982cb..00b26ea 100644 --- a/tests/Server/QueryExecutionTest.php +++ b/tests/Server/QueryExecutionTest.php @@ -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 ', + 'Syntax Error: Expected Name, found ', $result->errors[0]->getMessage() ); } diff --git a/tests/ServerTest.php b/tests/ServerTest.php index a466842..03c6895 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -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 ', $error->getMessage()); + } } public function testValidate()