Passed
Pull Request — master (#184)
by Christoffer
02:16
created

TokenReader::readBlockString()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 40
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 40
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 27
nc 5
nop 6
1
<?php
2
3
namespace Digia\GraphQL\Language;
4
5
use Digia\GraphQL\Error\SyntaxErrorException;
6
7
class TokenReader implements TokenReaderInterface
8
{
9
10
    /**
11
     * The lexer owning this token reader.
12
     *
13
     * @var LexerInterface
14
     */
15
    protected $lexer;
16
17
    /**
18
     * @inheritdoc
19
     */
20
    public function setLexer(LexerInterface $lexer)
21
    {
22
        $this->lexer = $lexer;
23
        return $this;
24
    }
25
26
    /**
27
     * @inheritdoc
28
     */
29
    public function getLexer(): LexerInterface
30
    {
31
        return $this->lexer;
32
    }
33
34
    /**
35
     * @inheritdoc
36
     * @throws SyntaxErrorException
37
     */
38
    public function read(string $body, int $bodyLength, int $code, int $pos, int $line, int $col, Token $prev): ?Token
39
    {
40
        switch ($code) {
41
            case 33: // !
42
                return $this->readBang($pos, $line, $col, $prev);
43
            case 35: // #
44
                return $this->readComment($pos, $line, $col, $prev);
45
            case 36: // $
46
                return $this->readDollar($pos, $line, $col, $prev);
47
            case 38: // &
48
                return $this->readAmp($pos, $line, $col, $prev);
49
            case 58: // :
50
                return $this->readColon($pos, $line, $col, $prev);
51
            case 61: // =
52
                return $this->readEquals($pos, $line, $col, $prev);
53
            case 64: // @
54
                return $this->readAt($pos, $line, $col, $prev);
55
            case 124: // |
56
                return $this->readPipe($pos, $line, $col, $prev);
57
            case 40:
58
            case 41: // ( or )~
59
                return $this->readParenthesis($code, $pos, $line, $col, $prev);
60
            case 91:
61
            case 93: // [ or ]
62
                return $this->readBracket($code, $pos, $line, $col, $prev);
63
            case 123:
64
            case 125: // { or }
65
                return $this->readBrace($code, $pos, $line, $col, $prev);
66
        }
67
68
        // Int:   -?(0|[1-9][0-9]*)
69
        // Float: -?(0|[1-9][0-9]*)(\.[0-9]+)?((E|e)(+|-)?[0-9]+)?
70
        if ($code === 45 || isNumber($code)) {
71
            return $this->readNumber($code, $pos, $line, $col, $prev);
72
        }
73
74
        // Name: [_A-Za-z][_0-9A-Za-z]*
75
        if (isAlphaNumeric($code)) {
76
            return $this->readName($body, $bodyLength, $pos, $line, $col, $prev);
77
        }
78
79
        // Spread: ...
80
        if ($bodyLength >= 3 && isSpread($body, $code, $pos)) {
81
            return $this->readSpread($pos, $line, $col, $prev);
82
        }
83
84
        // String: "([^"\\\u000A\u000D]|(\\(u[0-9a-fA-F]{4}|["\\/bfnrt])))*"
85
        if (isString($body, $code, $pos)) {
86
            return $this->readString($body, $bodyLength, $pos, $line, $col, $prev);
87
        }
88
89
        // Block String: """("?"?(\\"""|\\(?!=""")|[^"\\]))*"""
90
        if ($bodyLength >= 3 && isTripleQuote($body, $code, $pos)) {
91
            return $this->readBlockString($body, $bodyLength, $pos, $line, $col, $prev);
92
        }
93
94
        return null;
95
    }
96
97
    /**
98
     * @param string $body
99
     * @param int    $bodyLength
100
     * @param int    $pos
101
     * @param int    $line
102
     * @param int    $col
103
     * @param Token  $prev
104
     * @return Token
105
     */
106
    protected function readName(string $body, int $bodyLength, int $pos, int $line, int $col, Token $prev): Token
107
    {
108
        $start = $pos;
109
        $pos   = $start + 1;
110
111
        while ($pos !== $bodyLength && ($code = charCodeAt($body, $pos)) !== null && isAlphaNumeric($code)) {
112
            ++$pos;
113
        }
114
115
        return new Token(TokenKindEnum::NAME, $start, $pos, $line, $col, $prev, sliceString($body, $start, $pos));
116
    }
117
118
    /**
119
     * @param string $body
120
     * @param int    $bodyLength
121
     * @param int    $pos
122
     * @param int    $line
123
     * @param int    $col
124
     * @param Token  $prev
125
     * @return Token
126
     * @throws SyntaxErrorException
127
     */
128
    protected function readBlockString(string $body, int $bodyLength, int $pos, int $line, int $col, Token $prev): Token
129
    {
130
        $start      = $pos;
131
        $pos        = $start + 3;
132
        $chunkStart = $pos;
133
        $rawValue   = '';
134
135
        while ($pos < $bodyLength && ($code = charCodeAt($body, $pos)) !== null) {
136
            // Closing Triple-Quote (""")
137
            if (isTripleQuote($body, $code, $pos)) {
138
                $rawValue .= sliceString($body, $chunkStart, $pos);
139
                return new Token(
140
                    TokenKindEnum::BLOCK_STRING,
141
                    $start,
142
                    $pos + 3,
143
                    $line,
144
                    $col,
145
                    $prev,
146
                    blockStringValue($rawValue)
147
                );
148
            }
149
150
            if (isSourceCharacter($code) && !isLineTerminator($code)) {
151
                throw new SyntaxErrorException(
152
                    $this->lexer->getSource(),
153
                    $pos,
154
                    \sprintf('Invalid character within String: %s.', printCharCode($code))
155
                );
156
            }
157
158
            if (isEscapedTripleQuote($body, $code, $pos)) {
159
                $rawValue   .= sliceString($body, $chunkStart, $pos) . '"""';
160
                $pos        += 4;
161
                $chunkStart = $pos;
162
            } else {
163
                ++$pos;
164
            }
165
        }
166
167
        throw new SyntaxErrorException($this->lexer->getSource(), $pos, 'Unterminated string.');
168
    }
169
170
    /**
171
     * @param int   $code
172
     * @param int   $pos
173
     * @param int   $line
174
     * @param int   $col
175
     * @param Token $prev
176
     * @return Token
177
     * @throws SyntaxErrorException
178
     */
179
    protected function readNumber(int $code, int $pos, int $line, int $col, Token $prev): Token
180
    {
181
        $body    = $this->lexer->getBody();
182
        $start   = $pos;
183
        $isFloat = false;
184
185
        if ($code === 45) {
186
            // -
187
            $code = charCodeAt($body, ++$pos);
188
        }
189
190
        if ($code === 48) {
191
            // 0
192
            $code = charCodeAt($body, ++$pos);
193
            if (isNumber($code)) {
194
                throw new SyntaxErrorException(
195
                    $this->lexer->getSource(),
196
                    $pos,
197
                    \sprintf('Invalid number, unexpected digit after 0: %s.', printCharCode($code))
198
                );
199
            }
200
        } else {
201
            $pos  = $this->readDigits($code, $pos);
202
            $code = charCodeAt($body, $pos);
203
        }
204
205
        if ($code === 46) {
206
            // .
207
            $isFloat = true;
208
            $code    = charCodeAt($body, ++$pos);
209
            $pos     = $this->readDigits($code, $pos);
210
            $code    = charCodeAt($body, $pos);
211
        }
212
213
        if ($code === 69 || $code === 101) {
214
            // e or E
215
            $isFloat = true;
216
            $code    = charCodeAt($body, ++$pos);
217
218
            if ($code === 43 || $code === 45) {
219
                // + or -
220
                $code = charCodeAt($body, ++$pos);
221
            }
222
223
            $pos = $this->readDigits($code, $pos);
224
        }
225
226
        return new Token(
227
            $isFloat ? TokenKindEnum::FLOAT : TokenKindEnum::INT,
228
            $start,
229
            $pos,
230
            $line,
231
            $col,
232
            $prev,
233
            sliceString($body, $start, $pos)
234
        );
235
    }
236
237
    /**
238
     * @param string $body
239
     * @param int    $bodyLength
240
     * @param int    $pos
241
     * @param int    $line
242
     * @param int    $col
243
     * @param Token  $prev
244
     * @return Token
245
     * @throws SyntaxErrorException
246
     */
247
    protected function readString(string $body, int $bodyLength, int $pos, int $line, int $col, Token $prev): Token
248
    {
249
        $start      = $pos;
250
        $pos        = $start + 1;
251
        $chunkStart = $pos;
252
        $value      = '';
253
254
        while ($pos < $bodyLength && ($code = charCodeAt($body, $pos)) !== null && !isLineTerminator($code)) {
255
            // Closing Quote (")
256
            if ($code === 34) {
257
                $value .= sliceString($body, $chunkStart, $pos);
258
                return new Token(TokenKindEnum::STRING, $start, $pos + 1, $line, $col, $prev, $value);
259
            }
260
261
            if (isSourceCharacter($code)) {
262
                throw new SyntaxErrorException(
263
                    $this->lexer->getSource(),
264
                    $pos,
265
                    \sprintf('Invalid character within String: %s.', printCharCode($code))
266
                );
267
            }
268
269
            ++$pos;
270
271
            if ($code === 92) {
272
                // \
273
                $value .= sliceString($body, $chunkStart, $pos - 1);
274
                $code  = charCodeAt($body, $pos);
275
276
                switch ($code) {
277
                    case 34:
278
                        $value .= '"';
279
                        break;
280
                    case 47:
281
                        $value .= '/';
282
                        break;
283
                    case 92:
284
                        $value .= '\\';
285
                        break;
286
                    case 98:
287
                        $value .= '\b';
288
                        break;
289
                    case 102:
290
                        $value .= '\f';
291
                        break;
292
                    case 110:
293
                        $value .= '\n';
294
                        break;
295
                    case 114:
296
                        $value .= '\r';
297
                        break;
298
                    case 116:
299
                        $value .= '\t';
300
                        break;
301
                    case 117:
302
                        // u
303
                        $unicodeString = sliceString($body, $pos + 1, $pos + 5);
304
305
                        if (!\preg_match('/[0-9A-Fa-f]{4}/', $unicodeString)) {
306
                            throw new SyntaxErrorException(
307
                                $this->lexer->getSource(),
308
                                $pos,
309
                                \sprintf('Invalid character escape sequence: \\u%s.', $unicodeString)
310
                            );
311
                        }
312
313
                        $value .= '\\u' . $unicodeString;
314
                        $pos   += 4;
315
                        break;
316
                    default:
317
                        throw new SyntaxErrorException(
318
                            $this->lexer->getSource(),
319
                            $pos,
320
                            \sprintf('Invalid character escape sequence: \\%s.', \chr($code))
321
                        );
322
                }
323
324
                ++$pos;
325
326
                $chunkStart = $pos;
327
            }
328
        }
329
330
        throw new SyntaxErrorException($this->lexer->getSource(), $pos, 'Unterminated string.');
331
    }
332
333
    /**
334
     * @param int   $pos
335
     * @param int   $line
336
     * @param int   $col
337
     * @param Token $prev
338
     * @return Token
339
     */
340
    protected function readSpread(int $pos, int $line, int $col, Token $prev): Token
341
    {
342
        return new Token(TokenKindEnum::SPREAD, $pos, $pos + 3, $line, $col, $prev);
343
    }
344
345
    /**
346
     * @param int   $pos
347
     * @param int   $line
348
     * @param int   $col
349
     * @param Token $prev
350
     * @return Token
351
     */
352
    protected function readDollar(int $pos, int $line, int $col, Token $prev): Token
353
    {
354
        return new Token(TokenKindEnum::DOLLAR, $pos, $pos + 1, $line, $col, $prev);
355
    }
356
357
    /**
358
     * @param int   $pos
359
     * @param int   $line
360
     * @param int   $col
361
     * @param Token $prev
362
     * @return Token
363
     */
364
    protected function readPipe(int $pos, int $line, int $col, Token $prev): Token
365
    {
366
        return new Token(TokenKindEnum::PIPE, $pos, $pos + 1, $line, $col, $prev);
367
    }
368
369
    /**
370
     * @param int   $code
371
     * @param int   $pos
372
     * @param int   $line
373
     * @param int   $col
374
     * @param Token $prev
375
     * @return Token
376
     */
377
    protected function readParenthesis(int $code, int $pos, int $line, int $col, Token $prev): Token
378
    {
379
        return $code === 40
380
            ? new Token(TokenKindEnum::PAREN_L, $pos, $pos + 1, $line, $col, $prev)
381
            : new Token(TokenKindEnum::PAREN_R, $pos, $pos + 1, $line, $col, $prev);
382
    }
383
384
    /**
385
     * @param int   $pos
386
     * @param int   $line
387
     * @param int   $col
388
     * @param Token $prev
389
     * @return Token
390
     */
391
    protected function readEquals(int $pos, int $line, int $col, Token $prev): Token
392
    {
393
        return new Token(TokenKindEnum::EQUALS, $pos, $pos + 1, $line, $col, $prev);
394
    }
395
396
    /**
397
     * @param int   $pos
398
     * @param int   $line
399
     * @param int   $col
400
     * @param Token $prev
401
     * @return Token
402
     */
403
    protected function readAt(int $pos, int $line, int $col, Token $prev): Token
404
    {
405
        return new Token(TokenKindEnum::AT, $pos, $pos + 1, $line, $col, $prev);
406
    }
407
408
    /**
409
     * @param int   $pos
410
     * @param int   $line
411
     * @param int   $col
412
     * @param Token $prev
413
     * @return Token
414
     */
415
    protected function readComment(int $pos, int $line, int $col, Token $prev): Token
416
    {
417
        $body  = $this->lexer->getBody();
418
        $start = $pos;
419
420
        do {
421
            $code = charCodeAt($body, ++$pos);
422
        } while ($code !== null && ($code > 0x001f || $code === 0x0009)); // SourceCharacter but not LineTerminator
423
424
        return new Token(
425
            TokenKindEnum::COMMENT,
426
            $start,
427
            $pos,
428
            $line,
429
            $col,
430
            $prev,
431
            sliceString($body, $start + 1, $pos)
432
        );
433
    }
434
435
    /**
436
     * @param int   $pos
437
     * @param int   $line
438
     * @param int   $col
439
     * @param Token $prev
440
     * @return Token
441
     */
442
    protected function readColon(int $pos, int $line, int $col, Token $prev): Token
443
    {
444
        return new Token(TokenKindEnum::COLON, $pos, $pos + 1, $line, $col, $prev);
445
    }
446
447
    /**
448
     * @param int   $pos
449
     * @param int   $line
450
     * @param int   $col
451
     * @param Token $prev
452
     * @return Token
453
     */
454
    protected function readAmp(int $pos, int $line, int $col, Token $prev): Token
455
    {
456
        return new Token(TokenKindEnum::AMP, $pos, $pos + 1, $line, $col, $prev);
457
    }
458
459
    /**
460
     * @param int   $pos
461
     * @param int   $line
462
     * @param int   $col
463
     * @param Token $prev
464
     * @return Token
465
     */
466
    protected function readBang(int $pos, int $line, int $col, Token $prev): Token
467
    {
468
        return new Token(TokenKindEnum::BANG, $pos, $pos + 1, $line, $col, $prev);
469
    }
470
471
    /**
472
     * @param int   $code
473
     * @param int   $pos
474
     * @param int   $line
475
     * @param int   $col
476
     * @param Token $prev
477
     * @return Token
478
     */
479
    protected function readBrace(int $code, int $pos, int $line, int $col, Token $prev): Token
480
    {
481
        return $code === 123
482
            ? new Token(TokenKindEnum::BRACE_L, $pos, $pos + 1, $line, $col, $prev)
483
            : new Token(TokenKindEnum::BRACE_R, $pos, $pos + 1, $line, $col, $prev);
484
    }
485
486
    /**
487
     * @param int   $code
488
     * @param int   $pos
489
     * @param int   $line
490
     * @param int   $col
491
     * @param Token $prev
492
     * @return Token
493
     */
494
    protected function readBracket(int $code, int $pos, int $line, int $col, Token $prev): Token
495
    {
496
        return $code === 91
497
            ? new Token(TokenKindEnum::BRACKET_L, $pos, $pos + 1, $line, $col, $prev)
498
            : new Token(TokenKindEnum::BRACKET_R, $pos, $pos + 1, $line, $col, $prev);
499
    }
500
501
    /**
502
     * @param int $code
503
     * @param int $pos
504
     * @return int
505
     * @throws SyntaxErrorException
506
     */
507
    protected function readDigits(int $code, int $pos): int
508
    {
509
        $body = $this->lexer->getBody();
510
511
        if (isNumber($code)) {
512
            do {
513
                $code = charCodeAt($body, ++$pos);
514
            } while (isNumber($code));
515
516
            return $pos;
517
        }
518
519
        throw new SyntaxErrorException(
520
            $this->lexer->getSource(),
521
            $pos,
522
            sprintf('Invalid number, expected digit but got: %s.', printCharCode($code))
523
        );
524
    }
525
}
526