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.
Completed
Pull Request — master (#66)
by Tomasz
01:19
created

RegularParser::lookaheadN()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5.246

Importance

Changes 0
Metric Value
dl 0
loc 21
ccs 11
cts 14
cp 0.7856
rs 9.2728
c 0
b 0
f 0
cc 5
nc 6
nop 1
crap 5.246
1
<?php
2
namespace Thunder\Shortcode\Parser;
3
4
use Thunder\Shortcode\Shortcode\ParsedShortcode;
5
use Thunder\Shortcode\Shortcode\Shortcode;
6
use Thunder\Shortcode\Syntax\CommonSyntax;
7
use Thunder\Shortcode\Syntax\SyntaxInterface;
8
use Thunder\Shortcode\Utility\RegexBuilderUtility;
9
10
/**
11
 * @author Tomasz Kowalczyk <[email protected]>
12
 */
13
final class RegularParser implements ParserInterface
14
{
15
    /** @var string */
16
    private $lexerRegex;
17
    /** @var string */
18
    private $nameRegex;
19
    /** @psalm-var list<array{0:int,1:string,2:int}> */
20
    private $tokens = array();
21
    /** @var int */
22
    private $tokensCount = 0;
23
    /** @var int */
24
    private $position = 0;
25
    /** @var int[] */
26
    private $backtracks = array();
27
    /** @var int */
28
    private $lastBacktrack = 0;
29
30
    const TOKEN_OPEN = 1;
31
    const TOKEN_CLOSE = 2;
32
    const TOKEN_MARKER = 3;
33
    const TOKEN_SEPARATOR = 4;
34
    const TOKEN_DELIMITER = 5;
35
    const TOKEN_STRING = 6;
36
    const TOKEN_WS = 7;
37
38
    const VALUE_REGULAR = 0x01;
39
    const VALUE_AGGRESSIVE = 0x02;
40
41
    /** @var int */
42
    public $valueMode = self::VALUE_REGULAR;
43
44 16
    public function __construct(SyntaxInterface $syntax = null)
45
    {
46 16
        $this->lexerRegex = $this->prepareLexer($syntax ?: new CommonSyntax());
47 16
        $this->nameRegex = '~^'.RegexBuilderUtility::buildNameRegex().'$~us';
48 16
    }
49
50
    /**
51
     * @param string $text
52
     *
53
     * @return ParsedShortcode[]
54
     */
55 61
    public function parse($text)
56
    {
57 61
        $nestingLevel = ini_set('xdebug.max_nesting_level', '-1');
58 61
        $this->tokens = $this->tokenize($text);
59 61
        $this->backtracks = array();
60 61
        $this->lastBacktrack = 0;
61 61
        $this->position = 0;
62 61
        $this->tokensCount = \count($this->tokens);
63
64 61
        $shortcodes = array();
65 61
        while($this->position < $this->tokensCount) {
66 60
            while($this->position < $this->tokensCount && false === $this->lookahead(self::TOKEN_OPEN)) {
67 28
                $this->position++;
68 28
            }
69 60
            $names = array();
70 60
            $this->beginBacktrack();
71 60
            $matches = $this->shortcode($names);
72 60
            if(false === $matches) {
73 16
                $this->backtrack();
74 16
                $this->match(null, true);
75 16
                continue;
76
            }
77 52
            if(\is_array($matches)) {
78 52
                foreach($matches as $shortcode) {
79 52
                    $shortcodes[] = $shortcode;
80 52
                }
81 52
            }
82 52
        }
83 61
        ini_set('xdebug.max_nesting_level', $nestingLevel);
84
85 61
        return $shortcodes;
86
    }
87
88
    /**
89
     * @param string $name
90
     * @psalm-param array<string,string|null> $parameters
91
     * @param string|null $bbCode
92
     * @param int $offset
93
     * @param string|null $content
94
     * @param string $text
95
     *
96
     * @return ParsedShortcode
97
     */
98 52
    private function getObject($name, $parameters, $bbCode, $offset, $content, $text)
99
    {
100 52
        return new ParsedShortcode(new Shortcode($name, $parameters, $content, $bbCode), $text, $offset);
101
    }
102
103
    /* --- RULES ----------------------------------------------------------- */
104
105
    /**
106
     * @param string[] $names
107
     * @psalm-param list<string> $names
108
     * FIXME: investigate the reason Psalm complains about references
109
     * @psalm-suppress ReferenceConstraintViolation
110
     *
111
     * @return ParsedShortcode[]|string|false
112
     */
113 60
    private function shortcode(array &$names)
114
    {
115 60
        if(!$this->match(self::TOKEN_OPEN, false)) { return false; }
116 60
        $offset = $this->tokens[$this->position - 1][2];
117 60
        $this->match(self::TOKEN_WS, false);
118 60
        if('' === $name = $this->match(self::TOKEN_STRING, false)) { return false; }
119 57
        if($this->lookahead(self::TOKEN_STRING)) { return false; }
120 57
        if(1 !== preg_match($this->nameRegex, $name, $matches)) { return false; }
121 56
        $this->match(self::TOKEN_WS, false);
122
        // bbCode
123 56
        $bbCode = $this->match(self::TOKEN_SEPARATOR, true) ? $this->value() : null;
124 56
        if(false === $bbCode) { return false; }
125
        // parameters
126 55
        if(false === ($parameters = $this->parameters())) { return false; }
127
128
        // self-closing
129 53
        if($this->match(self::TOKEN_MARKER, true)) {
130 17
            if(!$this->match(self::TOKEN_CLOSE, false)) { return false; }
131
132 16
            return array($this->getObject($name, $parameters, $bbCode, $offset, null, $this->getBacktrack()));
133
        }
134
135
        // just-closed or with-content
136 41
        if(!$this->match(self::TOKEN_CLOSE, false)) { return false; }
137 41
        $this->beginBacktrack();
138 41
        $names[] = $name;
139
140
        // begin inlined content()
141 41
        $content = '';
142 41
        $shortcodes = array();
143 41
        $closingName = null;
144
145 41
        while($this->position < $this->tokensCount) {
146 31 View Code Duplication
            while($this->position < $this->tokensCount && false === $this->lookahead(self::TOKEN_OPEN)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
147 27
                $content .= $this->match(null, true);
148 27
            }
149
150 31
            $this->beginBacktrack();
151
            /** @psalm-suppress MixedArgumentTypeCoercion */
152 31
            $contentMatchedShortcodes = $this->shortcode($names);
153 31
            if(\is_string($contentMatchedShortcodes)) {
154 6
                $closingName = $contentMatchedShortcodes;
155 6
                break;
156
            }
157 31
            if(\is_array($contentMatchedShortcodes)) {
158 17
                foreach($contentMatchedShortcodes as $matchedShortcode) {
159 17
                    $shortcodes[] = $matchedShortcode;
160 17
                }
161 17
                continue;
162
            }
163 24
            $this->backtrack();
164
165 24
            $this->beginBacktrack();
166 24
            if(false !== ($closingName = $this->close($names))) {
167 21
                $this->backtrack();
168 21
                $shortcodes = array();
169 21
                break;
170
            }
171 9
            $closingName = null;
172 9
            $this->backtrack();
173
174 9
            $content .= $this->match(null, false);
175 9
        }
176 41
        $content = $this->position < $this->tokensCount ? $content : false;
177
        // end inlined content()
178
179 41
        if(null !== $closingName && $closingName !== $name) {
180 6
            array_pop($names);
181 6
            array_pop($this->backtracks);
182 6
            array_pop($this->backtracks);
183
184 6
            return $closingName;
185
        }
186 41
        if(false === $content || $closingName !== $name) {
187 27
            $this->backtrack(false);
188 27
            $text = $this->backtrack(false);
189 27
            array_pop($names);
190
191 27
            return array_merge(array($this->getObject($name, $parameters, $bbCode, $offset, null, $text)), $shortcodes);
192
        }
193 21
        $content = $this->getBacktrack();
194 21
        if(!$this->close($names)) { return false; }
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->close($names) of type false|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
195 21
        array_pop($names);
196
197 21
        return array($this->getObject($name, $parameters, $bbCode, $offset, $content, $this->getBacktrack()));
198
    }
199
200
    /**
201
     * @param string[] $names
202
     *
203
     * @return string|false
204
     */
205 24
    private function close(array &$names)
206
    {
207 24
        if(!$this->match(self::TOKEN_OPEN, true)) { return false; }
208 22
        if(!$this->match(self::TOKEN_MARKER, true)) { return false; }
209 22
        if(!$closingName = $this->match(self::TOKEN_STRING, true)) { return false; }
210 22
        if(!$this->match(self::TOKEN_CLOSE, false)) { return false; }
211
212 22
        return \in_array($closingName, $names, true) ? $closingName : false;
213
    }
214
215
    /** @psalm-return array<string,string|null>|false */
216 55
    private function parameters()
217
    {
218 55
        $parameters = array();
219
220 55
        while(true) {
221 55
            $this->match(self::TOKEN_WS, false);
222 55
            if($this->lookahead(self::TOKEN_MARKER) || $this->lookahead(self::TOKEN_CLOSE)) { break; }
223 31
            if(!$name = $this->match(self::TOKEN_STRING, true)) { return false; }
224 30
            if(!$this->match(self::TOKEN_SEPARATOR, true)) { $parameters[$name] = null; continue; }
225 29
            if(false === ($value = $this->value())) { return false; }
226 27
            $this->match(self::TOKEN_WS, false);
227
228 27
            $parameters[$name] = $value;
229 27
        }
230
231 53
        return $parameters;
232
    }
233
234
    /** @return false|string */
235 31
    private function value()
236
    {
237 31
        $value = '';
238
239 31
        if($this->match(self::TOKEN_DELIMITER, false)) {
240 20 View Code Duplication
            while($this->position < $this->tokensCount && false === $this->lookahead(self::TOKEN_DELIMITER)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
241 20
                $value .= $this->match(null, false);
242 20
            }
243
244 20
            return $this->match(self::TOKEN_DELIMITER, false) ? $value : false;
245
        }
246
247 17
        if($this->lookahead(self::TOKEN_STRING) || $this->lookahead(self::TOKEN_MARKER)) {
248 16
            while(true) {
249 16
                if($this->lookahead(self::TOKEN_WS) || $this->lookahead(self::TOKEN_CLOSE)) {
250 16
                    break;
251
                }
252 16
                if($this->lookaheadN(array(self::TOKEN_MARKER, self::TOKEN_CLOSE))) {
253 3
                    if($this->valueMode === self::VALUE_AGGRESSIVE) {
254 1
                        $value .= $this->match(null, false);
255 1
                    }
256 3
                    break;
257
                }
258 16
                $value .= $this->match(null, false);
259 16
            }
260
261 16
            return $value;
262
        }
263
264 1
        return false;
265
    }
266
267
    /* --- PARSER ---------------------------------------------------------- */
268
269
    /** @return void */
270 60
    private function beginBacktrack()
271
    {
272 60
        $this->backtracks[] = $this->position;
273 60
        $this->lastBacktrack = $this->position;
274 60
    }
275
276
    /** @return string */
277 33
    private function getBacktrack()
278
    {
279 33
        $position = array_pop($this->backtracks);
280 33
        $backtrack = '';
281 33 View Code Duplication
        for($i = $position; $i < $this->position; $i++) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
282 33
            $backtrack .= $this->tokens[$i][1];
283 33
        }
284
285 33
        return $backtrack;
286
    }
287
288
    /**
289
     * @param bool $modifyPosition
290
     *
291
     * @return string
292
     */
293 50
    private function backtrack($modifyPosition = true)
294
    {
295 50
        $position = array_pop($this->backtracks);
296 50
        if($modifyPosition) {
297 33
            $this->position = $position;
298 33
        }
299
300 50
        $backtrack = '';
301 50 View Code Duplication
        for($i = $position; $i < $this->lastBacktrack; $i++) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
302 27
            $backtrack .= $this->tokens[$i][1];
303 27
        }
304 50
        $this->lastBacktrack = $position;
305
306 50
        return $backtrack;
307
    }
308
309
    /**
310
     * @param int $type
311
     *
312
     * @return bool
313
     */
314 60
    private function lookahead($type)
315
    {
316 60
        return $this->position < $this->tokensCount && $this->tokens[$this->position][0] === $type;
317
    }
318
319
    /**
320
     * @param int[] $types
321
     *
322
     * @return bool
323
     */
324 16
    private function lookaheadN(array $types)
325
    {
326 16
        $count = count($types);
327 16
        if($this->position + $count > $this->tokensCount) {
328
            return false;
329
        }
330
331 16
        $position = $this->position;
332 16
        foreach($types as $type) {
333
            // note: automatically skips whitespace tokens
334 16
            if($this->tokens[$position][0] === self::TOKEN_WS) {
335
                $position++;
336
            }
337 16
            if($type !== $this->tokens[$position][0]) {
338 16
                return false;
339
            }
340 3
            $position++;
341 3
        }
342
343 3
        return true;
344
    }
345
346
    /**
347
     * @param int|null $type
348
     * @param bool $ws
349
     *
350
     * @return string
351
     */
352 60
    private function match($type, $ws)
353
    {
354 60
        if($this->position >= $this->tokensCount) {
355 21
            return '';
356
        }
357
358 60
        $token = $this->tokens[$this->position];
359 60
        if(!empty($type) && $token[0] !== $type) {
360 60
            return '';
361
        }
362
363 60
        $this->position++;
364 60
        if($ws && $this->position < $this->tokensCount && $this->tokens[$this->position][0] === self::TOKEN_WS) {
365 18
            $this->position++;
366 18
        }
367
368 60
        return $token[1];
369
    }
370
371
    /* --- LEXER ----------------------------------------------------------- */
372
373
    /**
374
     * @param string $text
375
     *
376
     * @psalm-return list<array{0:int,1:string,2:int}>
377
     */
378 61
    private function tokenize($text)
379
    {
380 61
        $count = preg_match_all($this->lexerRegex, $text, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
381 61
        if(false === $count || preg_last_error() !== PREG_NO_ERROR) {
382
            throw new \RuntimeException(sprintf('PCRE failure `%s`.', preg_last_error()));
383
        }
384
385 61
        $tokens = array();
386 61
        $position = 0;
387
388 61
        foreach($matches as $match) {
389 60
            switch(true) {
390 60 View Code Duplication
                case -1 !== $match['string'][1]: { $token = $match['string'][0]; $type = self::TOKEN_STRING; break; }
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of -1 (integer) and $match['string'][1] (string) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
391 60 View Code Duplication
                case -1 !== $match['ws'][1]: { $token = $match['ws'][0]; $type = self::TOKEN_WS; break; }
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of -1 (integer) and $match['ws'][1] (string) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
392 60 View Code Duplication
                case -1 !== $match['marker'][1]: { $token = $match['marker'][0]; $type = self::TOKEN_MARKER; break; }
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of -1 (integer) and $match['marker'][1] (string) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
393 60 View Code Duplication
                case -1 !== $match['delimiter'][1]: { $token = $match['delimiter'][0]; $type = self::TOKEN_DELIMITER; break; }
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of -1 (integer) and $match['delimiter'][1] (string) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
394 60 View Code Duplication
                case -1 !== $match['separator'][1]: { $token = $match['separator'][0]; $type = self::TOKEN_SEPARATOR; break; }
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of -1 (integer) and $match['separator'][1] (string) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
395 60 View Code Duplication
                case -1 !== $match['open'][1]: { $token = $match['open'][0]; $type = self::TOKEN_OPEN; break; }
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of -1 (integer) and $match['open'][1] (string) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
396 58 View Code Duplication
                case -1 !== $match['close'][1]: { $token = $match['close'][0]; $type = self::TOKEN_CLOSE; break; }
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of -1 (integer) and $match['close'][1] (string) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
397
                default: { throw new \RuntimeException(sprintf('Invalid token.')); }
398
            }
399 60
            $tokens[] = array($type, $token, $position);
400 60
            $position += mb_strlen($token, 'utf-8');
401 61
        }
402
403 61
        return $tokens;
404
    }
405
406
    /** @return string */
407 16
    private function prepareLexer(SyntaxInterface $syntax)
408
    {
409
        // FIXME: for some reason Psalm does not understand the `@psalm-var callable() $var` annotation
410
        /** @psalm-suppress MissingClosureParamType, MissingClosureReturnType */
411
        $group = function($text, $group) {
412 16
            return '(?<'.(string)$group.'>'.preg_replace('/(.)/us', '\\\\$0', (string)$text).')';
413 16
        };
414
        /** @psalm-suppress MissingClosureParamType, MissingClosureReturnType */
415 16
        $quote = function($text) {
416 16
            return preg_replace('/(.)/us', '\\\\$0', (string)$text);
417 16
        };
418
419
        $rules = array(
420 16
            '(?<string>\\\\.|(?:(?!'.implode('|', array(
421 16
                $quote($syntax->getOpeningTag()),
422 16
                $quote($syntax->getClosingTag()),
423 16
                $quote($syntax->getClosingTagMarker()),
424 16
                $quote($syntax->getParameterValueSeparator()),
425 16
                $quote($syntax->getParameterValueDelimiter()),
426 16
                '\s+',
427 16
            )).').)+)',
428 16
            '(?<ws>\s+)',
429 16
            $group($syntax->getClosingTagMarker(), 'marker'),
430 16
            $group($syntax->getParameterValueDelimiter(), 'delimiter'),
431 16
            $group($syntax->getParameterValueSeparator(), 'separator'),
432 16
            $group($syntax->getOpeningTag(), 'open'),
433 16
            $group($syntax->getClosingTag(), 'close'),
434 16
        );
435
436 16
        return '~('.implode('|', $rules).')~us';
437
    }
438
}
439