1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace GraphQL\Tests\Language; |
6
|
|
|
|
7
|
|
|
use GraphQL\Error\SyntaxError; |
8
|
|
|
use GraphQL\Language\Lexer; |
9
|
|
|
use GraphQL\Language\Source; |
10
|
|
|
use GraphQL\Language\SourceLocation; |
11
|
|
|
use GraphQL\Language\Token; |
12
|
|
|
use GraphQL\Utils\Utils; |
13
|
|
|
use PHPUnit\Framework\TestCase; |
14
|
|
|
use function count; |
15
|
|
|
use function json_decode; |
16
|
|
|
|
17
|
|
|
class LexerTest extends TestCase |
18
|
|
|
{ |
19
|
|
|
/** |
20
|
|
|
* @see it('disallows uncommon control characters') |
21
|
|
|
*/ |
22
|
|
|
public function testDissallowsUncommonControlCharacters() : void |
23
|
|
|
{ |
24
|
|
|
$this->expectSyntaxError( |
25
|
|
|
Utils::chr(0x0007), |
26
|
|
|
'Cannot contain the invalid character "\u0007"', |
27
|
|
|
$this->loc(1, 1) |
28
|
|
|
); |
29
|
|
|
} |
30
|
|
|
|
31
|
|
|
private function expectSyntaxError($text, $message, $location) |
32
|
|
|
{ |
33
|
|
|
$this->expectException(SyntaxError::class); |
34
|
|
|
$this->expectExceptionMessage($message); |
35
|
|
|
try { |
36
|
|
|
$this->lexOne($text); |
37
|
|
|
} catch (SyntaxError $error) { |
38
|
|
|
self::assertEquals([$location], $error->getLocations()); |
39
|
|
|
throw $error; |
40
|
|
|
} |
41
|
|
|
} |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @param string $body |
45
|
|
|
* |
46
|
|
|
* @return Token |
47
|
|
|
*/ |
48
|
|
|
private function lexOne($body) |
49
|
|
|
{ |
50
|
|
|
$lexer = new Lexer(new Source($body)); |
51
|
|
|
|
52
|
|
|
return $lexer->advance(); |
53
|
|
|
} |
54
|
|
|
|
55
|
|
|
private function loc($line, $column) |
56
|
|
|
{ |
57
|
|
|
return new SourceLocation($line, $column); |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* @see it('accepts BOM header') |
62
|
|
|
*/ |
63
|
|
|
public function testAcceptsBomHeader() : void |
64
|
|
|
{ |
65
|
|
|
$bom = Utils::chr(0xFEFF); |
66
|
|
|
$expected = [ |
67
|
|
|
'kind' => Token::NAME, |
68
|
|
|
'start' => 2, |
69
|
|
|
'end' => 5, |
70
|
|
|
'value' => 'foo', |
71
|
|
|
]; |
72
|
|
|
|
73
|
|
|
self::assertArraySubset($expected, (array) $this->lexOne($bom . ' foo')); |
|
|
|
|
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* @see it('records line and column') |
78
|
|
|
*/ |
79
|
|
|
public function testRecordsLineAndColumn() : void |
80
|
|
|
{ |
81
|
|
|
$expected = [ |
82
|
|
|
'kind' => Token::NAME, |
83
|
|
|
'start' => 8, |
84
|
|
|
'end' => 11, |
85
|
|
|
'line' => 4, |
86
|
|
|
'column' => 3, |
87
|
|
|
'value' => 'foo', |
88
|
|
|
]; |
89
|
|
|
self::assertArraySubset($expected, (array) $this->lexOne("\n \r\n \r foo\n")); |
|
|
|
|
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* @see it('skips whitespace and comments') |
94
|
|
|
*/ |
95
|
|
|
public function testSkipsWhitespacesAndComments() : void |
96
|
|
|
{ |
97
|
|
|
$example1 = ' |
98
|
|
|
|
99
|
|
|
foo |
100
|
|
|
|
101
|
|
|
|
102
|
|
|
'; |
103
|
|
|
$expected = [ |
104
|
|
|
'kind' => Token::NAME, |
105
|
|
|
'start' => 6, |
106
|
|
|
'end' => 9, |
107
|
|
|
'value' => 'foo', |
108
|
|
|
]; |
109
|
|
|
self::assertArraySubset($expected, (array) $this->lexOne($example1)); |
|
|
|
|
110
|
|
|
|
111
|
|
|
$example2 = ' |
112
|
|
|
#comment |
113
|
|
|
foo#comment |
114
|
|
|
'; |
115
|
|
|
|
116
|
|
|
$expected = [ |
117
|
|
|
'kind' => Token::NAME, |
118
|
|
|
'start' => 18, |
119
|
|
|
'end' => 21, |
120
|
|
|
'value' => 'foo', |
121
|
|
|
]; |
122
|
|
|
self::assertArraySubset($expected, (array) $this->lexOne($example2)); |
|
|
|
|
123
|
|
|
|
124
|
|
|
$expected = [ |
125
|
|
|
'kind' => Token::NAME, |
126
|
|
|
'start' => 3, |
127
|
|
|
'end' => 6, |
128
|
|
|
'value' => 'foo', |
129
|
|
|
]; |
130
|
|
|
|
131
|
|
|
$example3 = ',,,foo,,,'; |
132
|
|
|
self::assertArraySubset($expected, (array) $this->lexOne($example3)); |
|
|
|
|
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* @see it('errors respect whitespace') |
137
|
|
|
*/ |
138
|
|
|
public function testErrorsRespectWhitespace() : void |
139
|
|
|
{ |
140
|
|
|
$str = '' . |
141
|
|
|
"\n" . |
142
|
|
|
"\n" . |
143
|
|
|
" ?\n" . |
144
|
|
|
"\n"; |
145
|
|
|
|
146
|
|
|
try { |
147
|
|
|
$this->lexOne($str); |
148
|
|
|
self::fail('Expected exception not thrown'); |
149
|
|
|
} catch (SyntaxError $error) { |
150
|
|
|
self::assertEquals( |
151
|
|
|
'Syntax Error: Cannot parse the unexpected character "?".' . "\n" . |
152
|
|
|
"\n" . |
153
|
|
|
"GraphQL request (3:5)\n" . |
154
|
|
|
"2: \n" . |
155
|
|
|
"3: ?\n" . |
156
|
|
|
" ^\n" . |
157
|
|
|
"4: \n", |
158
|
|
|
(string) $error |
159
|
|
|
); |
160
|
|
|
} |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* @see it('updates line numbers in error for file context') |
165
|
|
|
*/ |
166
|
|
|
public function testUpdatesLineNumbersInErrorForFileContext() : void |
167
|
|
|
{ |
168
|
|
|
$str = '' . |
169
|
|
|
"\n" . |
170
|
|
|
"\n" . |
171
|
|
|
" ?\n" . |
172
|
|
|
"\n"; |
173
|
|
|
$source = new Source($str, 'foo.js', new SourceLocation(11, 12)); |
174
|
|
|
|
175
|
|
|
try { |
176
|
|
|
$lexer = new Lexer($source); |
177
|
|
|
$lexer->advance(); |
178
|
|
|
self::fail('Expected exception not thrown'); |
179
|
|
|
} catch (SyntaxError $error) { |
180
|
|
|
self::assertEquals( |
181
|
|
|
'Syntax Error: Cannot parse the unexpected character "?".' . "\n" . |
182
|
|
|
"\n" . |
183
|
|
|
"foo.js (13:6)\n" . |
184
|
|
|
"12: \n" . |
185
|
|
|
"13: ?\n" . |
186
|
|
|
" ^\n" . |
187
|
|
|
"14: \n", |
188
|
|
|
(string) $error |
189
|
|
|
); |
190
|
|
|
} |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
public function testUpdatesColumnNumbersInErrorForFileContext() : void |
194
|
|
|
{ |
195
|
|
|
$source = new Source('?', 'foo.js', new SourceLocation(1, 5)); |
196
|
|
|
|
197
|
|
|
try { |
198
|
|
|
$lexer = new Lexer($source); |
199
|
|
|
$lexer->advance(); |
200
|
|
|
self::fail('Expected exception not thrown'); |
201
|
|
|
} catch (SyntaxError $error) { |
202
|
|
|
self::assertEquals( |
203
|
|
|
'Syntax Error: Cannot parse the unexpected character "?".' . "\n" . |
204
|
|
|
"\n" . |
205
|
|
|
"foo.js (1:5)\n" . |
206
|
|
|
'1: ?' . "\n" . |
207
|
|
|
' ^' . "\n", |
208
|
|
|
(string) $error |
209
|
|
|
); |
210
|
|
|
} |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* @see it('lexes strings') |
215
|
|
|
*/ |
216
|
|
|
public function testLexesStrings() : void |
217
|
|
|
{ |
218
|
|
|
self::assertArraySubset( |
|
|
|
|
219
|
|
|
[ |
220
|
|
|
'kind' => Token::STRING, |
221
|
|
|
'start' => 0, |
222
|
|
|
'end' => 8, |
223
|
|
|
'value' => 'simple', |
224
|
|
|
], |
225
|
|
|
(array) $this->lexOne('"simple"') |
226
|
|
|
); |
227
|
|
|
|
228
|
|
|
self::assertArraySubset( |
|
|
|
|
229
|
|
|
[ |
230
|
|
|
'kind' => Token::STRING, |
231
|
|
|
'start' => 0, |
232
|
|
|
'end' => 15, |
233
|
|
|
'value' => ' white space ', |
234
|
|
|
], |
235
|
|
|
(array) $this->lexOne('" white space "') |
236
|
|
|
); |
237
|
|
|
|
238
|
|
|
self::assertArraySubset( |
|
|
|
|
239
|
|
|
[ |
240
|
|
|
'kind' => Token::STRING, |
241
|
|
|
'start' => 0, |
242
|
|
|
'end' => 10, |
243
|
|
|
'value' => 'quote "', |
244
|
|
|
], |
245
|
|
|
(array) $this->lexOne('"quote \\""') |
246
|
|
|
); |
247
|
|
|
|
248
|
|
|
self::assertArraySubset( |
|
|
|
|
249
|
|
|
[ |
250
|
|
|
'kind' => Token::STRING, |
251
|
|
|
'start' => 0, |
252
|
|
|
'end' => 25, |
253
|
|
|
'value' => 'escaped \n\r\b\t\f', |
254
|
|
|
], |
255
|
|
|
(array) $this->lexOne('"escaped \\\\n\\\\r\\\\b\\\\t\\\\f"') |
256
|
|
|
); |
257
|
|
|
|
258
|
|
|
self::assertArraySubset( |
|
|
|
|
259
|
|
|
[ |
260
|
|
|
'kind' => Token::STRING, |
261
|
|
|
'start' => 0, |
262
|
|
|
'end' => 16, |
263
|
|
|
'value' => 'slashes \\ \/', |
264
|
|
|
], |
265
|
|
|
(array) $this->lexOne('"slashes \\\\ \\\\/"') |
266
|
|
|
); |
267
|
|
|
|
268
|
|
|
self::assertArraySubset( |
|
|
|
|
269
|
|
|
[ |
270
|
|
|
'kind' => Token::STRING, |
271
|
|
|
'start' => 0, |
272
|
|
|
'end' => 13, |
273
|
|
|
'value' => 'unicode яуц', |
274
|
|
|
], |
275
|
|
|
(array) $this->lexOne('"unicode яуц"') |
276
|
|
|
); |
277
|
|
|
|
278
|
|
|
$unicode = json_decode('"\u1234\u5678\u90AB\uCDEF"'); |
279
|
|
|
self::assertArraySubset( |
|
|
|
|
280
|
|
|
[ |
281
|
|
|
'kind' => Token::STRING, |
282
|
|
|
'start' => 0, |
283
|
|
|
'end' => 34, |
284
|
|
|
'value' => 'unicode ' . $unicode, |
285
|
|
|
], |
286
|
|
|
(array) $this->lexOne('"unicode \u1234\u5678\u90AB\uCDEF"') |
287
|
|
|
); |
288
|
|
|
|
289
|
|
|
self::assertArraySubset( |
|
|
|
|
290
|
|
|
[ |
291
|
|
|
'kind' => Token::STRING, |
292
|
|
|
'start' => 0, |
293
|
|
|
'end' => 26, |
294
|
|
|
'value' => $unicode, |
295
|
|
|
], |
296
|
|
|
(array) $this->lexOne('"\u1234\u5678\u90AB\uCDEF"') |
297
|
|
|
); |
298
|
|
|
|
299
|
|
|
self::assertArraySubset( |
|
|
|
|
300
|
|
|
[ |
301
|
|
|
'kind' => Token::STRING, |
302
|
|
|
'start' => 0, |
303
|
|
|
'end' => 41, |
304
|
|
|
'value' => '𝕌𝕋𝔽-16', |
305
|
|
|
], |
306
|
|
|
(array) $this->lexOne('"\ud835\udd4C\ud835\udd4B\ud835\udd3d-16"') |
307
|
|
|
); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* @see it('lexes block strings') |
312
|
|
|
*/ |
313
|
|
|
public function testLexesBlockString() : void |
314
|
|
|
{ |
315
|
|
|
self::assertArraySubset( |
|
|
|
|
316
|
|
|
[ |
317
|
|
|
'kind' => Token::BLOCK_STRING, |
318
|
|
|
'start' => 0, |
319
|
|
|
'end' => 12, |
320
|
|
|
'value' => 'simple', |
321
|
|
|
], |
322
|
|
|
(array) $this->lexOne('"""simple"""') |
323
|
|
|
); |
324
|
|
|
|
325
|
|
|
self::assertArraySubset( |
|
|
|
|
326
|
|
|
[ |
327
|
|
|
'kind' => Token::BLOCK_STRING, |
328
|
|
|
'start' => 0, |
329
|
|
|
'end' => 19, |
330
|
|
|
'value' => ' white space ', |
331
|
|
|
], |
332
|
|
|
(array) $this->lexOne('""" white space """') |
333
|
|
|
); |
334
|
|
|
|
335
|
|
|
self::assertArraySubset( |
|
|
|
|
336
|
|
|
[ |
337
|
|
|
'kind' => Token::BLOCK_STRING, |
338
|
|
|
'start' => 0, |
339
|
|
|
'end' => 22, |
340
|
|
|
'value' => 'contains " quote', |
341
|
|
|
], |
342
|
|
|
(array) $this->lexOne('"""contains " quote"""') |
343
|
|
|
); |
344
|
|
|
|
345
|
|
|
self::assertArraySubset( |
|
|
|
|
346
|
|
|
[ |
347
|
|
|
'kind' => Token::BLOCK_STRING, |
348
|
|
|
'start' => 0, |
349
|
|
|
'end' => 31, |
350
|
|
|
'value' => 'contains """ triplequote', |
351
|
|
|
], |
352
|
|
|
(array) $this->lexOne('"""contains \\""" triplequote"""') |
353
|
|
|
); |
354
|
|
|
|
355
|
|
|
self::assertArraySubset( |
|
|
|
|
356
|
|
|
[ |
357
|
|
|
'kind' => Token::BLOCK_STRING, |
358
|
|
|
'start' => 0, |
359
|
|
|
'end' => 16, |
360
|
|
|
'value' => "multi\nline", |
361
|
|
|
], |
362
|
|
|
(array) $this->lexOne("\"\"\"multi\nline\"\"\"") |
363
|
|
|
); |
364
|
|
|
|
365
|
|
|
self::assertArraySubset( |
|
|
|
|
366
|
|
|
[ |
367
|
|
|
'kind' => Token::BLOCK_STRING, |
368
|
|
|
'start' => 0, |
369
|
|
|
'end' => 28, |
370
|
|
|
'value' => "multi\nline\nnormalized", |
371
|
|
|
], |
372
|
|
|
(array) $this->lexOne("\"\"\"multi\rline\r\nnormalized\"\"\"") |
373
|
|
|
); |
374
|
|
|
|
375
|
|
|
self::assertArraySubset( |
|
|
|
|
376
|
|
|
[ |
377
|
|
|
'kind' => Token::BLOCK_STRING, |
378
|
|
|
'start' => 0, |
379
|
|
|
'end' => 32, |
380
|
|
|
'value' => 'unescaped \\n\\r\\b\\t\\f\\u1234', |
381
|
|
|
], |
382
|
|
|
(array) $this->lexOne('"""unescaped \\n\\r\\b\\t\\f\\u1234"""') |
383
|
|
|
); |
384
|
|
|
|
385
|
|
|
self::assertArraySubset( |
|
|
|
|
386
|
|
|
[ |
387
|
|
|
'kind' => Token::BLOCK_STRING, |
388
|
|
|
'start' => 0, |
389
|
|
|
'end' => 19, |
390
|
|
|
'value' => 'slashes \\\\ \\/', |
391
|
|
|
], |
392
|
|
|
(array) $this->lexOne('"""slashes \\\\ \\/"""') |
393
|
|
|
); |
394
|
|
|
|
395
|
|
|
self::assertArraySubset( |
|
|
|
|
396
|
|
|
[ |
397
|
|
|
'kind' => Token::BLOCK_STRING, |
398
|
|
|
'start' => 0, |
399
|
|
|
'end' => 68, |
400
|
|
|
'value' => "spans\n multiple\n lines", |
401
|
|
|
], |
402
|
|
|
(array) $this->lexOne('""" |
403
|
|
|
|
404
|
|
|
spans |
405
|
|
|
multiple |
406
|
|
|
lines |
407
|
|
|
|
408
|
|
|
"""') |
409
|
|
|
); |
410
|
|
|
} |
411
|
|
|
|
412
|
|
|
public function reportsUsefulStringErrors() |
413
|
|
|
{ |
414
|
|
|
return [ |
415
|
|
|
['"', 'Unterminated string.', $this->loc(1, 2)], |
416
|
|
|
['"no end quote', 'Unterminated string.', $this->loc(1, 14)], |
417
|
|
|
[ |
418
|
|
|
"'single quotes'", |
419
|
|
|
"Unexpected single quote character ('), did you mean to use a double quote (\")?", |
420
|
|
|
$this->loc( |
421
|
|
|
1, |
422
|
|
|
1 |
423
|
|
|
), |
424
|
|
|
], |
425
|
|
|
[ |
426
|
|
|
'"contains unescaped \u0007 control char"', |
427
|
|
|
"Invalid character within String: \"\\u0007\"", |
428
|
|
|
$this->loc( |
429
|
|
|
1, |
430
|
|
|
21 |
431
|
|
|
), |
432
|
|
|
], |
433
|
|
|
['"null-byte is not \u0000 end of file"', 'Invalid character within String: "\\u0000"', $this->loc(1, 19)], |
434
|
|
|
['"multi' . "\n" . 'line"', 'Unterminated string.', $this->loc(1, 7)], |
435
|
|
|
['"multi' . "\r" . 'line"', 'Unterminated string.', $this->loc(1, 7)], |
436
|
|
|
['"bad \\z esc"', 'Invalid character escape sequence: \\z', $this->loc(1, 7)], |
437
|
|
|
['"bad \\x esc"', "Invalid character escape sequence: \\x", $this->loc(1, 7)], |
438
|
|
|
['"bad \\u1 esc"', "Invalid character escape sequence: \\u1 es", $this->loc(1, 7)], |
439
|
|
|
['"bad \\u0XX1 esc"', "Invalid character escape sequence: \\u0XX1", $this->loc(1, 7)], |
440
|
|
|
['"bad \\uXXXX esc"', "Invalid character escape sequence: \\uXXXX", $this->loc(1, 7)], |
441
|
|
|
['"bad \\uFXXX esc"', "Invalid character escape sequence: \\uFXXX", $this->loc(1, 7)], |
442
|
|
|
['"bad \\uXXXF esc"', "Invalid character escape sequence: \\uXXXF", $this->loc(1, 7)], |
443
|
|
|
['"bad \\uD835"', 'Invalid UTF-16 trailing surrogate: ', $this->loc(1, 13)], |
444
|
|
|
['"bad \\uD835\\u1"', "Invalid UTF-16 trailing surrogate: \\u1", $this->loc(1, 13)], |
445
|
|
|
['"bad \\uD835\\u1 esc"', "Invalid UTF-16 trailing surrogate: \\u1 es", $this->loc(1, 13)], |
446
|
|
|
['"bad \\uD835uuFFFF esc"', 'Invalid UTF-16 trailing surrogate: uuFFFF', $this->loc(1, 13)], |
447
|
|
|
['"bad \\uD835\\u0XX1 esc"', "Invalid UTF-16 trailing surrogate: \\u0XX1", $this->loc(1, 13)], |
448
|
|
|
['"bad \\uD835\\uXXXX esc"', "Invalid UTF-16 trailing surrogate: \\uXXXX", $this->loc(1, 13)], |
449
|
|
|
['"bad \\uD835\\uFXXX esc"', "Invalid UTF-16 trailing surrogate: \\uFXXX", $this->loc(1, 13)], |
450
|
|
|
['"bad \\uD835\\uXXXF esc"', "Invalid UTF-16 trailing surrogate: \\uXXXF", $this->loc(1, 13)], |
451
|
|
|
]; |
452
|
|
|
} |
453
|
|
|
|
454
|
|
|
/** |
455
|
|
|
* @see it('lex reports useful string errors') |
456
|
|
|
* |
457
|
|
|
* @dataProvider reportsUsefulStringErrors |
458
|
|
|
*/ |
459
|
|
|
public function testLexReportsUsefulStringErrors($str, $expectedMessage, $location) : void |
460
|
|
|
{ |
461
|
|
|
$this->expectSyntaxError($str, $expectedMessage, $location); |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
public function reportsUsefulBlockStringErrors() |
465
|
|
|
{ |
466
|
|
|
return [ |
467
|
|
|
['"""', 'Unterminated string.', $this->loc(1, 4)], |
468
|
|
|
['"""no end quote', 'Unterminated string.', $this->loc(1, 16)], |
469
|
|
|
[ |
470
|
|
|
'"""contains unescaped ' . json_decode('"\u0007"') . ' control char"""', |
471
|
|
|
"Invalid character within String: \"\\u0007\"", |
472
|
|
|
$this->loc( |
473
|
|
|
1, |
474
|
|
|
23 |
475
|
|
|
), |
476
|
|
|
], |
477
|
|
|
[ |
478
|
|
|
'"""null-byte is not ' . json_decode('"\u0000"') . ' end of file"""', |
479
|
|
|
"Invalid character within String: \"\\u0000\"", |
480
|
|
|
$this->loc( |
481
|
|
|
1, |
482
|
|
|
21 |
483
|
|
|
), |
484
|
|
|
], |
485
|
|
|
]; |
486
|
|
|
} |
487
|
|
|
|
488
|
|
|
/** |
489
|
|
|
* @see it('lex reports useful block string errors') |
490
|
|
|
* |
491
|
|
|
* @dataProvider reportsUsefulBlockStringErrors |
492
|
|
|
*/ |
493
|
|
|
public function testReportsUsefulBlockStringErrors($str, $expectedMessage, $location) : void |
494
|
|
|
{ |
495
|
|
|
$this->expectSyntaxError($str, $expectedMessage, $location); |
496
|
|
|
} |
497
|
|
|
|
498
|
|
|
/** |
499
|
|
|
* @see it('lexes numbers') |
500
|
|
|
*/ |
501
|
|
|
public function testLexesNumbers() : void |
502
|
|
|
{ |
503
|
|
|
self::assertArraySubset( |
|
|
|
|
504
|
|
|
['kind' => Token::INT, 'start' => 0, 'end' => 1, 'value' => '4'], |
505
|
|
|
(array) $this->lexOne('4') |
506
|
|
|
); |
507
|
|
|
self::assertArraySubset( |
|
|
|
|
508
|
|
|
['kind' => Token::FLOAT, 'start' => 0, 'end' => 5, 'value' => '4.123'], |
509
|
|
|
(array) $this->lexOne('4.123') |
510
|
|
|
); |
511
|
|
|
self::assertArraySubset( |
|
|
|
|
512
|
|
|
['kind' => Token::INT, 'start' => 0, 'end' => 2, 'value' => '-4'], |
513
|
|
|
(array) $this->lexOne('-4') |
514
|
|
|
); |
515
|
|
|
self::assertArraySubset( |
|
|
|
|
516
|
|
|
['kind' => Token::INT, 'start' => 0, 'end' => 1, 'value' => '9'], |
517
|
|
|
(array) $this->lexOne('9') |
518
|
|
|
); |
519
|
|
|
self::assertArraySubset( |
|
|
|
|
520
|
|
|
['kind' => Token::INT, 'start' => 0, 'end' => 1, 'value' => '0'], |
521
|
|
|
(array) $this->lexOne('0') |
522
|
|
|
); |
523
|
|
|
self::assertArraySubset( |
|
|
|
|
524
|
|
|
['kind' => Token::FLOAT, 'start' => 0, 'end' => 6, 'value' => '-4.123'], |
525
|
|
|
(array) $this->lexOne('-4.123') |
526
|
|
|
); |
527
|
|
|
self::assertArraySubset( |
|
|
|
|
528
|
|
|
['kind' => Token::FLOAT, 'start' => 0, 'end' => 5, 'value' => '0.123'], |
529
|
|
|
(array) $this->lexOne('0.123') |
530
|
|
|
); |
531
|
|
|
self::assertArraySubset( |
|
|
|
|
532
|
|
|
['kind' => Token::FLOAT, 'start' => 0, 'end' => 5, 'value' => '123e4'], |
533
|
|
|
(array) $this->lexOne('123e4') |
534
|
|
|
); |
535
|
|
|
self::assertArraySubset( |
|
|
|
|
536
|
|
|
['kind' => Token::FLOAT, 'start' => 0, 'end' => 5, 'value' => '123E4'], |
537
|
|
|
(array) $this->lexOne('123E4') |
538
|
|
|
); |
539
|
|
|
self::assertArraySubset( |
|
|
|
|
540
|
|
|
['kind' => Token::FLOAT, 'start' => 0, 'end' => 6, 'value' => '123e-4'], |
541
|
|
|
(array) $this->lexOne('123e-4') |
542
|
|
|
); |
543
|
|
|
self::assertArraySubset( |
|
|
|
|
544
|
|
|
['kind' => Token::FLOAT, 'start' => 0, 'end' => 6, 'value' => '123e+4'], |
545
|
|
|
(array) $this->lexOne('123e+4') |
546
|
|
|
); |
547
|
|
|
self::assertArraySubset( |
|
|
|
|
548
|
|
|
['kind' => Token::FLOAT, 'start' => 0, 'end' => 8, 'value' => '-1.123e4'], |
549
|
|
|
(array) $this->lexOne('-1.123e4') |
550
|
|
|
); |
551
|
|
|
self::assertArraySubset( |
|
|
|
|
552
|
|
|
['kind' => Token::FLOAT, 'start' => 0, 'end' => 8, 'value' => '-1.123E4'], |
553
|
|
|
(array) $this->lexOne('-1.123E4') |
554
|
|
|
); |
555
|
|
|
self::assertArraySubset( |
|
|
|
|
556
|
|
|
['kind' => Token::FLOAT, 'start' => 0, 'end' => 9, 'value' => '-1.123e-4'], |
557
|
|
|
(array) $this->lexOne('-1.123e-4') |
558
|
|
|
); |
559
|
|
|
self::assertArraySubset( |
|
|
|
|
560
|
|
|
['kind' => Token::FLOAT, 'start' => 0, 'end' => 9, 'value' => '-1.123e+4'], |
561
|
|
|
(array) $this->lexOne('-1.123e+4') |
562
|
|
|
); |
563
|
|
|
self::assertArraySubset( |
|
|
|
|
564
|
|
|
['kind' => Token::FLOAT, 'start' => 0, 'end' => 11, 'value' => '-1.123e4567'], |
565
|
|
|
(array) $this->lexOne('-1.123e4567') |
566
|
|
|
); |
567
|
|
|
} |
568
|
|
|
|
569
|
|
|
public function reportsUsefulNumberErrors() |
570
|
|
|
{ |
571
|
|
|
return [ |
572
|
|
|
['00', 'Invalid number, unexpected digit after 0: "0"', $this->loc(1, 2)], |
573
|
|
|
['+1', 'Cannot parse the unexpected character "+".', $this->loc(1, 1)], |
574
|
|
|
['1.', 'Invalid number, expected digit but got: <EOF>', $this->loc(1, 3)], |
575
|
|
|
['1.e1', 'Invalid number, expected digit but got: "e"', $this->loc(1, 3)], |
576
|
|
|
['.123', 'Cannot parse the unexpected character ".".', $this->loc(1, 1)], |
577
|
|
|
['1.A', 'Invalid number, expected digit but got: "A"', $this->loc(1, 3)], |
578
|
|
|
['-A', 'Invalid number, expected digit but got: "A"', $this->loc(1, 2)], |
579
|
|
|
['1.0e', 'Invalid number, expected digit but got: <EOF>', $this->loc(1, 5)], |
580
|
|
|
['1.0eA', 'Invalid number, expected digit but got: "A"', $this->loc(1, 5)], |
581
|
|
|
]; |
582
|
|
|
} |
583
|
|
|
|
584
|
|
|
/** |
585
|
|
|
* @see it('lex reports useful number errors') |
586
|
|
|
* |
587
|
|
|
* @dataProvider reportsUsefulNumberErrors |
588
|
|
|
*/ |
589
|
|
|
public function testReportsUsefulNumberErrors($str, $expectedMessage, $location) : void |
590
|
|
|
{ |
591
|
|
|
$this->expectSyntaxError($str, $expectedMessage, $location); |
592
|
|
|
} |
593
|
|
|
|
594
|
|
|
/** |
595
|
|
|
* @see it('lexes punctuation') |
596
|
|
|
*/ |
597
|
|
|
public function testLexesPunctuation() : void |
598
|
|
|
{ |
599
|
|
|
self::assertArraySubset( |
|
|
|
|
600
|
|
|
['kind' => Token::BANG, 'start' => 0, 'end' => 1, 'value' => null], |
601
|
|
|
(array) $this->lexOne('!') |
602
|
|
|
); |
603
|
|
|
self::assertArraySubset( |
|
|
|
|
604
|
|
|
['kind' => Token::DOLLAR, 'start' => 0, 'end' => 1, 'value' => null], |
605
|
|
|
(array) $this->lexOne('$') |
606
|
|
|
); |
607
|
|
|
self::assertArraySubset( |
|
|
|
|
608
|
|
|
['kind' => Token::PAREN_L, 'start' => 0, 'end' => 1, 'value' => null], |
609
|
|
|
(array) $this->lexOne('(') |
610
|
|
|
); |
611
|
|
|
self::assertArraySubset( |
|
|
|
|
612
|
|
|
['kind' => Token::PAREN_R, 'start' => 0, 'end' => 1, 'value' => null], |
613
|
|
|
(array) $this->lexOne(')') |
614
|
|
|
); |
615
|
|
|
self::assertArraySubset( |
|
|
|
|
616
|
|
|
['kind' => Token::SPREAD, 'start' => 0, 'end' => 3, 'value' => null], |
617
|
|
|
(array) $this->lexOne('...') |
618
|
|
|
); |
619
|
|
|
self::assertArraySubset( |
|
|
|
|
620
|
|
|
['kind' => Token::COLON, 'start' => 0, 'end' => 1, 'value' => null], |
621
|
|
|
(array) $this->lexOne(':') |
622
|
|
|
); |
623
|
|
|
self::assertArraySubset( |
|
|
|
|
624
|
|
|
['kind' => Token::EQUALS, 'start' => 0, 'end' => 1, 'value' => null], |
625
|
|
|
(array) $this->lexOne('=') |
626
|
|
|
); |
627
|
|
|
self::assertArraySubset( |
|
|
|
|
628
|
|
|
['kind' => Token::AT, 'start' => 0, 'end' => 1, 'value' => null], |
629
|
|
|
(array) $this->lexOne('@') |
630
|
|
|
); |
631
|
|
|
self::assertArraySubset( |
|
|
|
|
632
|
|
|
['kind' => Token::BRACKET_L, 'start' => 0, 'end' => 1, 'value' => null], |
633
|
|
|
(array) $this->lexOne('[') |
634
|
|
|
); |
635
|
|
|
self::assertArraySubset( |
|
|
|
|
636
|
|
|
['kind' => Token::BRACKET_R, 'start' => 0, 'end' => 1, 'value' => null], |
637
|
|
|
(array) $this->lexOne(']') |
638
|
|
|
); |
639
|
|
|
self::assertArraySubset( |
|
|
|
|
640
|
|
|
['kind' => Token::BRACE_L, 'start' => 0, 'end' => 1, 'value' => null], |
641
|
|
|
(array) $this->lexOne('{') |
642
|
|
|
); |
643
|
|
|
self::assertArraySubset( |
|
|
|
|
644
|
|
|
['kind' => Token::PIPE, 'start' => 0, 'end' => 1, 'value' => null], |
645
|
|
|
(array) $this->lexOne('|') |
646
|
|
|
); |
647
|
|
|
self::assertArraySubset( |
|
|
|
|
648
|
|
|
['kind' => Token::BRACE_R, 'start' => 0, 'end' => 1, 'value' => null], |
649
|
|
|
(array) $this->lexOne('}') |
650
|
|
|
); |
651
|
|
|
} |
652
|
|
|
|
653
|
|
|
public function reportsUsefulUnknownCharErrors() |
654
|
|
|
{ |
655
|
|
|
$unicode1 = json_decode('"\u203B"'); |
656
|
|
|
$unicode2 = json_decode('"\u200b"'); |
657
|
|
|
|
658
|
|
|
return [ |
659
|
|
|
['..', 'Cannot parse the unexpected character ".".', $this->loc(1, 1)], |
660
|
|
|
['?', 'Cannot parse the unexpected character "?".', $this->loc(1, 1)], |
661
|
|
|
[$unicode1, "Cannot parse the unexpected character \"\\u203b\".", $this->loc(1, 1)], |
662
|
|
|
[$unicode2, "Cannot parse the unexpected character \"\\u200b\".", $this->loc(1, 1)], |
663
|
|
|
]; |
664
|
|
|
} |
665
|
|
|
|
666
|
|
|
/** |
667
|
|
|
* @see it('lex reports useful unknown character error') |
668
|
|
|
* |
669
|
|
|
* @dataProvider reportsUsefulUnknownCharErrors |
670
|
|
|
*/ |
671
|
|
|
public function testReportsUsefulUnknownCharErrors($str, $expectedMessage, $location) : void |
672
|
|
|
{ |
673
|
|
|
$this->expectSyntaxError($str, $expectedMessage, $location); |
674
|
|
|
} |
675
|
|
|
|
676
|
|
|
/** |
677
|
|
|
* @see it('lex reports useful information for dashes in names') |
678
|
|
|
*/ |
679
|
|
|
public function testReportsUsefulDashesInfo() : void |
680
|
|
|
{ |
681
|
|
|
$q = 'a-b'; |
682
|
|
|
$lexer = new Lexer(new Source($q)); |
683
|
|
|
self::assertArraySubset( |
|
|
|
|
684
|
|
|
['kind' => Token::NAME, 'start' => 0, 'end' => 1, 'value' => 'a'], |
685
|
|
|
(array) $lexer->advance() |
686
|
|
|
); |
687
|
|
|
|
688
|
|
|
$this->expectException(SyntaxError::class); |
689
|
|
|
$this->expectExceptionMessage('Syntax Error: Invalid number, expected digit but got: "b"'); |
690
|
|
|
try { |
691
|
|
|
$lexer->advance(); |
692
|
|
|
self::fail('Expected exception not thrown'); |
693
|
|
|
} catch (SyntaxError $error) { |
694
|
|
|
self::assertEquals([$this->loc(1, 3)], $error->getLocations()); |
695
|
|
|
throw $error; |
696
|
|
|
} |
697
|
|
|
} |
698
|
|
|
|
699
|
|
|
/** |
700
|
|
|
* @see it('produces double linked list of tokens, including comments') |
701
|
|
|
*/ |
702
|
|
|
public function testDoubleLinkedList() : void |
703
|
|
|
{ |
704
|
|
|
$lexer = new Lexer(new Source('{ |
705
|
|
|
#comment |
706
|
|
|
field |
707
|
|
|
}')); |
708
|
|
|
|
709
|
|
|
$startToken = $lexer->token; |
710
|
|
|
do { |
711
|
|
|
$endToken = $lexer->advance(); |
712
|
|
|
// Lexer advances over ignored comment tokens to make writing parsers |
713
|
|
|
// easier, but will include them in the linked list result. |
714
|
|
|
self::assertNotEquals('Comment', $endToken->kind); |
715
|
|
|
} while ($endToken->kind !== '<EOF>'); |
716
|
|
|
|
717
|
|
|
self::assertEquals(null, $startToken->prev); |
718
|
|
|
self::assertEquals(null, $endToken->next); |
719
|
|
|
|
720
|
|
|
$tokens = []; |
721
|
|
|
for ($tok = $startToken; $tok; $tok = $tok->next) { |
722
|
|
|
if (! empty($tokens)) { |
723
|
|
|
// Tokens are double-linked, prev should point to last seen token. |
724
|
|
|
self::assertSame($tokens[count($tokens) - 1], $tok->prev); |
725
|
|
|
} |
726
|
|
|
$tokens[] = $tok; |
727
|
|
|
} |
728
|
|
|
|
729
|
|
|
self::assertEquals( |
730
|
|
|
[ |
731
|
|
|
'<SOF>', |
732
|
|
|
'{', |
733
|
|
|
'Comment', |
734
|
|
|
'Name', |
735
|
|
|
'}', |
736
|
|
|
'<EOF>', |
737
|
|
|
], |
738
|
|
|
Utils::map( |
739
|
|
|
$tokens, |
740
|
|
|
static function ($tok) { |
741
|
|
|
return $tok->kind; |
742
|
|
|
} |
743
|
|
|
) |
744
|
|
|
); |
745
|
|
|
} |
746
|
|
|
} |
747
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.