Passed
Push — master ( 79226d...44542b )
by Tomáš
03:53
created

Parser   F

Complexity

Total Complexity 80

Size/Duplication

Total Lines 630
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 219
dl 0
loc 630
ccs 217
cts 217
cp 1
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 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
A ignoreLogicalNotOperatorsPrecedingPreferenceOperator() 0 9 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 11
				...$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 59
	private function shiftLogicalNot(Token $token): void
215
	{
216 59
		$this->stack->push($token);
217 59
	}
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 52
		if ($node instanceof Mandatory || $node instanceof Prohibited) {
295 11
			$this->ignoreLogicalNotOperatorsPrecedingPreferenceOperator();
296
297 11
			return $node;
298
		}
299
300 41
		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