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

TokenReader::readBlockString()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 42
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

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