• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

keradus / PHP-CS-Fixer / 17377459942

01 Sep 2025 12:19PM UTC coverage: 94.684% (-0.009%) from 94.693%
17377459942

push

github

web-flow
chore: `Tokens::offsetSet` - explicit validation of input (#9004)

1 of 5 new or added lines in 1 file covered. (20.0%)

306 existing lines in 60 files now uncovered.

28390 of 29984 relevant lines covered (94.68%)

45.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.48
/src/Fixer/Operator/BinaryOperatorSpacesFixer.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of PHP CS Fixer.
7
 *
8
 * (c) Fabien Potencier <fabien@symfony.com>
9
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
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\Fixer\ConfigurableFixerTrait;
20
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
21
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
22
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
23
use PhpCsFixer\FixerDefinition\CodeSample;
24
use PhpCsFixer\FixerDefinition\FixerDefinition;
25
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
26
use PhpCsFixer\Preg;
27
use PhpCsFixer\Tokenizer\CT;
28
use PhpCsFixer\Tokenizer\Token;
29
use PhpCsFixer\Tokenizer\Tokens;
30
use PhpCsFixer\Tokenizer\TokensAnalyzer;
31
use PhpCsFixer\Utils;
32
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
33

34
/**
35
 * @phpstan-type _AutogeneratedInputConfiguration array{
36
 *  default?: 'align'|'align_by_scope'|'align_single_space'|'align_single_space_by_scope'|'align_single_space_minimal'|'align_single_space_minimal_by_scope'|'at_least_single_space'|'no_space'|'single_space'|null,
37
 *  operators?: array<string, ?string>,
38
 * }
39
 * @phpstan-type _AutogeneratedComputedConfiguration array{
40
 *  default: 'align'|'align_by_scope'|'align_single_space'|'align_single_space_by_scope'|'align_single_space_minimal'|'align_single_space_minimal_by_scope'|'at_least_single_space'|'no_space'|'single_space'|null,
41
 *  operators: array<string, ?string>,
42
 * }
43
 *
44
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
45
 *
46
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
47
 *
48
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
49
 */
50
final class BinaryOperatorSpacesFixer extends AbstractFixer implements ConfigurableFixerInterface
51
{
52
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
53
    use ConfigurableFixerTrait;
54

55
    /**
56
     * @internal
57
     */
58
    public const SINGLE_SPACE = 'single_space';
59

60
    /**
61
     * @internal
62
     */
63
    public const AT_LEAST_SINGLE_SPACE = 'at_least_single_space';
64

65
    /**
66
     * @internal
67
     */
68
    public const NO_SPACE = 'no_space';
69

70
    /**
71
     * @internal
72
     */
73
    public const ALIGN = 'align';
74

75
    /**
76
     * @internal
77
     */
78
    public const ALIGN_BY_SCOPE = 'align_by_scope';
79

80
    /**
81
     * @internal
82
     */
83
    public const ALIGN_SINGLE_SPACE = 'align_single_space';
84

85
    /**
86
     * @internal
87
     */
88
    public const ALIGN_SINGLE_SPACE_BY_SCOPE = 'align_single_space_by_scope';
89

90
    /**
91
     * @internal
92
     */
93
    public const ALIGN_SINGLE_SPACE_MINIMAL = 'align_single_space_minimal';
94

95
    /**
96
     * @internal
97
     */
98
    public const ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE = 'align_single_space_minimal_by_scope';
99

100
    /**
101
     * @internal
102
     *
103
     * @const Placeholder used as anchor for right alignment.
104
     */
105
    public const ALIGN_PLACEHOLDER = "\x2 ALIGNABLE%d \x3";
106

107
    /**
108
     * @var non-empty-list<string>
109
     */
110
    private const SUPPORTED_OPERATORS = [
111
        '=',
112
        '*',
113
        '/',
114
        '%',
115
        '<',
116
        '>',
117
        '|',
118
        '^',
119
        '+',
120
        '-',
121
        '&',
122
        '&=',
123
        '&&',
124
        '||',
125
        '.=',
126
        '/=',
127
        '=>',
128
        '==',
129
        '>=',
130
        '===',
131
        '!=',
132
        '<>',
133
        '!==',
134
        '<=',
135
        'and',
136
        'or',
137
        'xor',
138
        '-=',
139
        '%=',
140
        '*=',
141
        '|=',
142
        '+=',
143
        '<<',
144
        '<<=',
145
        '>>',
146
        '>>=',
147
        '^=',
148
        '**',
149
        '**=',
150
        '<=>',
151
        '??',
152
        '??=',
153
    ];
154

155
    /**
156
     * @var non-empty-list<null|string>
157
     */
158
    private const ALLOWED_VALUES = [
159
        self::ALIGN,
160
        self::ALIGN_BY_SCOPE,
161
        self::ALIGN_SINGLE_SPACE,
162
        self::ALIGN_SINGLE_SPACE_MINIMAL,
163
        self::ALIGN_SINGLE_SPACE_BY_SCOPE,
164
        self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE,
165
        self::SINGLE_SPACE,
166
        self::NO_SPACE,
167
        self::AT_LEAST_SINGLE_SPACE,
168
        null,
169
    ];
170

171
    /**
172
     * Keep track of the deepest level ever achieved while
173
     * parsing the code. Used later to replace alignment
174
     * placeholders with spaces.
175
     */
176
    private int $deepestLevel;
177

178
    /**
179
     * Level counter of the current nest level.
180
     * So one level alignments are not mixed with
181
     * other level ones.
182
     */
183
    private int $currentLevel;
184

185
    private TokensAnalyzer $tokensAnalyzer;
186

187
    /**
188
     * @var array<string, string>
189
     */
190
    private array $alignOperatorTokens = [];
191

192
    /**
193
     * @var array<string, string>
194
     */
195
    private array $operators = [];
196

197
    public function getDefinition(): FixerDefinitionInterface
198
    {
199
        return new FixerDefinition(
3✔
200
            'Binary operators should be surrounded by space as configured.',
3✔
201
            [
3✔
202
                new CodeSample(
3✔
203
                    <<<'PHP'
3✔
204
                        <?php
205
                        $a= 1  + $b^ $d !==  $e or   $f;
206

207
                        PHP
3✔
208
                ),
3✔
209
                new CodeSample(
3✔
210
                    <<<'PHP'
3✔
211
                        <?php
212
                        $aa=  1;
213
                        $b=2;
214

215
                        $c = $d    xor    $e;
216
                        $f    -=  1;
217

218
                        PHP,
3✔
219
                    ['operators' => ['=' => self::ALIGN, 'xor' => null]]
3✔
220
                ),
3✔
221
                new CodeSample(
3✔
222
                    <<<'PHP'
3✔
223
                        <?php
224
                        $a = $b +=$c;
225
                        $d = $ee+=$f;
226

227
                        $g = $b     +=$c;
228
                        $h = $ee+=$f;
229

230
                        PHP,
3✔
231
                    ['operators' => ['+=' => self::ALIGN_SINGLE_SPACE]]
3✔
232
                ),
3✔
233
                new CodeSample(
3✔
234
                    <<<'PHP'
3✔
235
                        <?php
236
                        $a = $b===$c;
237
                        $d = $f   ===  $g;
238
                        $h = $i===  $j;
239

240
                        PHP,
3✔
241
                    ['operators' => ['===' => self::ALIGN_SINGLE_SPACE_MINIMAL]]
3✔
242
                ),
3✔
243
                new CodeSample(
3✔
244
                    <<<'PHP'
3✔
245
                        <?php
246
                        $foo = \json_encode($bar, JSON_PRESERVE_ZERO_FRACTION | JSON_PRETTY_PRINT);
247

248
                        PHP,
3✔
249
                    ['operators' => ['|' => self::NO_SPACE]]
3✔
250
                ),
3✔
251
                new CodeSample(
3✔
252
                    <<<'PHP'
3✔
253
                        <?php
254
                        $array = [
255
                            "foo"            =>   1,
256
                            "baaaaaaaaaaar"  =>  11,
257
                        ];
258

259
                        PHP,
3✔
260
                    ['operators' => ['=>' => self::SINGLE_SPACE]]
3✔
261
                ),
3✔
262
                new CodeSample(
3✔
263
                    <<<'PHP'
3✔
264
                        <?php
265
                        $array = [
266
                            "foo" => 12,
267
                            "baaaaaaaaaaar"  => 13,
268

269
                            "baz" => 1,
270
                        ];
271

272
                        PHP,
3✔
273
                    ['operators' => ['=>' => self::ALIGN]]
3✔
274
                ),
3✔
275
                new CodeSample(
3✔
276
                    <<<'PHP'
3✔
277
                        <?php
278
                        $array = [
279
                            "foo" => 12,
280
                            "baaaaaaaaaaar"  => 13,
281

282
                            "baz" => 1,
283
                        ];
284

285
                        PHP,
3✔
286
                    ['operators' => ['=>' => self::ALIGN_BY_SCOPE]]
3✔
287
                ),
3✔
288
                new CodeSample(
3✔
289
                    <<<'PHP'
3✔
290
                        <?php
291
                        $array = [
292
                            "foo" => 12,
293
                            "baaaaaaaaaaar"  => 13,
294

295
                            "baz" => 1,
296
                        ];
297

298
                        PHP,
3✔
299
                    ['operators' => ['=>' => self::ALIGN_SINGLE_SPACE]]
3✔
300
                ),
3✔
301
                new CodeSample(
3✔
302
                    <<<'PHP'
3✔
303
                        <?php
304
                        $array = [
305
                            "foo" => 12,
306
                            "baaaaaaaaaaar"  => 13,
307

308
                            "baz" => 1,
309
                        ];
310

311
                        PHP,
3✔
312
                    ['operators' => ['=>' => self::ALIGN_SINGLE_SPACE_BY_SCOPE]]
3✔
313
                ),
3✔
314
                new CodeSample(
3✔
315
                    <<<'PHP'
3✔
316
                        <?php
317
                        $array = [
318
                            "foo" => 12,
319
                            "baaaaaaaaaaar"  => 13,
320

321
                            "baz" => 1,
322
                        ];
323

324
                        PHP,
3✔
325
                    ['operators' => ['=>' => self::ALIGN_SINGLE_SPACE_MINIMAL]]
3✔
326
                ),
3✔
327
                new CodeSample(
3✔
328
                    <<<'PHP'
3✔
329
                        <?php
330
                        $array = [
331
                            "foo" => 12,
332
                            "baaaaaaaaaaar"  => 13,
333

334
                            "baz" => 1,
335
                        ];
336

337
                        PHP,
3✔
338
                    ['operators' => ['=>' => self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE]]
3✔
339
                ),
3✔
340
            ]
3✔
341
        );
3✔
342
    }
343

344
    /**
345
     * {@inheritdoc}
346
     *
347
     * Must run after ArrayIndentationFixer, ArraySyntaxFixer, AssignNullCoalescingToCoalesceEqualFixer, ListSyntaxFixer, LongToShorthandOperatorFixer, ModernizeStrposFixer, NoMultilineWhitespaceAroundDoubleArrowFixer, NoUnsetCastFixer, PowToExponentiationFixer, StandardizeNotEqualsFixer, StrictComparisonFixer.
348
     */
349
    public function getPriority(): int
350
    {
351
        return -32;
1✔
352
    }
353

354
    public function isCandidate(Tokens $tokens): bool
355
    {
356
        return true;
229✔
357
    }
358

359
    protected function configurePostNormalisation(): void
360
    {
361
        $this->operators = $this->resolveOperatorsFromConfig();
242✔
362
    }
363

364
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
365
    {
366
        $this->tokensAnalyzer = new TokensAnalyzer($tokens);
229✔
367

368
        // last and first tokens cannot be an operator
369
        for ($index = $tokens->count() - 2; $index > 0; --$index) {
229✔
370
            if (!$this->tokensAnalyzer->isBinaryOperator($index)) {
229✔
371
                continue;
229✔
372
            }
373

374
            if ('=' === $tokens[$index]->getContent()) {
221✔
375
                $isDeclare = $this->isEqualPartOfDeclareStatement($tokens, $index);
147✔
376
                if (false === $isDeclare) {
147✔
377
                    $this->fixWhiteSpaceAroundOperator($tokens, $index);
145✔
378
                } else {
379
                    $index = $isDeclare; // skip `declare(foo ==bar)`, see `declare_equal_normalize`
5✔
380
                }
381
            } else {
382
                $this->fixWhiteSpaceAroundOperator($tokens, $index);
194✔
383
            }
384

385
            // previous of binary operator is now never an operator / previous of declare statement cannot be an operator
386
            --$index;
221✔
387
        }
388

389
        if (\count($this->alignOperatorTokens) > 0) {
229✔
390
            $this->fixAlignment($tokens, $this->alignOperatorTokens);
137✔
391
        }
392
    }
393

394
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
395
    {
396
        return new FixerConfigurationResolver([
242✔
397
            (new FixerOptionBuilder('default', 'Default fix strategy.'))
242✔
398
                ->setDefault(self::SINGLE_SPACE)
242✔
399
                ->setAllowedValues(self::ALLOWED_VALUES)
242✔
400
                ->getOption(),
242✔
401
            (new FixerOptionBuilder('operators', 'Dictionary of `binary operator` => `fix strategy` values that differ from the default strategy. Supported are: '.Utils::naturalLanguageJoinWithBackticks(self::SUPPORTED_OPERATORS).'.'))
242✔
402
                ->setAllowedTypes(['array<string, ?string>'])
242✔
403
                ->setAllowedValues([static function (array $option): bool {
242✔
404
                    foreach ($option as $operator => $value) {
242✔
405
                        if (!\in_array($operator, self::SUPPORTED_OPERATORS, true)) {
154✔
406
                            throw new InvalidOptionsException(
1✔
407
                                \sprintf(
1✔
408
                                    'Unexpected "operators" key, expected any of %s, got "%s".',
1✔
409
                                    Utils::naturalLanguageJoin(self::SUPPORTED_OPERATORS),
1✔
410
                                    \gettype($operator).'#'.$operator
1✔
411
                                )
1✔
412
                            );
1✔
413
                        }
414

415
                        if (!\in_array($value, self::ALLOWED_VALUES, true)) {
153✔
416
                            throw new InvalidOptionsException(
1✔
417
                                \sprintf(
1✔
418
                                    'Unexpected value for operator "%s", expected any of %s, got "%s".',
1✔
419
                                    $operator,
1✔
420
                                    Utils::naturalLanguageJoin(array_map(
1✔
421
                                        static fn ($value): string => Utils::toString($value),
1✔
422
                                        self::ALLOWED_VALUES
1✔
423
                                    )),
1✔
424
                                    \is_object($value) ? \get_class($value) : (null === $value ? 'null' : \gettype($value).'#'.$value)
1✔
425
                                )
1✔
426
                            );
1✔
427
                        }
428
                    }
429

430
                    return true;
242✔
431
                }])
242✔
432
                ->setDefault([])
242✔
433
                ->getOption(),
242✔
434
        ]);
242✔
435
    }
436

437
    private function fixWhiteSpaceAroundOperator(Tokens $tokens, int $index): void
438
    {
439
        $tokenContent = strtolower($tokens[$index]->getContent());
219✔
440

441
        if (!\array_key_exists($tokenContent, $this->operators)) {
219✔
442
            return; // not configured to be changed
6✔
443
        }
444

445
        if (self::SINGLE_SPACE === $this->operators[$tokenContent]) {
217✔
446
            $this->fixWhiteSpaceAroundOperatorToSingleSpace($tokens, $index);
166✔
447

448
            return;
166✔
449
        }
450

451
        if (self::AT_LEAST_SINGLE_SPACE === $this->operators[$tokenContent]) {
141✔
452
            $this->fixWhiteSpaceAroundOperatorToAtLeastSingleSpace($tokens, $index);
1✔
453

454
            return;
1✔
455
        }
456

457
        if (self::NO_SPACE === $this->operators[$tokenContent]) {
140✔
458
            $this->fixWhiteSpaceAroundOperatorToNoSpace($tokens, $index);
4✔
459

460
            return;
4✔
461
        }
462

463
        // schedule for alignment
464
        $this->alignOperatorTokens[$tokenContent] = $this->operators[$tokenContent];
137✔
465

466
        if (
467
            self::ALIGN === $this->operators[$tokenContent]
137✔
468
            || self::ALIGN_BY_SCOPE === $this->operators[$tokenContent]
137✔
469
        ) {
470
            return;
110✔
471
        }
472

473
        // fix white space after operator
474
        if ($tokens[$index + 1]->isWhitespace()) {
28✔
475
            if (
476
                self::ALIGN_SINGLE_SPACE_MINIMAL === $this->operators[$tokenContent]
28✔
477
                || self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE === $this->operators[$tokenContent]
28✔
478
            ) {
479
                $tokens[$index + 1] = new Token([\T_WHITESPACE, ' ']);
20✔
480
            }
481

482
            return;
28✔
483
        }
484

485
        $tokens->insertAt($index + 1, new Token([\T_WHITESPACE, ' ']));
16✔
486
    }
487

488
    private function fixWhiteSpaceAroundOperatorToSingleSpace(Tokens $tokens, int $index): void
489
    {
490
        // fix white space after operator
491
        if ($tokens[$index + 1]->isWhitespace()) {
166✔
492
            $content = $tokens[$index + 1]->getContent();
166✔
493
            if (' ' !== $content && !str_contains($content, "\n") && !$tokens[$tokens->getNextNonWhitespace($index + 1)]->isComment()) {
166✔
494
                $tokens[$index + 1] = new Token([\T_WHITESPACE, ' ']);
13✔
495
            }
496
        } else {
497
            $tokens->insertAt($index + 1, new Token([\T_WHITESPACE, ' ']));
36✔
498
        }
499

500
        // fix white space before operator
501
        if ($tokens[$index - 1]->isWhitespace()) {
166✔
502
            $content = $tokens[$index - 1]->getContent();
166✔
503
            if (' ' !== $content && !str_contains($content, "\n") && !$tokens[$tokens->getPrevNonWhitespace($index - 1)]->isComment()) {
166✔
504
                $tokens[$index - 1] = new Token([\T_WHITESPACE, ' ']);
31✔
505
            }
506
        } else {
507
            $tokens->insertAt($index, new Token([\T_WHITESPACE, ' ']));
36✔
508
        }
509
    }
510

511
    private function fixWhiteSpaceAroundOperatorToAtLeastSingleSpace(Tokens $tokens, int $index): void
512
    {
513
        // fix white space after operator
514
        if (!$tokens[$index + 1]->isWhitespace()) {
1✔
515
            $tokens->insertAt($index + 1, new Token([\T_WHITESPACE, ' ']));
1✔
516
        }
517

518
        // fix white space before operator
519
        if (!$tokens[$index - 1]->isWhitespace()) {
1✔
520
            $tokens->insertAt($index, new Token([\T_WHITESPACE, ' ']));
1✔
521
        }
522
    }
523

524
    private function fixWhiteSpaceAroundOperatorToNoSpace(Tokens $tokens, int $index): void
525
    {
526
        // fix white space after operator
527
        if ($tokens[$index + 1]->isWhitespace()) {
4✔
528
            $content = $tokens[$index + 1]->getContent();
4✔
529
            if (!str_contains($content, "\n") && !$tokens[$tokens->getNextNonWhitespace($index + 1)]->isComment()) {
4✔
530
                $tokens->clearAt($index + 1);
4✔
531
            }
532
        }
533

534
        // fix white space before operator
535
        if ($tokens[$index - 1]->isWhitespace()) {
4✔
536
            $content = $tokens[$index - 1]->getContent();
4✔
537
            if (!str_contains($content, "\n") && !$tokens[$tokens->getPrevNonWhitespace($index - 1)]->isComment()) {
4✔
538
                $tokens->clearAt($index - 1);
4✔
539
            }
540
        }
541
    }
542

543
    /**
544
     * @return false|int index of T_DECLARE where the `=` belongs to or `false`
545
     */
546
    private function isEqualPartOfDeclareStatement(Tokens $tokens, int $index)
547
    {
548
        $prevMeaningfulIndex = $tokens->getPrevMeaningfulToken($index);
147✔
549
        if ($tokens[$prevMeaningfulIndex]->isGivenKind(\T_STRING)) {
147✔
550
            $prevMeaningfulIndex = $tokens->getPrevMeaningfulToken($prevMeaningfulIndex);
10✔
551
            if ($tokens[$prevMeaningfulIndex]->equals('(')) {
10✔
552
                $prevMeaningfulIndex = $tokens->getPrevMeaningfulToken($prevMeaningfulIndex);
5✔
553
                if ($tokens[$prevMeaningfulIndex]->isGivenKind(\T_DECLARE)) {
5✔
554
                    return $prevMeaningfulIndex;
5✔
555
                }
556
            }
557
        }
558

559
        return false;
145✔
560
    }
561

562
    /**
563
     * @return array<string, string>
564
     */
565
    private function resolveOperatorsFromConfig(): array
566
    {
567
        $operators = [];
242✔
568

569
        if (null !== $this->configuration['default']) {
242✔
570
            foreach (self::SUPPORTED_OPERATORS as $operator) {
242✔
571
                $operators[$operator] = $this->configuration['default'];
242✔
572
            }
573
        }
574

575
        foreach ($this->configuration['operators'] as $operator => $value) {
242✔
576
            if (null === $value) {
152✔
577
                unset($operators[$operator]);
3✔
578
            } else {
579
                $operators[$operator] = $value;
151✔
580
            }
581
        }
582

583
        return $operators;
242✔
584
    }
585

586
    // Alignment logic related methods
587

588
    /**
589
     * @param array<string, string> $toAlign
590
     */
591
    private function fixAlignment(Tokens $tokens, array $toAlign): void
592
    {
593
        $this->deepestLevel = 0;
137✔
594
        $this->currentLevel = 0;
137✔
595

596
        foreach ($toAlign as $tokenContent => $alignStrategy) {
137✔
597
            // This fixer works partially on Tokens and partially on string representation of code.
598
            // During the process of fixing internal state of single Token may be affected by injecting ALIGN_PLACEHOLDER to its content.
599
            // The placeholder will be resolved by `replacePlaceholders` method by removing placeholder or changing it into spaces.
600
            // That way of fixing the code causes disturbances in marking Token as changed - if code is perfectly valid then placeholder
601
            // still be injected and removed, which will cause the `changed` flag to be set.
602
            // To handle that unwanted behavior we work on clone of Tokens collection and then override original collection with fixed collection.
603
            $tokensClone = clone $tokens;
137✔
604

605
            if ('=>' === $tokenContent) {
137✔
606
                $this->injectAlignmentPlaceholdersForArrow($tokensClone, 0, \count($tokens));
104✔
607
            } else {
608
                $this->injectAlignmentPlaceholdersDefault($tokensClone, 0, \count($tokens), $tokenContent);
36✔
609
            }
610

611
            // for all tokens that should be aligned but do not have anything to align with, fix spacing if needed
612
            if (
613
                self::ALIGN_SINGLE_SPACE === $alignStrategy
137✔
614
                || self::ALIGN_SINGLE_SPACE_MINIMAL === $alignStrategy
137✔
615
                || self::ALIGN_SINGLE_SPACE_BY_SCOPE === $alignStrategy
137✔
616
                || self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE === $alignStrategy
137✔
617
            ) {
618
                if ('=>' === $tokenContent) {
28✔
619
                    for ($index = $tokens->count() - 2; $index > 0; --$index) {
14✔
620
                        if ($tokens[$index]->isGivenKind(\T_DOUBLE_ARROW)) { // always binary operator, never part of declare statement
14✔
621
                            $this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy);
14✔
622
                        }
623
                    }
624
                } elseif ('=' === $tokenContent) {
17✔
625
                    for ($index = $tokens->count() - 2; $index > 0; --$index) {
8✔
626
                        if ('=' === $tokens[$index]->getContent() && false === $this->isEqualPartOfDeclareStatement($tokens, $index) && $this->tokensAnalyzer->isBinaryOperator($index)) {
8✔
627
                            $this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy);
8✔
628
                        }
629
                    }
630
                } else {
631
                    for ($index = $tokens->count() - 2; $index > 0; --$index) {
11✔
632
                        $content = $tokens[$index]->getContent();
11✔
633
                        if (strtolower($content) === $tokenContent && $this->tokensAnalyzer->isBinaryOperator($index)) { // never part of declare statement
11✔
634
                            $this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy);
11✔
635
                        }
636
                    }
637
                }
638
            }
639

640
            $tokens->setCode($this->replacePlaceholders($tokensClone, $alignStrategy, $tokenContent));
137✔
641
        }
642
    }
643

644
    private function injectAlignmentPlaceholdersDefault(Tokens $tokens, int $startAt, int $endAt, string $tokenContent): void
645
    {
646
        $newLineFoundSinceLastPlaceholder = true;
36✔
647

648
        for ($index = $startAt; $index < $endAt; ++$index) {
36✔
649
            $token = $tokens[$index];
36✔
650
            $content = $token->getContent();
36✔
651

652
            if (str_contains($content, "\n")) {
36✔
653
                $newLineFoundSinceLastPlaceholder = true;
31✔
654
            }
655

656
            if (
657
                strtolower($content) === $tokenContent
36✔
658
                && $this->tokensAnalyzer->isBinaryOperator($index)
36✔
659
                && ('=' !== $content || false === $this->isEqualPartOfDeclareStatement($tokens, $index))
36✔
660
                && $newLineFoundSinceLastPlaceholder
661
            ) {
662
                $tokens[$index] = new Token(\sprintf(self::ALIGN_PLACEHOLDER, $this->currentLevel).$content);
35✔
663
                $newLineFoundSinceLastPlaceholder = false;
35✔
664

665
                continue;
35✔
666
            }
667

668
            if ($token->isGivenKind(\T_FN)) {
36✔
669
                $from = $tokens->getNextMeaningfulToken($index);
2✔
670
                $until = $this->tokensAnalyzer->getLastTokenIndexOfArrowFunction($index);
2✔
671
                $this->injectAlignmentPlaceholders($tokens, $from + 1, $until - 1, $tokenContent);
2✔
672
                $index = $until;
2✔
673

674
                continue;
2✔
675
            }
676

677
            if ($token->isGivenKind([\T_FUNCTION, \T_CLASS])) {
36✔
678
                $index = $tokens->getNextTokenOfKind($index, ['{', ';', '(']);
8✔
679
                // We don't align `=` on multi-line definition of function parameters with default values
680
                if ($tokens[$index]->equals('(')) {
8✔
681
                    $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
8✔
682

683
                    continue;
8✔
684
                }
685

686
                if ($tokens[$index]->equals(';')) {
2✔
UNCOV
687
                    continue;
×
688
                }
689

690
                // Update the token to the `{` one in order to apply the following logic
691
                $token = $tokens[$index];
2✔
692
            }
693

694
            if ($token->equals('{')) {
36✔
695
                $until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
11✔
696
                $this->injectAlignmentPlaceholders($tokens, $index + 1, $until - 1, $tokenContent);
11✔
697
                $index = $until;
11✔
698

699
                continue;
11✔
700
            }
701

702
            if ($token->equals('(')) {
36✔
703
                $until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
17✔
704
                $this->injectAlignmentPlaceholders($tokens, $index + 1, $until - 1, $tokenContent);
17✔
705
                $index = $until;
17✔
706

707
                continue;
17✔
708
            }
709

710
            if ($token->equals('[')) {
36✔
711
                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE, $index);
5✔
712

713
                continue;
5✔
714
            }
715

716
            if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) {
36✔
717
                $until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index);
6✔
718
                $this->injectAlignmentPlaceholders($tokens, $index + 1, $until - 1, $tokenContent);
6✔
719
                $index = $until;
6✔
720

721
                continue;
6✔
722
            }
723
        }
724
    }
725

726
    private function injectAlignmentPlaceholders(Tokens $tokens, int $from, int $until, string $tokenContent): void
727
    {
728
        // Only inject placeholders for multi-line code
729
        if ($tokens->isPartialCodeMultiline($from, $until)) {
25✔
730
            ++$this->deepestLevel;
15✔
731
            $currentLevel = $this->currentLevel;
15✔
732
            $this->currentLevel = $this->deepestLevel;
15✔
733
            $this->injectAlignmentPlaceholdersDefault($tokens, $from, $until, $tokenContent);
15✔
734
            $this->currentLevel = $currentLevel;
15✔
735
        }
736
    }
737

738
    private function injectAlignmentPlaceholdersForArrow(Tokens $tokens, int $startAt, int $endAt): void
739
    {
740
        $newLineFoundSinceLastPlaceholder = true;
104✔
741
        $yieldFoundSinceLastPlaceholder = false;
104✔
742

743
        for ($index = $startAt; $index < $endAt; ++$index) {
104✔
744
            $token = $tokens[$index];
104✔
745
            $content = $token->getContent();
104✔
746

747
            if (str_contains($content, "\n")) {
104✔
748
                $newLineFoundSinceLastPlaceholder = true;
103✔
749
            }
750

751
            if ($token->isGivenKind(\T_YIELD)) {
104✔
752
                $yieldFoundSinceLastPlaceholder = true;
4✔
753
            }
754

755
            if ($token->isGivenKind(\T_FN)) {
104✔
756
                $yieldFoundSinceLastPlaceholder = false;
7✔
757
                $from = $tokens->getNextMeaningfulToken($index);
7✔
758
                $until = $this->tokensAnalyzer->getLastTokenIndexOfArrowFunction($index);
7✔
759
                $this->injectArrayAlignmentPlaceholders($tokens, $from + 1, $until - 1);
7✔
760
                $index = $until;
7✔
761

762
                continue;
7✔
763
            }
764

765
            if ($token->isGivenKind(\T_ARRAY)) { // don't use "$tokens->isArray()" here, short arrays are handled in the next case
104✔
766
                $yieldFoundSinceLastPlaceholder = false;
42✔
767
                $from = $tokens->getNextMeaningfulToken($index);
42✔
768
                $until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $from);
42✔
769
                $index = $until;
42✔
770

771
                $this->injectArrayAlignmentPlaceholders($tokens, $from + 1, $until - 1);
42✔
772

773
                continue;
42✔
774
            }
775

776
            if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) {
104✔
777
                $yieldFoundSinceLastPlaceholder = false;
56✔
778
                $from = $index;
56✔
779
                $until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $from);
56✔
780
                $index = $until;
56✔
781

782
                $this->injectArrayAlignmentPlaceholders($tokens, $from + 1, $until - 1);
56✔
783

784
                continue;
56✔
785
            }
786

787
            // no need to analyze for `isBinaryOperator` (always true), nor if part of declare statement (not valid PHP)
788
            // there is also no need to analyse the second arrow of a line
789
            if ($token->isGivenKind(\T_DOUBLE_ARROW) && $newLineFoundSinceLastPlaceholder) {
104✔
790
                if ($yieldFoundSinceLastPlaceholder) {
78✔
791
                    ++$this->deepestLevel;
4✔
792
                    ++$this->currentLevel;
4✔
793
                }
794
                $tokenContent = \sprintf(self::ALIGN_PLACEHOLDER, $this->currentLevel).$token->getContent();
78✔
795

796
                $nextToken = $tokens[$index + 1];
78✔
797
                if (!$nextToken->isWhitespace()) {
78✔
798
                    $tokenContent .= ' ';
2✔
799
                } elseif ($nextToken->isWhitespace(" \t")) {
78✔
800
                    $tokens[$index + 1] = new Token([\T_WHITESPACE, ' ']);
78✔
801
                }
802

803
                $tokens[$index] = new Token([\T_DOUBLE_ARROW, $tokenContent]);
78✔
804
                $newLineFoundSinceLastPlaceholder = false;
78✔
805
                $yieldFoundSinceLastPlaceholder = false;
78✔
806

807
                continue;
78✔
808
            }
809

810
            if ($token->equals(';')) {
104✔
811
                ++$this->deepestLevel;
102✔
812
                ++$this->currentLevel;
102✔
813

814
                continue;
102✔
815
            }
816

817
            if ($token->equals(',')) {
104✔
818
                for ($i = $index; $i < $endAt - 1; ++$i) {
76✔
819
                    if (str_contains($tokens[$i - 1]->getContent(), "\n")) {
75✔
820
                        $newLineFoundSinceLastPlaceholder = true;
75✔
821

822
                        break;
75✔
823
                    }
824

825
                    if ($tokens[$i + 1]->isGivenKind([\T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN])) {
75✔
826
                        $arrayStartIndex = $tokens[$i + 1]->isGivenKind(\T_ARRAY)
8✔
827
                            ? $tokens->getNextMeaningfulToken($i + 1)
6✔
828
                            : $i + 1;
2✔
829
                        $blockType = Tokens::detectBlockType($tokens[$arrayStartIndex]);
8✔
830
                        $arrayEndIndex = $tokens->findBlockEnd($blockType['type'], $arrayStartIndex);
8✔
831

832
                        if ($tokens->isPartialCodeMultiline($arrayStartIndex, $arrayEndIndex)) {
8✔
833
                            break;
8✔
834
                        }
835
                    }
836

837
                    ++$index;
75✔
838
                }
839
            }
840

841
            if ($token->equals('{')) {
104✔
842
                $until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
31✔
843
                $this->injectArrayAlignmentPlaceholders($tokens, $index + 1, $until - 1);
31✔
844
                $index = $until;
31✔
845

846
                continue;
31✔
847
            }
848

849
            if ($token->equals('(')) {
104✔
850
                $until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
55✔
851
                $this->injectArrayAlignmentPlaceholders($tokens, $index + 1, $until - 1);
55✔
852
                $index = $until;
55✔
853

854
                continue;
55✔
855
            }
856
        }
857
    }
858

859
    private function injectArrayAlignmentPlaceholders(Tokens $tokens, int $from, int $until): void
860
    {
861
        // Only inject placeholders for multi-line arrays
862
        if ($tokens->isPartialCodeMultiline($from, $until)) {
103✔
863
            ++$this->deepestLevel;
87✔
864
            $currentLevel = $this->currentLevel;
87✔
865
            $this->currentLevel = $this->deepestLevel;
87✔
866
            $this->injectAlignmentPlaceholdersForArrow($tokens, $from, $until);
87✔
867
            $this->currentLevel = $currentLevel;
87✔
868
        }
869
    }
870

871
    private function fixWhiteSpaceBeforeOperator(Tokens $tokens, int $index, string $alignStrategy): void
872
    {
873
        // fix white space after operator is not needed as BinaryOperatorSpacesFixer took care of this (if strategy is _not_ ALIGN)
874
        if (!$tokens[$index - 1]->isWhitespace()) {
28✔
875
            $tokens->insertAt($index, new Token([\T_WHITESPACE, ' ']));
17✔
876

877
            return;
17✔
878
        }
879

880
        if (
881
            self::ALIGN_SINGLE_SPACE_MINIMAL !== $alignStrategy && self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE !== $alignStrategy
28✔
882
            || $tokens[$tokens->getPrevNonWhitespace($index - 1)]->isComment()
28✔
883
        ) {
884
            return;
10✔
885
        }
886

887
        $content = $tokens[$index - 1]->getContent();
20✔
888
        if (' ' !== $content && !str_contains($content, "\n")) {
20✔
889
            $tokens[$index - 1] = new Token([\T_WHITESPACE, ' ']);
15✔
890
        }
891
    }
892

893
    /**
894
     * Look for group of placeholders and provide vertical alignment.
895
     */
896
    private function replacePlaceholders(Tokens $tokens, string $alignStrategy, string $tokenContent): string
897
    {
898
        $tmpCode = $tokens->generateCode();
137✔
899

900
        for ($j = 0; $j <= $this->deepestLevel; ++$j) {
137✔
901
            $placeholder = \sprintf(self::ALIGN_PLACEHOLDER, $j);
137✔
902

903
            if (!str_contains($tmpCode, $placeholder)) {
137✔
904
                continue;
114✔
905
            }
906

907
            $lines = explode("\n", $tmpCode);
110✔
908
            $groups = [];
110✔
909
            $groupIndex = 0;
110✔
910
            $groups[$groupIndex] = [];
110✔
911

912
            foreach ($lines as $index => $line) {
110✔
913
                if (substr_count($line, $placeholder) > 0) {
110✔
914
                    $groups[$groupIndex][] = $index;
110✔
915
                } elseif (
916
                    self::ALIGN_BY_SCOPE !== $alignStrategy
106✔
917
                    && self::ALIGN_SINGLE_SPACE_BY_SCOPE !== $alignStrategy
106✔
918
                    && self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE !== $alignStrategy
106✔
919
                ) {
920
                    ++$groupIndex;
72✔
921
                    $groups[$groupIndex] = [];
72✔
922
                }
923
            }
924

925
            foreach ($groups as $group) {
110✔
926
                if (\count($group) < 1) {
110✔
927
                    continue;
72✔
928
                }
929

930
                if (self::ALIGN !== $alignStrategy) {
110✔
931
                    // move placeholders to match strategy
932
                    foreach ($group as $index) {
57✔
933
                        $currentPosition = strpos($lines[$index], $placeholder);
57✔
934
                        $before = substr($lines[$index], 0, $currentPosition);
57✔
935

936
                        if (
937
                            self::ALIGN_SINGLE_SPACE === $alignStrategy
57✔
938
                            || self::ALIGN_SINGLE_SPACE_BY_SCOPE === $alignStrategy
57✔
939
                        ) {
940
                            if (!str_ends_with($before, ' ')) { // if last char of before-content is not ' '; add it
9✔
UNCOV
941
                                $before .= ' ';
×
942
                            }
943
                        } elseif (
944
                            self::ALIGN_SINGLE_SPACE_MINIMAL === $alignStrategy
50✔
945
                            || self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE === $alignStrategy
50✔
946
                        ) {
947
                            if (!Preg::match('/^\h+$/', $before)) { // if indent; do not move, leave to other fixer
16✔
948
                                $before = rtrim($before).' ';
15✔
949
                            }
950
                        }
951

952
                        $lines[$index] = $before.substr($lines[$index], $currentPosition);
57✔
953
                    }
954
                }
955

956
                $rightmostSymbol = 0;
110✔
957
                foreach ($group as $index) {
110✔
958
                    $rightmostSymbol = max($rightmostSymbol, $this->getSubstringWidth($lines[$index], $placeholder));
110✔
959
                }
960

961
                foreach ($group as $index) {
110✔
962
                    $line = $lines[$index];
110✔
963
                    $currentSymbol = $this->getSubstringWidth($line, $placeholder);
110✔
964
                    $delta = abs($rightmostSymbol - $currentSymbol);
110✔
965

966
                    if ($delta > 0) {
110✔
967
                        $line = str_replace($placeholder, str_repeat(' ', $delta).$placeholder, $line);
66✔
968
                        $lines[$index] = $line;
66✔
969
                    }
970
                }
971
            }
972

973
            $tmpCode = str_replace($placeholder, '', implode("\n", $lines));
110✔
974
        }
975

976
        return $tmpCode;
137✔
977
    }
978

979
    private function getSubstringWidth(string $haystack, string $needle): int
980
    {
981
        $position = strpos($haystack, $needle);
110✔
982
        \assert(\is_int($position));
110✔
983

984
        $substring = substr($haystack, 0, $position);
110✔
985

986
        return mb_strwidth($substring);
110✔
987
    }
988
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc