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

keradus / PHP-CS-Fixer / 17319949156

29 Aug 2025 09:20AM UTC coverage: 94.696% (-0.05%) from 94.744%
17319949156

push

github

keradus
CS

28333 of 29920 relevant lines covered (94.7%)

45.63 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
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
51
 */
52
final class MethodArgumentSpaceFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
53
{
54
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
55
    use ConfigurableFixerTrait;
56

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

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

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

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

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

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

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

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

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

173
            $isMultiline = $this->fixFunction($tokens, $index);
157✔
174

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

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

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

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

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

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

237
                continue;
73✔
238
            }
239

240
            $newLinesRemoved = $this->ensureSingleLine($tokens, $index);
15✔
241

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

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

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

253
                continue;
32✔
254
            }
255

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

259
                continue;
3✔
260
            }
261

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

265
                continue;
6✔
266
            }
267

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

276
        return $isMultiline;
157✔
277
    }
278

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

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

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

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

297
        return null;
123✔
298
    }
299

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

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

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

313
        $tokens->ensureWhitespaceAtIndex($index, 0, $content);
12✔
314

315
        return true;
12✔
316
    }
317

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

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

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

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

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

354
        if ($wasWhitespaceBeforeEndFunctionAddedAsNewToken) {
63✔
355
            ++$endFunctionIndex;
30✔
356
        }
357

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

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

365
                continue;
6✔
366
            }
367

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

372
                continue;
2✔
373
            }
374

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

378
                continue;
2✔
379
            }
380

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

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

394
                continue;
8✔
395
            }
396

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

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

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

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

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

428
            return;
6✔
429
        }
430

431
        $nextMeaningfulTokenIndex = $tokens->getNextMeaningfulToken($index);
59✔
432

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

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

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

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

457
        $nextIndex = $index + 1;
148✔
458
        $nextToken = $tokens[$nextIndex];
148✔
459

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

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

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

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

479
            return;
142✔
480
        }
481

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

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

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

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

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