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

keradus / PHP-CS-Fixer / 17377459942

01 Sep 2025 12:19PM UTC coverage: 94.684% (-0.009%) from 94.693%
17377459942

push

github

web-flow
chore: `Tokens::offsetSet` - explicit validation of input (#9004)

1 of 5 new or added lines in 1 file covered. (20.0%)

306 existing lines in 60 files now uncovered.

28390 of 29984 relevant lines covered (94.68%)

45.5 hits per line

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

99.57
/src/Fixer/PhpUnit/PhpUnitDedicateAssertFixer.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\PhpUnit;
16

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

31
/**
32
 * @phpstan-type _AutogeneratedInputConfiguration array{
33
 *  target?: '3.0'|'3.5'|'5.0'|'5.6'|'newest',
34
 * }
35
 * @phpstan-type _AutogeneratedComputedConfiguration array{
36
 *  target: '3.0'|'3.5'|'5.0'|'5.6'|'newest',
37
 * }
38
 *
39
 * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
40
 *
41
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
42
 *
43
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
44
 */
45
final class PhpUnitDedicateAssertFixer extends AbstractPhpUnitFixer implements ConfigurableFixerInterface
46
{
47
    /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
48
    use ConfigurableFixerTrait;
49

50
    /**
51
     * @var array<string, array{positive: string, negative: false|string, argument_count?: int, swap_arguments?: true}|true>
52
     */
53
    private const FIX_MAP = [
54
        'array_key_exists' => [
55
            'positive' => 'assertArrayHasKey',
56
            'negative' => 'assertArrayNotHasKey',
57
            'argument_count' => 2,
58
        ],
59
        'empty' => [
60
            'positive' => 'assertEmpty',
61
            'negative' => 'assertNotEmpty',
62
        ],
63
        'file_exists' => [
64
            'positive' => 'assertFileExists',
65
            'negative' => 'assertFileNotExists',
66
        ],
67
        'is_array' => true,
68
        'is_bool' => true,
69
        'is_callable' => true,
70
        'is_dir' => [
71
            'positive' => 'assertDirectoryExists',
72
            'negative' => 'assertDirectoryNotExists',
73
        ],
74
        'is_double' => true,
75
        'is_float' => true,
76
        'is_infinite' => [
77
            'positive' => 'assertInfinite',
78
            'negative' => 'assertFinite',
79
        ],
80
        'is_int' => true,
81
        'is_integer' => true,
82
        'is_long' => true,
83
        'is_nan' => [
84
            'positive' => 'assertNan',
85
            'negative' => false,
86
        ],
87
        'is_null' => [
88
            'positive' => 'assertNull',
89
            'negative' => 'assertNotNull',
90
        ],
91
        'is_numeric' => true,
92
        'is_object' => true,
93
        'is_readable' => [
94
            'positive' => 'assertIsReadable',
95
            'negative' => 'assertNotIsReadable',
96
        ],
97
        'is_real' => true,
98
        'is_resource' => true,
99
        'is_scalar' => true,
100
        'is_string' => true,
101
        'is_writable' => [
102
            'positive' => 'assertIsWritable',
103
            'negative' => 'assertNotIsWritable',
104
        ],
105
        'str_contains' => [ // since 7.5
106
            'positive' => 'assertStringContainsString',
107
            'negative' => 'assertStringNotContainsString',
108
            'argument_count' => 2,
109
            'swap_arguments' => true,
110
        ],
111
        'str_ends_with' => [ // since 3.4
112
            'positive' => 'assertStringEndsWith',
113
            'negative' => 'assertStringEndsNotWith',
114
            'argument_count' => 2,
115
            'swap_arguments' => true,
116
        ],
117
        'str_starts_with' => [ // since 3.4
118
            'positive' => 'assertStringStartsWith',
119
            'negative' => 'assertStringStartsNotWith',
120
            'argument_count' => 2,
121
            'swap_arguments' => true,
122
        ],
123
    ];
124

125
    /**
126
     * @var list<string>
127
     */
128
    private array $functions = [];
129

130
    public function isRisky(): bool
131
    {
132
        return true;
1✔
133
    }
134

135
    public function getDefinition(): FixerDefinitionInterface
136
    {
137
        return new FixerDefinition(
3✔
138
            'PHPUnit assertions like `assertInternalType`, `assertFileExists`, should be used over `assertTrue`.',
3✔
139
            [
3✔
140
                new CodeSample(
3✔
141
                    <<<'PHP'
3✔
142
                        <?php
143
                        final class MyTest extends \PHPUnit_Framework_TestCase
144
                        {
145
                            public function testSomeTest()
146
                            {
147
                                $this->assertTrue(is_float( $a), "my message");
148
                                $this->assertTrue(is_nan($a));
149
                            }
150
                        }
151

152
                        PHP
3✔
153
                ),
3✔
154
                new CodeSample(
3✔
155
                    <<<'PHP'
3✔
156
                        <?php
157
                        final class MyTest extends \PHPUnit_Framework_TestCase
158
                        {
159
                            public function testSomeTest()
160
                            {
161
                                $this->assertTrue(is_dir($a));
162
                                $this->assertTrue(is_writable($a));
163
                                $this->assertTrue(is_readable($a));
164
                            }
165
                        }
166

167
                        PHP,
3✔
168
                    ['target' => PhpUnitTargetVersion::VERSION_5_6]
3✔
169
                ),
3✔
170
            ],
3✔
171
            null,
3✔
172
            'Fixer could be risky if one is overriding PHPUnit\'s native methods.'
3✔
173
        );
3✔
174
    }
175

176
    /**
177
     * {@inheritdoc}
178
     *
179
     * Must run before NoUnusedImportsFixer, PhpUnitAssertNewNamesFixer, PhpUnitDedicateAssertInternalTypeFixer.
180
     * Must run after ModernizeStrposFixer, NoAliasFunctionsFixer, PhpUnitConstructFixer.
181
     */
182
    public function getPriority(): int
183
    {
184
        return -9;
1✔
185
    }
186

187
    protected function configurePostNormalisation(): void
188
    {
189
        // assertions added in 3.0: assertArrayNotHasKey assertArrayHasKey assertFileNotExists assertFileExists assertNotNull, assertNull
190
        $this->functions = [
123✔
191
            'array_key_exists',
123✔
192
            'file_exists',
123✔
193
            'is_null',
123✔
194
            'str_ends_with',
123✔
195
            'str_starts_with',
123✔
196
        ];
123✔
197

198
        if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_3_5)) {
123✔
199
            // assertions added in 3.5: assertInternalType assertNotEmpty assertEmpty
200
            $this->functions = array_merge($this->functions, [
123✔
201
                'empty',
123✔
202
                'is_array',
123✔
203
                'is_bool',
123✔
204
                'is_boolean',
123✔
205
                'is_callable',
123✔
206
                'is_double',
123✔
207
                'is_float',
123✔
208
                'is_int',
123✔
209
                'is_integer',
123✔
210
                'is_long',
123✔
211
                'is_numeric',
123✔
212
                'is_object',
123✔
213
                'is_real',
123✔
214
                'is_scalar',
123✔
215
                'is_string',
123✔
216
            ]);
123✔
217
        }
218

219
        if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_5_0)) {
123✔
220
            // assertions added in 5.0: assertFinite assertInfinite assertNan
221
            $this->functions = array_merge($this->functions, [
123✔
222
                'is_infinite',
123✔
223
                'is_nan',
123✔
224
            ]);
123✔
225
        }
226

227
        if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_5_6)) {
123✔
228
            // assertions added in 5.6: assertDirectoryExists assertDirectoryNotExists assertIsReadable assertNotIsReadable assertIsWritable assertNotIsWritable
229
            $this->functions = array_merge($this->functions, [
123✔
230
                'is_dir',
123✔
231
                'is_readable',
123✔
232
                'is_writable',
123✔
233
            ]);
123✔
234
        }
235

236
        if (PhpUnitTargetVersion::fulfills($this->configuration['target'], PhpUnitTargetVersion::VERSION_7_5)) {
123✔
237
            $this->functions = array_merge($this->functions, [
123✔
238
                'str_contains',
123✔
239
            ]);
123✔
240
        }
241
    }
242

243
    protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void
244
    {
245
        $argumentsAnalyzer = new ArgumentsAnalyzer();
112✔
246

247
        foreach ($this->getPreviousAssertCall($tokens, $startIndex, $endIndex) as $assertCall) {
112✔
248
            // test and fix for assertTrue/False to dedicated asserts
249
            if (\in_array($assertCall['loweredName'], ['asserttrue', 'assertfalse'], true)) {
109✔
250
                $this->fixAssertTrueFalse($tokens, $argumentsAnalyzer, $assertCall);
71✔
251

252
                continue;
71✔
253
            }
254

255
            if (\in_array(
100✔
256
                $assertCall['loweredName'],
100✔
257
                ['assertsame', 'assertnotsame', 'assertequals', 'assertnotequals'],
100✔
258
                true
100✔
259
            )) {
100✔
260
                $this->fixAssertSameEquals($tokens, $assertCall);
38✔
261
            }
262
        }
263
    }
264

265
    protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
266
    {
267
        return new FixerConfigurationResolver([
123✔
268
            (new FixerOptionBuilder('target', 'Target version of PHPUnit.'))
123✔
269
                ->setAllowedTypes(['string'])
123✔
270
                ->setAllowedValues([
123✔
271
                    PhpUnitTargetVersion::VERSION_3_0,
123✔
272
                    PhpUnitTargetVersion::VERSION_3_5,
123✔
273
                    PhpUnitTargetVersion::VERSION_5_0,
123✔
274
                    PhpUnitTargetVersion::VERSION_5_6,
123✔
275
                    PhpUnitTargetVersion::VERSION_NEWEST,
123✔
276
                ])
123✔
277
                ->setDefault(PhpUnitTargetVersion::VERSION_NEWEST)
123✔
278
                ->getOption(),
123✔
279
        ]);
123✔
280
    }
281

282
    /**
283
     * @param array{
284
     *     index: int,
285
     *     loweredName: string,
286
     *     openBraceIndex: int,
287
     *     closeBraceIndex: int,
288
     * } $assertCall
289
     */
290
    private function fixAssertTrueFalse(Tokens $tokens, ArgumentsAnalyzer $argumentsAnalyzer, array $assertCall): void
291
    {
292
        $testDefaultNamespaceTokenIndex = null;
71✔
293
        $testIndex = $tokens->getNextMeaningfulToken($assertCall['openBraceIndex']);
71✔
294

295
        if (!$tokens[$testIndex]->isGivenKind([\T_EMPTY, \T_STRING])) {
71✔
296
            if ($this->fixAssertTrueFalseInstanceof($tokens, $assertCall, $testIndex)) {
16✔
297
                return;
6✔
298
            }
299

300
            if (!$tokens[$testIndex]->isGivenKind(\T_NS_SEPARATOR)) {
10✔
301
                return;
2✔
302
            }
303

304
            $testDefaultNamespaceTokenIndex = $testIndex;
8✔
305
            $testIndex = $tokens->getNextMeaningfulToken($testIndex);
8✔
306
        }
307

308
        $testOpenIndex = $tokens->getNextMeaningfulToken($testIndex);
64✔
309

310
        if (!$tokens[$testOpenIndex]->equals('(')) {
64✔
311
            return;
2✔
312
        }
313

314
        $testCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $testOpenIndex);
64✔
315
        $assertCallCloseIndex = $tokens->getNextMeaningfulToken($testCloseIndex);
64✔
316

317
        if (!$tokens[$assertCallCloseIndex]->equalsAny([')', ','])) {
64✔
318
            return;
1✔
319
        }
320

321
        $content = strtolower($tokens[$testIndex]->getContent());
64✔
322

323
        if (!\in_array($content, $this->functions, true)) {
64✔
324
            return;
3✔
325
        }
326

327
        $arguments = $argumentsAnalyzer->getArguments($tokens, $testOpenIndex, $testCloseIndex);
61✔
328
        $isPositive = 'asserttrue' === $assertCall['loweredName'];
61✔
329

330
        if (isset(self::FIX_MAP[$content]) && \is_array(self::FIX_MAP[$content])) {
61✔
331
            $fixDetails = self::FIX_MAP[$content];
28✔
332
            $expectedCount = $fixDetails['argument_count'] ?? 1;
28✔
333

334
            if ($expectedCount !== \count($arguments)) {
28✔
335
                return;
1✔
336
            }
337

338
            $isPositive = $isPositive ? 'positive' : 'negative';
27✔
339

340
            if (false === $fixDetails[$isPositive]) {
27✔
341
                return;
1✔
342
            }
343

344
            $tokens[$assertCall['index']] = new Token([\T_STRING, $fixDetails[$isPositive]]);
26✔
345
            $this->removeFunctionCall($tokens, $testDefaultNamespaceTokenIndex, $testIndex, $testOpenIndex, $testCloseIndex);
26✔
346

347
            if ($fixDetails['swap_arguments'] ?? false) {
26✔
348
                if (2 !== $expectedCount) {
8✔
UNCOV
349
                    throw new \RuntimeException('Can only swap two arguments, please update map or logic.');
×
350
                }
351

352
                $this->swapArguments($tokens, $arguments);
8✔
353
            }
354

355
            return;
26✔
356
        }
357

358
        if (1 !== \count($arguments)) {
34✔
359
            return;
1✔
360
        }
361

362
        $type = substr($content, 3);
33✔
363

364
        $tokens[$assertCall['index']] = new Token([\T_STRING, $isPositive ? 'assertInternalType' : 'assertNotInternalType']);
33✔
365
        $tokens[$testIndex] = new Token([\T_CONSTANT_ENCAPSED_STRING, "'".$type."'"]);
33✔
366
        $tokens[$testOpenIndex] = new Token(',');
33✔
367

368
        $tokens->clearTokenAndMergeSurroundingWhitespace($testCloseIndex);
33✔
369
        $commaIndex = $tokens->getPrevMeaningfulToken($testCloseIndex);
33✔
370

371
        if ($tokens[$commaIndex]->equals(',')) {
33✔
372
            $tokens->removeTrailingWhitespace($commaIndex);
1✔
373
            $tokens->clearAt($commaIndex);
1✔
374
        }
375

376
        if (!$tokens[$testOpenIndex + 1]->isWhitespace()) {
33✔
377
            $tokens->insertAt($testOpenIndex + 1, new Token([\T_WHITESPACE, ' ']));
29✔
378
        }
379

380
        if (null !== $testDefaultNamespaceTokenIndex) {
33✔
381
            $tokens->clearTokenAndMergeSurroundingWhitespace($testDefaultNamespaceTokenIndex);
2✔
382
        }
383
    }
384

385
    /**
386
     * @param array{
387
     *     index: int,
388
     *     loweredName: string,
389
     *     openBraceIndex: int,
390
     *     closeBraceIndex: int,
391
     * } $assertCall
392
     */
393
    private function fixAssertTrueFalseInstanceof(Tokens $tokens, array $assertCall, int $testIndex): bool
394
    {
395
        $isPositiveAssertion = 'asserttrue' === $assertCall['loweredName'];
16✔
396

397
        if ($tokens[$testIndex]->equals('!')) {
16✔
398
            $variableIndex = $tokens->getNextMeaningfulToken($testIndex);
2✔
399
            $isPositiveCondition = false;
2✔
400
        } else {
401
            $variableIndex = $testIndex;
15✔
402
            $isPositiveCondition = true;
15✔
403
        }
404

405
        if (!$tokens[$variableIndex]->isGivenKind(\T_VARIABLE)) {
16✔
406
            return false;
9✔
407
        }
408

409
        $instanceOfIndex = $tokens->getNextMeaningfulToken($variableIndex);
7✔
410

411
        if (!$tokens[$instanceOfIndex]->isGivenKind(\T_INSTANCEOF)) {
7✔
412
            return false;
1✔
413
        }
414

415
        $classEndIndex = $instanceOfIndex;
6✔
416
        $classPartTokens = [];
6✔
417

418
        do {
419
            $classEndIndex = $tokens->getNextMeaningfulToken($classEndIndex);
6✔
420
            $classPartTokens[] = $tokens[$classEndIndex];
6✔
421
        } while ($tokens[$classEndIndex]->isGivenKind([\T_STRING, \T_NS_SEPARATOR, \T_VARIABLE]));
6✔
422

423
        if ($tokens[$classEndIndex]->equalsAny([',', ')'])) { // do the fixing
6✔
424
            array_pop($classPartTokens);
5✔
425
            $isInstanceOfVar = reset($classPartTokens)->isGivenKind(\T_VARIABLE);
5✔
426
            $insertIndex = $testIndex - 1;
5✔
427
            $newTokens = [];
5✔
428

429
            foreach ($classPartTokens as $token) {
5✔
430
                $newTokens[++$insertIndex] = clone $token;
5✔
431
            }
432

433
            if (!$isInstanceOfVar) {
5✔
434
                $newTokens[++$insertIndex] = new Token([\T_DOUBLE_COLON, '::']);
4✔
435
                $newTokens[++$insertIndex] = new Token([CT::T_CLASS_CONSTANT, 'class']);
4✔
436
            }
437

438
            $newTokens[++$insertIndex] = new Token(',');
5✔
439
            $newTokens[++$insertIndex] = new Token([\T_WHITESPACE, ' ']);
5✔
440
            $newTokens[++$insertIndex] = clone $tokens[$variableIndex];
5✔
441

442
            for ($i = $classEndIndex - 1; $i >= $testIndex; --$i) {
5✔
443
                if (!$tokens[$i]->isComment()) {
5✔
444
                    $tokens->clearTokenAndMergeSurroundingWhitespace($i);
5✔
445
                }
446
            }
447

448
            $name = $isPositiveAssertion && $isPositiveCondition || !$isPositiveAssertion && !$isPositiveCondition
5✔
449
                ? 'assertInstanceOf'
4✔
450
                : 'assertNotInstanceOf';
2✔
451

452
            $tokens->insertSlices($newTokens);
5✔
453
            $tokens[$assertCall['index']] = new Token([\T_STRING, $name]);
5✔
454
        }
455

456
        return true;
6✔
457
    }
458

459
    /**
460
     * @param array{
461
     *     index: int,
462
     *     loweredName: string,
463
     *     openBraceIndex: int,
464
     *     closeBraceIndex: int,
465
     * } $assertCall
466
     */
467
    private function fixAssertSameEquals(Tokens $tokens, array $assertCall): void
468
    {
469
        // @ $this->/self::assertEquals/Same([$nextIndex])
470
        $expectedIndex = $tokens->getNextMeaningfulToken($assertCall['openBraceIndex']);
38✔
471

472
        // do not fix
473
        // let $a = [1,2]; $b = "2";
474
        // "$this->assertEquals("2", count($a)); $this->assertEquals($b, count($a)); $this->assertEquals(2.1, count($a));"
475

476
        if ($tokens[$expectedIndex]->isGivenKind(\T_VARIABLE)) {
38✔
477
            if (!$tokens[$tokens->getNextMeaningfulToken($expectedIndex)]->equals(',')) {
4✔
478
                return;
2✔
479
            }
480
        } elseif (!$tokens[$expectedIndex]->isGivenKind([\T_LNUMBER, \T_VARIABLE])) {
34✔
481
            return;
4✔
482
        }
483

484
        // @ $this->/self::assertEquals/Same([$nextIndex,$commaIndex])
485
        $commaIndex = $tokens->getNextMeaningfulToken($expectedIndex);
32✔
486

487
        if (!$tokens[$commaIndex]->equals(',')) {
32✔
488
            return;
2✔
489
        }
490

491
        // @ $this->/self::assertEquals/Same([$nextIndex,$commaIndex,$countCallIndex])
492
        $countCallIndex = $tokens->getNextMeaningfulToken($commaIndex);
30✔
493

494
        if ($tokens[$countCallIndex]->isGivenKind(\T_NS_SEPARATOR)) {
30✔
495
            $defaultNamespaceTokenIndex = $countCallIndex;
6✔
496
            $countCallIndex = $tokens->getNextMeaningfulToken($countCallIndex);
6✔
497
        } else {
498
            $defaultNamespaceTokenIndex = null;
26✔
499
        }
500

501
        if (!$tokens[$countCallIndex]->isGivenKind(\T_STRING)) {
30✔
502
            return;
2✔
503
        }
504

505
        $lowerContent = strtolower($tokens[$countCallIndex]->getContent());
28✔
506

507
        if (!\in_array($lowerContent, ['count', 'sizeof'], true)) {
28✔
508
            return; // not a call to "count" or "sizeOf"
2✔
509
        }
510

511
        // @ $this->/self::assertEquals/Same([$nextIndex,$commaIndex,[$defaultNamespaceTokenIndex,]$countCallIndex,$countCallOpenBraceIndex])
512
        $countCallOpenBraceIndex = $tokens->getNextMeaningfulToken($countCallIndex);
28✔
513

514
        if (!$tokens[$countCallOpenBraceIndex]->equals('(')) {
28✔
515
            return;
2✔
516
        }
517

518
        $countCallCloseBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $countCallOpenBraceIndex);
26✔
519
        $afterCountCallCloseBraceIndex = $tokens->getNextMeaningfulToken($countCallCloseBraceIndex);
26✔
520

521
        if (!$tokens[$afterCountCallCloseBraceIndex]->equalsAny([')', ','])) {
26✔
522
            return;
2✔
523
        }
524

525
        $this->removeFunctionCall(
24✔
526
            $tokens,
24✔
527
            $defaultNamespaceTokenIndex,
24✔
528
            $countCallIndex,
24✔
529
            $countCallOpenBraceIndex,
24✔
530
            $countCallCloseBraceIndex
24✔
531
        );
24✔
532

533
        $tokens[$assertCall['index']] = new Token([
24✔
534
            \T_STRING,
24✔
535
            false === strpos($assertCall['loweredName'], 'not', 6) ? 'assertCount' : 'assertNotCount',
24✔
536
        ]);
24✔
537
    }
538

539
    private function removeFunctionCall(Tokens $tokens, ?int $callNSIndex, int $callIndex, int $openIndex, int $closeIndex): void
540
    {
541
        $tokens->clearTokenAndMergeSurroundingWhitespace($callIndex);
50✔
542

543
        if (null !== $callNSIndex) {
50✔
544
            $tokens->clearTokenAndMergeSurroundingWhitespace($callNSIndex);
12✔
545
        }
546

547
        $tokens->clearTokenAndMergeSurroundingWhitespace($openIndex);
50✔
548
        $commaIndex = $tokens->getPrevMeaningfulToken($closeIndex);
50✔
549

550
        if ($tokens[$commaIndex]->equals(',')) {
50✔
551
            $tokens->removeTrailingWhitespace($commaIndex);
5✔
552
            $tokens->clearAt($commaIndex);
5✔
553
        }
554

555
        $tokens->clearTokenAndMergeSurroundingWhitespace($closeIndex);
50✔
556
    }
557

558
    /**
559
     * @param array<int, int> $argumentsIndices
560
     */
561
    private function swapArguments(Tokens $tokens, array $argumentsIndices): void
562
    {
563
        [$firstArgumentIndex, $secondArgumentIndex] = array_keys($argumentsIndices);
8✔
564

565
        $firstArgumentEndIndex = $argumentsIndices[$firstArgumentIndex];
8✔
566
        $secondArgumentEndIndex = $argumentsIndices[$secondArgumentIndex];
8✔
567

568
        $firstClone = $this->cloneAndClearTokens($tokens, $firstArgumentIndex, $firstArgumentEndIndex);
8✔
569
        $secondClone = $this->cloneAndClearTokens($tokens, $secondArgumentIndex, $secondArgumentEndIndex);
8✔
570

571
        if (!$firstClone[0]->isWhitespace()) {
8✔
572
            array_unshift($firstClone, new Token([\T_WHITESPACE, ' ']));
8✔
573
        }
574

575
        $tokens->insertAt($secondArgumentIndex, $firstClone);
8✔
576

577
        if ($secondClone[0]->isWhitespace()) {
8✔
578
            array_shift($secondClone);
7✔
579
        }
580

581
        $tokens->insertAt($firstArgumentIndex, $secondClone);
8✔
582
    }
583

584
    /**
585
     * @return list<Token>
586
     */
587
    private function cloneAndClearTokens(Tokens $tokens, int $start, int $end): array
588
    {
589
        $clone = [];
8✔
590

591
        for ($i = $start; $i <= $end; ++$i) {
8✔
592
            if ('' === $tokens[$i]->getContent()) {
8✔
593
                continue;
1✔
594
            }
595

596
            $clone[] = clone $tokens[$i];
8✔
597
            $tokens->clearAt($i);
8✔
598
        }
599

600
        return $clone;
8✔
601
    }
602
}
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