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

keradus / PHP-CS-Fixer / 13747606772

08 Mar 2025 10:38PM UTC coverage: 94.847% (-0.08%) from 94.929%
13747606772

push

github

web-flow
fix: `MbStrFunctionsFixer` - fix imports (#8474)

23 of 24 new or added lines in 1 file covered. (95.83%)

181 existing lines in 6 files now uncovered.

28127 of 29655 relevant lines covered (94.85%)

43.08 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
 * @author Kuanhung Chen <ericj.tw@gmail.com>
34
 *
35
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
36
 *
37
 * @phpstan-type _AutogeneratedInputConfiguration array{
38
 *  after_heredoc?: bool,
39
 *  attribute_placement?: 'ignore'|'same_line'|'standalone',
40
 *  keep_multiple_spaces_after_comma?: bool,
41
 *  on_multiline?: 'ensure_fully_multiline'|'ensure_single_line'|'ignore'
42
 * }
43
 * @phpstan-type _AutogeneratedComputedConfiguration array{
44
 *  after_heredoc: bool,
45
 *  attribute_placement: 'ignore'|'same_line'|'standalone',
46
 *  keep_multiple_spaces_after_comma: bool,
47
 *  on_multiline: 'ensure_fully_multiline'|'ensure_single_line'|'ignore'
48
 * }
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
                    ,
3✔
128
                    ['after_heredoc' => true]
3✔
129
                ),
3✔
130
            ],
3✔
131
            'This fixer covers rules defined in PSR2 ¶4.4, ¶4.6.'
3✔
132
        );
3✔
133
    }
134

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

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

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

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

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

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

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

172
            $isMultiline = $this->fixFunction($tokens, $index);
155✔
173

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

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

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

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

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

233
            if ('ensure_single_line' !== $this->configuration['on_multiline']) {
85✔
234
                $isMultiline = true;
71✔
235

236
                continue;
71✔
237
            }
238

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

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

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

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

252
                continue;
32✔
253
            }
254

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

258
                continue;
3✔
259
            }
260

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

264
                continue;
6✔
265
            }
266

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

275
        return $isMultiline;
155✔
276
    }
277

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

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

287
            if ($token->isWhitespace()) {
155✔
288
                return $index;
88✔
289
            }
290

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

296
        return null;
123✔
297
    }
298

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

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

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

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

314
        return true;
12✔
315
    }
316

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

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

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

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

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

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

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

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

364
                continue;
6✔
365
            }
366

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

371
                continue;
2✔
372
            }
373

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

377
                continue;
2✔
378
            }
379

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

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

393
                continue;
8✔
394
            }
395

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

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

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

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

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

427
            return;
6✔
428
        }
429

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

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

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

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

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

456
        $nextIndex = $index + 1;
146✔
457
        $nextToken = $tokens[$nextIndex];
146✔
458

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

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

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

476
            $tokens[$nextIndex] = new Token([T_WHITESPACE, '' === $newContent ? ' ' : $newContent]);
140✔
477

478
            return;
140✔
479
        }
480

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

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

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

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

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