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

keradus / PHP-CS-Fixer / 17252691116

26 Aug 2025 11:09PM UTC coverage: 94.743% (-0.01%) from 94.755%
17252691116

push

github

keradus
chore: apply phpdoc_tag_no_named_arguments

28313 of 29884 relevant lines covered (94.74%)

45.64 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\n\$a= 1  + \$b^ \$d !==  \$e or   \$f;\n"
3✔
204
                ),
3✔
205
                new CodeSample(
3✔
206
                    '<?php
3✔
207
$aa=  1;
208
$b=2;
209

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

422
            return;
166✔
423
        }
424

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

428
            return;
1✔
429
        }
430

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

434
            return;
4✔
435
        }
436

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

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

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

456
            return;
28✔
457
        }
458

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

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

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

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

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

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

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

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

533
        return false;
145✔
534
    }
535

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

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

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

557
        return $operators;
242✔
558
    }
559

560
    // Alignment logic related methods
561

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

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

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

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

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

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

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

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

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

639
                continue;
35✔
640
            }
641

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

648
                continue;
2✔
649
            }
650

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

657
                    continue;
8✔
658
                }
659

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

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

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

673
                continue;
11✔
674
            }
675

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

681
                continue;
17✔
682
            }
683

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

687
                continue;
5✔
688
            }
689

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

695
                continue;
6✔
696
            }
697
        }
698
    }
699

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

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

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

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

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

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

736
                continue;
7✔
737
            }
738

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

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

747
                continue;
42✔
748
            }
749

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

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

758
                continue;
56✔
759
            }
760

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

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

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

781
                continue;
78✔
782
            }
783

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

788
                continue;
102✔
789
            }
790

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

796
                        break;
75✔
797
                    }
798

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

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

811
                    ++$index;
75✔
812
                }
813
            }
814

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

820
                continue;
31✔
821
            }
822

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

828
                continue;
55✔
829
            }
830
        }
831
    }
832

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

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

851
            return;
17✔
852
        }
853

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

950
        return $tmpCode;
137✔
951
    }
952

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

958
        $substring = substr($haystack, 0, $position);
110✔
959

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