Parser   F
last analyzed

Complexity

Total Complexity 82

Size/Duplication

Total Lines 634
Duplicated Lines 0 %

Test Coverage

Coverage 99.09%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 221
c 2
b 0
f 0
dl 0
loc 634
ccs 217
cts 219
cp 0.9909
rs 2
wmc 82

35 Methods

Rating   Name   Duplication   Size   Complexity  
A shiftGroupEnd() 0 5 1
A shiftBinaryOperator() 0 15 4
A shiftWhitespace() 0 4 2
A shiftPreference() 0 3 1
A reduceGroup() 0 22 2
A parse() 0 15 3
A shiftGroupBegin() 0 3 1
A shiftTerm() 0 3 1
A reduceLogicalNot() 0 13 4
A reduceLogicalOr() 0 22 5
A reducePreference() 0 13 3
A shiftBailout() 0 3 1
A reduceLogicalAnd() 0 10 3
A shiftLogicalNot() 0 3 1
A ignoreLogicalNotOperatorsPrecedingPreferenceOperator() 0 9 2
A shiftAdjacentUnaryOperator() 0 9 2
A shiftLogicalNot2() 0 5 1
A isToken() 0 11 4
A ignorePrecedingOperators() 0 8 2
A popTokens() 0 9 3
A addCorrection() 0 3 1
A cleanupGroupDelimiters() 0 13 3
A getUnmatchedGroupDelimiterIndexes() 0 23 5
A popWhitespace() 0 4 2
A reduceRemainingLogicalOr() 0 5 3
A shift() 0 10 2
A ignoreEmptyGroup() 0 8 1
A ignoreFollowingOperators() 0 12 4
A init() 0 6 1
A reduce() 0 22 4
A collectTopStackNodes() 0 9 3
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
        if ($token === null) {
380
            return null;
381
        }
382
383 134
        $shift = self::$shifts[$token->getType()];
384
385 134
        return $this->{$shift}($token);
386
    }
387
388
389 134
    private function reduce(AbstractNode $node): void
390
    {
391 134
        $previousNode = null;
392 134
        $reductionIndex = 0;
393
394 134
        while ($node instanceof AbstractNode) {
395
            // Reset reduction index on first iteration or on Node change
396 134
            if ($node !== $previousNode) {
397 134
                $reductionIndex = 0;
398
            }
399
400
            // If there are no reductions to try, put the Node on the stack
401
            // and continue shifting
402 134
            $reduction = $this->getReduction($node, $reductionIndex);
403 134
            if ($reduction === null) {
404 134
                $this->stack->push($node);
405 134
                break;
406
            }
407
408 134
            $previousNode = $node;
409 134
            $node = $this->{$reduction}($node);
410 134
            ++$reductionIndex;
411
        }
412 134
    }
413
414
415 14
    private function ignoreBinaryOperatorFollowingOperator(Token $token): void
416
    {
417 14
        $precedingOperators = $this->ignorePrecedingOperators(self::$tokenShortcuts['operator']);
418 14
        $followingOperators = $this->ignoreFollowingOperators();
419
420 14
        $this->addCorrection(
421 14
            self::CORRECTION_BINARY_OPERATOR_FOLLOWING_OPERATOR_IGNORED,
422 14
            ...array_merge($precedingOperators, [$token], $followingOperators)
423
        );
424 14
    }
425
426
427
    /**
428
     * Collect all Nodes from the top of the stack.
429
     *
430
     * @return AbstractNode[]
431
     */
432 24
    private function collectTopStackNodes()
433
    {
434 24
        $nodes = [];
435
436 24
        while (! $this->stack->isEmpty() && $this->stack->top() instanceof AbstractNode) {
437 24
            array_unshift($nodes, $this->stack->pop());
438
        }
439
440 24
        return $nodes;
441
    }
442
443
444 29
    private function ignoreEmptyGroup(Token $leftDelimiter, Token $rightDelimiter): void
445
    {
446 29
        $precedingOperators = $this->ignorePrecedingOperators(self::$tokenShortcuts['operator']);
447 29
        $followingOperators = $this->ignoreFollowingOperators();
448
449 29
        $this->addCorrection(
450 29
            self::CORRECTION_EMPTY_GROUP_IGNORED,
451 29
            ...array_merge($precedingOperators, [$leftDelimiter, $rightDelimiter], $followingOperators)
452
        );
453 29
    }
454
455
456
    /**
457
     * Initialize the parser with given array of $tokens.
458
     *
459
     * @param Token[] $tokens
460
     */
461 135
    private function init(array $tokens): void
462
    {
463 135
        $this->corrections = [];
464 135
        $this->tokens = $tokens;
465 135
        $this->cleanupGroupDelimiters($this->tokens);
466 135
        $this->stack = new SplStack();
467 135
    }
468
469
470 134
    private function getReduction(AbstractNode $node, int $reductionIndex): ?string
471
    {
472 134
        $reductionGroup = self::$nodeToReductionGroup[get_class($node)];
473
474 134
        if (isset(self::$reductionGroups[$reductionGroup][$reductionIndex])) {
475 134
            return self::$reductionGroups[$reductionGroup][$reductionIndex];
476
        }
477
478 134
        return null;
479
    }
480
481
482 135
    private function reduceQuery(): void
483
    {
484 135
        $this->popTokens();
485 135
        $this->reduceRemainingLogicalOr();
486 135
        $nodes = [];
487
488 135
        while (! $this->stack->isEmpty()) {
489 134
            array_unshift($nodes, $this->stack->pop());
490
        }
491
492 135
        $this->stack->push(new Query($nodes));
493 135
    }
494
495
496
    /**
497
     * Check if the given $token is an instance of Token.
498
     *
499
     * Optionally also checks given Token $typeMask.
500
     *
501
     * @param mixed $token
502
     * @param int $typeMask
503
     *
504
     * @return bool
505
     */
506 134
    private function isToken($token, $typeMask = null)
507
    {
508 134
        if (! $token instanceof Token) {
509 134
            return false;
510
        }
511
512 134
        if ($typeMask === null || (bool) ($token->getType() & $typeMask)) {
513 131
            return true;
514
        }
515
516 134
        return false;
517
    }
518
519
520 135
    private function isTopStackToken(?int $type = null): bool
521
    {
522 135
        return ! $this->stack->isEmpty() && $this->isToken($this->stack->top(), $type);
523
    }
524
525
526
    /**
527
     * Remove whitespace Tokens from the beginning of the token array.
528
     */
529 29
    private function popWhitespace(): void
530
    {
531 29
        while ($this->isToken(reset($this->tokens), Tokenizer::TOKEN_WHITESPACE)) {
532 14
            array_shift($this->tokens);
533
        }
534 29
    }
535
536
537
    /**
538
     * Remove all Tokens from the top of the query stack and log Corrections as necessary.
539
     *
540
     * Optionally also checks that Token matches given $typeMask.
541
     *
542
     * @param int $typeMask
543
     */
544 135
    private function popTokens($typeMask = null): void
545
    {
546 135
        while ($this->isTopStackToken($typeMask)) {
547
            /** @var Token $token */
548 14
            $token = $this->stack->pop();
549 14
            if ((bool) ($token->getType() & self::$tokenShortcuts['operatorUnary'])) {
550 11
                $this->addCorrection(self::CORRECTION_UNARY_OPERATOR_MISSING_OPERAND_IGNORED, $token);
551
            } else {
552 6
                $this->addCorrection(self::CORRECTION_BINARY_OPERATOR_MISSING_RIGHT_OPERAND_IGNORED, $token);
553
            }
554
        }
555 135
    }
556
557
558 52
    private function ignorePrecedingOperators(?int $type): array
559
    {
560 52
        $tokens = [];
561 52
        while ($this->isTopStackToken($type)) {
562 47
            array_unshift($tokens, $this->stack->pop());
563
        }
564
565 52
        return $tokens;
566
    }
567
568
569 43
    private function ignoreFollowingOperators(): array
570
    {
571 43
        $tokenMask = self::$tokenShortcuts['binaryOperatorAndWhitespace'];
572 43
        $tokens = [];
573 43
        while ($this->isToken(reset($this->tokens), $tokenMask)) {
574 21
            $token = array_shift($this->tokens);
575 21
            if ($token !== null && (bool) ($token->getType() & self::$tokenShortcuts['operatorBinary'])) {
576 4
                $tokens[] = $token;
577
            }
578
        }
579
580 43
        return $tokens;
581
    }
582
583
584
    /**
585
     * Reduce logical OR possibly remaining after reaching end of group or query.
586
     *
587
     * @param bool $inGroup Reduce inside a group
588
     */
589 135
    private function reduceRemainingLogicalOr($inGroup = false): void
590
    {
591 135
        if (! $this->stack->isEmpty() && ! $this->isTopStackToken()) {
592 134
            $node = $this->reduceLogicalOr($this->stack->pop(), $inGroup);
593 134
            $this->stack->push($node);
594
        }
595 135
    }
596
597
598
    /**
599
     * Clean up group delimiter tokens, removing unmatched left and right delimiter.
600
     *
601
     * Closest group delimiters will be matched first, unmatched remainder is removed.
602
     *
603
     * @param Token[] $tokens
604
     */
605 135
    private function cleanupGroupDelimiters(array &$tokens): void
606
    {
607 135
        $indexes = $this->getUnmatchedGroupDelimiterIndexes($tokens);
608
609 135
        while (count($indexes) > 0) {
610 10
            $lastIndex = array_pop($indexes);
611 10
            $token = $tokens[$lastIndex];
612 10
            unset($tokens[$lastIndex]);
613
614 10
            if ($token->getType() === Tokenizer::TOKEN_GROUP_BEGIN) {
615 9
                $this->addCorrection(self::CORRECTION_UNMATCHED_GROUP_LEFT_DELIMITER_IGNORED, $token);
616
            } else {
617 1
                $this->addCorrection(self::CORRECTION_UNMATCHED_GROUP_RIGHT_DELIMITER_IGNORED, $token);
618
            }
619
        }
620 135
    }
621
622
623 135
    private function getUnmatchedGroupDelimiterIndexes(array &$tokens): array
624
    {
625 135
        $trackLeft = [];
626 135
        $trackRight = [];
627
628 135
        foreach ($tokens as $index => $token) {
629 134
            if (! $this->isToken($token, self::$tokenShortcuts['groupDelimiter'])) {
630 134
                continue;
631
            }
632
633 60
            if ($this->isToken($token, Tokenizer::TOKEN_GROUP_BEGIN)) {
634 60
                $trackLeft[] = $index;
635 60
                continue;
636
            }
637
638 52
            if (count($trackLeft) === 0) {
639 1
                $trackRight[] = $index;
640
            } else {
641 52
                array_pop($trackLeft);
642
            }
643
        }
644
645 135
        return array_merge($trackLeft, $trackRight);
646
    }
647
648
649
    /**
650
     * @param mixed $type
651
     */
652 93
    private function addCorrection($type, Token ...$tokens): void
653
    {
654 93
        $this->corrections[] = new Correction($type, ...$tokens);
655 93
    }
656
657
}
658