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

PHPCSStandards / PHP_CodeSniffer / 17662127818

12 Sep 2025 01:50AM UTC coverage: 78.786%. Remained the same
17662127818

push

github

web-flow
Merge pull request #1241 from PHPCSStandards/phpcs-4.x/feature/155-normalize-some-code-style-rules-3

CS: normalize code style rules [3]

343 of 705 new or added lines in 108 files covered. (48.65%)

3 existing lines in 3 files now uncovered.

19732 of 25045 relevant lines covered (78.79%)

96.47 hits per line

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

26.02
/src/Fixer.php
1
<?php
2
/**
3
 * A helper class for fixing errors.
4
 *
5
 * Provides helper functions that act upon a token array and modify the file
6
 * content.
7
 *
8
 * @author    Greg Sherwood <gsherwood@squiz.net>
9
 * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
10
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
11
 */
12

13
namespace PHP_CodeSniffer;
14

15
use InvalidArgumentException;
16
use PHP_CodeSniffer\Exceptions\RuntimeException;
17
use PHP_CodeSniffer\Files\File;
18
use PHP_CodeSniffer\Util\Common;
19
use PHP_CodeSniffer\Util\Writers\StatusWriter;
20

21
class Fixer
22
{
23

24
    /**
25
     * Is the fixer enabled and fixing a file?
26
     *
27
     * Sniffs should check this value to ensure they are not
28
     * doing extra processing to prepare for a fix when fixing is
29
     * not required.
30
     *
31
     * @var boolean
32
     */
33
    public $enabled = false;
34

35
    /**
36
     * The number of times we have looped over a file.
37
     *
38
     * @var integer
39
     */
40
    public $loops = 0;
41

42
    /**
43
     * The file being fixed.
44
     *
45
     * @var \PHP_CodeSniffer\Files\File
46
     */
47
    private $currentFile = null;
48

49
    /**
50
     * The list of tokens that make up the file contents.
51
     *
52
     * This is a simplified list which just contains the token content and nothing
53
     * else. This is the array that is updated as fixes are made, not the file's
54
     * token array. Imploding this array will give you the file content back.
55
     *
56
     * @var array<int, string>
57
     */
58
    private $tokens = [];
59

60
    /**
61
     * A list of tokens that have already been fixed.
62
     *
63
     * We don't allow the same token to be fixed more than once each time
64
     * through a file as this can easily cause conflicts between sniffs.
65
     *
66
     * @var int[]
67
     */
68
    private $fixedTokens = [];
69

70
    /**
71
     * The last value of each fixed token.
72
     *
73
     * If a token is being "fixed" back to its last value, the fix is
74
     * probably conflicting with another.
75
     *
76
     * @var array<int, array<string, mixed>>
77
     */
78
    private $oldTokenValues = [];
79

80
    /**
81
     * A list of tokens that have been fixed during a changeset.
82
     *
83
     * All changes in changeset must be able to be applied, or else
84
     * the entire changeset is rejected.
85
     *
86
     * @var array
87
     */
88
    private $changeset = [];
89

90
    /**
91
     * Is there an open changeset.
92
     *
93
     * @var boolean
94
     */
95
    private $inChangeset = false;
96

97
    /**
98
     * Is the current fixing loop in conflict?
99
     *
100
     * @var boolean
101
     */
102
    private $inConflict = false;
103

104
    /**
105
     * The actual number of fixes that have been performed.
106
     *
107
     * I.e. how many fixes were applied. This may be higher than the originally found
108
     * issues if the fixer from one sniff causes other sniffs to come into play in follow-up loops.
109
     * Example: if a brace is moved to a new line, the `ScopeIndent` sniff may need to ensure
110
     * the brace is indented correctly in the next loop.
111
     *
112
     * @var integer
113
     */
114
    private $numFixes = 0;
115

116

117
    /**
118
     * Starts fixing a new file.
119
     *
120
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being fixed.
121
     *
122
     * @return void
123
     */
124
    public function startFile(File $phpcsFile)
×
125
    {
126
        $this->currentFile = $phpcsFile;
×
127
        $this->numFixes    = 0;
×
128
        $this->fixedTokens = [];
×
129

130
        $tokens       = $phpcsFile->getTokens();
×
131
        $this->tokens = [];
×
132
        foreach ($tokens as $index => $token) {
×
133
            if (isset($token['orig_content']) === true) {
×
134
                $this->tokens[$index] = $token['orig_content'];
×
135
            } else {
136
                $this->tokens[$index] = $token['content'];
×
137
            }
138
        }
139

140
    }//end startFile()
141

142

143
    /**
144
     * Attempt to fix the file by processing it until no fixes are made.
145
     *
146
     * @return boolean
147
     */
148
    public function fixFile()
9✔
149
    {
150
        $fixable = $this->currentFile->getFixableCount();
9✔
151
        if ($fixable === 0) {
9✔
152
            // Nothing to fix.
153
            return false;
×
154
        }
155

156
        $this->enabled = true;
9✔
157

158
        // Pause the StatusWriter to silence Tokenizer debug info about the file being retokenized for each loop.
159
        StatusWriter::pause();
9✔
160

161
        $this->loops = 0;
9✔
162
        while ($this->loops < 50) {
9✔
163
            // Only needed once file content has changed.
164
            $contents = $this->getContents();
9✔
165

166
            if (PHP_CODESNIFFER_VERBOSITY > 2) {
9✔
167
                StatusWriter::forceWrite('---START FILE CONTENT---');
×
168
                $lines = explode($this->currentFile->eolChar, $contents);
×
169
                $max   = strlen(count($lines));
×
170
                foreach ($lines as $lineNum => $line) {
×
171
                    $lineNum++;
×
NEW
172
                    StatusWriter::forceWrite(str_pad($lineNum, $max, ' ', STR_PAD_LEFT) . '|' . $line);
×
173
                }
174

175
                StatusWriter::forceWrite('--- END FILE CONTENT ---');
×
176
            }
177

178
            $this->inConflict = false;
9✔
179
            $this->currentFile->ruleset->populateTokenListeners();
9✔
180
            $this->currentFile->setContent($contents);
9✔
181
            $this->currentFile->process();
9✔
182

183
            $this->loops++;
9✔
184

185
            if (PHP_CODESNIFFER_CBF === true && PHP_CODESNIFFER_VERBOSITY > 0) {
9✔
NEW
186
                StatusWriter::forceWrite("\r" . str_repeat(' ', 80) . "\r", 0, 0);
×
187
                $statusMessage = "=> Fixing file: $this->numFixes/$fixable violations remaining [made $this->loops pass";
×
188
                if ($this->loops > 1) {
×
189
                    $statusMessage .= 'es';
×
190
                }
191

192
                $statusMessage .= ']... ';
×
193
                $newlines       = 0;
×
194
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
195
                    $newlines = 1;
×
196
                }
197

198
                StatusWriter::forceWrite($statusMessage, 1, $newlines);
×
199
            }
200

201
            if ($this->numFixes === 0 && $this->inConflict === false) {
9✔
202
                // Nothing left to do.
203
                break;
3✔
204
            } else if (PHP_CODESNIFFER_VERBOSITY > 1) {
9✔
NEW
205
                StatusWriter::forceWrite("* fixed $this->numFixes violations, starting loop " . ($this->loops + 1) . ' *', 1);
×
206
            }
207
        }//end while
208

209
        $this->enabled = false;
9✔
210

211
        StatusWriter::resume();
9✔
212

213
        if ($this->numFixes > 0 || $this->inConflict === true) {
9✔
214
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
6✔
215
                StatusWriter::write("*** Reached maximum number of loops with $this->numFixes violations left unfixed ***", 1);
×
216
            }
217

218
            return false;
6✔
219
        }
220

221
        return true;
3✔
222

223
    }//end fixFile()
224

225

226
    /**
227
     * Generates a text diff of the original file and the new content.
228
     *
229
     * @param string|null $filePath Optional file path to diff the file against.
230
     *                              If not specified, the original version of the
231
     *                              file will be used.
232
     * @param boolean     $colors   Print coloured output or not.
233
     *
234
     * @return string
235
     *
236
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException When the diff command fails.
237
     */
238
    public function generateDiff(?string $filePath = null, bool $colors = true)
65✔
239
    {
240
        if ($filePath === null) {
65✔
241
            $filePath = $this->currentFile->getFilename();
5✔
242
        }
243

244
        $cwd = getcwd() . DIRECTORY_SEPARATOR;
65✔
245
        if (strpos($filePath, $cwd) === 0) {
65✔
246
            $filename = substr($filePath, strlen($cwd));
65✔
247
        } else {
248
            $filename = $filePath;
×
249
        }
250

251
        $contents = $this->getContents();
65✔
252

253
        $tempName  = tempnam(sys_get_temp_dir(), 'phpcs-fixer');
65✔
254
        $fixedFile = fopen($tempName, 'w');
65✔
255
        fwrite($fixedFile, $contents);
65✔
256

257
        // We must use something like shell_exec() or proc_open() because whitespace at the end
258
        // of lines is critical to diff files.
259
        // Using proc_open() instead of shell_exec improves performance on Windows significantly,
260
        // while the results are the same (though more code is needed to get the results).
261
        // This is specifically due to proc_open allowing to set the "bypass_shell" option.
262
        $filename = escapeshellarg($filename);
65✔
263
        $cmd      = "diff -u -L$filename -LPHP_CodeSniffer $filename \"$tempName\"";
65✔
264

265
        // Stream 0 = STDIN, 1 = STDOUT, 2 = STDERR.
266
        $descriptorspec = [
39✔
267
            0 => [
65✔
268
                'pipe',
39✔
269
                'r',
39✔
270
            ],
39✔
271
            1 => [
39✔
272
                'pipe',
39✔
273
                'w',
39✔
274
            ],
39✔
275
            2 => [
39✔
276
                'pipe',
39✔
277
                'w',
39✔
278
            ],
39✔
279
        ];
39✔
280

281
        $options = null;
65✔
282
        if (PHP_OS_FAMILY === 'Windows') {
65✔
283
            $options = ['bypass_shell' => true];
26✔
284
        }
285

286
        $process = proc_open($cmd, $descriptorspec, $pipes, $cwd, null, $options);
65✔
287
        if (is_resource($process) === false) {
65✔
288
            throw new RuntimeException('Could not obtain a resource to execute the diff command.');
×
289
        }
290

291
        // We don't need these.
292
        fclose($pipes[0]);
65✔
293
        fclose($pipes[2]);
65✔
294

295
        // Stdout will contain the actual diff.
296
        $diff = stream_get_contents($pipes[1]);
65✔
297
        fclose($pipes[1]);
65✔
298

299
        proc_close($process);
65✔
300

301
        fclose($fixedFile);
65✔
302
        if (is_file($tempName) === true) {
65✔
303
            unlink($tempName);
65✔
304
        }
305

306
        if ($diff === false || $diff === '') {
65✔
307
            return '';
10✔
308
        }
309

310
        if ($colors === false) {
55✔
311
            return $diff;
50✔
312
        }
313

314
        $diffLines = explode(PHP_EOL, $diff);
5✔
315
        if (count($diffLines) === 1) {
5✔
316
            // Seems to be required for cygwin.
317
            $diffLines = explode("\n", $diff);
2✔
318
        }
319

320
        $diff = [];
5✔
321
        foreach ($diffLines as $line) {
5✔
322
            if (isset($line[0]) === true) {
5✔
323
                switch ($line[0]) {
5✔
324
                case '-':
5✔
325
                    $diff[] = "\033[31m$line\033[0m";
5✔
326
                    break;
5✔
327
                case '+':
5✔
328
                    $diff[] = "\033[32m$line\033[0m";
5✔
329
                    break;
5✔
330
                default:
331
                    $diff[] = $line;
5✔
332
                }
333
            }
334
        }
335

336
        $diff = implode(PHP_EOL, $diff);
5✔
337

338
        return $diff;
5✔
339

340
    }//end generateDiff()
341

342

343
    /**
344
     * Get a count of the actual number of fixes that have been performed on the file.
345
     *
346
     * This value is reset every time a new file is started, or an existing
347
     * file is restarted.
348
     *
349
     * @return int
350
     */
351
    public function getFixCount()
×
352
    {
353
        return $this->numFixes;
×
354

355
    }//end getFixCount()
356

357

358
    /**
359
     * Get the current content of the file, as a string.
360
     *
361
     * @return string
362
     */
363
    public function getContents()
×
364
    {
365
        $contents = implode($this->tokens);
×
366
        return $contents;
×
367

368
    }//end getContents()
369

370

371
    /**
372
     * Get the current fixed content of a token.
373
     *
374
     * This function takes changesets into account so should be used
375
     * instead of directly accessing the token array.
376
     *
377
     * @param int $stackPtr The position of the token in the token stack.
378
     *
379
     * @return string
380
     */
381
    public function getTokenContent(int $stackPtr)
×
382
    {
383
        if ($this->inChangeset === true
×
384
            && isset($this->changeset[$stackPtr]) === true
×
385
        ) {
386
            return $this->changeset[$stackPtr];
×
387
        } else {
388
            return $this->tokens[$stackPtr];
×
389
        }
390

391
    }//end getTokenContent()
392

393

394
    /**
395
     * Start recording actions for a changeset.
396
     *
397
     * @return void|false
398
     */
399
    public function beginChangeset()
×
400
    {
401
        if ($this->inConflict === true) {
×
402
            return false;
×
403
        }
404

405
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
406
            $bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
×
407
            if ($bt[1]['class'] === self::class) {
×
408
                $sniff = 'Fixer';
×
409
            } else {
410
                $sniff = $this->getSniffCodeForDebug($bt[1]['class']);
×
411
            }
412

413
            $line = $bt[0]['line'];
×
414

415
            StatusWriter::forceWrite("=> Changeset started by $sniff:$line", 1);
×
416
        }
417

418
        $this->changeset   = [];
×
419
        $this->inChangeset = true;
×
420

421
    }//end beginChangeset()
422

423

424
    /**
425
     * Stop recording actions for a changeset, and apply logged changes.
426
     *
427
     * @return boolean
428
     */
429
    public function endChangeset()
×
430
    {
431
        if ($this->inConflict === true) {
×
432
            return false;
×
433
        }
434

435
        $this->inChangeset = false;
×
436

437
        $success = true;
×
438
        $applied = [];
×
439
        foreach ($this->changeset as $stackPtr => $content) {
×
440
            $success = $this->replaceToken($stackPtr, $content);
×
441
            if ($success === false) {
×
442
                break;
×
443
            } else {
444
                $applied[] = $stackPtr;
×
445
            }
446
        }
447

448
        if ($success === false) {
×
449
            // Rolling back all changes.
450
            foreach ($applied as $stackPtr) {
×
451
                $this->revertToken($stackPtr);
×
452
            }
453

454
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
455
                StatusWriter::forceWrite('=> Changeset failed to apply', 1);
×
456
            }
457
        } else if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
458
            $fixes = count($this->changeset);
×
459
            StatusWriter::forceWrite("=> Changeset ended: $fixes changes applied", 1);
×
460
        }
461

462
        $this->changeset = [];
×
463
        return true;
×
464

465
    }//end endChangeset()
466

467

468
    /**
469
     * Stop recording actions for a changeset, and discard logged changes.
470
     *
471
     * @return void
472
     */
473
    public function rollbackChangeset()
×
474
    {
475
        $this->inChangeset = false;
×
476
        $this->inConflict  = false;
×
477

478
        if (empty($this->changeset) === false) {
×
479
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
480
                $bt = debug_backtrace();
×
481
                if ($bt[1]['class'] === self::class) {
×
482
                    $sniff = $bt[2]['class'];
×
483
                    $line  = $bt[1]['line'];
×
484
                } else {
485
                    $sniff = $bt[1]['class'];
×
486
                    $line  = $bt[0]['line'];
×
487
                }
488

489
                $sniff = $this->getSniffCodeForDebug($sniff);
×
490

491
                $numChanges = count($this->changeset);
×
492

493
                StatusWriter::forceWrite("R: $sniff:$line rolled back the changeset ($numChanges changes)", 2);
×
494
                StatusWriter::forceWrite('=> Changeset rolled back', 1);
×
495
            }
496

497
            $this->changeset = [];
×
498
        }//end if
499

500
    }//end rollbackChangeset()
501

502

503
    /**
504
     * Replace the entire contents of a token.
505
     *
506
     * @param int    $stackPtr The position of the token in the token stack.
507
     * @param string $content  The new content of the token.
508
     *
509
     * @return bool If the change was accepted.
510
     */
511
    public function replaceToken(int $stackPtr, string $content)
×
512
    {
513
        if ($this->inConflict === true) {
×
514
            return false;
×
515
        }
516

517
        if ($this->inChangeset === false
×
518
            && isset($this->fixedTokens[$stackPtr]) === true
×
519
        ) {
520
            $depth = 1;
×
521
            if (empty($this->changeset) === false) {
×
522
                $depth = 2;
×
523
            }
524

525
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
526
                StatusWriter::forceWrite("* token $stackPtr has already been modified, skipping *", $depth);
×
527
            }
528

529
            return false;
×
530
        }
531

532
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
533
            $bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
×
534
            if ($bt[1]['class'] === self::class) {
×
535
                $sniff = $bt[2]['class'];
×
536
                $line  = $bt[1]['line'];
×
537
            } else {
538
                $sniff = $bt[1]['class'];
×
539
                $line  = $bt[0]['line'];
×
540
            }
541

542
            $sniff = $this->getSniffCodeForDebug($sniff);
×
543

544
            $tokens     = $this->currentFile->getTokens();
×
545
            $type       = $tokens[$stackPtr]['type'];
×
546
            $tokenLine  = $tokens[$stackPtr]['line'];
×
547
            $oldContent = Common::prepareForOutput($this->tokens[$stackPtr]);
×
548
            $newContent = Common::prepareForOutput($content);
×
549
            if (trim($this->tokens[$stackPtr]) === '' && isset($this->tokens[($stackPtr + 1)]) === true) {
×
550
                // Add some context for whitespace only changes.
551
                $append      = Common::prepareForOutput($this->tokens[($stackPtr + 1)]);
×
552
                $oldContent .= $append;
×
553
                $newContent .= $append;
×
554
            }
555
        }//end if
556

557
        if ($this->inChangeset === true) {
×
558
            $this->changeset[$stackPtr] = $content;
×
559

560
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
561
                StatusWriter::forceWrite("Q: $sniff:$line replaced token $stackPtr ($type on line $tokenLine) \"$oldContent\" => \"$newContent\"", 2);
×
562
            }
563

564
            return true;
×
565
        }
566

567
        if (isset($this->oldTokenValues[$stackPtr]) === false) {
×
568
            $this->oldTokenValues[$stackPtr] = [
×
569
                'curr' => $content,
×
570
                'prev' => $this->tokens[$stackPtr],
×
571
                'loop' => $this->loops,
×
572
            ];
573
        } else {
574
            if ($this->oldTokenValues[$stackPtr]['prev'] === $content
×
575
                && $this->oldTokenValues[$stackPtr]['loop'] === ($this->loops - 1)
×
576
            ) {
577
                if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
578
                    $depth = 1;
×
579
                    if (empty($this->changeset) === false) {
×
580
                        $depth = 2;
×
581
                    }
582

583
                    $loop = $this->oldTokenValues[$stackPtr]['loop'];
×
584

585
                    StatusWriter::forceWrite("**** $sniff:$line has possible conflict with another sniff on loop $loop; caused by the following change ****", $depth);
×
586
                    StatusWriter::forceWrite("**** replaced token $stackPtr ($type on line $tokenLine) \"$oldContent\" => \"$newContent\" ****", $depth);
×
587
                }
588

589
                if ($this->oldTokenValues[$stackPtr]['loop'] >= ($this->loops - 1)) {
×
590
                    $this->inConflict = true;
×
591
                    if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
592
                        StatusWriter::forceWrite('**** ignoring all changes until next loop ****', $depth);
×
593
                    }
594
                }
595

596
                return false;
×
597
            }//end if
598

599
            $this->oldTokenValues[$stackPtr]['prev'] = $this->oldTokenValues[$stackPtr]['curr'];
×
600
            $this->oldTokenValues[$stackPtr]['curr'] = $content;
×
601
            $this->oldTokenValues[$stackPtr]['loop'] = $this->loops;
×
602
        }//end if
603

604
        $this->fixedTokens[$stackPtr] = $this->tokens[$stackPtr];
×
605
        $this->tokens[$stackPtr]      = $content;
×
606
        $this->numFixes++;
×
607

608
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
609
            $statusMessage = "$sniff:$line replaced token $stackPtr ($type on line $tokenLine) \"$oldContent\" => \"$newContent\"";
×
610
            $depth         = 1;
×
611
            if (empty($this->changeset) === false) {
×
NEW
612
                $statusMessage = 'A: ' . $statusMessage;
×
613
                $depth         = 2;
×
614
            }
615

616
            StatusWriter::forceWrite($statusMessage, $depth);
×
617
        }
618

619
        return true;
×
620

621
    }//end replaceToken()
622

623

624
    /**
625
     * Reverts the previous fix made to a token.
626
     *
627
     * @param int $stackPtr The position of the token in the token stack.
628
     *
629
     * @return bool If a change was reverted.
630
     */
631
    public function revertToken(int $stackPtr)
×
632
    {
633
        if (isset($this->fixedTokens[$stackPtr]) === false) {
×
634
            return false;
×
635
        }
636

637
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
638
            $bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
×
639
            if ($bt[1]['class'] === self::class) {
×
640
                $sniff = $bt[2]['class'];
×
641
                $line  = $bt[1]['line'];
×
642
            } else {
643
                $sniff = $bt[1]['class'];
×
644
                $line  = $bt[0]['line'];
×
645
            }
646

647
            $sniff = $this->getSniffCodeForDebug($sniff);
×
648

649
            $tokens     = $this->currentFile->getTokens();
×
650
            $type       = $tokens[$stackPtr]['type'];
×
651
            $tokenLine  = $tokens[$stackPtr]['line'];
×
652
            $oldContent = Common::prepareForOutput($this->tokens[$stackPtr]);
×
653
            $newContent = Common::prepareForOutput($this->fixedTokens[$stackPtr]);
×
654
            if (trim($this->tokens[$stackPtr]) === '' && isset($tokens[($stackPtr + 1)]) === true) {
×
655
                // Add some context for whitespace only changes.
656
                $append      = Common::prepareForOutput($this->tokens[($stackPtr + 1)]);
×
657
                $oldContent .= $append;
×
658
                $newContent .= $append;
×
659
            }
660
        }//end if
661

662
        $this->tokens[$stackPtr] = $this->fixedTokens[$stackPtr];
×
663
        unset($this->fixedTokens[$stackPtr]);
×
664
        $this->numFixes--;
×
665

666
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
667
            $statusMessage = "$sniff:$line reverted token $stackPtr ($type on line $tokenLine) \"$oldContent\" => \"$newContent\"";
×
668
            $depth         = 1;
×
669
            if (empty($this->changeset) === false) {
×
NEW
670
                $statusMessage = 'R: ' . $statusMessage;
×
671
                $depth         = 2;
×
672
            }
673

674
            StatusWriter::forceWrite($statusMessage, $depth);
×
675
        }
676

677
        return true;
×
678

679
    }//end revertToken()
680

681

682
    /**
683
     * Replace the content of a token with a part of its current content.
684
     *
685
     * @param int      $stackPtr The position of the token in the token stack.
686
     * @param int      $start    The first character to keep.
687
     * @param int|null $length   The number of characters to keep. If NULL, the content of
688
     *                           the token from $start to the end of the content is kept.
689
     *
690
     * @return bool If the change was accepted.
691
     */
692
    public function substrToken(int $stackPtr, int $start, ?int $length = null)
×
693
    {
694
        $current = $this->getTokenContent($stackPtr);
×
695

696
        if ($length === null) {
×
697
            $newContent = substr($current, $start);
×
698
        } else {
699
            $newContent = substr($current, $start, $length);
×
700
        }
701

702
        return $this->replaceToken($stackPtr, $newContent);
×
703

704
    }//end substrToken()
705

706

707
    /**
708
     * Adds a newline to end of a token's content.
709
     *
710
     * @param int $stackPtr The position of the token in the token stack.
711
     *
712
     * @return bool If the change was accepted.
713
     */
714
    public function addNewline(int $stackPtr)
×
715
    {
716
        $current = $this->getTokenContent($stackPtr);
×
NEW
717
        return $this->replaceToken($stackPtr, $current . $this->currentFile->eolChar);
×
718

719
    }//end addNewline()
720

721

722
    /**
723
     * Adds a newline to the start of a token's content.
724
     *
725
     * @param int $stackPtr The position of the token in the token stack.
726
     *
727
     * @return bool If the change was accepted.
728
     */
729
    public function addNewlineBefore(int $stackPtr)
×
730
    {
731
        $current = $this->getTokenContent($stackPtr);
×
NEW
732
        return $this->replaceToken($stackPtr, $this->currentFile->eolChar . $current);
×
733

734
    }//end addNewlineBefore()
735

736

737
    /**
738
     * Adds content to the end of a token's current content.
739
     *
740
     * @param int    $stackPtr The position of the token in the token stack.
741
     * @param string $content  The content to add.
742
     *
743
     * @return bool If the change was accepted.
744
     */
745
    public function addContent(int $stackPtr, string $content)
×
746
    {
747
        $current = $this->getTokenContent($stackPtr);
×
NEW
748
        return $this->replaceToken($stackPtr, $current . $content);
×
749

750
    }//end addContent()
751

752

753
    /**
754
     * Adds content to the start of a token's current content.
755
     *
756
     * @param int    $stackPtr The position of the token in the token stack.
757
     * @param string $content  The content to add.
758
     *
759
     * @return bool If the change was accepted.
760
     */
761
    public function addContentBefore(int $stackPtr, string $content)
×
762
    {
763
        $current = $this->getTokenContent($stackPtr);
×
NEW
764
        return $this->replaceToken($stackPtr, $content . $current);
×
765

766
    }//end addContentBefore()
767

768

769
    /**
770
     * Adjust the indent of a code block.
771
     *
772
     * @param int $start  The position of the token in the token stack
773
     *                    to start adjusting the indent from.
774
     * @param int $end    The position of the token in the token stack
775
     *                    to end adjusting the indent.
776
     * @param int $change The number of spaces to adjust the indent by
777
     *                    (positive or negative).
778
     *
779
     * @return void
780
     */
781
    public function changeCodeBlockIndent(int $start, int $end, int $change)
×
782
    {
783
        $tokens = $this->currentFile->getTokens();
×
784

785
        $baseIndent = '';
×
786
        if ($change > 0) {
×
787
            $baseIndent = str_repeat(' ', $change);
×
788
        }
789

790
        $useChangeset = false;
×
791
        if ($this->inChangeset === false) {
×
792
            $this->beginChangeset();
×
793
            $useChangeset = true;
×
794
        }
795

796
        for ($i = $start; $i <= $end; $i++) {
×
797
            if ($tokens[$i]['column'] !== 1
×
798
                || $tokens[($i + 1)]['line'] !== $tokens[$i]['line']
×
799
            ) {
800
                continue;
×
801
            }
802

803
            $length = 0;
×
804
            if ($tokens[$i]['code'] === T_WHITESPACE
×
805
                || $tokens[$i]['code'] === T_DOC_COMMENT_WHITESPACE
×
806
            ) {
807
                $length = $tokens[$i]['length'];
×
808

809
                $padding = ($length + $change);
×
810
                if ($padding > 0) {
×
811
                    $padding = str_repeat(' ', $padding);
×
812
                } else {
813
                    $padding = '';
×
814
                }
815

NEW
816
                $newContent = $padding . ltrim($tokens[$i]['content']);
×
817
            } else {
NEW
818
                $newContent = $baseIndent . $tokens[$i]['content'];
×
819
            }
820

821
            $this->replaceToken($i, $newContent);
×
822
        }//end for
823

824
        if ($useChangeset === true) {
×
825
            $this->endChangeset();
×
826
        }
827

828
    }//end changeCodeBlockIndent()
829

830

831
    /**
832
     * Get the sniff code for the current sniff or the class name if the passed class is not a sniff.
833
     *
834
     * @param string $className Class name.
835
     *
836
     * @return string
837
     */
838
    private function getSniffCodeForDebug(string $className)
×
839
    {
840
        try {
841
            $sniffCode = Common::getSniffCode($className);
×
842
            return $sniffCode;
×
843
        } catch (InvalidArgumentException $e) {
×
844
            // Sniff code could not be determined. This may be an abstract sniff class or a helper class.
845
            return $className;
×
846
        }
847

848
    }//end getSniffCodeForDebug()
849

850

851
}//end class
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