BinaryOperatorSpacesFixer::replacePlaceholders()   C
last analyzed

Complexity

Conditions 17
Paths 3

Size

Total Lines 71
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 41
c 1
b 0
f 0
dl 0
loc 71
rs 5.2166
cc 17
nc 3
nop 2

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
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of PHP CS Fixer.
7
 *
8
 * (c) Fabien Potencier <[email protected]>
9
 *     Dariusz Rumiński <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace PhpCsFixer\Fixer\Operator;
16
17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
19
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
20
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
21
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
22
use PhpCsFixer\FixerDefinition\CodeSample;
23
use PhpCsFixer\FixerDefinition\FixerDefinition;
24
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
25
use PhpCsFixer\Preg;
26
use PhpCsFixer\Tokenizer\CT;
27
use PhpCsFixer\Tokenizer\Token;
28
use PhpCsFixer\Tokenizer\Tokens;
29
use PhpCsFixer\Tokenizer\TokensAnalyzer;
30
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
31
32
/**
33
 * @author Dariusz Rumiński <[email protected]>
34
 * @author SpacePossum
35
 */
36
final class BinaryOperatorSpacesFixer extends AbstractFixer implements ConfigurableFixerInterface
37
{
38
    /**
39
     * @internal
40
     */
41
    public const SINGLE_SPACE = 'single_space';
42
43
    /**
44
     * @internal
45
     */
46
    public const NO_SPACE = 'no_space';
47
48
    /**
49
     * @internal
50
     */
51
    public const ALIGN = 'align';
52
53
    /**
54
     * @internal
55
     */
56
    public const ALIGN_SINGLE_SPACE = 'align_single_space';
57
58
    /**
59
     * @internal
60
     */
61
    public const ALIGN_SINGLE_SPACE_MINIMAL = 'align_single_space_minimal';
62
63
    /**
64
     * @internal
65
     * @const Placeholder used as anchor for right alignment.
66
     */
67
    public const ALIGN_PLACEHOLDER = "\x2 ALIGNABLE%d \x3";
68
69
    /**
70
     * @var string[]
71
     */
72
    private const SUPPORTED_OPERATORS = [
73
        '=',
74
        '*',
75
        '/',
76
        '%',
77
        '<',
78
        '>',
79
        '|',
80
        '^',
81
        '+',
82
        '-',
83
        '&',
84
        '&=',
85
        '&&',
86
        '||',
87
        '.=',
88
        '/=',
89
        '=>',
90
        '==',
91
        '>=',
92
        '===',
93
        '!=',
94
        '<>',
95
        '!==',
96
        '<=',
97
        'and',
98
        'or',
99
        'xor',
100
        '-=',
101
        '%=',
102
        '*=',
103
        '|=',
104
        '+=',
105
        '<<',
106
        '<<=',
107
        '>>',
108
        '>>=',
109
        '^=',
110
        '**',
111
        '**=',
112
        '<=>',
113
        '??',
114
        '??=',
115
    ];
116
117
    /**
118
     * Keep track of the deepest level ever achieved while
119
     * parsing the code. Used later to replace alignment
120
     * placeholders with spaces.
121
     *
122
     * @var int
123
     */
124
    private $deepestLevel;
125
126
    /**
127
     * Level counter of the current nest level.
128
     * So one level alignments are not mixed with
129
     * other level ones.
130
     *
131
     * @var int
132
     */
133
    private $currentLevel;
134
135
    private static $allowedValues = [
136
        self::ALIGN,
137
        self::ALIGN_SINGLE_SPACE,
138
        self::ALIGN_SINGLE_SPACE_MINIMAL,
139
        self::SINGLE_SPACE,
140
        self::NO_SPACE,
141
        null,
142
    ];
143
144
    /**
145
     * @var TokensAnalyzer
146
     */
147
    private $tokensAnalyzer;
148
149
    /**
150
     * @var array<string, string>
151
     */
152
    private $alignOperatorTokens = [];
153
154
    /**
155
     * @var array<string, string>
156
     */
157
    private $operators = [];
158
159
    /**
160
     * {@inheritdoc}
161
     */
162
    public function configure(array $configuration): void
163
    {
164
        parent::configure($configuration);
165
166
        $this->operators = $this->resolveOperatorsFromConfig();
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172
    public function getDefinition(): FixerDefinitionInterface
173
    {
174
        return new FixerDefinition(
175
            'Binary operators should be surrounded by space as configured.',
176
            [
177
                new CodeSample(
178
                    "<?php\n\$a= 1  + \$b^ \$d !==  \$e or   \$f;\n"
179
                ),
180
                new CodeSample(
181
                    '<?php
182
$aa=  1;
183
$b=2;
184
185
$c = $d    xor    $e;
186
$f    -=  1;
187
',
188
                    ['operators' => ['=' => 'align', 'xor' => null]]
189
                ),
190
                new CodeSample(
191
                    '<?php
192
$a = $b +=$c;
193
$d = $ee+=$f;
194
195
$g = $b     +=$c;
196
$h = $ee+=$f;
197
',
198
                    ['operators' => ['+=' => 'align_single_space']]
199
                ),
200
                new CodeSample(
201
                    '<?php
202
$a = $b===$c;
203
$d = $f   ===  $g;
204
$h = $i===  $j;
205
',
206
                    ['operators' => ['===' => 'align_single_space_minimal']]
207
                ),
208
                new CodeSample(
209
                    '<?php
210
$foo = \json_encode($bar, JSON_PRESERVE_ZERO_FRACTION | JSON_PRETTY_PRINT);
211
',
212
                    ['operators' => ['|' => 'no_space']]
213
                ),
214
                new CodeSample(
215
                    '<?php
216
$array = [
217
    "foo"            =>   1,
218
    "baaaaaaaaaaar"  =>  11,
219
];
220
',
221
                    ['operators' => ['=>' => 'single_space']]
222
                ),
223
                new CodeSample(
224
                    '<?php
225
$array = [
226
    "foo" => 12,
227
    "baaaaaaaaaaar"  => 13,
228
];
229
',
230
                    ['operators' => ['=>' => 'align']]
231
                ),
232
                new CodeSample(
233
                    '<?php
234
$array = [
235
    "foo" => 12,
236
    "baaaaaaaaaaar"  => 13,
237
];
238
',
239
                    ['operators' => ['=>' => 'align_single_space']]
240
                ),
241
                new CodeSample(
242
                    '<?php
243
$array = [
244
    "foo" => 12,
245
    "baaaaaaaaaaar"  => 13,
246
];
247
',
248
                    ['operators' => ['=>' => 'align_single_space_minimal']]
249
                ),
250
            ]
251
        );
252
    }
253
254
    /**
255
     * {@inheritdoc}
256
     *
257
     * Must run after ArrayIndentationFixer, ArraySyntaxFixer, ListSyntaxFixer, NoMultilineWhitespaceAroundDoubleArrowFixer, NoUnsetCastFixer, PowToExponentiationFixer, StandardizeNotEqualsFixer, StrictComparisonFixer.
258
     */
259
    public function getPriority(): int
260
    {
261
        return -32;
262
    }
263
264
    /**
265
     * {@inheritdoc}
266
     */
267
    public function isCandidate(Tokens $tokens): bool
268
    {
269
        return true;
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
276
    {
277
        $this->tokensAnalyzer = new TokensAnalyzer($tokens);
278
279
        // last and first tokens cannot be an operator
280
        for ($index = $tokens->count() - 2; $index > 0; --$index) {
281
            if (!$this->tokensAnalyzer->isBinaryOperator($index)) {
282
                continue;
283
            }
284
285
            if ('=' === $tokens[$index]->getContent()) {
286
                $isDeclare = $this->isEqualPartOfDeclareStatement($tokens, $index);
287
                if (false === $isDeclare) {
288
                    $this->fixWhiteSpaceAroundOperator($tokens, $index);
289
                } else {
290
                    $index = $isDeclare; // skip `declare(foo ==bar)`, see `declare_equal_normalize`
291
                }
292
            } else {
293
                $this->fixWhiteSpaceAroundOperator($tokens, $index);
294
            }
295
296
            // previous of binary operator is now never an operator / previous of declare statement cannot be an operator
297
            --$index;
298
        }
299
300
        if (\count($this->alignOperatorTokens)) {
301
            $this->fixAlignment($tokens, $this->alignOperatorTokens);
302
        }
303
    }
304
305
    /**
306
     * {@inheritdoc}
307
     */
308
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
309
    {
310
        return new FixerConfigurationResolver([
311
            (new FixerOptionBuilder('default', 'Default fix strategy.'))
312
                ->setDefault(self::SINGLE_SPACE)
313
                ->setAllowedValues(self::$allowedValues)
314
                ->getOption(),
315
            (new FixerOptionBuilder('operators', 'Dictionary of `binary operator` => `fix strategy` values that differ from the default strategy.'))
316
                ->setAllowedTypes(['array'])
317
                ->setAllowedValues([static function (array $option) {
318
                    foreach ($option as $operator => $value) {
319
                        if (!\in_array($operator, self::SUPPORTED_OPERATORS, true)) {
320
                            throw new InvalidOptionsException(
321
                                sprintf(
322
                                    'Unexpected "operators" key, expected any of "%s", got "%s".',
323
                                    implode('", "', self::SUPPORTED_OPERATORS),
324
                                    \gettype($operator).'#'.$operator
325
                                )
326
                            );
327
                        }
328
329
                        if (!\in_array($value, self::$allowedValues, true)) {
330
                            throw new InvalidOptionsException(
331
                                sprintf(
332
                                    'Unexpected value for operator "%s", expected any of "%s", got "%s".',
333
                                    $operator,
334
                                    implode('", "', self::$allowedValues),
335
                                    \is_object($value) ? \get_class($value) : (null === $value ? 'null' : \gettype($value).'#'.$value)
336
                                )
337
                            );
338
                        }
339
                    }
340
341
                    return true;
342
                }])
343
                ->setDefault([])
344
                ->getOption(),
345
        ]);
346
    }
347
348
    private function fixWhiteSpaceAroundOperator(Tokens $tokens, int $index): void
349
    {
350
        $tokenContent = strtolower($tokens[$index]->getContent());
351
352
        if (!\array_key_exists($tokenContent, $this->operators)) {
353
            return; // not configured to be changed
354
        }
355
356
        if (self::SINGLE_SPACE === $this->operators[$tokenContent]) {
357
            $this->fixWhiteSpaceAroundOperatorToSingleSpace($tokens, $index);
358
359
            return;
360
        }
361
362
        if (self::NO_SPACE === $this->operators[$tokenContent]) {
363
            $this->fixWhiteSpaceAroundOperatorToNoSpace($tokens, $index);
364
365
            return;
366
        }
367
368
        // schedule for alignment
369
        $this->alignOperatorTokens[$tokenContent] = $this->operators[$tokenContent];
370
371
        if (self::ALIGN === $this->operators[$tokenContent]) {
372
            return;
373
        }
374
375
        // fix white space after operator
376
        if ($tokens[$index + 1]->isWhitespace()) {
377
            if (self::ALIGN_SINGLE_SPACE_MINIMAL === $this->operators[$tokenContent]) {
378
                $tokens[$index + 1] = new Token([T_WHITESPACE, ' ']);
379
            }
380
381
            return;
382
        }
383
384
        $tokens->insertAt($index + 1, new Token([T_WHITESPACE, ' ']));
385
    }
386
387
    private function fixWhiteSpaceAroundOperatorToSingleSpace(Tokens $tokens, int $index): void
388
    {
389
        // fix white space after operator
390
        if ($tokens[$index + 1]->isWhitespace()) {
391
            $content = $tokens[$index + 1]->getContent();
392
            if (' ' !== $content && false === strpos($content, "\n") && !$tokens[$tokens->getNextNonWhitespace($index + 1)]->isComment()) {
393
                $tokens[$index + 1] = new Token([T_WHITESPACE, ' ']);
394
            }
395
        } else {
396
            $tokens->insertAt($index + 1, new Token([T_WHITESPACE, ' ']));
397
        }
398
399
        // fix white space before operator
400
        if ($tokens[$index - 1]->isWhitespace()) {
401
            $content = $tokens[$index - 1]->getContent();
402
            if (' ' !== $content && false === strpos($content, "\n") && !$tokens[$tokens->getPrevNonWhitespace($index - 1)]->isComment()) {
403
                $tokens[$index - 1] = new Token([T_WHITESPACE, ' ']);
404
            }
405
        } else {
406
            $tokens->insertAt($index, new Token([T_WHITESPACE, ' ']));
407
        }
408
    }
409
410
    private function fixWhiteSpaceAroundOperatorToNoSpace(Tokens $tokens, int $index): void
411
    {
412
        // fix white space after operator
413
        if ($tokens[$index + 1]->isWhitespace()) {
414
            $content = $tokens[$index + 1]->getContent();
415
            if (false === strpos($content, "\n") && !$tokens[$tokens->getNextNonWhitespace($index + 1)]->isComment()) {
416
                $tokens->clearAt($index + 1);
417
            }
418
        }
419
420
        // fix white space before operator
421
        if ($tokens[$index - 1]->isWhitespace()) {
422
            $content = $tokens[$index - 1]->getContent();
423
            if (false === strpos($content, "\n") && !$tokens[$tokens->getPrevNonWhitespace($index - 1)]->isComment()) {
424
                $tokens->clearAt($index - 1);
425
            }
426
        }
427
    }
428
429
    /**
430
     * @return false|int index of T_DECLARE where the `=` belongs to or `false`
431
     */
432
    private function isEqualPartOfDeclareStatement(Tokens $tokens, int $index)
433
    {
434
        $prevMeaningfulIndex = $tokens->getPrevMeaningfulToken($index);
435
        if ($tokens[$prevMeaningfulIndex]->isGivenKind(T_STRING)) {
436
            $prevMeaningfulIndex = $tokens->getPrevMeaningfulToken($prevMeaningfulIndex);
0 ignored issues
show
Bug introduced by
It seems like $prevMeaningfulIndex can also be of type null; however, parameter $index of PhpCsFixer\Tokenizer\Tok...etPrevMeaningfulToken() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

436
            $prevMeaningfulIndex = $tokens->getPrevMeaningfulToken(/** @scrutinizer ignore-type */ $prevMeaningfulIndex);
Loading history...
437
            if ($tokens[$prevMeaningfulIndex]->equals('(')) {
438
                $prevMeaningfulIndex = $tokens->getPrevMeaningfulToken($prevMeaningfulIndex);
439
                if ($tokens[$prevMeaningfulIndex]->isGivenKind(T_DECLARE)) {
440
                    return $prevMeaningfulIndex;
441
                }
442
            }
443
        }
444
445
        return false;
446
    }
447
448
    /**
449
     * @return array<string, string>
450
     */
451
    private function resolveOperatorsFromConfig(): array
452
    {
453
        $operators = [];
454
455
        if (null !== $this->configuration['default']) {
456
            foreach (self::SUPPORTED_OPERATORS as $operator) {
457
                $operators[$operator] = $this->configuration['default'];
458
            }
459
        }
460
461
        foreach ($this->configuration['operators'] as $operator => $value) {
462
            if (null === $value) {
463
                unset($operators[$operator]);
464
            } else {
465
                $operators[$operator] = $value;
466
            }
467
        }
468
469
        // @TODO: drop condition when PHP 7.4+ is required
470
        if (!\defined('T_COALESCE_EQUAL')) {
471
            unset($operators['??=']);
472
        }
473
474
        return $operators;
475
    }
476
477
    // Alignment logic related methods
478
479
    /**
480
     * @param array<string, string> $toAlign
481
     */
482
    private function fixAlignment(Tokens $tokens, array $toAlign): void
483
    {
484
        $this->deepestLevel = 0;
485
        $this->currentLevel = 0;
486
487
        foreach ($toAlign as $tokenContent => $alignStrategy) {
488
            // This fixer works partially on Tokens and partially on string representation of code.
489
            // During the process of fixing internal state of single Token may be affected by injecting ALIGN_PLACEHOLDER to its content.
490
            // The placeholder will be resolved by `replacePlaceholders` method by removing placeholder or changing it into spaces.
491
            // That way of fixing the code causes disturbances in marking Token as changed - if code is perfectly valid then placeholder
492
            // still be injected and removed, which will cause the `changed` flag to be set.
493
            // To handle that unwanted behavior we work on clone of Tokens collection and then override original collection with fixed collection.
494
            $tokensClone = clone $tokens;
495
496
            if ('=>' === $tokenContent) {
497
                $this->injectAlignmentPlaceholdersForArrow($tokensClone, 0, \count($tokens));
498
            } else {
499
                $this->injectAlignmentPlaceholders($tokensClone, 0, \count($tokens), $tokenContent);
500
            }
501
502
            // for all tokens that should be aligned but do not have anything to align with, fix spacing if needed
503
            if (self::ALIGN_SINGLE_SPACE === $alignStrategy || self::ALIGN_SINGLE_SPACE_MINIMAL === $alignStrategy) {
504
                if ('=>' === $tokenContent) {
505
                    for ($index = $tokens->count() - 2; $index > 0; --$index) {
506
                        if ($tokens[$index]->isGivenKind(T_DOUBLE_ARROW)) { // always binary operator, never part of declare statement
507
                            $this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy);
508
                        }
509
                    }
510
                } elseif ('=' === $tokenContent) {
511
                    for ($index = $tokens->count() - 2; $index > 0; --$index) {
512
                        if ('=' === $tokens[$index]->getContent() && !$this->isEqualPartOfDeclareStatement($tokens, $index) && $this->tokensAnalyzer->isBinaryOperator($index)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->isEqualPartOfDecl...tement($tokens, $index) of type false|integer is loosely compared to false; this is ambiguous if the integer can be 0. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
513
                            $this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy);
514
                        }
515
                    }
516
                } else {
517
                    for ($index = $tokens->count() - 2; $index > 0; --$index) {
518
                        $content = $tokens[$index]->getContent();
519
                        if (strtolower($content) === $tokenContent && $this->tokensAnalyzer->isBinaryOperator($index)) { // never part of declare statement
520
                            $this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy);
521
                        }
522
                    }
523
                }
524
            }
525
526
            $tokens->setCode($this->replacePlaceholders($tokensClone, $alignStrategy));
527
        }
528
    }
529
530
    private function injectAlignmentPlaceholders(Tokens $tokens, int $startAt, int $endAt, string $tokenContent): void
531
    {
532
        for ($index = $startAt; $index < $endAt; ++$index) {
533
            $token = $tokens[$index];
534
535
            $content = $token->getContent();
536
            if (
537
                strtolower($content) === $tokenContent
538
                && $this->tokensAnalyzer->isBinaryOperator($index)
539
                && ('=' !== $content || !$this->isEqualPartOfDeclareStatement($tokens, $index))
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->isEqualPartOfDecl...tement($tokens, $index) of type false|integer is loosely compared to false; this is ambiguous if the integer can be 0. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
540
            ) {
541
                $tokens[$index] = new Token(sprintf(self::ALIGN_PLACEHOLDER, $this->deepestLevel).$content);
542
543
                continue;
544
            }
545
546
            if ($token->isGivenKind(T_FUNCTION)) {
547
                ++$this->deepestLevel;
548
549
                continue;
550
            }
551
552
            if ($token->equals('(')) {
553
                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
554
555
                continue;
556
            }
557
558
            if ($token->equals('[')) {
559
                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE, $index);
560
561
                continue;
562
            }
563
564
            if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) {
565
                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index);
566
567
                continue;
568
            }
569
        }
570
    }
571
572
    private function injectAlignmentPlaceholdersForArrow(Tokens $tokens, int $startAt, int $endAt): void
573
    {
574
        for ($index = $startAt; $index < $endAt; ++$index) {
575
            $token = $tokens[$index];
576
577
            if ($token->isGivenKind([T_FOREACH, T_FOR, T_WHILE, T_IF, T_SWITCH])) {
578
                $index = $tokens->getNextMeaningfulToken($index);
579
                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
0 ignored issues
show
Bug introduced by
It seems like $index can also be of type null; however, parameter $searchIndex of PhpCsFixer\Tokenizer\Tokens::findBlockEnd() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

579
                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, /** @scrutinizer ignore-type */ $index);
Loading history...
580
581
                continue;
582
            }
583
584
            if ($token->isGivenKind(T_ARRAY)) { // don't use "$tokens->isArray()" here, short arrays are handled in the next case
585
                $from = $tokens->getNextMeaningfulToken($index);
586
                $until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $from);
587
                $index = $until;
588
589
                $this->injectArrayAlignmentPlaceholders($tokens, $from + 1, $until - 1);
590
591
                continue;
592
            }
593
594
            if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) {
595
                $from = $index;
596
                $until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $from);
597
                $index = $until;
598
599
                $this->injectArrayAlignmentPlaceholders($tokens, $from + 1, $until - 1);
600
601
                continue;
602
            }
603
604
            if ($token->isGivenKind(T_DOUBLE_ARROW)) { // no need to analyze for `isBinaryOperator` (always true), nor if part of declare statement (not valid PHP)
605
                $tokenContent = sprintf(self::ALIGN_PLACEHOLDER, $this->currentLevel).$token->getContent();
606
607
                $nextToken = $tokens[$index + 1];
608
                if (!$nextToken->isWhitespace()) {
609
                    $tokenContent .= ' ';
610
                } elseif ($nextToken->isWhitespace(" \t")) {
611
                    $tokens[$index + 1] = new Token([T_WHITESPACE, ' ']);
612
                }
613
614
                $tokens[$index] = new Token([T_DOUBLE_ARROW, $tokenContent]);
615
616
                continue;
617
            }
618
619
            if ($token->equals(';')) {
620
                ++$this->deepestLevel;
621
                ++$this->currentLevel;
622
623
                continue;
624
            }
625
626
            if ($token->equals(',')) {
627
                for ($i = $index; $i < $endAt - 1; ++$i) {
628
                    if (false !== strpos($tokens[$i - 1]->getContent(), "\n")) {
629
                        break;
630
                    }
631
632
                    if ($tokens[$i + 1]->isGivenKind([T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN])) {
633
                        $arrayStartIndex = $tokens[$i + 1]->isGivenKind(T_ARRAY)
634
                            ? $tokens->getNextMeaningfulToken($i + 1)
635
                            : $i + 1
636
                        ;
637
                        $blockType = Tokens::detectBlockType($tokens[$arrayStartIndex]);
638
                        $arrayEndIndex = $tokens->findBlockEnd($blockType['type'], $arrayStartIndex);
639
640
                        if ($tokens->isPartialCodeMultiline($arrayStartIndex, $arrayEndIndex)) {
0 ignored issues
show
Bug introduced by
It seems like $arrayStartIndex can also be of type null; however, parameter $start of PhpCsFixer\Tokenizer\Tok...sPartialCodeMultiline() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

640
                        if ($tokens->isPartialCodeMultiline(/** @scrutinizer ignore-type */ $arrayStartIndex, $arrayEndIndex)) {
Loading history...
641
                            break;
642
                        }
643
                    }
644
645
                    ++$index;
646
                }
647
            }
648
        }
649
    }
650
651
    private function injectArrayAlignmentPlaceholders(Tokens $tokens, int $from, int $until): void
652
    {
653
        // Only inject placeholders for multi-line arrays
654
        if ($tokens->isPartialCodeMultiline($from, $until)) {
655
            ++$this->deepestLevel;
656
            ++$this->currentLevel;
657
            $this->injectAlignmentPlaceholdersForArrow($tokens, $from, $until);
658
            --$this->currentLevel;
659
        }
660
    }
661
662
    private function fixWhiteSpaceBeforeOperator(Tokens $tokens, int $index, string $alignStrategy): void
663
    {
664
        // fix white space after operator is not needed as BinaryOperatorSpacesFixer took care of this (if strategy is _not_ ALIGN)
665
        if (!$tokens[$index - 1]->isWhitespace()) {
666
            $tokens->insertAt($index, new Token([T_WHITESPACE, ' ']));
667
668
            return;
669
        }
670
671
        if (self::ALIGN_SINGLE_SPACE_MINIMAL !== $alignStrategy || $tokens[$tokens->getPrevNonWhitespace($index - 1)]->isComment()) {
672
            return;
673
        }
674
675
        $content = $tokens[$index - 1]->getContent();
676
        if (' ' !== $content && false === strpos($content, "\n")) {
677
            $tokens[$index - 1] = new Token([T_WHITESPACE, ' ']);
678
        }
679
    }
680
681
    /**
682
     * Look for group of placeholders and provide vertical alignment.
683
     */
684
    private function replacePlaceholders(Tokens $tokens, string $alignStrategy): string
685
    {
686
        $tmpCode = $tokens->generateCode();
687
688
        for ($j = 0; $j <= $this->deepestLevel; ++$j) {
689
            $placeholder = sprintf(self::ALIGN_PLACEHOLDER, $j);
690
691
            if (false === strpos($tmpCode, $placeholder)) {
692
                continue;
693
            }
694
695
            $lines = explode("\n", $tmpCode);
696
            $groups = [];
697
            $groupIndex = 0;
698
            $groups[$groupIndex] = [];
699
700
            foreach ($lines as $index => $line) {
701
                if (substr_count($line, $placeholder) > 0) {
702
                    $groups[$groupIndex][] = $index;
703
                } else {
704
                    ++$groupIndex;
705
                    $groups[$groupIndex] = [];
706
                }
707
            }
708
709
            foreach ($groups as $group) {
710
                if (\count($group) < 1) {
711
                    continue;
712
                }
713
714
                if (self::ALIGN !== $alignStrategy) {
715
                    // move place holders to match strategy
716
                    foreach ($group as $index) {
717
                        $currentPosition = strpos($lines[$index], $placeholder);
718
                        $before = substr($lines[$index], 0, $currentPosition);
719
720
                        if (self::ALIGN_SINGLE_SPACE === $alignStrategy) {
721
                            if (1 > \strlen($before) || ' ' !== substr($before, -1)) { // if last char of before-content is not ' '; add it
722
                                $before .= ' ';
723
                            }
724
                        } elseif (self::ALIGN_SINGLE_SPACE_MINIMAL === $alignStrategy) {
725
                            if (1 !== Preg::match('/^\h+$/', $before)) { // if indent; do not move, leave to other fixer
726
                                $before = rtrim($before).' ';
727
                            }
728
                        }
729
730
                        $lines[$index] = $before.substr($lines[$index], $currentPosition);
731
                    }
732
                }
733
734
                $rightmostSymbol = 0;
735
                foreach ($group as $index) {
736
                    $rightmostSymbol = max($rightmostSymbol, strpos(utf8_decode($lines[$index]), $placeholder));
737
                }
738
739
                foreach ($group as $index) {
740
                    $line = $lines[$index];
741
                    $currentSymbol = strpos(utf8_decode($line), $placeholder);
742
                    $delta = abs($rightmostSymbol - $currentSymbol);
743
744
                    if ($delta > 0) {
745
                        $line = str_replace($placeholder, str_repeat(' ', $delta).$placeholder, $line);
0 ignored issues
show
Bug introduced by
It seems like $delta can also be of type double; however, parameter $times of str_repeat() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

745
                        $line = str_replace($placeholder, str_repeat(' ', /** @scrutinizer ignore-type */ $delta).$placeholder, $line);
Loading history...
746
                        $lines[$index] = $line;
747
                    }
748
                }
749
            }
750
751
            $tmpCode = str_replace($placeholder, '', implode("\n", $lines));
752
        }
753
754
        return $tmpCode;
755
    }
756
}
757