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 (#88)
by Tomasz
07:08
created

RegularParser::shortcode()   D

Complexity

Conditions 24
Paths 94

Size

Total Lines 86

Duplication

Lines 3
Ratio 3.49 %

Code Coverage

Tests 58
CRAP Score 24

Importance

Changes 0
Metric Value
dl 3
loc 86
ccs 58
cts 58
cp 1
rs 4.1666
c 0
b 0
f 0
cc 24
nc 94
nop 1
crap 24

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 15
    const TOKEN_MARKER = 3;
33
    const TOKEN_SEPARATOR = 4;
34 15
    const TOKEN_DELIMITER = 5;
35 15
    const TOKEN_STRING = 6;
36 15
    const TOKEN_WS = 7;
37
38
    public function __construct(SyntaxInterface $syntax = null)
39
    {
40
        $this->lexerRegex = $this->prepareLexer($syntax ?: new CommonSyntax());
41
        $this->nameRegex = '~^'.RegexBuilderUtility::buildNameRegex().'$~us';
42
    }
43 59
44
    /**
45 59
     * @param string $text
46 59
     *
47 59
     * @return ParsedShortcode[]
48 59
     */
49 59
    public function parse($text)
50 59
    {
51
        $nestingLevel = ini_set('xdebug.max_nesting_level', '-1');
52 59
        $this->tokens = $this->tokenize($text);
53 59
        $this->backtracks = array();
54 58
        $this->lastBacktrack = 0;
55 27
        $this->position = 0;
56
        $this->tokensCount = \count($this->tokens);
57 58
58 58
        $shortcodes = array();
59 58
        while($this->position < $this->tokensCount) {
60 58
            while($this->position < $this->tokensCount && false === $this->lookahead(self::TOKEN_OPEN)) {
61 16
                $this->position++;
62 16
            }
63 16
            $names = array();
64
            $this->beginBacktrack();
65 50
            $matches = $this->shortcode($names);
66 50
            if(false === $matches) {
67 50
                $this->backtrack();
68
                $this->match(null, true);
69
                continue;
70
            }
71 59
            if(\is_array($matches)) {
72
                foreach($matches as $shortcode) {
73 59
                    $shortcodes[] = $shortcode;
74
                }
75
            }
76 50
        }
77
        ini_set('xdebug.max_nesting_level', $nestingLevel);
78 50
79
        return $shortcodes;
80
    }
81
82
    /**
83 58
     * @param string $name
84
     * @psalm-param array<string,string|null> $parameters
85 58
     * @param string|null $bbCode
86 58
     * @param int $offset
87 58
     * @param string|null $content
88 58
     * @param string $text
89 55
     *
90 55
     * @return ParsedShortcode
91 54
     */
92
    private function getObject($name, $parameters, $bbCode, $offset, $content, $text)
93 54
    {
94 54
        return new ParsedShortcode(new Shortcode($name, $parameters, $content, $bbCode), $text, $offset);
95
    }
96 53
97
    /* --- RULES ----------------------------------------------------------- */
98
99 51
    /**
100 16
     * @param string[] $names
101
     * @psalm-param list<string> $names
102 15
     * FIXME: investigate the reason Psalm complains about references
103
     * @psalm-suppress ReferenceConstraintViolation
104
     *
105
     * @return ParsedShortcode[]|string|false
106 39
     */
107 39
    private function shortcode(array &$names)
108 39
    {
109
        if(!$this->match(self::TOKEN_OPEN, false)) { return false; }
110
        $offset = $this->tokens[$this->position - 1][2];
111 39
        $this->match(self::TOKEN_WS, false);
112 39
        if('' === $name = $this->match(self::TOKEN_STRING, false)) { return false; }
113 39
        if($this->lookahead(self::TOKEN_STRING)) { return false; }
114
        if(1 !== preg_match($this->nameRegex, $name, $matches)) { return false; }
115 39
        $this->match(self::TOKEN_WS, false);
116 30
        // bbCode
117 26
        $bbCode = $this->match(self::TOKEN_SEPARATOR, true) ? $this->value() : null;
118
        if(false === $bbCode) { return false; }
119
        // parameters
120 30
        if(false === ($parameters = $this->parameters())) { return false; }
121 30
122 30
        // self-closing
123 6
        if($this->match(self::TOKEN_MARKER, true)) {
124 6
            if(!$this->match(self::TOKEN_CLOSE, false)) { return false; }
125
126 30
            return array($this->getObject($name, $parameters, $bbCode, $offset, null, $this->getBacktrack()));
127 16
        }
128 16
129
        // just-closed or with-content
130 16
        if(!$this->match(self::TOKEN_CLOSE, false)) { return false; }
131
        $this->beginBacktrack();
132 24
        $names[] = $name;
133
134 24
        // begin inlined content()
135 24
        $content = '';
136 21
        $shortcodes = array();
137 21
        $closingName = null;
138 21
139 21
        while($this->position < $this->tokensCount) {
140 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...
141 9
                $content .= $this->match(null, true);
142 9
            }
143
144 9
            $this->beginBacktrack();
145
            /** @psalm-suppress MixedArgumentTypeCoercion */
146 39
            $contentMatchedShortcodes = $this->shortcode($names);
147
            if(\is_string($contentMatchedShortcodes)) {
148
                $closingName = $contentMatchedShortcodes;
149 39
                break;
150 6
            }
151 6
            if(\is_array($contentMatchedShortcodes)) {
152 6
                foreach($contentMatchedShortcodes as $matchedShortcode) {
153
                    $shortcodes[] = $matchedShortcode;
154 6
                }
155
                continue;
156 39
            }
157 25
            $this->backtrack();
158 25
159 25
            $this->beginBacktrack();
160
            if(false !== ($closingName = $this->close($names))) {
161 25
                $this->backtrack();
162
                $shortcodes = array();
163 21
                break;
164 21
            }
165 21
            $closingName = null;
166
            $this->backtrack();
167 21
168
            $content .= $this->match(null, false);
169
        }
170 24
        $content = $this->position < $this->tokensCount ? $content : false;
171
        // end inlined content()
172 24
173 22
        if(null !== $closingName && $closingName !== $name) {
174 22
            array_pop($names);
175 22
            array_pop($this->backtracks);
176
            array_pop($this->backtracks);
177 22
178
            return $closingName;
179
        }
180 53
        if(false === $content || $closingName !== $name) {
181
            $this->backtrack(false);
182 53
            $text = $this->backtrack(false);
183
            array_pop($names);
184 53
185 53
            return array_merge(array($this->getObject($name, $parameters, $bbCode, $offset, null, $text)), $shortcodes);
186 53
        }
187 29
        $content = $this->getBacktrack();
188 28
        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...
189 27
        array_pop($names);
190 25
191
        return array($this->getObject($name, $parameters, $bbCode, $offset, $content, $this->getBacktrack()));
192 25
    }
193
194
    /**
195 51
     * @param string[] $names
196
     *
197
     * @return string|false
198 29
     */
199
    private function close(array &$names)
200 29
    {
201
        if(!$this->match(self::TOKEN_OPEN, true)) { return false; }
202 29
        if(!$this->match(self::TOKEN_MARKER, true)) { return false; }
203 20
        if(!$closingName = $this->match(self::TOKEN_STRING, true)) { return false; }
204 20
        if(!$this->match(self::TOKEN_CLOSE, false)) { return false; }
205
206
        return \in_array($closingName, $names, true) ? $closingName : false;
207 20
    }
208
209
    /** @psalm-return array<string,string|null>|false */
210 15
    private function parameters()
211 14
    {
212 14
        $parameters = array();
213
214
        while(true) {
215
            $this->match(self::TOKEN_WS, false);
216 14
            if($this->lookahead(self::TOKEN_MARKER) || $this->lookahead(self::TOKEN_CLOSE)) { break; }
217
            if(!$name = $this->match(self::TOKEN_STRING, true)) { return false; }
218
            if(!$this->match(self::TOKEN_SEPARATOR, true)) { $parameters[$name] = null; continue; }
219 1
            if(false === ($value = $this->value())) { return false; }
220
            $this->match(self::TOKEN_WS, false);
221
222
            $parameters[$name] = $value;
223
        }
224 58
225
        return $parameters;
226 58
    }
227 58
228 58
    /** @return false|string */
229
    private function value()
230 32
    {
231
        $value = '';
232 32
233 32
        if($this->match(self::TOKEN_DELIMITER, false)) {
234 32 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...
235 32
                $value .= $this->match(null, false);
236
            }
237
238 32
            return $this->match(self::TOKEN_DELIMITER, false) ? $value : false;
239
        }
240
241 48
        if('' !== $tmp = $this->match(self::TOKEN_STRING, false)) {
242
            $value .= $tmp;
243 48
            while('' !== $tmp = $this->match(self::TOKEN_STRING, false)) {
244 48
                $value .= $tmp;
245 33
            }
246
247
            return $value;
248 48
        }
249 48
250 25
        return false;
251
    }
252 48
253
    /* --- PARSER ---------------------------------------------------------- */
254 48
255
    /** @return void */
256
    private function beginBacktrack()
257 58
    {
258
        $this->backtracks[] = $this->position;
259 58
        $this->lastBacktrack = $this->position;
260
    }
261
262 58
    /** @return string */
263
    private function getBacktrack()
264 58
    {
265 21
        $position = array_pop($this->backtracks);
266
        $backtrack = '';
267 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...
268 58
            $backtrack .= $this->tokens[$i][1];
269 58
        }
270 58
271
        return $backtrack;
272
    }
273 58
274 58
    /**
275 18
     * @param bool $modifyPosition
276
     *
277
     * @return string
278 58
     */
279
    private function backtrack($modifyPosition = true)
280
    {
281
        $position = array_pop($this->backtracks);
282
        if($modifyPosition) {
283 59
            $this->position = $position;
284
        }
285 59
286 59
        $backtrack = '';
287 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...
288
            $backtrack .= $this->tokens[$i][1];
289
        }
290 59
        $this->lastBacktrack = $position;
291 59
292
        return $backtrack;
293 59
    }
294
295 58
    /**
296 58
     * @param int $type
297 58
     *
298 58
     * @return bool
299 58
     */
300 58
    private function lookahead($type)
301 56
    {
302
        return $this->position < $this->tokensCount && $this->tokens[$this->position][0] === $type;
303
    }
304 58
305 58
    /**
306
     * @param int|null $type
307
     * @param bool $ws
308 59
     *
309
     * @return string
310
     */
311 15
    private function match($type, $ws)
312
    {
313
        if($this->position >= $this->tokensCount) {
314 15
            return '';
315 15
        }
316
317 15
        $token = $this->tokens[$this->position];
318 15
        if(!empty($type) && $token[0] !== $type) {
319
            return '';
320
        }
321 15
322 15
        $this->position++;
323 15
        if($ws && $this->position < $this->tokensCount && $this->tokens[$this->position][0] === self::TOKEN_WS) {
324 15
            $this->position++;
325 15
        }
326 15
327 15
        return $token[1];
328 15
    }
329 15
330 15
    /* --- LEXER ----------------------------------------------------------- */
331 15
332 15
    /**
333 15
     * @param string $text
334 15
     *
335
     * @psalm-return list<array{0:int,1:string,2:int}>
336
     */
337 15
    private function tokenize($text)
338
    {
339
        $count = preg_match_all($this->lexerRegex, $text, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
340
        if(false === $count || preg_last_error() !== PREG_NO_ERROR) {
341
            throw new \RuntimeException(sprintf('PCRE failure `%s`.', preg_last_error()));
342
        }
343
344
        $tokens = array();
345
        $position = 0;
346
347
        foreach($matches as $match) {
348
            switch(true) {
349 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...
350 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...
351 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...
352 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...
353 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...
354 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...
355 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...
356
                default: { throw new \RuntimeException(sprintf('Invalid token.')); }
357
            }
358
            $tokens[] = array($type, $token, $position);
359
            $position += mb_strlen($token, 'utf-8');
360
        }
361
362
        return $tokens;
363
    }
364
365
    /** @return string */
366
    private function prepareLexer(SyntaxInterface $syntax)
367
    {
368
        // FIXME: for some reason Psalm does not understand the `@psalm-var callable() $var` annotation
369
        /** @psalm-suppress MissingClosureParamType, MissingClosureReturnType */
370
        $group = function($text, $group) {
371
            return '(?<'.(string)$group.'>'.preg_replace('/(.)/us', '\\\\$0', (string)$text).')';
372
        };
373
        /** @psalm-suppress MissingClosureParamType, MissingClosureReturnType */
374
        $quote = function($text) {
375
            return preg_replace('/(.)/us', '\\\\$0', (string)$text);
376
        };
377
378
        $rules = array(
379
            '(?<string>\\\\.|(?:(?!'.implode('|', array(
380
                $quote($syntax->getOpeningTag()),
381
                $quote($syntax->getClosingTag()),
382
                $quote($syntax->getClosingTagMarker()),
383
                $quote($syntax->getParameterValueSeparator()),
384
                $quote($syntax->getParameterValueDelimiter()),
385
                '\s+',
386
            )).').)+)',
387
            '(?<ws>\s+)',
388
            $group($syntax->getClosingTagMarker(), 'marker'),
389
            $group($syntax->getParameterValueDelimiter(), 'delimiter'),
390
            $group($syntax->getParameterValueSeparator(), 'separator'),
391
            $group($syntax->getOpeningTag(), 'open'),
392
            $group($syntax->getClosingTag(), 'close'),
393
        );
394
395
        return '~('.implode('|', $rules).')~us';
396
    }
397
}
398