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

keradus / PHP-CS-Fixer / 16428450756

21 Jul 2025 06:19PM UTC coverage: 94.756% (-0.002%) from 94.758%
16428450756

push

github

web-flow
fix: always reach 100% of checked files (#8861)

4 of 4 new or added lines in 1 file covered. (100.0%)

111 existing lines in 19 files now uncovered.

28186 of 29746 relevant lines covered (94.76%)

45.93 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
final class BinaryOperatorSpacesFixer extends AbstractFixer implements ConfigurableFixerInterface
49
{
50
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
51
    use ConfigurableFixerTrait;
52

53
    /**
54
     * @internal
55
     */
56
    public const SINGLE_SPACE = 'single_space';
57

58
    /**
59
     * @internal
60
     */
61
    public const AT_LEAST_SINGLE_SPACE = 'at_least_single_space';
62

63
    /**
64
     * @internal
65
     */
66
    public const NO_SPACE = 'no_space';
67

68
    /**
69
     * @internal
70
     */
71
    public const ALIGN = 'align';
72

73
    /**
74
     * @internal
75
     */
76
    public const ALIGN_BY_SCOPE = 'align_by_scope';
77

78
    /**
79
     * @internal
80
     */
81
    public const ALIGN_SINGLE_SPACE = 'align_single_space';
82

83
    /**
84
     * @internal
85
     */
86
    public const ALIGN_SINGLE_SPACE_BY_SCOPE = 'align_single_space_by_scope';
87

88
    /**
89
     * @internal
90
     */
91
    public const ALIGN_SINGLE_SPACE_MINIMAL = 'align_single_space_minimal';
92

93
    /**
94
     * @internal
95
     */
96
    public const ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE = 'align_single_space_minimal_by_scope';
97

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

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

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

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

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

183
    private TokensAnalyzer $tokensAnalyzer;
184

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

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

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

208
$c = $d    xor    $e;
209
$f    -=  1;
210
',
3✔
211
                    ['operators' => ['=' => self::ALIGN, 'xor' => null]]
3✔
212
                ),
3✔
213
                new CodeSample(
3✔
214
                    '<?php
3✔
215
$a = $b +=$c;
216
$d = $ee+=$f;
217

218
$g = $b     +=$c;
219
$h = $ee+=$f;
220
',
3✔
221
                    ['operators' => ['+=' => self::ALIGN_SINGLE_SPACE]]
3✔
222
                ),
3✔
223
                new CodeSample(
3✔
224
                    '<?php
3✔
225
$a = $b===$c;
226
$d = $f   ===  $g;
227
$h = $i===  $j;
228
',
3✔
229
                    ['operators' => ['===' => self::ALIGN_SINGLE_SPACE_MINIMAL]]
3✔
230
                ),
3✔
231
                new CodeSample(
3✔
232
                    '<?php
3✔
233
$foo = \json_encode($bar, JSON_PRESERVE_ZERO_FRACTION | JSON_PRETTY_PRINT);
234
',
3✔
235
                    ['operators' => ['|' => self::NO_SPACE]]
3✔
236
                ),
3✔
237
                new CodeSample(
3✔
238
                    '<?php
3✔
239
$array = [
240
    "foo"            =>   1,
241
    "baaaaaaaaaaar"  =>  11,
242
];
243
',
3✔
244
                    ['operators' => ['=>' => self::SINGLE_SPACE]]
3✔
245
                ),
3✔
246
                new CodeSample(
3✔
247
                    '<?php
3✔
248
$array = [
249
    "foo" => 12,
250
    "baaaaaaaaaaar"  => 13,
251

252
    "baz" => 1,
253
];
254
',
3✔
255
                    ['operators' => ['=>' => self::ALIGN]]
3✔
256
                ),
3✔
257
                new CodeSample(
3✔
258
                    '<?php
3✔
259
$array = [
260
    "foo" => 12,
261
    "baaaaaaaaaaar"  => 13,
262

263
    "baz" => 1,
264
];
265
',
3✔
266
                    ['operators' => ['=>' => self::ALIGN_BY_SCOPE]]
3✔
267
                ),
3✔
268
                new CodeSample(
3✔
269
                    '<?php
3✔
270
$array = [
271
    "foo" => 12,
272
    "baaaaaaaaaaar"  => 13,
273

274
    "baz" => 1,
275
];
276
',
3✔
277
                    ['operators' => ['=>' => self::ALIGN_SINGLE_SPACE]]
3✔
278
                ),
3✔
279
                new CodeSample(
3✔
280
                    '<?php
3✔
281
$array = [
282
    "foo" => 12,
283
    "baaaaaaaaaaar"  => 13,
284

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

296
    "baz" => 1,
297
];
298
',
3✔
299
                    ['operators' => ['=>' => self::ALIGN_SINGLE_SPACE_MINIMAL]]
3✔
300
                ),
3✔
301
                new CodeSample(
3✔
302
                    '<?php
3✔
303
$array = [
304
    "foo" => 12,
305
    "baaaaaaaaaaar"  => 13,
306

307
    "baz" => 1,
308
];
309
',
3✔
310
                    ['operators' => ['=>' => self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE]]
3✔
311
                ),
3✔
312
            ]
3✔
313
        );
3✔
314
    }
315

316
    /**
317
     * {@inheritdoc}
318
     *
319
     * Must run after ArrayIndentationFixer, ArraySyntaxFixer, AssignNullCoalescingToCoalesceEqualFixer, ListSyntaxFixer, LongToShorthandOperatorFixer, ModernizeStrposFixer, NoMultilineWhitespaceAroundDoubleArrowFixer, NoUnsetCastFixer, PowToExponentiationFixer, StandardizeNotEqualsFixer, StrictComparisonFixer.
320
     */
321
    public function getPriority(): int
322
    {
323
        return -32;
1✔
324
    }
325

326
    public function isCandidate(Tokens $tokens): bool
327
    {
328
        return true;
229✔
329
    }
330

331
    protected function configurePostNormalisation(): void
332
    {
333
        $this->operators = $this->resolveOperatorsFromConfig();
242✔
334
    }
335

336
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
337
    {
338
        $this->tokensAnalyzer = new TokensAnalyzer($tokens);
229✔
339

340
        // last and first tokens cannot be an operator
341
        for ($index = $tokens->count() - 2; $index > 0; --$index) {
229✔
342
            if (!$this->tokensAnalyzer->isBinaryOperator($index)) {
229✔
343
                continue;
229✔
344
            }
345

346
            if ('=' === $tokens[$index]->getContent()) {
221✔
347
                $isDeclare = $this->isEqualPartOfDeclareStatement($tokens, $index);
147✔
348
                if (false === $isDeclare) {
147✔
349
                    $this->fixWhiteSpaceAroundOperator($tokens, $index);
145✔
350
                } else {
351
                    $index = $isDeclare; // skip `declare(foo ==bar)`, see `declare_equal_normalize`
5✔
352
                }
353
            } else {
354
                $this->fixWhiteSpaceAroundOperator($tokens, $index);
194✔
355
            }
356

357
            // previous of binary operator is now never an operator / previous of declare statement cannot be an operator
358
            --$index;
221✔
359
        }
360

361
        if (\count($this->alignOperatorTokens) > 0) {
229✔
362
            $this->fixAlignment($tokens, $this->alignOperatorTokens);
137✔
363
        }
364
    }
365

366
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
367
    {
368
        return new FixerConfigurationResolver([
242✔
369
            (new FixerOptionBuilder('default', 'Default fix strategy.'))
242✔
370
                ->setDefault(self::SINGLE_SPACE)
242✔
371
                ->setAllowedValues(self::ALLOWED_VALUES)
242✔
372
                ->getOption(),
242✔
373
            (new FixerOptionBuilder('operators', 'Dictionary of `binary operator` => `fix strategy` values that differ from the default strategy. Supported are: '.Utils::naturalLanguageJoinWithBackticks(self::SUPPORTED_OPERATORS).'.'))
242✔
374
                ->setAllowedTypes(['array<string, ?string>'])
242✔
375
                ->setAllowedValues([static function (array $option): bool {
242✔
376
                    foreach ($option as $operator => $value) {
242✔
377
                        if (!\in_array($operator, self::SUPPORTED_OPERATORS, true)) {
154✔
378
                            throw new InvalidOptionsException(
1✔
379
                                \sprintf(
1✔
380
                                    'Unexpected "operators" key, expected any of %s, got "%s".',
1✔
381
                                    Utils::naturalLanguageJoin(self::SUPPORTED_OPERATORS),
1✔
382
                                    \gettype($operator).'#'.$operator
1✔
383
                                )
1✔
384
                            );
1✔
385
                        }
386

387
                        if (!\in_array($value, self::ALLOWED_VALUES, true)) {
153✔
388
                            throw new InvalidOptionsException(
1✔
389
                                \sprintf(
1✔
390
                                    'Unexpected value for operator "%s", expected any of %s, got "%s".',
1✔
391
                                    $operator,
1✔
392
                                    Utils::naturalLanguageJoin(array_map(
1✔
393
                                        static fn ($value): string => Utils::toString($value),
1✔
394
                                        self::ALLOWED_VALUES
1✔
395
                                    )),
1✔
396
                                    \is_object($value) ? \get_class($value) : (null === $value ? 'null' : \gettype($value).'#'.$value)
1✔
397
                                )
1✔
398
                            );
1✔
399
                        }
400
                    }
401

402
                    return true;
242✔
403
                }])
242✔
404
                ->setDefault([])
242✔
405
                ->getOption(),
242✔
406
        ]);
242✔
407
    }
408

409
    private function fixWhiteSpaceAroundOperator(Tokens $tokens, int $index): void
410
    {
411
        $tokenContent = strtolower($tokens[$index]->getContent());
219✔
412

413
        if (!\array_key_exists($tokenContent, $this->operators)) {
219✔
414
            return; // not configured to be changed
6✔
415
        }
416

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

420
            return;
166✔
421
        }
422

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

426
            return;
1✔
427
        }
428

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

432
            return;
4✔
433
        }
434

435
        // schedule for alignment
436
        $this->alignOperatorTokens[$tokenContent] = $this->operators[$tokenContent];
137✔
437

438
        if (
439
            self::ALIGN === $this->operators[$tokenContent]
137✔
440
            || self::ALIGN_BY_SCOPE === $this->operators[$tokenContent]
137✔
441
        ) {
442
            return;
110✔
443
        }
444

445
        // fix white space after operator
446
        if ($tokens[$index + 1]->isWhitespace()) {
28✔
447
            if (
448
                self::ALIGN_SINGLE_SPACE_MINIMAL === $this->operators[$tokenContent]
28✔
449
                || self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE === $this->operators[$tokenContent]
28✔
450
            ) {
451
                $tokens[$index + 1] = new Token([\T_WHITESPACE, ' ']);
20✔
452
            }
453

454
            return;
28✔
455
        }
456

457
        $tokens->insertAt($index + 1, new Token([\T_WHITESPACE, ' ']));
16✔
458
    }
459

460
    private function fixWhiteSpaceAroundOperatorToSingleSpace(Tokens $tokens, int $index): void
461
    {
462
        // fix white space after operator
463
        if ($tokens[$index + 1]->isWhitespace()) {
166✔
464
            $content = $tokens[$index + 1]->getContent();
166✔
465
            if (' ' !== $content && !str_contains($content, "\n") && !$tokens[$tokens->getNextNonWhitespace($index + 1)]->isComment()) {
166✔
466
                $tokens[$index + 1] = new Token([\T_WHITESPACE, ' ']);
13✔
467
            }
468
        } else {
469
            $tokens->insertAt($index + 1, new Token([\T_WHITESPACE, ' ']));
36✔
470
        }
471

472
        // fix white space before operator
473
        if ($tokens[$index - 1]->isWhitespace()) {
166✔
474
            $content = $tokens[$index - 1]->getContent();
166✔
475
            if (' ' !== $content && !str_contains($content, "\n") && !$tokens[$tokens->getPrevNonWhitespace($index - 1)]->isComment()) {
166✔
476
                $tokens[$index - 1] = new Token([\T_WHITESPACE, ' ']);
31✔
477
            }
478
        } else {
479
            $tokens->insertAt($index, new Token([\T_WHITESPACE, ' ']));
36✔
480
        }
481
    }
482

483
    private function fixWhiteSpaceAroundOperatorToAtLeastSingleSpace(Tokens $tokens, int $index): void
484
    {
485
        // fix white space after operator
486
        if (!$tokens[$index + 1]->isWhitespace()) {
1✔
487
            $tokens->insertAt($index + 1, new Token([\T_WHITESPACE, ' ']));
1✔
488
        }
489

490
        // fix white space before operator
491
        if (!$tokens[$index - 1]->isWhitespace()) {
1✔
492
            $tokens->insertAt($index, new Token([\T_WHITESPACE, ' ']));
1✔
493
        }
494
    }
495

496
    private function fixWhiteSpaceAroundOperatorToNoSpace(Tokens $tokens, int $index): void
497
    {
498
        // fix white space after operator
499
        if ($tokens[$index + 1]->isWhitespace()) {
4✔
500
            $content = $tokens[$index + 1]->getContent();
4✔
501
            if (!str_contains($content, "\n") && !$tokens[$tokens->getNextNonWhitespace($index + 1)]->isComment()) {
4✔
502
                $tokens->clearAt($index + 1);
4✔
503
            }
504
        }
505

506
        // fix white space before operator
507
        if ($tokens[$index - 1]->isWhitespace()) {
4✔
508
            $content = $tokens[$index - 1]->getContent();
4✔
509
            if (!str_contains($content, "\n") && !$tokens[$tokens->getPrevNonWhitespace($index - 1)]->isComment()) {
4✔
510
                $tokens->clearAt($index - 1);
4✔
511
            }
512
        }
513
    }
514

515
    /**
516
     * @return false|int index of T_DECLARE where the `=` belongs to or `false`
517
     */
518
    private function isEqualPartOfDeclareStatement(Tokens $tokens, int $index)
519
    {
520
        $prevMeaningfulIndex = $tokens->getPrevMeaningfulToken($index);
147✔
521
        if ($tokens[$prevMeaningfulIndex]->isGivenKind(\T_STRING)) {
147✔
522
            $prevMeaningfulIndex = $tokens->getPrevMeaningfulToken($prevMeaningfulIndex);
10✔
523
            if ($tokens[$prevMeaningfulIndex]->equals('(')) {
10✔
524
                $prevMeaningfulIndex = $tokens->getPrevMeaningfulToken($prevMeaningfulIndex);
5✔
525
                if ($tokens[$prevMeaningfulIndex]->isGivenKind(\T_DECLARE)) {
5✔
526
                    return $prevMeaningfulIndex;
5✔
527
                }
528
            }
529
        }
530

531
        return false;
145✔
532
    }
533

534
    /**
535
     * @return array<string, string>
536
     */
537
    private function resolveOperatorsFromConfig(): array
538
    {
539
        $operators = [];
242✔
540

541
        if (null !== $this->configuration['default']) {
242✔
542
            foreach (self::SUPPORTED_OPERATORS as $operator) {
242✔
543
                $operators[$operator] = $this->configuration['default'];
242✔
544
            }
545
        }
546

547
        foreach ($this->configuration['operators'] as $operator => $value) {
242✔
548
            if (null === $value) {
152✔
549
                unset($operators[$operator]);
3✔
550
            } else {
551
                $operators[$operator] = $value;
151✔
552
            }
553
        }
554

555
        return $operators;
242✔
556
    }
557

558
    // Alignment logic related methods
559

560
    /**
561
     * @param array<string, string> $toAlign
562
     */
563
    private function fixAlignment(Tokens $tokens, array $toAlign): void
564
    {
565
        $this->deepestLevel = 0;
137✔
566
        $this->currentLevel = 0;
137✔
567

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

577
            if ('=>' === $tokenContent) {
137✔
578
                $this->injectAlignmentPlaceholdersForArrow($tokensClone, 0, \count($tokens));
104✔
579
            } else {
580
                $this->injectAlignmentPlaceholdersDefault($tokensClone, 0, \count($tokens), $tokenContent);
36✔
581
            }
582

583
            // for all tokens that should be aligned but do not have anything to align with, fix spacing if needed
584
            if (
585
                self::ALIGN_SINGLE_SPACE === $alignStrategy
137✔
586
                || self::ALIGN_SINGLE_SPACE_MINIMAL === $alignStrategy
137✔
587
                || self::ALIGN_SINGLE_SPACE_BY_SCOPE === $alignStrategy
137✔
588
                || self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE === $alignStrategy
137✔
589
            ) {
590
                if ('=>' === $tokenContent) {
28✔
591
                    for ($index = $tokens->count() - 2; $index > 0; --$index) {
14✔
592
                        if ($tokens[$index]->isGivenKind(\T_DOUBLE_ARROW)) { // always binary operator, never part of declare statement
14✔
593
                            $this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy);
14✔
594
                        }
595
                    }
596
                } elseif ('=' === $tokenContent) {
17✔
597
                    for ($index = $tokens->count() - 2; $index > 0; --$index) {
8✔
598
                        if ('=' === $tokens[$index]->getContent() && false === $this->isEqualPartOfDeclareStatement($tokens, $index) && $this->tokensAnalyzer->isBinaryOperator($index)) {
8✔
599
                            $this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy);
8✔
600
                        }
601
                    }
602
                } else {
603
                    for ($index = $tokens->count() - 2; $index > 0; --$index) {
11✔
604
                        $content = $tokens[$index]->getContent();
11✔
605
                        if (strtolower($content) === $tokenContent && $this->tokensAnalyzer->isBinaryOperator($index)) { // never part of declare statement
11✔
606
                            $this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy);
11✔
607
                        }
608
                    }
609
                }
610
            }
611

612
            $tokens->setCode($this->replacePlaceholders($tokensClone, $alignStrategy, $tokenContent));
137✔
613
        }
614
    }
615

616
    private function injectAlignmentPlaceholdersDefault(Tokens $tokens, int $startAt, int $endAt, string $tokenContent): void
617
    {
618
        $newLineFoundSinceLastPlaceholder = true;
36✔
619

620
        for ($index = $startAt; $index < $endAt; ++$index) {
36✔
621
            $token = $tokens[$index];
36✔
622
            $content = $token->getContent();
36✔
623

624
            if (str_contains($content, "\n")) {
36✔
625
                $newLineFoundSinceLastPlaceholder = true;
31✔
626
            }
627

628
            if (
629
                strtolower($content) === $tokenContent
36✔
630
                && $this->tokensAnalyzer->isBinaryOperator($index)
36✔
631
                && ('=' !== $content || false === $this->isEqualPartOfDeclareStatement($tokens, $index))
36✔
632
                && $newLineFoundSinceLastPlaceholder
633
            ) {
634
                $tokens[$index] = new Token(\sprintf(self::ALIGN_PLACEHOLDER, $this->currentLevel).$content);
35✔
635
                $newLineFoundSinceLastPlaceholder = false;
35✔
636

637
                continue;
35✔
638
            }
639

640
            if ($token->isGivenKind(\T_FN)) {
36✔
641
                $from = $tokens->getNextMeaningfulToken($index);
2✔
642
                $until = $this->tokensAnalyzer->getLastTokenIndexOfArrowFunction($index);
2✔
643
                $this->injectAlignmentPlaceholders($tokens, $from + 1, $until - 1, $tokenContent);
2✔
644
                $index = $until;
2✔
645

646
                continue;
2✔
647
            }
648

649
            if ($token->isGivenKind([\T_FUNCTION, \T_CLASS])) {
36✔
650
                $index = $tokens->getNextTokenOfKind($index, ['{', ';', '(']);
8✔
651
                // We don't align `=` on multi-line definition of function parameters with default values
652
                if ($tokens[$index]->equals('(')) {
8✔
653
                    $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
8✔
654

655
                    continue;
8✔
656
                }
657

658
                if ($tokens[$index]->equals(';')) {
2✔
659
                    continue;
×
660
                }
661

662
                // Update the token to the `{` one in order to apply the following logic
663
                $token = $tokens[$index];
2✔
664
            }
665

666
            if ($token->equals('{')) {
36✔
667
                $until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
11✔
668
                $this->injectAlignmentPlaceholders($tokens, $index + 1, $until - 1, $tokenContent);
11✔
669
                $index = $until;
11✔
670

671
                continue;
11✔
672
            }
673

674
            if ($token->equals('(')) {
36✔
675
                $until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
17✔
676
                $this->injectAlignmentPlaceholders($tokens, $index + 1, $until - 1, $tokenContent);
17✔
677
                $index = $until;
17✔
678

679
                continue;
17✔
680
            }
681

682
            if ($token->equals('[')) {
36✔
683
                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE, $index);
5✔
684

685
                continue;
5✔
686
            }
687

688
            if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) {
36✔
689
                $until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index);
6✔
690
                $this->injectAlignmentPlaceholders($tokens, $index + 1, $until - 1, $tokenContent);
6✔
691
                $index = $until;
6✔
692

693
                continue;
6✔
694
            }
695
        }
696
    }
697

698
    private function injectAlignmentPlaceholders(Tokens $tokens, int $from, int $until, string $tokenContent): void
699
    {
700
        // Only inject placeholders for multi-line code
701
        if ($tokens->isPartialCodeMultiline($from, $until)) {
25✔
702
            ++$this->deepestLevel;
15✔
703
            $currentLevel = $this->currentLevel;
15✔
704
            $this->currentLevel = $this->deepestLevel;
15✔
705
            $this->injectAlignmentPlaceholdersDefault($tokens, $from, $until, $tokenContent);
15✔
706
            $this->currentLevel = $currentLevel;
15✔
707
        }
708
    }
709

710
    private function injectAlignmentPlaceholdersForArrow(Tokens $tokens, int $startAt, int $endAt): void
711
    {
712
        $newLineFoundSinceLastPlaceholder = true;
104✔
713
        $yieldFoundSinceLastPlaceholder = false;
104✔
714

715
        for ($index = $startAt; $index < $endAt; ++$index) {
104✔
716
            $token = $tokens[$index];
104✔
717
            $content = $token->getContent();
104✔
718

719
            if (str_contains($content, "\n")) {
104✔
720
                $newLineFoundSinceLastPlaceholder = true;
103✔
721
            }
722

723
            if ($token->isGivenKind(\T_YIELD)) {
104✔
724
                $yieldFoundSinceLastPlaceholder = true;
4✔
725
            }
726

727
            if ($token->isGivenKind(\T_FN)) {
104✔
728
                $yieldFoundSinceLastPlaceholder = false;
7✔
729
                $from = $tokens->getNextMeaningfulToken($index);
7✔
730
                $until = $this->tokensAnalyzer->getLastTokenIndexOfArrowFunction($index);
7✔
731
                $this->injectArrayAlignmentPlaceholders($tokens, $from + 1, $until - 1);
7✔
732
                $index = $until;
7✔
733

734
                continue;
7✔
735
            }
736

737
            if ($token->isGivenKind(\T_ARRAY)) { // don't use "$tokens->isArray()" here, short arrays are handled in the next case
104✔
738
                $yieldFoundSinceLastPlaceholder = false;
42✔
739
                $from = $tokens->getNextMeaningfulToken($index);
42✔
740
                $until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $from);
42✔
741
                $index = $until;
42✔
742

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

745
                continue;
42✔
746
            }
747

748
            if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) {
104✔
749
                $yieldFoundSinceLastPlaceholder = false;
56✔
750
                $from = $index;
56✔
751
                $until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $from);
56✔
752
                $index = $until;
56✔
753

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

756
                continue;
56✔
757
            }
758

759
            // no need to analyze for `isBinaryOperator` (always true), nor if part of declare statement (not valid PHP)
760
            // there is also no need to analyse the second arrow of a line
761
            if ($token->isGivenKind(\T_DOUBLE_ARROW) && $newLineFoundSinceLastPlaceholder) {
104✔
762
                if ($yieldFoundSinceLastPlaceholder) {
78✔
763
                    ++$this->deepestLevel;
4✔
764
                    ++$this->currentLevel;
4✔
765
                }
766
                $tokenContent = \sprintf(self::ALIGN_PLACEHOLDER, $this->currentLevel).$token->getContent();
78✔
767

768
                $nextToken = $tokens[$index + 1];
78✔
769
                if (!$nextToken->isWhitespace()) {
78✔
770
                    $tokenContent .= ' ';
2✔
771
                } elseif ($nextToken->isWhitespace(" \t")) {
78✔
772
                    $tokens[$index + 1] = new Token([\T_WHITESPACE, ' ']);
78✔
773
                }
774

775
                $tokens[$index] = new Token([\T_DOUBLE_ARROW, $tokenContent]);
78✔
776
                $newLineFoundSinceLastPlaceholder = false;
78✔
777
                $yieldFoundSinceLastPlaceholder = false;
78✔
778

779
                continue;
78✔
780
            }
781

782
            if ($token->equals(';')) {
104✔
783
                ++$this->deepestLevel;
102✔
784
                ++$this->currentLevel;
102✔
785

786
                continue;
102✔
787
            }
788

789
            if ($token->equals(',')) {
104✔
790
                for ($i = $index; $i < $endAt - 1; ++$i) {
76✔
791
                    if (str_contains($tokens[$i - 1]->getContent(), "\n")) {
75✔
792
                        $newLineFoundSinceLastPlaceholder = true;
75✔
793

794
                        break;
75✔
795
                    }
796

797
                    if ($tokens[$i + 1]->isGivenKind([\T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN])) {
75✔
798
                        $arrayStartIndex = $tokens[$i + 1]->isGivenKind(\T_ARRAY)
8✔
799
                            ? $tokens->getNextMeaningfulToken($i + 1)
6✔
800
                            : $i + 1;
2✔
801
                        $blockType = Tokens::detectBlockType($tokens[$arrayStartIndex]);
8✔
802
                        $arrayEndIndex = $tokens->findBlockEnd($blockType['type'], $arrayStartIndex);
8✔
803

804
                        if ($tokens->isPartialCodeMultiline($arrayStartIndex, $arrayEndIndex)) {
8✔
805
                            break;
8✔
806
                        }
807
                    }
808

809
                    ++$index;
75✔
810
                }
811
            }
812

813
            if ($token->equals('{')) {
104✔
814
                $until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
31✔
815
                $this->injectArrayAlignmentPlaceholders($tokens, $index + 1, $until - 1);
31✔
816
                $index = $until;
31✔
817

818
                continue;
31✔
819
            }
820

821
            if ($token->equals('(')) {
104✔
822
                $until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
55✔
823
                $this->injectArrayAlignmentPlaceholders($tokens, $index + 1, $until - 1);
55✔
824
                $index = $until;
55✔
825

826
                continue;
55✔
827
            }
828
        }
829
    }
830

831
    private function injectArrayAlignmentPlaceholders(Tokens $tokens, int $from, int $until): void
832
    {
833
        // Only inject placeholders for multi-line arrays
834
        if ($tokens->isPartialCodeMultiline($from, $until)) {
103✔
835
            ++$this->deepestLevel;
87✔
836
            $currentLevel = $this->currentLevel;
87✔
837
            $this->currentLevel = $this->deepestLevel;
87✔
838
            $this->injectAlignmentPlaceholdersForArrow($tokens, $from, $until);
87✔
839
            $this->currentLevel = $currentLevel;
87✔
840
        }
841
    }
842

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

849
            return;
17✔
850
        }
851

852
        if (
853
            self::ALIGN_SINGLE_SPACE_MINIMAL !== $alignStrategy && self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE !== $alignStrategy
28✔
854
            || $tokens[$tokens->getPrevNonWhitespace($index - 1)]->isComment()
28✔
855
        ) {
856
            return;
10✔
857
        }
858

859
        $content = $tokens[$index - 1]->getContent();
20✔
860
        if (' ' !== $content && !str_contains($content, "\n")) {
20✔
861
            $tokens[$index - 1] = new Token([\T_WHITESPACE, ' ']);
15✔
862
        }
863
    }
864

865
    /**
866
     * Look for group of placeholders and provide vertical alignment.
867
     */
868
    private function replacePlaceholders(Tokens $tokens, string $alignStrategy, string $tokenContent): string
869
    {
870
        $tmpCode = $tokens->generateCode();
137✔
871

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

875
            if (!str_contains($tmpCode, $placeholder)) {
137✔
876
                continue;
114✔
877
            }
878

879
            $lines = explode("\n", $tmpCode);
110✔
880
            $groups = [];
110✔
881
            $groupIndex = 0;
110✔
882
            $groups[$groupIndex] = [];
110✔
883

884
            foreach ($lines as $index => $line) {
110✔
885
                if (substr_count($line, $placeholder) > 0) {
110✔
886
                    $groups[$groupIndex][] = $index;
110✔
887
                } elseif (
888
                    self::ALIGN_BY_SCOPE !== $alignStrategy
106✔
889
                    && self::ALIGN_SINGLE_SPACE_BY_SCOPE !== $alignStrategy
106✔
890
                    && self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE !== $alignStrategy
106✔
891
                ) {
892
                    ++$groupIndex;
72✔
893
                    $groups[$groupIndex] = [];
72✔
894
                }
895
            }
896

897
            foreach ($groups as $group) {
110✔
898
                if (\count($group) < 1) {
110✔
899
                    continue;
72✔
900
                }
901

902
                if (self::ALIGN !== $alignStrategy) {
110✔
903
                    // move placeholders to match strategy
904
                    foreach ($group as $index) {
57✔
905
                        $currentPosition = strpos($lines[$index], $placeholder);
57✔
906
                        $before = substr($lines[$index], 0, $currentPosition);
57✔
907

908
                        if (
909
                            self::ALIGN_SINGLE_SPACE === $alignStrategy
57✔
910
                            || self::ALIGN_SINGLE_SPACE_BY_SCOPE === $alignStrategy
57✔
911
                        ) {
912
                            if (!str_ends_with($before, ' ')) { // if last char of before-content is not ' '; add it
9✔
UNCOV
913
                                $before .= ' ';
×
914
                            }
915
                        } elseif (
916
                            self::ALIGN_SINGLE_SPACE_MINIMAL === $alignStrategy
50✔
917
                            || self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE === $alignStrategy
50✔
918
                        ) {
919
                            if (!Preg::match('/^\h+$/', $before)) { // if indent; do not move, leave to other fixer
16✔
920
                                $before = rtrim($before).' ';
15✔
921
                            }
922
                        }
923

924
                        $lines[$index] = $before.substr($lines[$index], $currentPosition);
57✔
925
                    }
926
                }
927

928
                $rightmostSymbol = 0;
110✔
929
                foreach ($group as $index) {
110✔
930
                    $rightmostSymbol = max($rightmostSymbol, $this->getSubstringWidth($lines[$index], $placeholder));
110✔
931
                }
932

933
                foreach ($group as $index) {
110✔
934
                    $line = $lines[$index];
110✔
935
                    $currentSymbol = $this->getSubstringWidth($line, $placeholder);
110✔
936
                    $delta = abs($rightmostSymbol - $currentSymbol);
110✔
937

938
                    if ($delta > 0) {
110✔
939
                        $line = str_replace($placeholder, str_repeat(' ', $delta).$placeholder, $line);
66✔
940
                        $lines[$index] = $line;
66✔
941
                    }
942
                }
943
            }
944

945
            $tmpCode = str_replace($placeholder, '', implode("\n", $lines));
110✔
946
        }
947

948
        return $tmpCode;
137✔
949
    }
950

951
    private function getSubstringWidth(string $haystack, string $needle): int
952
    {
953
        $position = strpos($haystack, $needle);
110✔
954
        \assert(\is_int($position));
110✔
955

956
        $substring = substr($haystack, 0, $position);
110✔
957

958
        return mb_strwidth($substring);
110✔
959
    }
960
}
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