graphql-php/tests/Language/LexerTest.php
2018-10-05 10:47:57 +02:00

729 lines
22 KiB
PHP

<?php
declare(strict_types=1);
namespace GraphQL\Tests\Language;
use GraphQL\Error\SyntaxError;
use GraphQL\Language\Lexer;
use GraphQL\Language\Source;
use GraphQL\Language\SourceLocation;
use GraphQL\Language\Token;
use GraphQL\Utils\Utils;
use PHPUnit\Framework\TestCase;
use function count;
use function json_decode;
class LexerTest extends TestCase
{
/**
* @see it('disallows uncommon control characters')
*/
public function testDissallowsUncommonControlCharacters() : void
{
$this->expectSyntaxError(
Utils::chr(0x0007),
'Cannot contain the invalid character "\u0007"',
$this->loc(1, 1)
);
}
private function expectSyntaxError($text, $message, $location)
{
$this->expectException(SyntaxError::class);
$this->expectExceptionMessage($message);
try {
$this->lexOne($text);
} catch (SyntaxError $error) {
self::assertEquals([$location], $error->getLocations());
throw $error;
}
}
/**
* @param string $body
*
* @return Token
*/
private function lexOne($body)
{
$lexer = new Lexer(new Source($body));
return $lexer->advance();
}
private function loc($line, $column)
{
return new SourceLocation($line, $column);
}
/**
* @see it('accepts BOM header')
*/
public function testAcceptsBomHeader() : void
{
$bom = Utils::chr(0xFEFF);
$expected = [
'kind' => Token::NAME,
'start' => 2,
'end' => 5,
'value' => 'foo',
];
self::assertArraySubset($expected, (array) $this->lexOne($bom . ' foo'));
}
/**
* @see it('records line and column')
*/
public function testRecordsLineAndColumn() : void
{
$expected = [
'kind' => Token::NAME,
'start' => 8,
'end' => 11,
'line' => 4,
'column' => 3,
'value' => 'foo',
];
self::assertArraySubset($expected, (array) $this->lexOne("\n \r\n \r foo\n"));
}
/**
* @see it('skips whitespace and comments')
*/
public function testSkipsWhitespacesAndComments() : void
{
$example1 = '
foo
';
$expected = [
'kind' => Token::NAME,
'start' => 6,
'end' => 9,
'value' => 'foo',
];
self::assertArraySubset($expected, (array) $this->lexOne($example1));
$example2 = '
#comment
foo#comment
';
$expected = [
'kind' => Token::NAME,
'start' => 18,
'end' => 21,
'value' => 'foo',
];
self::assertArraySubset($expected, (array) $this->lexOne($example2));
$expected = [
'kind' => Token::NAME,
'start' => 3,
'end' => 6,
'value' => 'foo',
];
$example3 = ',,,foo,,,';
self::assertArraySubset($expected, (array) $this->lexOne($example3));
}
/**
* @see it('errors respect whitespace')
*/
public function testErrorsRespectWhitespace() : void
{
$str = '' .
"\n" .
"\n" .
" ?\n" .
"\n";
try {
$this->lexOne($str);
$this->fail('Expected exception not thrown');
} catch (SyntaxError $error) {
self::assertEquals(
'Syntax Error: Cannot parse the unexpected character "?".' . "\n" .
"\n" .
"GraphQL request (3:5)\n" .
"2: \n" .
"3: ?\n" .
" ^\n" .
"4: \n",
(string) $error
);
}
}
/**
* @see it('updates line numbers in error for file context')
*/
public function testUpdatesLineNumbersInErrorForFileContext() : void
{
$str = '' .
"\n" .
"\n" .
" ?\n" .
"\n";
$source = new Source($str, 'foo.js', new SourceLocation(11, 12));
try {
$lexer = new Lexer($source);
$lexer->advance();
$this->fail('Expected exception not thrown');
} catch (SyntaxError $error) {
self::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() : void
{
$source = new Source('?', 'foo.js', new SourceLocation(1, 5));
try {
$lexer = new Lexer($source);
$lexer->advance();
$this->fail('Expected exception not thrown');
} catch (SyntaxError $error) {
self::assertEquals(
'Syntax Error: Cannot parse the unexpected character "?".' . "\n" .
"\n" .
"foo.js (1:5)\n" .
'1: ?' . "\n" .
' ^' . "\n",
(string) $error
);
}
}
/**
* @see it('lexes strings')
*/
public function testLexesStrings() : void
{
self::assertArraySubset(
[
'kind' => Token::STRING,
'start' => 0,
'end' => 8,
'value' => 'simple',
],
(array) $this->lexOne('"simple"')
);
self::assertArraySubset(
[
'kind' => Token::STRING,
'start' => 0,
'end' => 15,
'value' => ' white space ',
],
(array) $this->lexOne('" white space "')
);
self::assertArraySubset(
[
'kind' => Token::STRING,
'start' => 0,
'end' => 10,
'value' => 'quote "',
],
(array) $this->lexOne('"quote \\""')
);
self::assertArraySubset(
[
'kind' => Token::STRING,
'start' => 0,
'end' => 25,
'value' => 'escaped \n\r\b\t\f',
],
(array) $this->lexOne('"escaped \\\\n\\\\r\\\\b\\\\t\\\\f"')
);
self::assertArraySubset(
[
'kind' => Token::STRING,
'start' => 0,
'end' => 16,
'value' => 'slashes \\ \/',
],
(array) $this->lexOne('"slashes \\\\ \\\\/"')
);
self::assertArraySubset(
[
'kind' => Token::STRING,
'start' => 0,
'end' => 13,
'value' => 'unicode яуц',
],
(array) $this->lexOne('"unicode яуц"')
);
$unicode = json_decode('"\u1234\u5678\u90AB\uCDEF"');
self::assertArraySubset(
[
'kind' => Token::STRING,
'start' => 0,
'end' => 34,
'value' => 'unicode ' . $unicode,
],
(array) $this->lexOne('"unicode \u1234\u5678\u90AB\uCDEF"')
);
self::assertArraySubset(
[
'kind' => Token::STRING,
'start' => 0,
'end' => 26,
'value' => $unicode,
],
(array) $this->lexOne('"\u1234\u5678\u90AB\uCDEF"')
);
}
/**
* @see it('lexes block strings')
*/
public function testLexesBlockString() : void
{
self::assertArraySubset(
[
'kind' => Token::BLOCK_STRING,
'start' => 0,
'end' => 12,
'value' => 'simple',
],
(array) $this->lexOne('"""simple"""')
);
self::assertArraySubset(
[
'kind' => Token::BLOCK_STRING,
'start' => 0,
'end' => 19,
'value' => ' white space ',
],
(array) $this->lexOne('""" white space """')
);
self::assertArraySubset(
[
'kind' => Token::BLOCK_STRING,
'start' => 0,
'end' => 22,
'value' => 'contains " quote',
],
(array) $this->lexOne('"""contains " quote"""')
);
self::assertArraySubset(
[
'kind' => Token::BLOCK_STRING,
'start' => 0,
'end' => 31,
'value' => 'contains """ triplequote',
],
(array) $this->lexOne('"""contains \\""" triplequote"""')
);
self::assertArraySubset(
[
'kind' => Token::BLOCK_STRING,
'start' => 0,
'end' => 16,
'value' => "multi\nline",
],
(array) $this->lexOne("\"\"\"multi\nline\"\"\"")
);
self::assertArraySubset(
[
'kind' => Token::BLOCK_STRING,
'start' => 0,
'end' => 28,
'value' => "multi\nline\nnormalized",
],
(array) $this->lexOne("\"\"\"multi\rline\r\nnormalized\"\"\"")
);
self::assertArraySubset(
[
'kind' => Token::BLOCK_STRING,
'start' => 0,
'end' => 32,
'value' => 'unescaped \\n\\r\\b\\t\\f\\u1234',
],
(array) $this->lexOne('"""unescaped \\n\\r\\b\\t\\f\\u1234"""')
);
self::assertArraySubset(
[
'kind' => Token::BLOCK_STRING,
'start' => 0,
'end' => 19,
'value' => 'slashes \\\\ \\/',
],
(array) $this->lexOne('"""slashes \\\\ \\/"""')
);
self::assertArraySubset(
[
'kind' => Token::BLOCK_STRING,
'start' => 0,
'end' => 68,
'value' => "spans\n multiple\n lines",
],
(array) $this->lexOne('"""
spans
multiple
lines
"""')
);
}
public function reportsUsefulStringErrors()
{
return [
['"', '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)],
];
}
/**
* @see it('lex reports useful string errors')
*
* @dataProvider reportsUsefulStringErrors
*/
public function testLexReportsUsefulStringErrors($str, $expectedMessage, $location) : void
{
$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
),
],
];
}
/**
* @see it('lex reports useful block string errors')
*
* @dataProvider reportsUsefulBlockStringErrors
*/
public function testReportsUsefulBlockStringErrors($str, $expectedMessage, $location) : void
{
$this->expectSyntaxError($str, $expectedMessage, $location);
}
/**
* @see it('lexes numbers')
*/
public function testLexesNumbers() : void
{
self::assertArraySubset(
['kind' => Token::INT, 'start' => 0, 'end' => 1, 'value' => '4'],
(array) $this->lexOne('4')
);
self::assertArraySubset(
['kind' => Token::FLOAT, 'start' => 0, 'end' => 5, 'value' => '4.123'],
(array) $this->lexOne('4.123')
);
self::assertArraySubset(
['kind' => Token::INT, 'start' => 0, 'end' => 2, 'value' => '-4'],
(array) $this->lexOne('-4')
);
self::assertArraySubset(
['kind' => Token::INT, 'start' => 0, 'end' => 1, 'value' => '9'],
(array) $this->lexOne('9')
);
self::assertArraySubset(
['kind' => Token::INT, 'start' => 0, 'end' => 1, 'value' => '0'],
(array) $this->lexOne('0')
);
self::assertArraySubset(
['kind' => Token::FLOAT, 'start' => 0, 'end' => 6, 'value' => '-4.123'],
(array) $this->lexOne('-4.123')
);
self::assertArraySubset(
['kind' => Token::FLOAT, 'start' => 0, 'end' => 5, 'value' => '0.123'],
(array) $this->lexOne('0.123')
);
self::assertArraySubset(
['kind' => Token::FLOAT, 'start' => 0, 'end' => 5, 'value' => '123e4'],
(array) $this->lexOne('123e4')
);
self::assertArraySubset(
['kind' => Token::FLOAT, 'start' => 0, 'end' => 5, 'value' => '123E4'],
(array) $this->lexOne('123E4')
);
self::assertArraySubset(
['kind' => Token::FLOAT, 'start' => 0, 'end' => 6, 'value' => '123e-4'],
(array) $this->lexOne('123e-4')
);
self::assertArraySubset(
['kind' => Token::FLOAT, 'start' => 0, 'end' => 6, 'value' => '123e+4'],
(array) $this->lexOne('123e+4')
);
self::assertArraySubset(
['kind' => Token::FLOAT, 'start' => 0, 'end' => 8, 'value' => '-1.123e4'],
(array) $this->lexOne('-1.123e4')
);
self::assertArraySubset(
['kind' => Token::FLOAT, 'start' => 0, 'end' => 8, 'value' => '-1.123E4'],
(array) $this->lexOne('-1.123E4')
);
self::assertArraySubset(
['kind' => Token::FLOAT, 'start' => 0, 'end' => 9, 'value' => '-1.123e-4'],
(array) $this->lexOne('-1.123e-4')
);
self::assertArraySubset(
['kind' => Token::FLOAT, 'start' => 0, 'end' => 9, 'value' => '-1.123e+4'],
(array) $this->lexOne('-1.123e+4')
);
self::assertArraySubset(
['kind' => Token::FLOAT, 'start' => 0, 'end' => 11, 'value' => '-1.123e4567'],
(array) $this->lexOne('-1.123e4567')
);
}
public function reportsUsefulNumberErrors()
{
return [
['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)],
];
}
/**
* @see it('lex reports useful number errors')
*
* @dataProvider reportsUsefulNumberErrors
*/
public function testReportsUsefulNumberErrors($str, $expectedMessage, $location) : void
{
$this->expectSyntaxError($str, $expectedMessage, $location);
}
/**
* @see it('lexes punctuation')
*/
public function testLexesPunctuation() : void
{
self::assertArraySubset(
['kind' => Token::BANG, 'start' => 0, 'end' => 1, 'value' => null],
(array) $this->lexOne('!')
);
self::assertArraySubset(
['kind' => Token::DOLLAR, 'start' => 0, 'end' => 1, 'value' => null],
(array) $this->lexOne('$')
);
self::assertArraySubset(
['kind' => Token::PAREN_L, 'start' => 0, 'end' => 1, 'value' => null],
(array) $this->lexOne('(')
);
self::assertArraySubset(
['kind' => Token::PAREN_R, 'start' => 0, 'end' => 1, 'value' => null],
(array) $this->lexOne(')')
);
self::assertArraySubset(
['kind' => Token::SPREAD, 'start' => 0, 'end' => 3, 'value' => null],
(array) $this->lexOne('...')
);
self::assertArraySubset(
['kind' => Token::COLON, 'start' => 0, 'end' => 1, 'value' => null],
(array) $this->lexOne(':')
);
self::assertArraySubset(
['kind' => Token::EQUALS, 'start' => 0, 'end' => 1, 'value' => null],
(array) $this->lexOne('=')
);
self::assertArraySubset(
['kind' => Token::AT, 'start' => 0, 'end' => 1, 'value' => null],
(array) $this->lexOne('@')
);
self::assertArraySubset(
['kind' => Token::BRACKET_L, 'start' => 0, 'end' => 1, 'value' => null],
(array) $this->lexOne('[')
);
self::assertArraySubset(
['kind' => Token::BRACKET_R, 'start' => 0, 'end' => 1, 'value' => null],
(array) $this->lexOne(']')
);
self::assertArraySubset(
['kind' => Token::BRACE_L, 'start' => 0, 'end' => 1, 'value' => null],
(array) $this->lexOne('{')
);
self::assertArraySubset(
['kind' => Token::PIPE, 'start' => 0, 'end' => 1, 'value' => null],
(array) $this->lexOne('|')
);
self::assertArraySubset(
['kind' => Token::BRACE_R, 'start' => 0, 'end' => 1, 'value' => null],
(array) $this->lexOne('}')
);
}
public function reportsUsefulUnknownCharErrors()
{
$unicode1 = json_decode('"\u203B"');
$unicode2 = json_decode('"\u200b"');
return [
['..', '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)],
];
}
/**
* @see it('lex reports useful unknown character error')
*
* @dataProvider reportsUsefulUnknownCharErrors
*/
public function testReportsUsefulUnknownCharErrors($str, $expectedMessage, $location) : void
{
$this->expectSyntaxError($str, $expectedMessage, $location);
}
/**
* @see it('lex reports useful information for dashes in names')
*/
public function testReportsUsefulDashesInfo() : void
{
$q = 'a-b';
$lexer = new Lexer(new Source($q));
self::assertArraySubset(
['kind' => Token::NAME, 'start' => 0, 'end' => 1, 'value' => 'a'],
(array) $lexer->advance()
);
$this->expectException(SyntaxError::class);
$this->expectExceptionMessage('Syntax Error: Invalid number, expected digit but got: "b"');
try {
$lexer->advance();
$this->fail('Expected exception not thrown');
} catch (SyntaxError $error) {
self::assertEquals([$this->loc(1, 3)], $error->getLocations());
throw $error;
}
}
/**
* @see it('produces double linked list of tokens, including comments')
*/
public function testDoubleLinkedList() : void
{
$lexer = new Lexer(new Source('{
#comment
field
}'));
$startToken = $lexer->token;
do {
$endToken = $lexer->advance();
// Lexer advances over ignored comment tokens to make writing parsers
// easier, but will include them in the linked list result.
self::assertNotEquals('Comment', $endToken->kind);
} while ($endToken->kind !== '<EOF>');
self::assertEquals(null, $startToken->prev);
self::assertEquals(null, $endToken->next);
$tokens = [];
for ($tok = $startToken; $tok; $tok = $tok->next) {
if (! empty($tokens)) {
// Tokens are double-linked, prev should point to last seen token.
self::assertSame($tokens[count($tokens) - 1], $tok->prev);
}
$tokens[] = $tok;
}
self::assertEquals(
[
'<SOF>',
'{',
'Comment',
'Name',
'}',
'<EOF>',
],
Utils::map(
$tokens,
static function ($tok) {
return $tok->kind;
}
)
);
}
}