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

keradus / PHP-CS-Fixer / 25961188992

15 May 2026 03:27PM UTC coverage: 93.797% (+0.7%) from 93.053%
25961188992

push

github

web-flow
fix: `MultilinePromotedPropertiesFixer` - fix for `new` in initializers (#9619)

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

49 existing lines in 7 files now uncovered.

29908 of 31886 relevant lines covered (93.8%)

51.7 hits per line

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

98.94
/src/Fixer/FunctionNotation/MethodArgumentSpaceFixer.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\FunctionNotation;
16

17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
19
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
20
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
21
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
22
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
23
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
24
use PhpCsFixer\FixerDefinition\CodeSample;
25
use PhpCsFixer\FixerDefinition\FixerDefinition;
26
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
27
use PhpCsFixer\Future;
28
use PhpCsFixer\Preg;
29
use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
30
use PhpCsFixer\Tokenizer\CT;
31
use PhpCsFixer\Tokenizer\Token;
32
use PhpCsFixer\Tokenizer\Tokens;
33

34
/**
35
 * @phpstan-type _AutogeneratedInputConfiguration array{
36
 *  after_heredoc?: bool,
37
 *  attribute_placement?: 'ignore'|'same_line'|'standalone',
38
 *  keep_multiple_spaces_after_comma?: bool,
39
 *  on_multiline?: 'ensure_fully_multiline'|'ensure_single_line'|'ensure_single_line_for_single_argument'|'ignore',
40
 * }
41
 * @phpstan-type _AutogeneratedComputedConfiguration array{
42
 *  after_heredoc: bool,
43
 *  attribute_placement: 'ignore'|'same_line'|'standalone',
44
 *  keep_multiple_spaces_after_comma: bool,
45
 *  on_multiline: 'ensure_fully_multiline'|'ensure_single_line'|'ensure_single_line_for_single_argument'|'ignore',
46
 * }
47
 *
48
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
49
 *
50
 * @author Kuanhung Chen <ericj.tw@gmail.com>
51
 *
52
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
53
 */
54
final class MethodArgumentSpaceFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
55
{
56
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
57
    use ConfigurableFixerTrait;
58

59
    public function getDefinition(): FixerDefinitionInterface
60
    {
61
        return new FixerDefinition(
3✔
62
            'In method arguments and method call, there MUST NOT be a space before each comma and there MUST be one space after each comma. Argument lists MAY be split across multiple lines, where each subsequent line is indented once. When doing so, the first item in the list MUST be on the next line, and there MUST be only one argument per line.',
3✔
63
            [
3✔
64
                new CodeSample(
3✔
65
                    "<?php\nfunction sample(\$a=10,\$b=20,\$c=30) {}\nsample(1,  2);\n",
3✔
66
                    null,
3✔
67
                ),
3✔
68
                new CodeSample(
3✔
69
                    "<?php\nfunction sample(\$a=10,\$b=20,\$c=30) {}\nsample(1,  2);\n",
3✔
70
                    ['keep_multiple_spaces_after_comma' => false],
3✔
71
                ),
3✔
72
                new CodeSample(
3✔
73
                    "<?php\nfunction sample(\$a=10,\$b=20,\$c=30) {}\nsample(1,  2);\n",
3✔
74
                    ['keep_multiple_spaces_after_comma' => true],
3✔
75
                ),
3✔
76
                new CodeSample(
3✔
77
                    "<?php\nfunction sample(\$a=10,\n    \$b=20,\$c=30) {}\nsample(1,\n    2);\n",
3✔
78
                    ['on_multiline' => 'ensure_fully_multiline'],
3✔
79
                ),
3✔
80
                new CodeSample(
3✔
81
                    "<?php\nfunction sample(\n    \$a=10,\n    \$b=20,\n    \$c=30\n) {}\nsample(\n    1,\n    2\n);\n",
3✔
82
                    ['on_multiline' => 'ensure_single_line'],
3✔
83
                ),
3✔
84
                new CodeSample(
3✔
85
                    "<?php\nfunction sample(\$a=10,\n    \$b=20,\$c=30) {}\nsample(1,  \n    2);\nsample('foo',    'foobarbaz', 'baz');\nsample('foobar', 'bar',       'baz');\n",
3✔
86
                    [
3✔
87
                        'on_multiline' => 'ensure_fully_multiline',
3✔
88
                        'keep_multiple_spaces_after_comma' => true,
3✔
89
                    ],
3✔
90
                ),
3✔
91
                new CodeSample(
3✔
92
                    "<?php\nfunction sample(\$a=10,\n    \$b=20,\$c=30) {}\nsample(1,  \n    2);\nsample('foo',    'foobarbaz', 'baz');\nsample('foobar', 'bar',       'baz');\n",
3✔
93
                    [
3✔
94
                        'on_multiline' => 'ensure_fully_multiline',
3✔
95
                        'keep_multiple_spaces_after_comma' => false,
3✔
96
                    ],
3✔
97
                ),
3✔
98
                new CodeSample(
3✔
99
                    "<?php\nfunction sample(#[Foo] #[Bar] \$a=10,\n    \$b=20,\$c=30) {}\nsample(1,  2);\n",
3✔
100
                    [
3✔
101
                        'on_multiline' => 'ensure_fully_multiline',
3✔
102
                        'attribute_placement' => 'ignore',
3✔
103
                    ],
3✔
104
                ),
3✔
105
                new CodeSample(
3✔
106
                    "<?php\nfunction sample(#[Foo]\n    #[Bar]\n    \$a=10,\n    \$b=20,\$c=30) {}\nsample(1,  2);\n",
3✔
107
                    [
3✔
108
                        'on_multiline' => 'ensure_fully_multiline',
3✔
109
                        'attribute_placement' => 'same_line',
3✔
110
                    ],
3✔
111
                ),
3✔
112
                new CodeSample(
3✔
113
                    "<?php\nfunction sample(#[Foo] #[Bar] \$a=10,\n    \$b=20,\$c=30) {}\nsample(1,  2);\n",
3✔
114
                    [
3✔
115
                        'on_multiline' => 'ensure_fully_multiline',
3✔
116
                        'attribute_placement' => 'standalone',
3✔
117
                    ],
3✔
118
                ),
3✔
119
                new CodeSample(
3✔
120
                    "<?php\nfunction sample(\n    \$a\n) {}\nsample(\n    1\n);\nsample(\n    1,\n    2\n);\n",
3✔
121
                    ['on_multiline' => 'ensure_single_line_for_single_argument'],
3✔
122
                ),
3✔
123
                new CodeSample(
3✔
124
                    <<<'SAMPLE'
3✔
125
                        <?php
126
                        sample(
127
                            <<<EOD
128
                                foo
129
                                EOD
130
                            ,
131
                            'bar'
132
                        );
133

134
                        SAMPLE,
3✔
135
                    ['after_heredoc' => true],
3✔
136
                ),
3✔
137
            ],
3✔
138
            'This fixer covers rules defined in PSR2 ¶4.4, ¶4.6.',
3✔
139
        );
3✔
140
    }
141

142
    public function isCandidate(Tokens $tokens): bool
143
    {
144
        return $tokens->isTokenKindFound('(');
222✔
145
    }
146

147
    /**
148
     * {@inheritdoc}
149
     *
150
     * Must run before ArrayIndentationFixer, NoTrailingCommaInSinglelineFixer, StatementIndentationFixer.
151
     * Must run after CombineNestedDirnameFixer, FunctionDeclarationFixer, ImplodeCallFixer, LambdaNotUsedImportFixer, NoMultilineWhitespaceAroundDoubleArrowFixer, NoUselessSprintfFixer, PowToExponentiationFixer, StrictParamFixer.
152
     */
153
    public function getPriority(): int
154
    {
155
        return 30;
1✔
156
    }
157

158
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
159
    {
160
        $argumentsAnalyzer = new ArgumentsAnalyzer();
220✔
161

162
        $expectedTokens = [\T_LIST, \T_FUNCTION, CT::T_USE_LAMBDA, \T_FN, \T_CLASS];
220✔
163

164
        $tokenCount = $tokens->count();
220✔
165
        for ($index = 1; $index < $tokenCount; ++$index) {
220✔
166
            $token = $tokens[$index];
220✔
167

168
            if (!$token->equals('(')) {
220✔
169
                continue;
220✔
170
            }
171

172
            $meaningfulTokenBeforeParenthesis = $tokens[$tokens->getPrevMeaningfulToken($index)];
220✔
173

174
            if (
175
                $meaningfulTokenBeforeParenthesis->isKeyword()
220✔
176
                && !$meaningfulTokenBeforeParenthesis->isGivenKind($expectedTokens)
220✔
177
            ) {
178
                continue;
29✔
179
            }
180

181
            $isMultiline = $this->fixFunction($tokens, $index);
212✔
182

183
            if (
184
                $isMultiline
212✔
185
                && \in_array($this->configuration['on_multiline'], ['ensure_fully_multiline', 'ensure_single_line_for_single_argument'], true)
212✔
186
                && !$meaningfulTokenBeforeParenthesis->isGivenKind(\T_LIST)
212✔
187
            ) {
188
                if ('ensure_single_line_for_single_argument' === $this->configuration['on_multiline']) {
120✔
189
                    $endFunctionIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
52✔
190

191
                    if (1 === $argumentsAnalyzer->countArguments($tokens, $index, $endFunctionIndex)) {
52✔
192
                        $this->ensureSingleLineForParentheses($tokens, $index, $endFunctionIndex);
46✔
193
                    } else {
194
                        $this->ensureFunctionFullyMultiline($tokens, $index);
13✔
195
                    }
196
                } else {
197
                    $this->ensureFunctionFullyMultiline($tokens, $index);
69✔
198
                }
199
            }
200
        }
201
    }
202

203
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
204
    {
205
        return new FixerConfigurationResolver([
231✔
206
            (new FixerOptionBuilder('keep_multiple_spaces_after_comma', 'Whether keep multiple spaces after comma.'))
231✔
207
                ->setAllowedTypes(['bool'])
231✔
208
                ->setDefault(false)
231✔
209
                ->getOption(),
231✔
210
            (new FixerOptionBuilder(
231✔
211
                'on_multiline',
231✔
212
                'Defines how to handle function arguments lists that contain newlines.',
231✔
213
            ))
231✔
214
                ->setAllowedValues(['ignore', 'ensure_single_line', 'ensure_fully_multiline', 'ensure_single_line_for_single_argument'])
231✔
215
                ->setDefault('ensure_fully_multiline')
231✔
216
                ->getOption(),
231✔
217
            (new FixerOptionBuilder('after_heredoc', 'Whether the whitespace between heredoc end and comma should be removed.'))
231✔
218
                ->setAllowedTypes(['bool'])
231✔
219
                ->setDefault(Future::getV4OrV3(true, false))
231✔
220
                ->getOption(),
231✔
221
            (new FixerOptionBuilder(
231✔
222
                'attribute_placement',
231✔
223
                'Defines how to handle argument attributes when function definition is multiline.',
231✔
224
            ))
231✔
225
                ->setAllowedValues(['ignore', 'same_line', 'standalone'])
231✔
226
                ->setDefault('standalone')
231✔
227
                ->getOption(),
231✔
228
        ]);
231✔
229
    }
230

231
    /**
232
     * Fix arguments spacing for given function.
233
     *
234
     * @param Tokens $tokens             Tokens to handle
235
     * @param int    $startFunctionIndex Start parenthesis position
236
     *
237
     * @return bool whether the function is multiline
238
     */
239
    private function fixFunction(Tokens $tokens, int $startFunctionIndex): bool
240
    {
241
        $isMultiline = false;
212✔
242

243
        $endFunctionIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $startFunctionIndex);
212✔
244
        $firstWhitespaceIndex = $this->findWhitespaceIndexAfterParenthesis($tokens, $startFunctionIndex, $endFunctionIndex);
212✔
245
        $lastWhitespaceIndex = $this->findWhitespaceIndexAfterParenthesis($tokens, $endFunctionIndex, $startFunctionIndex);
212✔
246

247
        foreach ([$firstWhitespaceIndex, $lastWhitespaceIndex] as $index) {
212✔
248
            if (null === $index || !Preg::match('/\R/', $tokens[$index]->getContent())) {
212✔
249
                continue;
158✔
250
            }
251

252
            if ('ensure_single_line' !== $this->configuration['on_multiline']) {
138✔
253
                $isMultiline = true;
124✔
254

255
                continue;
124✔
256
            }
257

258
            $newLinesRemoved = $this->ensureSingleLine($tokens, $index);
15✔
259

260
            if (!$newLinesRemoved) {
15✔
261
                $isMultiline = true;
4✔
262
            }
263
        }
264

265
        for ($index = $endFunctionIndex - 1; $index > $startFunctionIndex; --$index) {
212✔
266
            $token = $tokens[$index];
208✔
267

268
            if ($token->equals(')')) {
208✔
269
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
45✔
270

271
                continue;
45✔
272
            }
273

274
            if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_CLOSE)) {
208✔
275
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index);
7✔
276

277
                continue;
7✔
278
            }
279

280
            if ($token->equals('}')) {
208✔
281
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
8✔
282

283
                continue;
8✔
284
            }
285

286
            if ($token->equals(',')) {
208✔
287
                $this->fixSpace($tokens, $index);
164✔
288
                if (!$isMultiline && $this->isNewline($tokens[$index + 1])) {
164✔
289
                    $isMultiline = true;
22✔
290
                }
291
            }
292
        }
293

294
        return $isMultiline;
212✔
295
    }
296

297
    private function findWhitespaceIndexAfterParenthesis(Tokens $tokens, int $startParenthesisIndex, int $endParenthesisIndex): ?int
298
    {
299
        $direction = $endParenthesisIndex > $startParenthesisIndex ? 1 : -1;
212✔
300
        $startIndex = $startParenthesisIndex + $direction;
212✔
301
        $endIndex = $endParenthesisIndex - $direction;
212✔
302

303
        for ($index = $startIndex; $index !== $endIndex; $index += $direction) {
212✔
304
            $token = $tokens[$index];
212✔
305

306
            if ($token->isWhitespace()) {
212✔
307
                return $index;
141✔
308
            }
309

310
            if (!$token->isComment()) {
157✔
311
                break;
149✔
312
            }
313
        }
314

315
        return null;
157✔
316
    }
317

318
    /**
319
     * @return bool Whether newlines were removed from the whitespace token
320
     */
321
    private function ensureSingleLine(Tokens $tokens, int $index): bool
322
    {
323
        $previousToken = $tokens[$index - 1];
16✔
324

325
        if ($previousToken->isComment() && !str_starts_with($previousToken->getContent(), '/*')) {
16✔
326
            return false;
4✔
327
        }
328

329
        $content = Preg::replace('/\R\h*/', '', $tokens[$index]->getContent());
12✔
330

331
        $tokens->ensureWhitespaceAtIndex($index, 0, $content);
12✔
332

333
        return true;
12✔
334
    }
335

336
    private function ensureFunctionFullyMultiline(Tokens $tokens, int $startFunctionIndex): void
337
    {
338
        // find out what the indentation is
339
        $searchIndex = $startFunctionIndex;
81✔
340
        do {
341
            $prevWhitespaceTokenIndex = $tokens->getPrevTokenOfKind(
81✔
342
                $searchIndex,
81✔
343
                [[\T_ENCAPSED_AND_WHITESPACE], [\T_INLINE_HTML], [\T_WHITESPACE]],
81✔
344
            );
81✔
345

346
            $searchIndex = $prevWhitespaceTokenIndex;
81✔
347
        } while (null !== $prevWhitespaceTokenIndex
81✔
348
            && !str_contains($tokens[$prevWhitespaceTokenIndex]->getContent(), "\n")
81✔
349
        );
350

351
        if (null === $prevWhitespaceTokenIndex) {
81✔
352
            $existingIndentation = '';
50✔
353
        } elseif (!$tokens[$prevWhitespaceTokenIndex]->isGivenKind(\T_WHITESPACE)) {
40✔
354
            return;
6✔
355
        } else {
356
            $existingIndentation = $tokens[$prevWhitespaceTokenIndex]->getContent();
34✔
357
            $lastLineIndex = strrpos($existingIndentation, "\n");
34✔
358
            $existingIndentation = false === $lastLineIndex
34✔
359
                ? $existingIndentation
×
360
                : substr($existingIndentation, $lastLineIndex + 1);
34✔
361
        }
362

363
        $indentation = $existingIndentation.$this->whitespacesConfig->getIndent();
75✔
364
        $endFunctionIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $startFunctionIndex);
75✔
365

366
        $wasWhitespaceBeforeEndFunctionAddedAsNewToken = $tokens->ensureWhitespaceAtIndex(
75✔
367
            $tokens[$endFunctionIndex - 1]->isWhitespace() ? $endFunctionIndex - 1 : $endFunctionIndex,
75✔
368
            0,
75✔
369
            $this->whitespacesConfig->getLineEnding().$existingIndentation,
75✔
370
        );
75✔
371

372
        if ($wasWhitespaceBeforeEndFunctionAddedAsNewToken) {
75✔
373
            ++$endFunctionIndex;
30✔
374
        }
375

376
        for ($index = $endFunctionIndex - 1; $index > $startFunctionIndex; --$index) {
75✔
377
            $token = $tokens[$index];
75✔
378

379
            // skip nested method calls and arrays
380
            if ($token->equals(')')) {
75✔
381
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
6✔
382

383
                continue;
6✔
384
            }
385

386
            // skip nested arrays
387
            if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_CLOSE)) {
75✔
388
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index);
2✔
389

390
                continue;
2✔
391
            }
392

393
            if ($token->equals('}')) {
75✔
394
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
2✔
395

396
                continue;
2✔
397
            }
398

399
            if ($tokens[$tokens->getNextMeaningfulToken($index)]->equals(')')) {
75✔
400
                continue;
75✔
401
            }
402

403
            if ($token->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
75✔
404
                if ('standalone' === $this->configuration['attribute_placement']) {
8✔
405
                    $this->fixNewline($tokens, $index, $indentation);
6✔
406
                } elseif ('same_line' === $this->configuration['attribute_placement']) {
3✔
407
                    $this->ensureSingleLine($tokens, $index + 1);
2✔
408
                    $tokens->ensureWhitespaceAtIndex($index + 1, 0, ' ');
2✔
409
                }
410
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ATTRIBUTE, $index);
8✔
411

412
                continue;
8✔
413
            }
414

415
            if ($token->equals(',')) {
75✔
416
                $this->fixNewline($tokens, $index, $indentation);
70✔
417
            }
418
        }
419

420
        $this->fixNewline($tokens, $startFunctionIndex, $indentation, false);
75✔
421
    }
422

423
    /**
424
     * Method to insert newline after comma, attribute or opening parenthesis.
425
     *
426
     * @param int    $index       index of a comma
427
     * @param string $indentation the indentation that should be used
428
     * @param bool   $override    whether to override the existing character or not
429
     */
430
    private function fixNewline(Tokens $tokens, int $index, string $indentation, bool $override = true): void
431
    {
432
        if ($tokens[$index + 1]->isComment()) {
75✔
433
            return;
4✔
434
        }
435

436
        if ($tokens[$index + 2]->isComment()) {
73✔
437
            $nextMeaningfulTokenIndex = $tokens->getNextMeaningfulToken($index + 2);
6✔
438
            if (!$this->isNewline($tokens[$nextMeaningfulTokenIndex - 1])) {
6✔
439
                if ($tokens[$nextMeaningfulTokenIndex - 1]->isWhitespace()) {
6✔
440
                    $tokens->clearAt($nextMeaningfulTokenIndex - 1);
2✔
441
                }
442

443
                $tokens->ensureWhitespaceAtIndex($nextMeaningfulTokenIndex, 0, $this->whitespacesConfig->getLineEnding().$indentation);
6✔
444
            }
445

446
            return;
6✔
447
        }
448

449
        $nextMeaningfulTokenIndex = $tokens->getNextMeaningfulToken($index);
71✔
450

451
        if ($tokens[$nextMeaningfulTokenIndex]->equals(')')) {
71✔
452
            return;
×
453
        }
454

455
        $tokens->ensureWhitespaceAtIndex($index + 1, 0, $this->whitespacesConfig->getLineEnding().$indentation);
71✔
456
    }
457

458
    /**
459
     * Method to insert space after comma and remove space before comma.
460
     */
461
    private function fixSpace(Tokens $tokens, int $index): void
462
    {
463
        // remove space before comma if exist
464
        if ($tokens[$index - 1]->isWhitespace()) {
164✔
465
            $prevIndex = $tokens->getPrevNonWhitespace($index - 1);
63✔
466

467
            if (
468
                !$tokens[$prevIndex]->equals(',') && !$tokens[$prevIndex]->isComment()
63✔
469
                && (true === $this->configuration['after_heredoc'] || !$tokens[$prevIndex]->isGivenKind(\T_END_HEREDOC))
63✔
470
            ) {
471
                $tokens->clearAt($index - 1);
47✔
472
            }
473
        }
474

475
        $nextIndex = $index + 1;
164✔
476
        $nextToken = $tokens[$nextIndex];
164✔
477

478
        // Two cases for fix space after comma (exclude multiline comments)
479
        //  1) multiple spaces after comma
480
        //  2) no space after comma
481
        if ($nextToken->isWhitespace()) {
164✔
482
            $newContent = $nextToken->getContent();
158✔
483

484
            if ('ensure_single_line' === $this->configuration['on_multiline']) {
158✔
485
                $newContent = Preg::replace('/\R/', '', $newContent);
13✔
486
            }
487

488
            if (
489
                (false === $this->configuration['keep_multiple_spaces_after_comma'] || Preg::match('/\R/', $newContent))
158✔
490
                && !$this->isCommentLastLineToken($tokens, $index + 2)
158✔
491
            ) {
492
                $newContent = ltrim($newContent, " \t");
138✔
493
            }
494

495
            $tokens[$nextIndex] = new Token([\T_WHITESPACE, '' === $newContent ? ' ' : $newContent]);
158✔
496

497
            return;
158✔
498
        }
499

500
        if (!$this->isCommentLastLineToken($tokens, $index + 1)) {
51✔
501
            $tokens->insertAt($index + 1, new Token([\T_WHITESPACE, ' ']));
45✔
502
        }
503
    }
504

505
    /**
506
     * Check if last item of current line is a comment.
507
     *
508
     * @param Tokens $tokens tokens to handle
509
     * @param int    $index  index of token
510
     */
511
    private function isCommentLastLineToken(Tokens $tokens, int $index): bool
512
    {
513
        if (!$tokens[$index]->isComment() || !$tokens[$index + 1]->isWhitespace()) {
152✔
514
            return false;
142✔
515
        }
516

517
        $content = $tokens[$index + 1]->getContent();
16✔
518

519
        return $content !== ltrim($content, "\r\n");
16✔
520
    }
521

522
    /**
523
     * Checks if token is new line.
524
     */
525
    private function isNewline(Token $token): bool
526
    {
527
        return $token->isWhitespace() && str_contains($token->getContent(), "\n");
144✔
528
    }
529

530
    /**
531
     * Check if the argument content should not be collapsed — either spans multiple lines
532
     * outside nested blocks and boundary whitespace, or contains comments or attributes.
533
     */
534
    private function argumentContentIsMultiline(Tokens $tokens, int $openParenthesis, int $closeParenthesis): bool
535
    {
536
        for ($index = $openParenthesis + 1; $index < $closeParenthesis; ++$index) {
46✔
537
            if ($tokens[$index]->isComment()) {
46✔
538
                return true;
12✔
539
            }
540

541
            if ($index === $openParenthesis + 1 || $index === $closeParenthesis - 1) {
46✔
542
                continue;
46✔
543
            }
544

545
            if ($tokens[$index]->equals('(')) {
40✔
546
                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
13✔
547

548
                continue;
13✔
549
            }
550

551
            if ($tokens[$index]->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) {
40✔
552
                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index);
4✔
553

554
                continue;
4✔
555
            }
556

557
            if ($tokens[$index]->equals('{')) {
36✔
558
                $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
2✔
559

560
                continue;
2✔
561
            }
562

563
            if ($this->isNewline($tokens[$index])) {
36✔
564
                return true;
8✔
565
            }
566
        }
567

568
        return false;
27✔
569
    }
570

571
    /**
572
     * Collapse a multiline single-argument call/declaration to a single line.
573
     */
574
    private function ensureSingleLineForParentheses(Tokens $tokens, int $openParenthesis, int $closeParenthesis): void
575
    {
576
        if ($this->argumentContentIsMultiline($tokens, $openParenthesis, $closeParenthesis)) {
46✔
577
            return;
20✔
578
        }
579

580
        for ($index = $closeParenthesis - 1; $index > $openParenthesis; --$index) {
27✔
581
            if ($tokens[$index]->equals(')')) {
27✔
582
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
8✔
583

584
                continue;
8✔
585
            }
586

587
            if ($tokens[$index]->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_CLOSE)) {
27✔
588
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index);
4✔
589

590
                continue;
4✔
591
            }
592

593
            if ($tokens[$index]->equals('}')) {
27✔
594
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
2✔
595

596
                continue;
2✔
597
            }
598

599
            if ($tokens[$index]->isWhitespace()) {
27✔
600
                $previousToken = $tokens[$index - 1];
27✔
601

602
                if ($previousToken->isComment() && !str_starts_with($previousToken->getContent(), '/*')) {
27✔
UNCOV
603
                    continue;
×
604
                }
605

606
                $content = Preg::replace('/\R\h*/', '', $tokens[$index]->getContent());
27✔
607
                $tokens->ensureWhitespaceAtIndex($index, 0, $content);
27✔
608
            }
609
        }
610
    }
611
}
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