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

keradus / PHP-CS-Fixer / 16999983712

15 Aug 2025 09:42PM UTC coverage: 94.75% (-0.09%) from 94.839%
16999983712

push

github

keradus
ci: more self-fixing checks on lowest/highest PHP

28263 of 29829 relevant lines covered (94.75%)

45.88 hits per line

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

99.16
/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\Preg;
28
use PhpCsFixer\Tokenizer\CT;
29
use PhpCsFixer\Tokenizer\Token;
30
use PhpCsFixer\Tokenizer\Tokens;
31

32
/**
33
 * @phpstan-type _AutogeneratedInputConfiguration array{
34
 *  after_heredoc?: bool,
35
 *  attribute_placement?: 'ignore'|'same_line'|'standalone',
36
 *  keep_multiple_spaces_after_comma?: bool,
37
 *  on_multiline?: 'ensure_fully_multiline'|'ensure_single_line'|'ignore',
38
 * }
39
 * @phpstan-type _AutogeneratedComputedConfiguration array{
40
 *  after_heredoc: bool,
41
 *  attribute_placement: 'ignore'|'same_line'|'standalone',
42
 *  keep_multiple_spaces_after_comma: bool,
43
 *  on_multiline: 'ensure_fully_multiline'|'ensure_single_line'|'ignore',
44
 * }
45
 *
46
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
47
 *
48
 * @author Kuanhung Chen <ericj.tw@gmail.com>
49
 */
50
final class MethodArgumentSpaceFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
51
{
52
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
53
    use ConfigurableFixerTrait;
54

55
    public function getDefinition(): FixerDefinitionInterface
56
    {
57
        return new FixerDefinition(
3✔
58
            '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✔
59
            [
3✔
60
                new CodeSample(
3✔
61
                    "<?php\nfunction sample(\$a=10,\$b=20,\$c=30) {}\nsample(1,  2);\n",
3✔
62
                    null
3✔
63
                ),
3✔
64
                new CodeSample(
3✔
65
                    "<?php\nfunction sample(\$a=10,\$b=20,\$c=30) {}\nsample(1,  2);\n",
3✔
66
                    ['keep_multiple_spaces_after_comma' => false]
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' => true]
3✔
71
                ),
3✔
72
                new CodeSample(
3✔
73
                    "<?php\nfunction sample(\$a=10,\n    \$b=20,\$c=30) {}\nsample(1,\n    2);\n",
3✔
74
                    ['on_multiline' => 'ensure_fully_multiline']
3✔
75
                ),
3✔
76
                new CodeSample(
3✔
77
                    "<?php\nfunction sample(\n    \$a=10,\n    \$b=20,\n    \$c=30\n) {}\nsample(\n    1,\n    2\n);\n",
3✔
78
                    ['on_multiline' => 'ensure_single_line']
3✔
79
                ),
3✔
80
                new CodeSample(
3✔
81
                    "<?php\nfunction sample(\$a=10,\n    \$b=20,\$c=30) {}\nsample(1,  \n    2);\nsample('foo',    'foobarbaz', 'baz');\nsample('foobar', 'bar',       'baz');\n",
3✔
82
                    [
3✔
83
                        'on_multiline' => 'ensure_fully_multiline',
3✔
84
                        'keep_multiple_spaces_after_comma' => true,
3✔
85
                    ]
3✔
86
                ),
3✔
87
                new CodeSample(
3✔
88
                    "<?php\nfunction sample(\$a=10,\n    \$b=20,\$c=30) {}\nsample(1,  \n    2);\nsample('foo',    'foobarbaz', 'baz');\nsample('foobar', 'bar',       'baz');\n",
3✔
89
                    [
3✔
90
                        'on_multiline' => 'ensure_fully_multiline',
3✔
91
                        'keep_multiple_spaces_after_comma' => false,
3✔
92
                    ]
3✔
93
                ),
3✔
94
                new CodeSample(
3✔
95
                    "<?php\nfunction sample(#[Foo] #[Bar] \$a=10,\n    \$b=20,\$c=30) {}\nsample(1,  2);\n",
3✔
96
                    [
3✔
97
                        'on_multiline' => 'ensure_fully_multiline',
3✔
98
                        'attribute_placement' => 'ignore',
3✔
99
                    ]
3✔
100
                ),
3✔
101
                new CodeSample(
3✔
102
                    "<?php\nfunction sample(#[Foo]\n    #[Bar]\n    \$a=10,\n    \$b=20,\$c=30) {}\nsample(1,  2);\n",
3✔
103
                    [
3✔
104
                        'on_multiline' => 'ensure_fully_multiline',
3✔
105
                        'attribute_placement' => 'same_line',
3✔
106
                    ]
3✔
107
                ),
3✔
108
                new CodeSample(
3✔
109
                    "<?php\nfunction sample(#[Foo] #[Bar] \$a=10,\n    \$b=20,\$c=30) {}\nsample(1,  2);\n",
3✔
110
                    [
3✔
111
                        'on_multiline' => 'ensure_fully_multiline',
3✔
112
                        'attribute_placement' => 'standalone',
3✔
113
                    ]
3✔
114
                ),
3✔
115
                new CodeSample(
3✔
116
                    <<<'SAMPLE'
3✔
117
                        <?php
118
                        sample(
119
                            <<<EOD
120
                                foo
121
                                EOD
122
                            ,
123
                            'bar'
124
                        );
125

126
                        SAMPLE,
3✔
127
                    ['after_heredoc' => true]
3✔
128
                ),
3✔
129
            ],
3✔
130
            'This fixer covers rules defined in PSR2 ¶4.4, ¶4.6.'
3✔
131
        );
3✔
132
    }
133

134
    public function isCandidate(Tokens $tokens): bool
135
    {
136
        return $tokens->isTokenKindFound('(');
167✔
137
    }
138

139
    /**
140
     * {@inheritdoc}
141
     *
142
     * Must run before ArrayIndentationFixer, StatementIndentationFixer.
143
     * Must run after CombineNestedDirnameFixer, FunctionDeclarationFixer, ImplodeCallFixer, LambdaNotUsedImportFixer, NoMultilineWhitespaceAroundDoubleArrowFixer, NoUselessSprintfFixer, PowToExponentiationFixer, StrictParamFixer.
144
     */
145
    public function getPriority(): int
146
    {
147
        return 30;
1✔
148
    }
149

150
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
151
    {
152
        $expectedTokens = [\T_LIST, \T_FUNCTION, CT::T_USE_LAMBDA, \T_FN, \T_CLASS];
165✔
153

154
        $tokenCount = $tokens->count();
165✔
155
        for ($index = 1; $index < $tokenCount; ++$index) {
165✔
156
            $token = $tokens[$index];
165✔
157

158
            if (!$token->equals('(')) {
165✔
159
                continue;
165✔
160
            }
161

162
            $meaningfulTokenBeforeParenthesis = $tokens[$tokens->getPrevMeaningfulToken($index)];
165✔
163

164
            if (
165
                $meaningfulTokenBeforeParenthesis->isKeyword()
165✔
166
                && !$meaningfulTokenBeforeParenthesis->isGivenKind($expectedTokens)
165✔
167
            ) {
168
                continue;
29✔
169
            }
170

171
            $isMultiline = $this->fixFunction($tokens, $index);
157✔
172

173
            if (
174
                $isMultiline
157✔
175
                && 'ensure_fully_multiline' === $this->configuration['on_multiline']
157✔
176
                && !$meaningfulTokenBeforeParenthesis->isGivenKind(\T_LIST)
157✔
177
            ) {
178
                $this->ensureFunctionFullyMultiline($tokens, $index);
69✔
179
            }
180
        }
181
    }
182

183
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
184
    {
185
        return new FixerConfigurationResolver([
176✔
186
            (new FixerOptionBuilder('keep_multiple_spaces_after_comma', 'Whether keep multiple spaces after comma.'))
176✔
187
                ->setAllowedTypes(['bool'])
176✔
188
                ->setDefault(false)
176✔
189
                ->getOption(),
176✔
190
            (new FixerOptionBuilder(
176✔
191
                'on_multiline',
176✔
192
                'Defines how to handle function arguments lists that contain newlines.'
176✔
193
            ))
176✔
194
                ->setAllowedValues(['ignore', 'ensure_single_line', 'ensure_fully_multiline'])
176✔
195
                ->setDefault('ensure_fully_multiline')
176✔
196
                ->getOption(),
176✔
197
            (new FixerOptionBuilder('after_heredoc', 'Whether the whitespace between heredoc end and comma should be removed.'))
176✔
198
                ->setAllowedTypes(['bool'])
176✔
199
                ->setDefault(false) // @TODO 4.0: set to true
176✔
200
                ->getOption(),
176✔
201
            (new FixerOptionBuilder(
176✔
202
                'attribute_placement',
176✔
203
                'Defines how to handle argument attributes when function definition is multiline.'
176✔
204
            ))
176✔
205
                ->setAllowedValues(['ignore', 'same_line', 'standalone'])
176✔
206
                ->setDefault('standalone')
176✔
207
                ->getOption(),
176✔
208
        ]);
176✔
209
    }
210

211
    /**
212
     * Fix arguments spacing for given function.
213
     *
214
     * @param Tokens $tokens             Tokens to handle
215
     * @param int    $startFunctionIndex Start parenthesis position
216
     *
217
     * @return bool whether the function is multiline
218
     */
219
    private function fixFunction(Tokens $tokens, int $startFunctionIndex): bool
220
    {
221
        $isMultiline = false;
157✔
222

223
        $endFunctionIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $startFunctionIndex);
157✔
224
        $firstWhitespaceIndex = $this->findWhitespaceIndexAfterParenthesis($tokens, $startFunctionIndex, $endFunctionIndex);
157✔
225
        $lastWhitespaceIndex = $this->findWhitespaceIndexAfterParenthesis($tokens, $endFunctionIndex, $startFunctionIndex);
157✔
226

227
        foreach ([$firstWhitespaceIndex, $lastWhitespaceIndex] as $index) {
157✔
228
            if (null === $index || !Preg::match('/\R/', $tokens[$index]->getContent())) {
157✔
229
                continue;
124✔
230
            }
231

232
            if ('ensure_single_line' !== $this->configuration['on_multiline']) {
87✔
233
                $isMultiline = true;
73✔
234

235
                continue;
73✔
236
            }
237

238
            $newLinesRemoved = $this->ensureSingleLine($tokens, $index);
15✔
239

240
            if (!$newLinesRemoved) {
15✔
241
                $isMultiline = true;
4✔
242
            }
243
        }
244

245
        for ($index = $endFunctionIndex - 1; $index > $startFunctionIndex; --$index) {
157✔
246
            $token = $tokens[$index];
155✔
247

248
            if ($token->equals(')')) {
155✔
249
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
32✔
250

251
                continue;
32✔
252
            }
253

254
            if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_CLOSE)) {
155✔
255
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index);
3✔
256

257
                continue;
3✔
258
            }
259

260
            if ($token->equals('}')) {
155✔
261
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
6✔
262

263
                continue;
6✔
264
            }
265

266
            if ($token->equals(',')) {
155✔
267
                $this->fixSpace($tokens, $index);
148✔
268
                if (!$isMultiline && $this->isNewline($tokens[$index + 1])) {
148✔
269
                    $isMultiline = true;
22✔
270
                }
271
            }
272
        }
273

274
        return $isMultiline;
157✔
275
    }
276

277
    private function findWhitespaceIndexAfterParenthesis(Tokens $tokens, int $startParenthesisIndex, int $endParenthesisIndex): ?int
278
    {
279
        $direction = $endParenthesisIndex > $startParenthesisIndex ? 1 : -1;
157✔
280
        $startIndex = $startParenthesisIndex + $direction;
157✔
281
        $endIndex = $endParenthesisIndex - $direction;
157✔
282

283
        for ($index = $startIndex; $index !== $endIndex; $index += $direction) {
157✔
284
            $token = $tokens[$index];
157✔
285

286
            if ($token->isWhitespace()) {
157✔
287
                return $index;
90✔
288
            }
289

290
            if (!$token->isComment()) {
131✔
291
                break;
123✔
292
            }
293
        }
294

295
        return null;
123✔
296
    }
297

298
    /**
299
     * @return bool Whether newlines were removed from the whitespace token
300
     */
301
    private function ensureSingleLine(Tokens $tokens, int $index): bool
302
    {
303
        $previousToken = $tokens[$index - 1];
16✔
304

305
        if ($previousToken->isComment() && !str_starts_with($previousToken->getContent(), '/*')) {
16✔
306
            return false;
4✔
307
        }
308

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

311
        $tokens->ensureWhitespaceAtIndex($index, 0, $content);
12✔
312

313
        return true;
12✔
314
    }
315

316
    private function ensureFunctionFullyMultiline(Tokens $tokens, int $startFunctionIndex): void
317
    {
318
        // find out what the indentation is
319
        $searchIndex = $startFunctionIndex;
69✔
320
        do {
321
            $prevWhitespaceTokenIndex = $tokens->getPrevTokenOfKind(
69✔
322
                $searchIndex,
69✔
323
                [[\T_ENCAPSED_AND_WHITESPACE], [\T_INLINE_HTML], [\T_WHITESPACE]],
69✔
324
            );
69✔
325

326
            $searchIndex = $prevWhitespaceTokenIndex;
69✔
327
        } while (null !== $prevWhitespaceTokenIndex
69✔
328
            && !str_contains($tokens[$prevWhitespaceTokenIndex]->getContent(), "\n")
69✔
329
        );
330

331
        if (null === $prevWhitespaceTokenIndex) {
69✔
332
            $existingIndentation = '';
42✔
333
        } elseif (!$tokens[$prevWhitespaceTokenIndex]->isGivenKind(\T_WHITESPACE)) {
34✔
334
            return;
6✔
335
        } else {
336
            $existingIndentation = $tokens[$prevWhitespaceTokenIndex]->getContent();
28✔
337
            $lastLineIndex = strrpos($existingIndentation, "\n");
28✔
338
            $existingIndentation = false === $lastLineIndex
28✔
339
                ? $existingIndentation
×
340
                : substr($existingIndentation, $lastLineIndex + 1);
28✔
341
        }
342

343
        $indentation = $existingIndentation.$this->whitespacesConfig->getIndent();
63✔
344
        $endFunctionIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $startFunctionIndex);
63✔
345

346
        $wasWhitespaceBeforeEndFunctionAddedAsNewToken = $tokens->ensureWhitespaceAtIndex(
63✔
347
            $tokens[$endFunctionIndex - 1]->isWhitespace() ? $endFunctionIndex - 1 : $endFunctionIndex,
63✔
348
            0,
63✔
349
            $this->whitespacesConfig->getLineEnding().$existingIndentation
63✔
350
        );
63✔
351

352
        if ($wasWhitespaceBeforeEndFunctionAddedAsNewToken) {
63✔
353
            ++$endFunctionIndex;
30✔
354
        }
355

356
        for ($index = $endFunctionIndex - 1; $index > $startFunctionIndex; --$index) {
63✔
357
            $token = $tokens[$index];
63✔
358

359
            // skip nested method calls and arrays
360
            if ($token->equals(')')) {
63✔
361
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
6✔
362

363
                continue;
6✔
364
            }
365

366
            // skip nested arrays
367
            if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_CLOSE)) {
63✔
368
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index);
2✔
369

370
                continue;
2✔
371
            }
372

373
            if ($token->equals('}')) {
63✔
374
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
2✔
375

376
                continue;
2✔
377
            }
378

379
            if ($tokens[$tokens->getNextMeaningfulToken($index)]->equals(')')) {
63✔
380
                continue;
63✔
381
            }
382

383
            if ($token->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
63✔
384
                if ('standalone' === $this->configuration['attribute_placement']) {
8✔
385
                    $this->fixNewline($tokens, $index, $indentation);
6✔
386
                } elseif ('same_line' === $this->configuration['attribute_placement']) {
3✔
387
                    $this->ensureSingleLine($tokens, $index + 1);
2✔
388
                    $tokens->ensureWhitespaceAtIndex($index + 1, 0, ' ');
2✔
389
                }
390
                $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ATTRIBUTE, $index);
8✔
391

392
                continue;
8✔
393
            }
394

395
            if ($token->equals(',')) {
63✔
396
                $this->fixNewline($tokens, $index, $indentation);
58✔
397
            }
398
        }
399

400
        $this->fixNewline($tokens, $startFunctionIndex, $indentation, false);
63✔
401
    }
402

403
    /**
404
     * Method to insert newline after comma, attribute or opening parenthesis.
405
     *
406
     * @param int    $index       index of a comma
407
     * @param string $indentation the indentation that should be used
408
     * @param bool   $override    whether to override the existing character or not
409
     */
410
    private function fixNewline(Tokens $tokens, int $index, string $indentation, bool $override = true): void
411
    {
412
        if ($tokens[$index + 1]->isComment()) {
63✔
413
            return;
4✔
414
        }
415

416
        if ($tokens[$index + 2]->isComment()) {
61✔
417
            $nextMeaningfulTokenIndex = $tokens->getNextMeaningfulToken($index + 2);
6✔
418
            if (!$this->isNewline($tokens[$nextMeaningfulTokenIndex - 1])) {
6✔
419
                if ($tokens[$nextMeaningfulTokenIndex - 1]->isWhitespace()) {
6✔
420
                    $tokens->clearAt($nextMeaningfulTokenIndex - 1);
2✔
421
                }
422

423
                $tokens->ensureWhitespaceAtIndex($nextMeaningfulTokenIndex, 0, $this->whitespacesConfig->getLineEnding().$indentation);
6✔
424
            }
425

426
            return;
6✔
427
        }
428

429
        $nextMeaningfulTokenIndex = $tokens->getNextMeaningfulToken($index);
59✔
430

431
        if ($tokens[$nextMeaningfulTokenIndex]->equals(')')) {
59✔
432
            return;
×
433
        }
434

435
        $tokens->ensureWhitespaceAtIndex($index + 1, 0, $this->whitespacesConfig->getLineEnding().$indentation);
59✔
436
    }
437

438
    /**
439
     * Method to insert space after comma and remove space before comma.
440
     */
441
    private function fixSpace(Tokens $tokens, int $index): void
442
    {
443
        // remove space before comma if exist
444
        if ($tokens[$index - 1]->isWhitespace()) {
148✔
445
            $prevIndex = $tokens->getPrevNonWhitespace($index - 1);
63✔
446

447
            if (
448
                !$tokens[$prevIndex]->equals(',') && !$tokens[$prevIndex]->isComment()
63✔
449
                && (true === $this->configuration['after_heredoc'] || !$tokens[$prevIndex]->isGivenKind(\T_END_HEREDOC))
63✔
450
            ) {
451
                $tokens->clearAt($index - 1);
47✔
452
            }
453
        }
454

455
        $nextIndex = $index + 1;
148✔
456
        $nextToken = $tokens[$nextIndex];
148✔
457

458
        // Two cases for fix space after comma (exclude multiline comments)
459
        //  1) multiple spaces after comma
460
        //  2) no space after comma
461
        if ($nextToken->isWhitespace()) {
148✔
462
            $newContent = $nextToken->getContent();
142✔
463

464
            if ('ensure_single_line' === $this->configuration['on_multiline']) {
142✔
465
                $newContent = Preg::replace('/\R/', '', $newContent);
13✔
466
            }
467

468
            if (
469
                (false === $this->configuration['keep_multiple_spaces_after_comma'] || Preg::match('/\R/', $newContent))
142✔
470
                && !$this->isCommentLastLineToken($tokens, $index + 2)
142✔
471
            ) {
472
                $newContent = ltrim($newContent, " \t");
122✔
473
            }
474

475
            $tokens[$nextIndex] = new Token([\T_WHITESPACE, '' === $newContent ? ' ' : $newContent]);
142✔
476

477
            return;
142✔
478
        }
479

480
        if (!$this->isCommentLastLineToken($tokens, $index + 1)) {
51✔
481
            $tokens->insertAt($index + 1, new Token([\T_WHITESPACE, ' ']));
45✔
482
        }
483
    }
484

485
    /**
486
     * Check if last item of current line is a comment.
487
     *
488
     * @param Tokens $tokens tokens to handle
489
     * @param int    $index  index of token
490
     */
491
    private function isCommentLastLineToken(Tokens $tokens, int $index): bool
492
    {
493
        if (!$tokens[$index]->isComment() || !$tokens[$index + 1]->isWhitespace()) {
136✔
494
            return false;
126✔
495
        }
496

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

499
        return $content !== ltrim($content, "\r\n");
16✔
500
    }
501

502
    /**
503
     * Checks if token is new line.
504
     */
505
    private function isNewline(Token $token): bool
506
    {
507
        return $token->isWhitespace() && str_contains($token->getContent(), "\n");
107✔
508
    }
509
}
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