Passed
Pull Request — master (#184)
by Christoffer
04:17 queued 01:55
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
                        $charCode      = uniCharCode(
305
                            charCodeAt($body, $pos + 1),
306
                            charCodeAt($body, $pos + 2),
307
                            charCodeAt($body, $pos + 3),
308
                            charCodeAt($body, $pos + 4)
309
                        );
310
                        if ($charCode < 0) {
311
                            throw new SyntaxErrorException(
312
                                $this->lexer->getSource(),
313
                                $pos,
314
                                \sprintf('Invalid character escape sequence: %s.', $unicodeString)
315
                            );
316
                        }
317
                        $value .= $unicodeString;
318
                        $pos   += 4;
319
                        break;
320
                    default:
321
                        throw new SyntaxErrorException(
322
                            $this->lexer->getSource(),
323
                            $pos,
324
                            \sprintf('Invalid character escape sequence: \\%s.', \chr($code))
325
                        );
326
                }
327
328
                ++$pos;
329
330
                $chunkStart = $pos;
331
            }
332
        }
333
334
        throw new SyntaxErrorException($this->lexer->getSource(), $pos, 'Unterminated string.');
335
    }
336
337
    /**
338
     * @param int   $pos
339
     * @param int   $line
340
     * @param int   $col
341
     * @param Token $prev
342
     * @return Token
343
     */
344
    protected function readSpread(int $pos, int $line, int $col, Token $prev): Token
345
    {
346
        return new Token(TokenKindEnum::SPREAD, $pos, $pos + 3, $line, $col, $prev);
347
    }
348
349
    /**
350
     * @param int   $pos
351
     * @param int   $line
352
     * @param int   $col
353
     * @param Token $prev
354
     * @return Token
355
     */
356
    protected function readDollar(int $pos, int $line, int $col, Token $prev): Token
357
    {
358
        return new Token(TokenKindEnum::DOLLAR, $pos, $pos + 1, $line, $col, $prev);
359
    }
360
361
    /**
362
     * @param int   $pos
363
     * @param int   $line
364
     * @param int   $col
365
     * @param Token $prev
366
     * @return Token
367
     */
368
    protected function readPipe(int $pos, int $line, int $col, Token $prev): Token
369
    {
370
        return new Token(TokenKindEnum::PIPE, $pos, $pos + 1, $line, $col, $prev);
371
    }
372
373
    /**
374
     * @param int   $code
375
     * @param int   $pos
376
     * @param int   $line
377
     * @param int   $col
378
     * @param Token $prev
379
     * @return Token
380
     */
381
    protected function readParenthesis(int $code, int $pos, int $line, int $col, Token $prev): Token
382
    {
383
        return $code === 40
384
            ? new Token(TokenKindEnum::PAREN_L, $pos, $pos + 1, $line, $col, $prev)
385
            : new Token(TokenKindEnum::PAREN_R, $pos, $pos + 1, $line, $col, $prev);
386
    }
387
388
    /**
389
     * @param int   $pos
390
     * @param int   $line
391
     * @param int   $col
392
     * @param Token $prev
393
     * @return Token
394
     */
395
    protected function readEquals(int $pos, int $line, int $col, Token $prev): Token
396
    {
397
        return new Token(TokenKindEnum::EQUALS, $pos, $pos + 1, $line, $col, $prev);
398
    }
399
400
    /**
401
     * @param int   $pos
402
     * @param int   $line
403
     * @param int   $col
404
     * @param Token $prev
405
     * @return Token
406
     */
407
    protected function readAt(int $pos, int $line, int $col, Token $prev): Token
408
    {
409
        return new Token(TokenKindEnum::AT, $pos, $pos + 1, $line, $col, $prev);
410
    }
411
412
    /**
413
     * @param int   $pos
414
     * @param int   $line
415
     * @param int   $col
416
     * @param Token $prev
417
     * @return Token
418
     */
419
    protected function readComment(int $pos, int $line, int $col, Token $prev): Token
420
    {
421
        $body  = $this->lexer->getBody();
422
        $start = $pos;
423
424
        do {
425
            $code = charCodeAt($body, ++$pos);
426
        } while ($code !== null && ($code > 0x001f || $code === 0x0009)); // SourceCharacter but not LineTerminator
427
428
        return new Token(
429
            TokenKindEnum::COMMENT,
430
            $start,
431
            $pos,
432
            $line,
433
            $col,
434
            $prev,
435
            sliceString($body, $start + 1, $pos)
436
        );
437
    }
438
439
    /**
440
     * @param int   $pos
441
     * @param int   $line
442
     * @param int   $col
443
     * @param Token $prev
444
     * @return Token
445
     */
446
    protected function readColon(int $pos, int $line, int $col, Token $prev): Token
447
    {
448
        return new Token(TokenKindEnum::COLON, $pos, $pos + 1, $line, $col, $prev);
449
    }
450
451
    /**
452
     * @param int   $pos
453
     * @param int   $line
454
     * @param int   $col
455
     * @param Token $prev
456
     * @return Token
457
     */
458
    protected function readAmp(int $pos, int $line, int $col, Token $prev): Token
459
    {
460
        return new Token(TokenKindEnum::AMP, $pos, $pos + 1, $line, $col, $prev);
461
    }
462
463
    /**
464
     * @param int   $pos
465
     * @param int   $line
466
     * @param int   $col
467
     * @param Token $prev
468
     * @return Token
469
     */
470
    protected function readBang(int $pos, int $line, int $col, Token $prev): Token
471
    {
472
        return new Token(TokenKindEnum::BANG, $pos, $pos + 1, $line, $col, $prev);
473
    }
474
475
    /**
476
     * @param int   $code
477
     * @param int   $pos
478
     * @param int   $line
479
     * @param int   $col
480
     * @param Token $prev
481
     * @return Token
482
     */
483
    protected function readBrace(int $code, int $pos, int $line, int $col, Token $prev): Token
484
    {
485
        return $code === 123
486
            ? new Token(TokenKindEnum::BRACE_L, $pos, $pos + 1, $line, $col, $prev)
487
            : new Token(TokenKindEnum::BRACE_R, $pos, $pos + 1, $line, $col, $prev);
488
    }
489
490
    /**
491
     * @param int   $code
492
     * @param int   $pos
493
     * @param int   $line
494
     * @param int   $col
495
     * @param Token $prev
496
     * @return Token
497
     */
498
    protected function readBracket(int $code, int $pos, int $line, int $col, Token $prev): Token
499
    {
500
        return $code === 91
501
            ? new Token(TokenKindEnum::BRACKET_L, $pos, $pos + 1, $line, $col, $prev)
502
            : new Token(TokenKindEnum::BRACKET_R, $pos, $pos + 1, $line, $col, $prev);
503
    }
504
505
    /**
506
     * @param int $code
507
     * @param int $pos
508
     * @return int
509
     * @throws SyntaxErrorException
510
     */
511
    protected function readDigits(int $code, int $pos): int
512
    {
513
        $body = $this->lexer->getBody();
514
515
        if (isNumber($code)) {
516
            do {
517
                $code = charCodeAt($body, ++$pos);
518
            } while (isNumber($code));
519
520
            return $pos;
521
        }
522
523
        throw new SyntaxErrorException(
524
            $this->lexer->getSource(),
525
            $pos,
526
            sprintf('Invalid number, expected digit but got: %s.', printCharCode($code))
527
        );
528
    }
529
}
530