GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( d274d7...b4c702 )
by
unknown
10:03 queued 17s
created

TokenStream   F

Complexity

Total Complexity 181

Size/Duplication

Total Lines 795
Duplicated Lines 0 %

Test Coverage

Coverage 99.44%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 379
dl 0
loc 795
rs 2
c 3
b 0
f 0
ccs 357
cts 359
cp 0.9944
wmc 181

30 Methods

Rating   Name   Duplication   Size   Complexity  
A getNumber() 0 11 3
A getQuotedIdentifier() 0 10 2
A getConditionalStart() 0 19 3
A getSpecialSymbol() 0 7 1
A getMultilineComment() 0 13 3
F getNextTokenRaw() 0 168 111
A getConditionEnd() 0 13 3
A getBin() 0 11 3
A getIdentifier() 0 6 1
A getComment() 0 16 3
A getHex() 0 26 5
A getSymbol() 0 10 2
A getString() 0 28 2
A nextTokenRaw() 0 25 3
A expect() 0 7 2
A expectNumber() 0 7 2
A nextToken() 0 10 3
A expectString() 0 7 2
A peek() 0 13 1
A expectOpenParen() 0 3 1
A contextualise() 0 43 4
A __construct() 0 2 1
A expectStringExtended() 0 12 4
A newFromText() 0 7 1
B consume() 0 36 8
A expectName() 0 7 2
A expectCloseParen() 0 3 1
A newFromFile() 0 11 2
A getMark() 0 5 1
A rewind() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like TokenStream often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TokenStream, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Graze\Morphism\Parse;
3
4
use LogicException;
5
use RuntimeException;
6
7
class TokenStream
8
{
9
    /** @var string */
10
    private $path;
11
    /** @var string */
12
    private $text;
13
    /** @var int */
14
    private $len;
15
    /** @var int */
16
    private $offset = 0;
17
    /** @var bool */
18
    private $inConditional = false;
19
    /** @var array */
20
    private $memo = [];
21
22
    /** @var array */
23
    private static $skipTokenTypes = [
24
        Token::CONDITIONAL_START => true,
25
        Token::CONDITIONAL_END   => true,
26
        Token::COMMENT           => true,
27
        Token::WHITESPACE        => true,
28
    ];
29
30 609
    private function __construct()
31
    {
32 609
    }
33
34
    // TODO - this is a hack that needs to be refactored
35
    // perhaps supply a LineStream interface that is satisfied
36
    // by a FileLineStream or ConnectionLineStream for example?
37
    /**
38
     * @param string $text
39
     * @param string $label
40
     * @return TokenStream
41
     */
42 1
    public static function newFromText($text, $label)
43
    {
44 1
        $stream = new self;
45 1
        $stream->path = $label;
46 1
        $stream->text = $text;
47 1
        $stream->len = strlen($text);
48 1
        return $stream;
49
    }
50
51
    /**
52
     * @param string $path
53
     * @return TokenStream
54
     */
55 609
    public static function newFromFile($path)
56
    {
57 609
        $text = @file_get_contents($path);
58 609
        if ($text === false) {
59 1
            throw new RuntimeException("$path: could not open file");
60
        }
61 608
        $stream = new self;
62 608
        $stream->path = $path;
63 608
        $stream->text = $text;
64 608
        $stream->len = strlen($text);
65 608
        return $stream;
66
    }
67
68
    /**
69
     * @return Token|mixed
70
     */
71 605
    private function nextTokenRaw()
72
    {
73 605
        $startOffset = $this->offset;
74 605
        if (isset($this->memo[$startOffset])) {
75 400
            $entry = $this->memo[$startOffset];
76 400
            $this->offset = $entry[0];
77 400
            $this->inConditional = $entry[1];
78 400
            return $entry[2];
79
        }
80
81 605
        if ($this->offset >= $this->len) {
82 353
            $token = new Token(Token::EOF);
83 353
            $this->offset = $this->len;
84
        } else {
85 602
            list($token, $offset) = $this->getNextTokenRaw($this->text, $this->offset);
86 590
            $this->offset = $offset;
87
        }
88
89 593
        $this->memo[$startOffset] = [
90 593
            $this->offset,
91 593
            $this->inConditional,
92 593
            $token,
93
        ];
94
95 593
        return $token;
96
    }
97
98
    /**
99
     * @param string $text
100
     * @param int $offset
101
     * @return array|null
102
     */
103 602
    private function getNextTokenRaw($text, $offset)
104
    {
105
        // currently unsupported:
106
        //
107
        //      charset prefixed strings, e.g. _utf8'wingwang'
108
        //      temporal literals, e.g. DATE'2014-07-08'
109
        //      the null literal, i.e. \N
110
111 602
        switch ($text[$offset]) {
112 602
            case " ":
113 602
            case "\n":
114 602
            case "\r":
115 602
            case "\t":
116 398
                $n = strspn($text, " \n\r\t", $offset);
117
                return [
118 398
                    new Token(Token::WHITESPACE, substr($text, $offset, $n)),
119 398
                    $offset + $n
120
                ];
121
122 602
            case '#':
123
                return
124 2
                    $this->getComment($text, $offset);
125
126 601
            case '.':
127 600
            case '+':
128
                return
129 5
                    $this->getNumber($text, $offset) ?:
130 5
                    $this->getSymbol($text, $offset);
131
132 597
            case '-':
133
                return
134 4
                    $this->getComment($text, $offset) ?:
135 2
                    $this->getNumber($text, $offset) ?:
136 4
                    $this->getSymbol($text, $offset);
137
138 595
            case '*':
139
                return
140 4
                    $this->getConditionEnd($text, $offset) ?:
141 3
                    $this->getSymbol($text, $offset);
142
143 593
            case '/':
144
                return
145 8
                    $this->getConditionalStart($text, $offset) ?:
146 4
                    $this->getMultilineComment($text, $offset) ?:
147 7
                    $this->getSymbol($text, $offset);
148
149 591
            case '0':
150
                // Handle hex if needed
151 22
                if (isset($text[$offset+1]) && $text[$offset+1] === 'x') {
152
                    return
153 4
                        $this->getHex($text, $offset) ?:
154 3
                        $this->getIdentifier($text, $offset);
155
                }
156
                // Handle non-hex leading zero.
157 586
            case '1':
158 567
            case '2':
159 567
            case '3':
160 567
            case '4':
161 566
            case '5':
162 566
            case '6':
163 566
            case '7':
164 566
            case '8':
165 566
            case '9':
166
                return
167 144
                    $this->getNumber($text, $offset) ?:
168 144
                    $this->getIdentifier($text, $offset);
169
170 566
            case '"':
171 561
            case "'":
172
                return
173 57
                    $this->getString($text, $offset);
174
175 553
            case '`':
176
                return
177 15
                    $this->getQuotedIdentifier($text, $offset);
178
179 547
            case 'B':
180 547
            case 'b':
181
                return
182 52
                    $this->getBin($text, $offset) ?:
183 52
                    $this->getIdentifier($text, $offset);
184
185 542
            case 'X':
186 541
            case 'x':
187
                return
188 298
                    $this->getHex($text, $offset) ?:
189 298
                    $this->getIdentifier($text, $offset);
190
191 528
            case '$':
192 526
            case '_':
193 523
            case 'A':
194 523
            case 'a':
195 511
            case 'C':
196 511
            case 'c':
197 492
            case 'D':
198 492
            case 'd':
199 472
            case 'E':
200 472
            case 'e':
201 472
            case 'F':
202 472
            case 'f':
203 461
            case 'G':
204 461
            case 'g':
205 461
            case 'H':
206 461
            case 'h':
207 461
            case 'I':
208 461
            case 'i':
209 445
            case 'J':
210 445
            case 'j':
211 444
            case 'K':
212 444
            case 'k':
213 442
            case 'L':
214 442
            case 'l':
215 436
            case 'M':
216 436
            case 'm':
217 431
            case 'N':
218 408
            case 'n':
219 403
            case 'O':
220 403
            case 'o':
221 403
            case 'P':
222 403
            case 'p':
223 399
            case 'Q':
224 399
            case 'q':
225 399
            case 'R':
226 399
            case 'r':
227 398
            case 'S':
228 398
            case 's':
229 390
            case 'T':
230 390
            case 't':
231 358
            case 'U':
232 357
            case 'u':
233 327
            case 'V':
234 327
            case 'v':
235 321
            case 'W':
236 321
            case 'w':
237 321
            case 'Y':
238 321
            case 'y':
239 311
            case 'Z':
240 311
            case 'z':
241
                return
242 500
                    $this->getIdentifier($text, $offset);
243
244 307
            case '!':
245 306
            case '%':
246 305
            case '&':
247 304
            case '(':
248 300
            case ')':
249 225
            case ',':
250 180
            case ':':
251 179
            case ';':
252 100
            case '<':
253 98
            case '=':
254 12
            case '>':
255 11
            case '@':
256 9
            case '^':
257 9
            case '|':
258 7
            case '~':
259
                return
260 300
                    $this->getSpecialSymbol($text, $offset);
261
262 7
            case '?':
263 6
            case '[':
264 5
            case '\\':
265 4
            case ']':
266 3
            case '{':
267 2
            case '}':
268
            default:
269 7
                $ch = $text[$offset];
270 7
                throw new LogicException("Lexer is confused by char '$ch' ord " . ord($ch));
271
        }
272
    }
273
274
    /**
275
     * @param string $text
276
     * @param int $offset
277
     * @return array
278
     */
279 15
    private function getQuotedIdentifier($text, $offset)
280
    {
281 15
        if (preg_match('/`((?:[^`]|``)*)`()/ms', $text, $pregMatch, PREG_OFFSET_CAPTURE, $offset)) {
282 14
            $token = Token::fromIdentifier($pregMatch[1][0]);
283
            return [
284 14
                $token,
285 14
                $pregMatch[2][1]
286
            ];
287
        }
288 1
        throw new RuntimeException("Unterminated identifier: $text");
289
    }
290
291
    /**
292
     * @param string $text
293
     * @param int $offset
294
     * @return array
295
     */
296 300
    private function getSpecialSymbol($text, $offset)
297
    {
298
        // TODO - should probably be a new token type 'variable' for @ and @@
299 300
        preg_match('/\A(?:<=|>=|<>|!=|:=|@@|&&|\|\||[=~!@%^&();:,<>|])()/xms', substr($text, $offset, 2), $pregMatch, PREG_OFFSET_CAPTURE);
300
        return [
301 300
            new Token(Token::SYMBOL, $pregMatch[0][0]),
302 300
            $offset + $pregMatch[1][1]
303
        ];
304
    }
305
306
    /**
307
     * Get the start of a conditional comment.
308
     *
309
     * https://dev.mysql.com/doc/refman/5.7/en/comments.html
310
     *
311
     * Does not support optimiser hints. See examples below.
312
     *
313
     * @param string $text
314
     * @param int $offset
315
     * @return array|null
316
     */
317 8
    private function getConditionalStart($text, $offset)
318
    {
319
        // Example conditional comments which can't be displayed in the docblock because they clash:
320
        // - /*! MySQL-specific code */ (execute the given code)
321
        // - /*!12345 MySQL-specific code */ (execute the given code only if the version matches)
322
        // Unsupported:
323
        // - SELECT /*+ BKA(t1) */ FROM ... ;
324
325
        if (// 10 comes from allowing for the /*! sequence, a MySQL version number, and a space
326 8
            preg_match('_\A/\*!([0-9]*)\s_ms', substr($text, $offset, 10)) &&
327 8
            preg_match('_/\*!([0-9]*)\s_ms', $text, $pregMatch, 0, $offset)
328
        ) {
329 4
            $this->inConditional = true;
330
            return [
331 4
                new Token(Token::CONDITIONAL_START, $pregMatch[1]),
332 4
                $offset + strlen($pregMatch[0])
333
            ];
334
        }
335 4
        return null;
336
    }
337
338
    /**
339
     * Get the end of a conditional comment. See _getConditionStart() for details.
340
     * @param string $text
341
     * @param int $offset
342
     * @return array|null
343
     */
344 4
    private function getConditionEnd($text, $offset)
345
    {
346 4
        if (substr($text, $offset, 2) === '*/') {
347 3
            if (!$this->inConditional) {
348 1
                throw new RuntimeException("Unexpected '*/'");
349
            }
350 2
            $this->inConditional = false;
351
            return [
352 2
                new Token(Token::CONDITIONAL_END),
353 2
                $offset + 2
354
            ];
355
        }
356 1
        return null;
357
    }
358
359
    /**
360
     * @param string $text
361
     * @param int $offset
362
     * @return array|null
363
     */
364 4
    private function getMultilineComment($text, $offset)
365
    {
366 4
        if (substr($text, $offset, 2) === '/*') {
367 3
            $pos = strpos($text, '*/', $offset);
368 3
            if ($pos !== false) {
369
                return [
370 2
                    new Token(Token::COMMENT, substr($text, $offset, $pos - $offset + 2)),
371 2
                    $pos + 2
372
                ];
373
            }
374 1
            throw new RuntimeException("Unterminated '/*'");
375
        }
376 1
        return null;
377
    }
378
379
    /**
380
     * @param string $text
381
     * @param int $offset
382
     * @return array|null
383
     */
384 6
    private function getComment($text, $offset)
385
    {
386 6
        if (preg_match('/\A(?:#|--\s)/ms', substr($text, $offset, 3))) {
387 4
            $pos = strpos($text, "\n", $offset);
388 4
            if ($pos !== false) {
389
                return [
390 3
                    new Token(Token::COMMENT, substr($text, $offset, $pos - $offset)),
391 3
                    $pos + 1
392
                ];
393
            }
394
            return [
395 1
                new Token(Token::COMMENT, $text),
396 1
                strlen($text)
397
            ];
398
        }
399 2
        return null;
400
    }
401
402
    /**
403
     * @param string $text
404
     * @param int $offset
405
     * @return array
406
     */
407 57
    private function getString($text, $offset)
408
    {
409 57
        $quote = $text[$offset];
410 57
        if (preg_match(
411 57
            '/' .
412 57
            $quote .
413 57
            '(' .
414 57
                '(?:' .
415 57
                    '[^\\\\' . $quote . ']' .   // not \ or "
416 57
                    '|\\\\.' .                  // escaped quotearacter
417 57
                    '|' .  $quote . $quote .    // ""
418 57
                ')*' .
419 57
            ')' .
420 57
            $quote .
421 57
            '/ms',
422
            $text,
423
            $pregMatch,
424 57
            0,
425
            $offset
426
        )
427
        ) {
428 56
            $token = Token::fromString($pregMatch[1], $quote);
429
            return [
430 56
                $token,
431 56
                $offset + strlen($pregMatch[0])
432
            ];
433
        }
434 1
        throw new RuntimeException("Unterminated string $quote...$quote");
435
    }
436
437
    /**
438
     * @param string $text
439
     * @param int $offset
440
     * @return array|null
441
     */
442 151
    private function getNumber($text, $offset)
443
    {
444 151
        if (preg_match('/\A[-+]?[.]?[0-9]/ms', substr($text, $offset, 3)) &&
445 151
            preg_match('/[-+]?(?:[0-9]+(?:[.][0-9]*)?|[.][0-9]+)(?:[eE][-+]?[0-9]+)?/ms', $text, $pregMatch, 0, $offset)
446
        ) {
447
            return [
448 148
                new Token(Token::NUMBER, $pregMatch[0]),
449 148
                $offset + strlen($pregMatch[0])
450
            ];
451
        }
452 3
        return null;
453
    }
454
455
    /**
456
     * Parse a hex string of the form "0x<hex digits>" or "x'<hex digits>'".
457
     *
458
     * https://dev.mysql.com/doc/refman/5.7/en/hexadecimal-literals.html
459
     *
460
     * - Only an even number of digits is valid.
461
     * - Case insensitive for hex digits.
462
     * - Case insensitive 'x' in quoted notation.
463
     * - Case sensitive 'x' for leading zero notation.
464
     *
465
     * Valid examples:
466
     * - x'BA5EBA11'
467
     * - x'decea5ed'
468
     * - X'5eed'
469
     * - 0xb01dface
470
     * - 0xBADC0DED
471
     *
472
     * Invalid examples
473
     * - x'00f' (odd number of digits)
474
     * - x'gg'  (invalid hex character)
475
     * - 0XFFFF (upper case 'x')
476
     *
477
     * @param string $text
478
     * @param int $offset
479
     * @return array|null
480
     */
481 302
    private function getHex($text, $offset)
482
    {
483 302
        $pregMatch = [];
484
485
        $matchesLeadingZeroNotation = function ($text, $offset, &$pregMatch) {
486
            return
487 302
                preg_match('/\A0x([0-9a-fA-F]*)/ms', $text, $pregMatch, 0, $offset);
488 302
        };
489
490
        $matchesXQuotedNotation = function ($text, $offset, &$pregMatch) {
491
            return
492 298
                preg_match('/\Ax\'[0-9a-f\']/ims', substr($text, $offset, 3)) &&
493 298
                preg_match('/x\'([0-9a-f]*)\'/ims', $text, $pregMatch, 0, $offset);
494 302
        };
495
496 302
        if ($matchesLeadingZeroNotation($text, $offset, $pregMatch) ||
497 302
            $matchesXQuotedNotation($text, $offset, $pregMatch)) {
498 11
            if (strlen($pregMatch[1]) % 2 != 0) {
499 1
                throw new RuntimeException("Invalid hex literal");
500
            }
501
            return [
502 10
                new Token(Token::HEX, $pregMatch[1]),
503 10
                $offset + strlen($pregMatch[0])
504
            ];
505
        }
506 292
        return null;
507
    }
508
509
    /**
510
     * @param string $text
511
     * @param int $offset
512
     * @return array|null
513
     */
514 52
    private function getBin($text, $offset)
515
    {
516 52
        if (preg_match('/\Ab\'[01\']/ms', substr($text, $offset, 3)) &&
517 52
            preg_match('/b\'([01]*)\'/ms', $text, $pregMatch, 0, $offset)
518
        ) {
519
            return [
520 5
                new Token(Token::BIN, $pregMatch[1]),
521 5
                $offset + strlen($pregMatch[0])
522
            ];
523
        }
524 48
        return null;
525
    }
526
527
    /**
528
     * @param string $text
529
     * @param int $offset
530
     * @return array
531
     */
532 513
    private function getIdentifier($text, $offset)
533
    {
534 513
        preg_match('/[a-zA-Z0-9$_]+()/ms', $text, $pregMatch, PREG_OFFSET_CAPTURE, $offset);
535
        return [
536 513
            new Token(Token::IDENTIFIER, $pregMatch[0][0]),
537 513
            $pregMatch[1][1]
538
        ];
539
    }
540
541
    /**
542
     * @param string $text
543
     * @param int $offset
544
     * @return array
545
     */
546 5
    private function getSymbol($text, $offset)
547
    {
548 5
        if (preg_match('/\A(?:[-+*.\/])/xms', substr($text, $offset, 2), $pregMatch)) {
549
            return [
550 5
                new Token(Token::SYMBOL, $pregMatch[0]),
551 5
                $offset + strlen($pregMatch[0])
552
            ];
553
        }
554
555
        return [];
556
    }
557
558
    /**
559
     * @return Token|mixed
560
     */
561 605
    public function nextToken()
562
    {
563 605
        while (true) {
564 605
            $token = $this->nextTokenRaw();
565 593
            if (!isset(self::$skipTokenTypes[$token->type])) {
566 593
                return $token;
567
            }
568
        }
569
570
        return null;
571
    }
572
573
    /**
574
     * @return object
575
     */
576 463
    public function getMark()
577
    {
578
        return (object)[
579 463
            'offset'        => $this->offset,
580 463
            'inConditional' => $this->inConditional,
581
        ];
582
    }
583
584
    /**
585
     * @param mixed $mark
586
     */
587 441
    public function rewind($mark)
588
    {
589 441
        $this->offset        = $mark->offset;
590 441
        $this->inConditional = $mark->inConditional;
591 441
    }
592
593
    /**
594
     * This function will consume the requested content from the stream without trying to parse and tokenise it.
595
     * It is used by {@see peek()}.
596
     *
597
     * @param mixed $spec
598
     * @return bool
599
     */
600 483
    public function consume($spec)
601
    {
602
        // inline getMark()
603 483
        $markOffset        = $this->offset;
604 483
        $markInConditional = $this->inConditional;
605
606 483
        if (is_string($spec)) {
607 374
            foreach (explode(' ', $spec) as $text) {
608 374
                $token = $this->nextToken();
609
                // inline $token->eq(...)
610 374
                if (strcasecmp($token->text, $text) !== 0 ||
611 374
                    $token->type !== Token::IDENTIFIER
612
                ) {
613
                    // inline rewind()
614 368
                    $this->offset        = $markOffset;
615 368
                    $this->inConditional = $markInConditional;
616 368
                    return false;
617
                }
618
            }
619
        } else {
620 440
            foreach ($spec as $match) {
621 440
                list($type, $text) = $match;
622 440
                $token = $this->nextToken();
623
                // inline $token->eq(...)
624 440
                if (strcasecmp($token->text, $text) !== 0 ||
625 440
                    $token->type !== $type
626
                ) {
627
                    // inline rewind()
628 336
                    $this->offset        = $markOffset;
629 336
                    $this->inConditional = $markInConditional;
630 336
                    return false;
631
                }
632
            }
633
        }
634
635 317
        return true;
636
    }
637
638
    /**
639
     * @param mixed $spec
640
     * @return bool
641
     */
642 7
    public function peek($spec)
643
    {
644
        // inline getMark()
645 7
        $markOffset        = $this->offset;
646 7
        $markInConditional = $this->inConditional;
647
648 7
        $result = $this->consume($spec);
649
650
        // inline rewind()
651 7
        $this->offset        = $markOffset;
652 7
        $this->inConditional = $markInConditional;
653
654 7
        return $result;
655
    }
656
657
    /**
658
     * @param string $type
659
     * @param string $text
660
     * @return string
661
     */
662 208
    public function expect($type, $text = null)
663
    {
664 208
        $token = $this->nextToken();
665 208
        if (!$token->eq($type, $text)) {
666 3
            throw new RuntimeException("Expected '$text'");
667
        }
668 205
        return $token->text;
669
    }
670
671
    /**
672
     * @return string
673
     */
674 379
    public function expectName()
675
    {
676 379
        $token = $this->nextToken();
677 379
        if ($token->type !== Token::IDENTIFIER) {
678 2
            throw new RuntimeException("Expected identifier");
679
        }
680 378
        return $token->text;
681
    }
682
683
    /**
684
     * @return string
685
     */
686 111
    public function expectOpenParen()
687
    {
688 111
        return $this->expect(Token::SYMBOL, '(');
689
    }
690
691
    /**
692
     * @return string
693
     */
694 56
    public function expectCloseParen()
695
    {
696 56
        return $this->expect(Token::SYMBOL, ')');
697
    }
698
699
    /**
700
     * @return int
701
     */
702 92
    public function expectNumber()
703
    {
704 92
        $token = $this->nextToken();
705 92
        if ($token->type !== Token::NUMBER) {
706 1
            throw new RuntimeException("Expected number");
707
        }
708 91
        return 1 * $token->text;
709
    }
710
711
    /**
712
     * @return string
713
     */
714 21
    public function expectString()
715
    {
716 21
        $token = $this->nextToken();
717 21
        if ($token->type !== Token::STRING) {
718 1
            throw new RuntimeException("Expected string");
719
        }
720 20
        return $token->text;
721
    }
722
723
    /**
724
     * @return string
725
     */
726 21
    public function expectStringExtended()
727
    {
728 21
        $token = $this->nextToken();
729 21
        switch ($token->type) {
730 21
            case Token::STRING:
731 16
                return $token->text;
732 5
            case Token::HEX:
733 3
                return $token->asString();
734 2
            case Token::BIN:
735 1
                return $token->asString();
736
            default:
737 1
                throw new RuntimeException("Expected string");
738
        }
739
    }
740
741
    /**
742
     * Provides context for error messages.
743
     *
744
     * For example, given this invalid table definition ...
745
     *
746
     *     CREATE TABLE `foo` (
747
     *         `a` bar DEFAULT NULL
748
     *     );
749
     *
750
     * ... this function will produce something like this:
751
     *
752
     *     schema/morphism test/foo.sql, line 2: unknown datatype 'bar'
753
     *     1: CREATE TABLE `foo` (
754
     *     2:   `a` bar<<HERE>> DEFAULT NULL
755
     *
756
     * @param string $message
757
     * @return string
758
     */
759 1
    public function contextualise($message)
760
    {
761 1
        $preContextLines = 4;
762 1
        $postContextLines = 0;
763
764
        // get position of eol strictly before offset
765 1
        $prevEolPos = strrpos($this->text, "\n", $this->offset - strlen($this->text) - 1);
766 1
        $prevEolPos = ($prevEolPos === false) ? -1 : $prevEolPos;
767
768
        // get position of eol on or after offset
769 1
        $nextEolPos = strpos($this->text, "\n", $this->offset);
770 1
        $nextEolPos = ($nextEolPos === false) ? strlen($this->text) : $nextEolPos;
771
772
        // count number of newlines up to but not including offset
773 1
        $lineNo = substr_count($this->text, "\n", 0, $this->offset);
774 1
        $lines = explode("\n", $this->text);
775
776 1
        $contextLines = array_slice(
777 1
            $lines,
778 1
            max(0, $lineNo - $preContextLines),
779 1
            min($lineNo, $preContextLines),
780 1
            true // preserve keys
781
        );
782
        $contextLines += [
783
            $lineNo =>
784 1
                substr($this->text, $prevEolPos + 1, $this->offset - ($prevEolPos + 1)) .
785 1
                "<<HERE>>".
786 1
                substr($this->text, $this->offset, $nextEolPos - $this->offset)
787
        ];
788 1
        $contextLines += array_slice(
789 1
            $lines,
790 1
            $lineNo + 1,
791
            $postContextLines,
792 1
            true // preserve keys
793
        );
794
795 1
        $context = '';
796 1
        $width = strlen($lineNo + 1 + $postContextLines);
797 1
        foreach ($contextLines as $i => $line) {
798 1
            $context .= sprintf("\n%{$width}d: %s", $i + 1, $line);
799
        }
800
801 1
        return sprintf("%s, line %d: %s%s", $this->path, $lineNo + 1, $message, $context);
802
    }
803
}
804