Passed
Push — master ( f3670f...6349c7 )
by Christoffer
02:45
created

TokenReader::setLexer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
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
     */
37
    public function read(
38
      int $code,
39
      int $pos,
40
      int $line,
41
      int $col,
42
      Token $prev
43
    ): Token {
44
        switch ($code) {
45
            case 33: // !
46
                return $this->readBang($code, $pos, $line, $col, $prev);
47
            case 35: // #
48
                return $this->readComment($code, $pos, $line, $col, $prev);
49
            case 36: // $
50
                return $this->readDollar($code, $pos, $line, $col, $prev);
51
            case 38: // &
52
                return $this->readAmp($code, $pos, $line, $col, $prev);
53
            case 58: // :
54
                return $this->readColon($code, $pos, $line, $col, $prev);
55
            case 61: // =
56
                return $this->readEquals($code, $pos, $line, $col, $prev);
57
            case 64: // @
58
                return $this->readAt($code, $pos, $line, $col, $prev);
59
            case 124: // |
60
                return $this->readPipe($code, $pos, $line, $col, $prev);
61
            case 40:
62
            case 41: // ( or )~
63
                return $this->readParenthesis($code, $pos, $line, $col, $prev);
64
            case 91:
65
            case 93: // [ or ]
66
                return $this->readBracket($code, $pos, $line, $col, $prev);
67
            case 123:
68
            case 125: // { or }
69
                return $this->readBrace($code, $pos, $line, $col, $prev);
70
        }
71
72
        // Int:   -?(0|[1-9][0-9]*)
73
        // Float: -?(0|[1-9][0-9]*)(\.[0-9]+)?((E|e)(+|-)?[0-9]+)?
74
        if (isNumber($code)) {
75
            return $this->readNumber($code, $pos, $line, $col, $prev);
76
        }
77
78
        if ($this->isLetter($code) || $this->isUnderscore($code)) {
79
            return $this->readName($code, $pos, $line, $col, $prev);
80
        }
81
82
        $body = $this->lexer->getBody();
83
84
        // Spread: ...
85
        if ($code === 46 && charCodeAt($body,
86
            $pos + 1) === 46 && charCodeAt($body, $pos + 2) === 46) {
87
            return $this->readSpread($code, $pos, $line, $col, $prev);
88
        }
89
90
        // String: "([^"\\\u000A\u000D]|(\\(u[0-9a-fA-F]{4}|["\\/bfnrt])))*"
91
        if ($code === 34 && charCodeAt($body, $pos + 1) !== 34) {
92
            return $this->readString($code, $pos, $line, $col, $prev);
93
        }
94
95
        // Block String: """("?"?(\\"""|\\(?!=""")|[^"\\]))*"""
96
        if ($this->isTripleQuote($body, $code, $pos)) {
97
            return $this->readBlockString($code, $pos, $line, $col, $prev);
98
        }
0 ignored issues
show
Bug Best Practice introduced by
The function implicitly returns null when the if condition on line 96 is false. This is incompatible with the type-hinted return Digia\GraphQL\Language\Token. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
99
    }
100
101
    /**
102
     * @param int $code
103
     * @param int $pos
104
     * @param int $line
105
     * @param int $col
106
     * @param Token $prev
107
     * @return Token
108
     */
109
    protected function readName(
110
      int $code,
0 ignored issues
show
Unused Code introduced by
The parameter $code is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

110
      /** @scrutinizer ignore-unused */ int $code,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
111
      int $pos,
112
      int $line,
113
      int $col,
114
      Token $prev
115
    ): Token {
116
        $body = $this->lexer->getBody();
117
        $bodyLength = mb_strlen($body);
118
        $start = $pos;
119
        $pos = $start + 1;
120
121
        while ($pos !== $bodyLength && ($code = charCodeAt($body,
122
            $pos)) !== null && $this->isAlphaNumeric($code)) {
123
            ++$pos;
124
        }
125
126
        return new Token(TokenKindEnum::NAME, $start, $pos, $line, $col, $prev,
127
          sliceString($body, $start, $pos));
128
    }
129
130
    /**
131
     * @param int $code
132
     * @param int $pos
133
     * @param int $line
134
     * @param int $col
135
     * @param Token $prev
136
     * @return Token
137
     * @throws SyntaxErrorException
138
     */
139
    protected function readBlockString(
140
      int $code,
0 ignored issues
show
Unused Code introduced by
The parameter $code is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

140
      /** @scrutinizer ignore-unused */ int $code,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
141
      int $pos,
142
      int $line,
143
      int $col,
144
      Token $prev
145
    ): Token {
146
        $body = $this->lexer->getBody();
147
        $bodyLength = mb_strlen($body);
148
        $start = $pos;
149
        $pos = $start + 3;
150
        $chunkStart = $pos;
151
        $rawValue = '';
152
153
        while ($pos < $bodyLength && ($code = charCodeAt($body,
154
            $pos)) !== null) {
155
            // Closing Triple-Quote (""")
156
            if ($this->isTripleQuote($body, $code, $pos)) {
157
                $rawValue .= sliceString($body, $chunkStart, $pos);
158
159
                return new Token(
160
                  TokenKindEnum::BLOCK_STRING,
161
                  $start,
162
                  $pos + 3,
163
                  $line,
164
                  $col,
165
                  $prev,
166
                  blockStringValue($rawValue)
167
                );
168
            }
169
170
            if (isSourceCharacter($code)) {
171
                throw new SyntaxErrorException(
172
                  $this->lexer->getSource(),
173
                  $pos,
174
                  sprintf('Invalid character within String: %s',
175
                    printCharCode($code))
176
                );
177
            }
178
179
            if ($this->isEscapedTripleQuote($body, $code, $pos)) {
180
                $rawValue .= sliceString($body, $chunkStart, $pos) . '"""';
181
                $pos += 4;
182
                $chunkStart = $pos;
183
            } else {
184
                ++$pos;
185
            }
186
        }
187
188
        throw new SyntaxErrorException($this->lexer->getSource(), $pos,
189
          'Unterminated string.');
190
    }
191
192
    /**
193
     * @param int $code
194
     * @param int $pos
195
     * @param int $line
196
     * @param int $col
197
     * @param Token $prev
198
     * @return Token
199
     * @throws SyntaxErrorException
200
     */
201
    protected function readNumber(
202
      int $code,
203
      int $pos,
204
      int $line,
205
      int $col,
206
      Token $prev
207
    ): Token {
208
        $body = $this->lexer->getBody();
209
        $start = $pos;
210
        $isFloat = false;
211
212
        if ($code === 45) {
213
            // -
214
            $code = charCodeAt($body, ++$pos);
215
        }
216
217
        if ($code === 48) {
218
            // 0
219
            $code = charCodeAt($body, ++$pos);
220
            if (isNumber($code)) {
221
                throw new SyntaxErrorException(
222
                  $this->lexer->getSource(),
223
                  $pos,
224
                  sprintf('Invalid number, unexpected digit after 0: %s.',
225
                    printCharCode($code))
226
                );
227
            }
228
        } else {
229
            $pos = $this->readDigits($code, $pos);
230
            $code = charCodeAt($body, $pos);
231
        }
232
233
        if ($code === 46) {
234
            // .
235
            $isFloat = true;
236
            $code = charCodeAt($body, ++$pos);
237
            $pos = $this->readDigits($code, $pos);
238
            $code = charCodeAt($body, $pos);
239
        }
240
241
        if ($code === 69 || $code === 101) {
242
            // e or E
243
            $isFloat = true;
244
            $code = charCodeAt($body, ++$pos);
245
246
            if ($code === 43 || $code === 45) {
247
                // + or -
248
                $code = charCodeAt($body, ++$pos);
249
            }
250
251
            $pos = $this->readDigits($code, $pos);
252
        }
253
254
        return new Token(
255
          $isFloat ? TokenKindEnum::FLOAT : TokenKindEnum::INT,
256
          $start,
257
          $pos,
258
          $line,
259
          $col,
260
          $prev,
261
          sliceString($body, $start, $pos)
262
        );
263
    }
264
265
    /**
266
     * @param int $code
267
     * @param int $pos
268
     * @param int $line
269
     * @param int $col
270
     * @param Token $prev
271
     * @return Token
272
     * @throws SyntaxErrorException
273
     */
274
    protected function readString(
275
      int $code,
0 ignored issues
show
Unused Code introduced by
The parameter $code is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

275
      /** @scrutinizer ignore-unused */ int $code,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
276
      int $pos,
277
      int $line,
278
      int $col,
279
      Token $prev
280
    ): Token {
281
        $body = $this->lexer->getBody();
282
        $bodyLength = mb_strlen($body);
283
        $start = $pos;
284
        $pos = $start + 1;
285
        $chunkStart = $pos;
286
        $value = '';
287
288
        while ($pos < $bodyLength && ($code = charCodeAt($body,
289
            $pos)) !== null && !$this->isLineTerminator($code)) {
290
            // Closing Quote (")
291
            if ($code === 34) {
292
                $value .= sliceString($body, $chunkStart, $pos);
293
294
                return new Token(TokenKindEnum::STRING, $start, $pos + 1, $line,
295
                  $col, $prev, $value);
296
            }
297
298
            if ($this->isSourceCharacter($code)) {
299
                throw new SyntaxErrorException(
300
                  $this->lexer->getSource(),
301
                  $pos,
302
                  sprintf('Invalid character within String: %s',
303
                    printCharCode($code))
304
                );
305
            }
306
307
            ++$pos;
308
309
            if ($code === 92) {
310
                // \
311
                $value .= sliceString($body, $chunkStart, $pos + 1);
312
                $code = charCodeAt($body, $pos);
313
314
                switch ($code) {
315
                    case 34:
316
                        $value .= '"';
317
                        break;
318
                    case 47:
319
                        $value .= '/';
320
                        break;
321
                    case 92:
322
                        $value .= '\\';
323
                        break;
324
                    case 98:
325
                        $value .= '\b';
326
                        break;
327
                    case 102:
328
                        $value .= '\f';
329
                        break;
330
                    case 110:
331
                        $value .= '\n';
332
                        break;
333
                    case 114:
334
                        $value .= '\r';
335
                        break;
336
                    case 116:
337
                        $value .= '\t';
338
                        break;
339
                    case 117:
340
                        // u
341
                        $charCode = uniCharCode(
342
                          charCodeAt($body, $pos + 1),
343
                          charCodeAt($body, $pos + 2),
344
                          charCodeAt($body, $pos + 3),
345
                          charCodeAt($body, $pos + 4)
346
                        );
347
                        if ($charCode < 0) {
348
                            throw new SyntaxErrorException(
349
                              $this->lexer->getSource(),
350
                              $pos,
351
                              sprintf(
352
                                'Invalid character escape sequence: \\u%s',
353
                                sliceString($body, $pos + 1, $pos + 5)
354
                              )
355
                            );
356
                        }
357
                        $value .= chr($charCode);
0 ignored issues
show
Bug introduced by
$charCode of type string is incompatible with the type integer expected by parameter $ascii of chr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

357
                        $value .= chr(/** @scrutinizer ignore-type */ $charCode);
Loading history...
358
                        $pos += 4;
359
                        break;
360
                    default:
361
                        throw new SyntaxErrorException(
362
                          $this->lexer->getSource(),
363
                          $pos,
364
                          sprintf('Invalid character escape sequence: \\%s',
365
                            chr($code))
366
                        );
367
                }
368
369
                ++$pos;
370
                $chunkStart = $pos;
371
            }
372
        }
373
374
        throw new SyntaxErrorException($this->lexer->getSource(), $pos,
375
          'Unterminated string.');
376
    }
377
378
    /**
379
     * @param int $code
380
     * @param int $pos
381
     * @param int $line
382
     * @param int $col
383
     * @param Token $prev
384
     * @return Token
385
     */
386
    protected function readSpread(
387
      int $code,
0 ignored issues
show
Unused Code introduced by
The parameter $code is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

387
      /** @scrutinizer ignore-unused */ int $code,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
388
      int $pos,
389
      int $line,
390
      int $col,
391
      Token $prev
392
    ): Token {
393
        return new Token(TokenKindEnum::SPREAD, $pos, $pos + 3, $line, $col,
394
          $prev);
395
    }
396
397
    /**
398
     * @param int $code
399
     * @param int $pos
400
     * @param int $line
401
     * @param int $col
402
     * @param Token $prev
403
     * @return Token
404
     */
405
    protected function readDollar(
406
      int $code,
0 ignored issues
show
Unused Code introduced by
The parameter $code is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

406
      /** @scrutinizer ignore-unused */ int $code,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
407
      int $pos,
408
      int $line,
409
      int $col,
410
      Token $prev
411
    ): Token {
412
        return new Token(TokenKindEnum::DOLLAR, $pos, $pos + 1, $line, $col,
413
          $prev);
414
    }
415
416
    /**
417
     * @param int $code
418
     * @param int $pos
419
     * @param int $line
420
     * @param int $col
421
     * @param Token $prev
422
     * @return Token
423
     */
424
    protected function readPipe(
425
      int $code,
0 ignored issues
show
Unused Code introduced by
The parameter $code is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

425
      /** @scrutinizer ignore-unused */ int $code,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
426
      int $pos,
427
      int $line,
428
      int $col,
429
      Token $prev
430
    ): Token {
431
        return new Token(TokenKindEnum::PIPE, $pos, $pos + 1, $line, $col,
432
          $prev);
433
    }
434
435
    /**
436
     * @param int $code
437
     * @param int $pos
438
     * @param int $line
439
     * @param int $col
440
     * @param Token $prev
441
     * @return Token
442
     */
443
    protected function readParenthesis(
444
      int $code,
445
      int $pos,
446
      int $line,
447
      int $col,
448
      Token $prev
449
    ): Token {
450
        return $code === 40
451
          ? new Token(TokenKindEnum::PAREN_L, $pos, $pos + 1, $line, $col,
452
            $prev)
453
          : new Token(TokenKindEnum::PAREN_R, $pos, $pos + 1, $line, $col,
454
            $prev);
455
    }
456
457
    /**
458
     * @param int $code
459
     * @param int $pos
460
     * @param int $line
461
     * @param int $col
462
     * @param Token $prev
463
     * @return Token
464
     */
465
    protected function readEquals(
466
      int $code,
0 ignored issues
show
Unused Code introduced by
The parameter $code is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

466
      /** @scrutinizer ignore-unused */ int $code,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
467
      int $pos,
468
      int $line,
469
      int $col,
470
      Token $prev
471
    ): Token {
472
        return new Token(TokenKindEnum::EQUALS, $pos, $pos + 1, $line, $col,
473
          $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 readAt(
485
      int $code,
0 ignored issues
show
Unused Code introduced by
The parameter $code is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

485
      /** @scrutinizer ignore-unused */ int $code,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
486
      int $pos,
487
      int $line,
488
      int $col,
489
      Token $prev
490
    ): Token {
491
        return new Token(TokenKindEnum::AT, $pos, $pos + 1, $line, $col, $prev);
492
    }
493
494
    /**
495
     * @param int $code
496
     * @param int $pos
497
     * @param int $line
498
     * @param int $col
499
     * @param Token $prev
500
     * @return Token
501
     */
502
    protected function readComment(
503
      int $code,
0 ignored issues
show
Unused Code introduced by
The parameter $code is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

503
      /** @scrutinizer ignore-unused */ int $code,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
504
      int $pos,
505
      int $line,
506
      int $col,
507
      Token $prev
508
    ): Token {
509
        $body = $this->lexer->getBody();
510
        $start = $pos;
511
512
        do {
513
            $code = charCodeAt($body, ++$pos);
514
        } while ($code !== null && ($code > 0x001f || $code === 0x0009)); // SourceCharacter but not LineTerminator
515
516
        return new Token(
517
          TokenKindEnum::COMMENT,
518
          $start,
519
          $pos,
520
          $line,
521
          $col,
522
          $prev,
523
          sliceString($body, $start + 1, $pos)
524
        );
525
    }
526
527
    /**
528
     * @param int $code
529
     * @param int $pos
530
     * @param int $line
531
     * @param int $col
532
     * @param Token $prev
533
     * @return Token
534
     */
535
    protected function readColon(
536
      int $code,
0 ignored issues
show
Unused Code introduced by
The parameter $code is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

536
      /** @scrutinizer ignore-unused */ int $code,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
537
      int $pos,
538
      int $line,
539
      int $col,
540
      Token $prev
541
    ): Token {
542
        return new Token(TokenKindEnum::COLON, $pos, $pos + 1, $line, $col,
543
          $prev);
544
    }
545
546
    /**
547
     * @param int $code
548
     * @param int $pos
549
     * @param int $line
550
     * @param int $col
551
     * @param Token $prev
552
     * @return Token
553
     */
554
    protected function readAmp(
555
      int $code,
0 ignored issues
show
Unused Code introduced by
The parameter $code is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

555
      /** @scrutinizer ignore-unused */ int $code,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
556
      int $pos,
557
      int $line,
558
      int $col,
559
      Token $prev
560
    ): Token {
561
        return new Token(TokenKindEnum::AMP, $pos, $pos + 1, $line, $col,
562
          $prev);
563
    }
564
565
    /**
566
     * @param int $code
567
     * @param int $pos
568
     * @param int $line
569
     * @param int $col
570
     * @param Token $prev
571
     * @return Token
572
     */
573
    protected function readBang(
574
      int $code,
0 ignored issues
show
Unused Code introduced by
The parameter $code is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

574
      /** @scrutinizer ignore-unused */ int $code,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
575
      int $pos,
576
      int $line,
577
      int $col,
578
      Token $prev
579
    ): Token {
580
        return new Token(TokenKindEnum::BANG, $pos, $pos + 1, $line, $col,
581
          $prev);
582
    }
583
584
    /**
585
     * @param int $code
586
     * @param int $pos
587
     * @param int $line
588
     * @param int $col
589
     * @param Token $prev
590
     * @return Token
591
     */
592
    protected function readBrace(
593
      int $code,
594
      int $pos,
595
      int $line,
596
      int $col,
597
      Token $prev
598
    ): Token {
599
        return $code === 123
600
          ? new Token(TokenKindEnum::BRACE_L, $pos, $pos + 1, $line, $col,
601
            $prev)
602
          : new Token(TokenKindEnum::BRACE_R, $pos, $pos + 1, $line, $col,
603
            $prev);
604
    }
605
606
    /**
607
     * @param int $code
608
     * @param int $pos
609
     * @param int $line
610
     * @param int $col
611
     * @param Token $prev
612
     * @return Token
613
     */
614
    protected function readBracket(
615
      int $code,
616
      int $pos,
617
      int $line,
618
      int $col,
619
      Token $prev
620
    ): Token {
621
        return $code === 91
622
          ? new Token(TokenKindEnum::BRACKET_L, $pos, $pos + 1, $line, $col,
623
            $prev)
624
          : new Token(TokenKindEnum::BRACKET_R, $pos, $pos + 1, $line, $col,
625
            $prev);
626
    }
627
628
    /**
629
     * @param int $code
630
     * @param int $pos
631
     * @return int
632
     * @throws SyntaxErrorException
633
     */
634
    protected function readDigits(int $code, int $pos): int
635
    {
636
        $body = $this->lexer->getBody();
637
638
        if (isNumber($code)) {
639
            do {
640
                $code = charCodeAt($body, ++$pos);
641
            } while (isNumber($code));
642
643
            return $pos;
644
        }
645
646
        throw new SyntaxErrorException(
647
          $this->lexer->getSource(),
648
          $pos,
649
          sprintf('Invalid number, expected digit but got: %s',
650
            printCharCode($code))
651
        );
652
    }
653
654
    /**
655
     * @TODO Move to utils.
656
     *
657
     * @param int $code
658
     * @return bool
659
     */
660
    protected function isLineTerminator(int $code): bool
661
    {
662
        return $code === 0x000a || $code === 0x000d;
663
    }
664
665
    /**
666
     * @TODO Move to utils.
667
     *
668
     * @param int $code
669
     * @return bool
670
     */
671
    protected function isSourceCharacter(int $code): bool
672
    {
673
        return $code < 0x0020 && $code !== 0x0009;
674
    }
675
676
    /**
677
     * @TODO Move to utils.
678
     *
679
     * @param string $body
680
     * @param int $code
681
     * @param int $pos
682
     * @return bool
683
     */
684
    protected function isTripleQuote(string $body, int $code, int $pos): bool
685
    {
686
        return $code === 34 && charCodeAt($body,
687
            $pos + 1) === 34 && charCodeAt($body, $pos + 2) === 34; // """
688
    }
689
690
    /**
691
     * @TODO Move to utils.
692
     *
693
     * @param string $body
694
     * @param int $code
695
     * @param int $pos
696
     * @return bool
697
     */
698
    protected function isEscapedTripleQuote(
699
      string $body,
700
      int $code,
701
      int $pos
702
    ): bool {
703
        return $code === 92 &&
704
          charCodeAt($body, $pos + 1) === 34 &&
705
          charCodeAt($body, $pos + 2) === 34 &&
706
          charCodeAt($body, $pos + 3) === 34;
707
    }
708
709
    /**
710
     * @TODO Move to utils.
711
     *
712
     * @param int $code
713
     * @return bool
714
     */
715
    protected function isLetter(int $code): bool
716
    {
717
        return ($code >= 65 && $code <= 90) || ($code >= 97 && $code <= 122); // a-z or A-Z
718
    }
719
720
    /**
721
     * @TODO Move to utils.
722
     *
723
     * @param int $code
724
     * @return bool
725
     */
726
    protected function isNumber(int $code): bool
727
    {
728
        return $code >= 48 && $code <= 57; // 0-9
729
    }
730
731
    /**
732
     * @TODO Move to utils.
733
     *
734
     * @param int $code
735
     * @return bool
736
     */
737
    protected function isUnderscore(int $code): bool
738
    {
739
        return $code === 95; // _
740
    }
741
742
    /**
743
     * @TODO Move to utils.
744
     *
745
     * @param int $code
746
     * @return bool
747
     */
748
    protected function isAlphaNumeric(int $code): bool
749
    {
750
        return $this->isLetter($code) || $this->isNumber($code) || $this->isUnderscore($code);
751
    }
752
}
753