Passed
Push — master ( 44542b...077e9d )
by Tomáš
03:58
created

Parser   F

Complexity

Total Complexity 80

Size/Duplication

Total Lines 630
Duplicated Lines 0 %

Test Coverage

Coverage 99.54%

Importance

Changes 0
Metric Value
eloc 219
dl 0
loc 630
ccs 216
cts 217
cp 0.9954
rs 2
c 0
b 0
f 0
wmc 80

35 Methods

Rating   Name   Duplication   Size   Complexity  
A parse() 0 15 3
A shiftGroupEnd() 0 5 1
A isToken() 0 11 4
A ignorePrecedingOperators() 0 8 2
A shiftBinaryOperator() 0 15 4
A popTokens() 0 9 3
A shiftWhitespace() 0 4 2
A addCorrection() 0 3 1
A shiftPreference() 0 3 1
A reduceGroup() 0 22 2
A cleanupGroupDelimiters() 0 13 3
A getUnmatchedGroupDelimiterIndexes() 0 23 5
A shiftGroupBegin() 0 3 1
A popWhitespace() 0 4 2
A shiftTerm() 0 3 1
A reduceLogicalNot() 0 13 4
A reduceRemainingLogicalOr() 0 5 3
A shift() 0 6 1
A reduceLogicalOr() 0 22 5
A ignoreEmptyGroup() 0 8 1
A reducePreference() 0 13 3
A shiftBailout() 0 3 1
A ignoreFollowingOperators() 0 12 3
A reduceLogicalAnd() 0 10 3
A shiftLogicalNot() 0 3 1
A ignoreLogicalNotOperatorsPrecedingPreferenceOperator() 0 9 2
A shiftAdjacentUnaryOperator() 0 9 2
A init() 0 6 1
A reduce() 0 22 4
A collectTopStackNodes() 0 9 3
A shiftLogicalNot2() 0 5 1
A getReduction() 0 9 2
A reduceQuery() 0 11 2
A ignoreBinaryOperatorFollowingOperator() 0 8 1
A isTopStackToken() 0 3 2

How to fix   Complexity   

Complex Class

Complex classes like Parser 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 Parser, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types = 1);
2
3
namespace Apicart\FQL\Tokenizer;
4
5
use Apicart\FQL\Contract\Parser\ParserInterface;
6
use Apicart\FQL\Token\Node\Group;
7
use Apicart\FQL\Token\Node\LogicalAnd;
8
use Apicart\FQL\Token\Node\LogicalNot;
9
use Apicart\FQL\Token\Node\LogicalOr;
10
use Apicart\FQL\Token\Node\Mandatory;
11
use Apicart\FQL\Token\Node\Prohibited;
12
use Apicart\FQL\Token\Node\Query;
13
use Apicart\FQL\Token\Node\Term;
14
use Apicart\FQL\Value\AbstractNode;
15
use Apicart\FQL\Value\Correction;
16
use Apicart\FQL\Value\SyntaxTree;
17
use Apicart\FQL\Value\Token;
18
use Apicart\FQL\Value\TokenSequence;
19
use SplStack;
20
21
final class Parser implements ParserInterface
22
{
23
24
    /**
25
     * Parser ignored adjacent unary operator preceding another operator.
26
     */
27
    public const CORRECTION_ADJACENT_UNARY_OPERATOR_PRECEDING_OPERATOR_IGNORED = 0;
28
29
    /**
30
     * Parser ignored unary operator missing an operand.
31
     */
32
    public const CORRECTION_UNARY_OPERATOR_MISSING_OPERAND_IGNORED = 1;
33
34
    /**
35
     * Parser ignored binary operator missing left side operand.
36
     */
37
    public const CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED = 2;
38
39
    /**
40
     * Parser ignored binary operator missing right side operand.
41
     */
42
    public const CORRECTION_BINARY_OPERATOR_MISSING_RIGHT_OPERAND_IGNORED = 3;
43
44
    /**
45
     * Parser ignored binary operator following another operator and connecting operators.
46
     */
47
    public const CORRECTION_BINARY_OPERATOR_FOLLOWING_OPERATOR_IGNORED = 4;
48
49
    /**
50
     * Parser ignored logical not operators preceding mandatory or prohibited operator.
51
     */
52
    public const CORRECTION_LOGICAL_NOT_OPERATORS_PRECEDING_PREFERENCE_IGNORED = 5;
53
54
    /**
55
     * Parser ignored empty group and connecting operators.
56
     */
57
    public const CORRECTION_EMPTY_GROUP_IGNORED = 6;
58
59
    /**
60
     * Parser ignored unmatched left side group delimiter.
61
     */
62
    public const CORRECTION_UNMATCHED_GROUP_LEFT_DELIMITER_IGNORED = 7;
63
64
    /**
65
     * Parser ignored unmatched right side group delimiter.
66
     */
67
    public const CORRECTION_UNMATCHED_GROUP_RIGHT_DELIMITER_IGNORED = 8;
68
69
    /**
70
     * Parser ignored bailout type token.
71
     *
72
     * @see Tokenizer::TOKEN_BAILOUT
73
     */
74
    public const CORRECTION_BAILOUT_TOKEN_IGNORED = 9;
75
76
    /**
77
     * @var array
78
     */
79
    private static $reductionGroups = [
80
        'group' => ['reduceGroup', 'reducePreference', 'reduceLogicalNot', 'reduceLogicalAnd', 'reduceLogicalOr'],
81
        'unaryOperator' => ['reduceLogicalNot', 'reduceLogicalAnd', 'reduceLogicalOr'],
82
        'logicalOr' => [],
83
        'logicalAnd' => ['reduceLogicalOr'],
84
        'term' => ['reducePreference', 'reduceLogicalNot', 'reduceLogicalAnd', 'reduceLogicalOr'],
85
    ];
86
87
    /**
88
     * @var int[]
89
     */
90
    private static $tokenShortcuts = [
91
        'operatorNot' => Tokenizer::TOKEN_LOGICAL_NOT | Tokenizer::TOKEN_LOGICAL_NOT_2,
92
        'operatorPreference' => Tokenizer::TOKEN_MANDATORY | Tokenizer::TOKEN_PROHIBITED,
93
        'operatorPrefix' => Tokenizer::TOKEN_MANDATORY | Tokenizer::TOKEN_PROHIBITED | Tokenizer::TOKEN_LOGICAL_NOT_2,
94
        'operatorUnary' => Tokenizer::TOKEN_MANDATORY | Tokenizer::TOKEN_PROHIBITED | Tokenizer::TOKEN_LOGICAL_NOT
95
        | Tokenizer::TOKEN_LOGICAL_NOT_2,
96
        'operatorBinary' => Tokenizer::TOKEN_LOGICAL_AND | Tokenizer::TOKEN_LOGICAL_OR,
97
        'operator' => Tokenizer::TOKEN_LOGICAL_AND | Tokenizer::TOKEN_LOGICAL_OR | Tokenizer::TOKEN_MANDATORY
98
        | Tokenizer::TOKEN_PROHIBITED | Tokenizer::TOKEN_LOGICAL_NOT | Tokenizer::TOKEN_LOGICAL_NOT_2,
99
        'groupDelimiter' => Tokenizer::TOKEN_GROUP_BEGIN | Tokenizer::TOKEN_GROUP_END,
100
        'binaryOperatorAndWhitespace' => Tokenizer::TOKEN_LOGICAL_AND | Tokenizer::TOKEN_LOGICAL_OR
101
        | Tokenizer::TOKEN_WHITESPACE,
102
    ];
103
104
    /**
105
     * @var string[]
106
     */
107
    private static $shifts = [
108
        Tokenizer::TOKEN_WHITESPACE => 'shiftWhitespace',
109
        Tokenizer::TOKEN_TERM => 'shiftTerm',
110
        Tokenizer::TOKEN_GROUP_BEGIN => 'shiftGroupBegin',
111
        Tokenizer::TOKEN_GROUP_END => 'shiftGroupEnd',
112
        Tokenizer::TOKEN_LOGICAL_AND => 'shiftBinaryOperator',
113
        Tokenizer::TOKEN_LOGICAL_OR => 'shiftBinaryOperator',
114
        Tokenizer::TOKEN_LOGICAL_NOT => 'shiftLogicalNot',
115
        Tokenizer::TOKEN_LOGICAL_NOT_2 => 'shiftLogicalNot2',
116
        Tokenizer::TOKEN_MANDATORY => 'shiftPreference',
117
        Tokenizer::TOKEN_PROHIBITED => 'shiftPreference',
118
        Tokenizer::TOKEN_BAILOUT => 'shiftBailout',
119
    ];
120
121
    /**
122
     * @var string[]
123
     */
124
    private static $nodeToReductionGroup = [
125
        Group::class => 'group',
126
        LogicalAnd::class => 'logicalAnd',
127
        LogicalOr::class => 'logicalOr',
128
        LogicalNot::class => 'unaryOperator',
129
        Mandatory::class => 'unaryOperator',
130
        Prohibited::class => 'unaryOperator',
131
        Term::class => 'term',
132
    ];
133
134
    /**
135
     * Input tokens.
136
     *
137
     * @var Token[]
138
     */
139
    private $tokens = [];
140
141
    /**
142
     * An array of applied corrections.
143
     *
144
     * @var Correction[]
145
     */
146
    private $corrections = [];
147
148
    /**
149
     * Query stack.
150
     *
151
     * @var SplStack
152
     */
153
    private $stack;
154
155
156 135
    public function parse(TokenSequence $tokenSequence): SyntaxTree
157
    {
158 135
        $this->init($tokenSequence->getTokens());
159
160 135
        while ($this->tokens !== []) {
161 134
            $node = $this->shift();
162
163 134
            if ($node instanceof AbstractNode) {
164 134
                $this->reduce($node);
165
            }
166
        }
167
168 135
        $this->reduceQuery();
169
170 135
        return new SyntaxTree($this->stack->top(), $tokenSequence, $this->corrections);
171
    }
172
173
174 11
    public function ignoreLogicalNotOperatorsPrecedingPreferenceOperator(): void
175
    {
176
        /** @var Token[] $precedingOperators */
177 11
        $precedingOperators = $this->ignorePrecedingOperators(self::$tokenShortcuts['operatorNot']);
178
179 11
        if ($precedingOperators !== []) {
180 11
            $this->addCorrection(
181 11
                self::CORRECTION_LOGICAL_NOT_OPERATORS_PRECEDING_PREFERENCE_IGNORED,
182
                ...$precedingOperators
183
            );
184
        }
185 11
    }
186
187
188 119
    private function shiftWhitespace(): void
189
    {
190 119
        if ($this->isTopStackToken(self::$tokenShortcuts['operatorPrefix'])) {
191 15
            $this->addCorrection(self::CORRECTION_UNARY_OPERATOR_MISSING_OPERAND_IGNORED, $this->stack->pop());
192
        }
193 119
    }
194
195
196 70
    private function shiftPreference(Token $token): void
197
    {
198 70
        $this->shiftAdjacentUnaryOperator($token, self::$tokenShortcuts['operator']);
199 70
    }
200
201
202 80
    private function shiftAdjacentUnaryOperator(Token $token, ?int $tokenMask): void
203
    {
204 80
        if ($this->isToken(reset($this->tokens), $tokenMask)) {
205 24
            $this->addCorrection(self::CORRECTION_ADJACENT_UNARY_OPERATOR_PRECEDING_OPERATOR_IGNORED, $token);
206
207 24
            return;
208
        }
209
210 71
        $this->stack->push($token);
211 71
    }
212
213
214 60
    private function shiftLogicalNot(Token $token): void
215
    {
216 60
        $this->stack->push($token);
217 60
    }
218
219
220 25
    private function shiftLogicalNot2(Token $token): void
221
    {
222 25
        $tokenMask = self::$tokenShortcuts['operator'] & ~Tokenizer::TOKEN_LOGICAL_NOT_2;
223
224 25
        $this->shiftAdjacentUnaryOperator($token, $tokenMask);
225 25
    }
226
227
228 73
    private function shiftBinaryOperator(Token $token): void
229
    {
230 73
        if ($this->stack->isEmpty() || $this->isTopStackToken(Tokenizer::TOKEN_GROUP_BEGIN)) {
231 17
            $this->addCorrection(self::CORRECTION_BINARY_OPERATOR_MISSING_LEFT_OPERAND_IGNORED, $token);
232
233 17
            return;
234
        }
235
236 66
        if ($this->isTopStackToken(self::$tokenShortcuts['operator'])) {
237 14
            $this->ignoreBinaryOperatorFollowingOperator($token);
238
239 14
            return;
240
        }
241
242 62
        $this->stack->push($token);
243 62
    }
244
245
246 134
    private function shiftTerm(Token $token): Term
247
    {
248 134
        return new Term($token);
249
    }
250
251
252 52
    private function shiftGroupBegin(Token $token): void
253
    {
254 52
        $this->stack->push($token);
255 52
    }
256
257
258 52
    private function shiftGroupEnd(Token $token): Group
259
    {
260 52
        $this->stack->push($token);
261
262 52
        return new Group;
263
    }
264
265
266 1
    private function shiftBailout(Token $token): void
267
    {
268 1
        $this->addCorrection(self::CORRECTION_BAILOUT_TOKEN_IGNORED, $token);
269 1
    }
270
271
272 134
    private function reducePreference(AbstractNode $node): AbstractNode
273
    {
274 134
        if (! $this->isTopStackToken(self::$tokenShortcuts['operatorPreference'])) {
275 106
            return $node;
276
        }
277
278 41
        $token = $this->stack->pop();
279
280 41
        if ($this->isToken($token, Tokenizer::TOKEN_MANDATORY)) {
281 25
            return new Mandatory($node, $token);
282
        }
283
284 17
        return new Prohibited($node, $token);
285
    }
286
287
288 134
    private function reduceLogicalNot(AbstractNode $node): AbstractNode
289
    {
290 134
        if (! $this->isTopStackToken(self::$tokenShortcuts['operatorNot'])) {
291 127
            return $node;
292
        }
293
294 53
        if ($node instanceof Mandatory || $node instanceof Prohibited) {
295 11
            $this->ignoreLogicalNotOperatorsPrecedingPreferenceOperator();
296
297 11
            return $node;
298
        }
299
300 42
        return new LogicalNot($node, $this->stack->pop());
301
    }
302
303
304 134
    private function reduceLogicalAnd(AbstractNode $node): AbstractNode
305
    {
306 134
        if ($this->stack->count() <= 1 || ! $this->isTopStackToken(Tokenizer::TOKEN_LOGICAL_AND)) {
307 134
            return $node;
308
        }
309
310 29
        $token = $this->stack->pop();
311 29
        $leftOperand = $this->stack->pop();
312
313 29
        return new LogicalAnd($leftOperand, $node, $token);
314
    }
315
316
317
    /**
318
     * Reduce logical OR.
319
     *
320
     * @param bool $inGroup Reduce inside a group
321
     * @return LogicalOr|AbstractNode|null
322
     */
323 134
    private function reduceLogicalOr(AbstractNode $node, bool $inGroup = false)
324
    {
325 134
        if ($this->stack->count() <= 1 || ! $this->isTopStackToken(Tokenizer::TOKEN_LOGICAL_OR)) {
326 134
            return $node;
327
        }
328
329
        // If inside a group don't look for following logical AND
330 29
        if (! $inGroup) {
331 29
            $this->popWhitespace();
332
            // If the next token is logical AND, put the node on stack
333
            // as that has precedence over logical OR
334 29
            if ($this->isToken(reset($this->tokens), Tokenizer::TOKEN_LOGICAL_AND)) {
335 7
                $this->stack->push($node);
336
337 7
                return null;
338
            }
339
        }
340
341 29
        $token = $this->stack->pop();
342 29
        $leftOperand = $this->stack->pop();
343
344 29
        return new LogicalOr($leftOperand, $node, $token);
345
    }
346
347
348 52
    private function reduceGroup(Group $group): ?Group
349
    {
350 52
        $rightDelimiter = $this->stack->pop();
351
352
        // Pop dangling tokens
353 52
        $this->popTokens(~Tokenizer::TOKEN_GROUP_BEGIN);
354
355 52
        if ($this->isTopStackToken(Tokenizer::TOKEN_GROUP_BEGIN)) {
356 29
            $leftDelimiter = $this->stack->pop();
357 29
            $this->ignoreEmptyGroup($leftDelimiter, $rightDelimiter);
358 29
            $this->reduceRemainingLogicalOr(true);
359
360 29
            return null;
361
        }
362
363 24
        $this->reduceRemainingLogicalOr(true);
364
365 24
        $group->setNodes($this->collectTopStackNodes());
366 24
        $group->setTokenLeft($this->stack->pop());
367 24
        $group->setTokenRight($rightDelimiter);
368
369 24
        return $group;
370
    }
371
372
373
    /**
374
     * @return mixed
375
     */
376 134
    private function shift()
377
    {
378 134
        $token = array_shift($this->tokens);
379 134
        $shift = self::$shifts[$token->getType()];
380
381 134
        return $this->{$shift}($token);
382
    }
383
384
385 134
    private function reduce(AbstractNode $node): void
386
    {
387 134
        $previousNode = null;
388 134
        $reductionIndex = null;
389
390 134
        while ($node instanceof AbstractNode) {
391
            // Reset reduction index on first iteration or on Node change
392 134
            if ($node !== $previousNode) {
393 134
                $reductionIndex = 0;
394
            }
395
396
            // If there are no reductions to try, put the Node on the stack
397
            // and continue shifting
398 134
            $reduction = $this->getReduction($node, $reductionIndex);
399 134
            if ($reduction === null) {
400 134
                $this->stack->push($node);
401 134
                break;
402
            }
403
404 134
            $previousNode = $node;
405 134
            $node = $this->{$reduction}($node);
406 134
            ++$reductionIndex;
407
        }
408 134
    }
409
410
411 14
    private function ignoreBinaryOperatorFollowingOperator(Token $token): void
412
    {
413 14
        $precedingOperators = $this->ignorePrecedingOperators(self::$tokenShortcuts['operator']);
414 14
        $followingOperators = $this->ignoreFollowingOperators();
415
416 14
        $this->addCorrection(
417 14
            self::CORRECTION_BINARY_OPERATOR_FOLLOWING_OPERATOR_IGNORED,
418 14
            ...array_merge($precedingOperators, [$token], $followingOperators)
419
        );
420 14
    }
421
422
423
    /**
424
     * Collect all Nodes from the top of the stack.
425
     *
426
     * @return AbstractNode[]
427
     */
428 24
    private function collectTopStackNodes()
429
    {
430 24
        $nodes = [];
431
432 24
        while (! $this->stack->isEmpty() && $this->stack->top() instanceof AbstractNode) {
433 24
            array_unshift($nodes, $this->stack->pop());
434
        }
435
436 24
        return $nodes;
437
    }
438
439
440 29
    private function ignoreEmptyGroup(Token $leftDelimiter, Token $rightDelimiter): void
441
    {
442 29
        $precedingOperators = $this->ignorePrecedingOperators(self::$tokenShortcuts['operator']);
443 29
        $followingOperators = $this->ignoreFollowingOperators();
444
445 29
        $this->addCorrection(
446 29
            self::CORRECTION_EMPTY_GROUP_IGNORED,
447 29
            ...array_merge($precedingOperators, [$leftDelimiter, $rightDelimiter], $followingOperators)
448
        );
449 29
    }
450
451
452
    /**
453
     * Initialize the parser with given array of $tokens.
454
     *
455
     * @param Token[] $tokens
456
     */
457 135
    private function init(array $tokens): void
458
    {
459 135
        $this->corrections = [];
460 135
        $this->tokens = $tokens;
461 135
        $this->cleanupGroupDelimiters($this->tokens);
462 135
        $this->stack = new SplStack();
463 135
    }
464
465
466 134
    private function getReduction(AbstractNode $node, int $reductionIndex): ?string
467
    {
468 134
        $reductionGroup = self::$nodeToReductionGroup[get_class($node)];
469
470 134
        if (isset(self::$reductionGroups[$reductionGroup][$reductionIndex])) {
471 134
            return self::$reductionGroups[$reductionGroup][$reductionIndex];
472
        }
473
474 134
        return null;
475
    }
476
477
478 135
    private function reduceQuery(): void
479
    {
480 135
        $this->popTokens();
481 135
        $this->reduceRemainingLogicalOr();
482 135
        $nodes = [];
483
484 135
        while (! $this->stack->isEmpty()) {
485 134
            array_unshift($nodes, $this->stack->pop());
486
        }
487
488 135
        $this->stack->push(new Query($nodes));
489 135
    }
490
491
492
    /**
493
     * Check if the given $token is an instance of Token.
494
     *
495
     * Optionally also checks given Token $typeMask.
496
     *
497
     * @param mixed $token
498
     * @param int $typeMask
499
     *
500
     * @return bool
501
     */
502 134
    private function isToken($token, $typeMask = null)
503
    {
504 134
        if (! $token instanceof Token) {
505 134
            return false;
506
        }
507
508 134
        if ($typeMask === null || (bool) ($token->getType() & $typeMask)) {
509 131
            return true;
510
        }
511
512 134
        return false;
513
    }
514
515
516 135
    private function isTopStackToken(?int $type = null): bool
517
    {
518 135
        return ! $this->stack->isEmpty() && $this->isToken($this->stack->top(), $type);
519
    }
520
521
522
    /**
523
     * Remove whitespace Tokens from the beginning of the token array.
524
     */
525 29
    private function popWhitespace(): void
526
    {
527 29
        while ($this->isToken(reset($this->tokens), Tokenizer::TOKEN_WHITESPACE)) {
528 14
            array_shift($this->tokens);
529
        }
530 29
    }
531
532
533
    /**
534
     * Remove all Tokens from the top of the query stack and log Corrections as necessary.
535
     *
536
     * Optionally also checks that Token matches given $typeMask.
537
     *
538
     * @param int $typeMask
539
     */
540 135
    private function popTokens($typeMask = null): void
541
    {
542 135
        while ($this->isTopStackToken($typeMask)) {
543
            /** @var Token $token */
544 14
            $token = $this->stack->pop();
545 14
            if ((bool) ($token->getType() & self::$tokenShortcuts['operatorUnary'])) {
546 11
                $this->addCorrection(self::CORRECTION_UNARY_OPERATOR_MISSING_OPERAND_IGNORED, $token);
547
            } else {
548 6
                $this->addCorrection(self::CORRECTION_BINARY_OPERATOR_MISSING_RIGHT_OPERAND_IGNORED, $token);
549
            }
550
        }
551 135
    }
552
553
554 52
    private function ignorePrecedingOperators(?int $type): array
555
    {
556 52
        $tokens = [];
557 52
        while ($this->isTopStackToken($type)) {
558 47
            array_unshift($tokens, $this->stack->pop());
559
        }
560
561 52
        return $tokens;
562
    }
563
564
565 43
    private function ignoreFollowingOperators(): array
566
    {
567 43
        $tokenMask = self::$tokenShortcuts['binaryOperatorAndWhitespace'];
568 43
        $tokens = [];
569 43
        while ($this->isToken(reset($this->tokens), $tokenMask)) {
570 21
            $token = array_shift($this->tokens);
571 21
            if ((bool) ($token->getType() & self::$tokenShortcuts['operatorBinary'])) {
572 4
                $tokens[] = $token;
573
            }
574
        }
575
576 43
        return $tokens;
577
    }
578
579
580
    /**
581
     * Reduce logical OR possibly remaining after reaching end of group or query.
582
     *
583
     * @param bool $inGroup Reduce inside a group
584
     */
585 135
    private function reduceRemainingLogicalOr($inGroup = false): void
586
    {
587 135
        if (! $this->stack->isEmpty() && ! $this->isTopStackToken()) {
588 134
            $node = $this->reduceLogicalOr($this->stack->pop(), $inGroup);
589 134
            $this->stack->push($node);
590
        }
591 135
    }
592
593
594
    /**
595
     * Clean up group delimiter tokens, removing unmatched left and right delimiter.
596
     *
597
     * Closest group delimiters will be matched first, unmatched remainder is removed.
598
     *
599
     * @param Token[] $tokens
600
     */
601 135
    private function cleanupGroupDelimiters(array &$tokens): void
602
    {
603 135
        $indexes = $this->getUnmatchedGroupDelimiterIndexes($tokens);
604
605 135
        while (count($indexes) > 0) {
606 10
            $lastIndex = array_pop($indexes);
607 10
            $token = $tokens[$lastIndex];
608 10
            unset($tokens[$lastIndex]);
609
610 10
            if ($token->getType() === Tokenizer::TOKEN_GROUP_BEGIN) {
611 9
                $this->addCorrection(self::CORRECTION_UNMATCHED_GROUP_LEFT_DELIMITER_IGNORED, $token);
612
            } else {
613 1
                $this->addCorrection(self::CORRECTION_UNMATCHED_GROUP_RIGHT_DELIMITER_IGNORED, $token);
614
            }
615
        }
616 135
    }
617
618
619 135
    private function getUnmatchedGroupDelimiterIndexes(array &$tokens): array
620
    {
621 135
        $trackLeft = [];
622 135
        $trackRight = [];
623
624 135
        foreach ($tokens as $index => $token) {
625 134
            if (! $this->isToken($token, self::$tokenShortcuts['groupDelimiter'])) {
626 134
                continue;
627
            }
628
629 60
            if ($this->isToken($token, Tokenizer::TOKEN_GROUP_BEGIN)) {
630 60
                $trackLeft[] = $index;
631 60
                continue;
632
            }
633
634 52
            if (count($trackLeft) === 0) {
635 1
                $trackRight[] = $index;
636
            } else {
637 52
                array_pop($trackLeft);
638
            }
639
        }
640
641 135
        return array_merge($trackLeft, $trackRight);
642
    }
643
644
645
    /**
646
     * @param mixed $type
647
     */
648 93
    private function addCorrection($type, Token ...$tokens): void
649
    {
650 93
        $this->corrections[] = new Correction($type, ...$tokens);
651 93
    }
652
653
}
654