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

PHPCSStandards / PHP_CodeSniffer / 25056686383

28 Apr 2026 01:47PM UTC coverage: 78.954% (-0.02%) from 78.969%
25056686383

Pull #1421

github

web-flow
Merge 06ac35aa7 into c3eb74d77
Pull Request #1421: Runner: distribute files round-robin by size in parallel mode

0 of 22 new or added lines in 1 file covered. (0.0%)

149 existing lines in 1 file now uncovered.

19875 of 25173 relevant lines covered (78.95%)

98.85 hits per line

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

21.07
/src/Runner.php
1
<?php
2
/**
3
 * Responsible for running PHPCS and PHPCBF.
4
 *
5
 * After creating an object of this class, you probably just want to
6
 * call runPHPCS() or runPHPCBF().
7
 *
8
 * @author    Greg Sherwood <gsherwood@squiz.net>
9
 * @copyright 2006-2023 Squiz Pty Ltd (ABN 77 084 670 600)
10
 * @copyright 2023 PHPCSStandards and contributors
11
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/HEAD/licence.txt BSD Licence
12
 */
13

14
namespace PHP_CodeSniffer;
15

16
use Exception;
17
use InvalidArgumentException;
18
use PHP_CodeSniffer\Exceptions\DeepExitException;
19
use PHP_CodeSniffer\Exceptions\RuntimeException;
20
use PHP_CodeSniffer\Files\DummyFile;
21
use PHP_CodeSniffer\Files\File;
22
use PHP_CodeSniffer\Files\FileList;
23
use PHP_CodeSniffer\Files\LocalFile;
24
use PHP_CodeSniffer\Util\Cache;
25
use PHP_CodeSniffer\Util\Common;
26
use PHP_CodeSniffer\Util\ExitCode;
27
use PHP_CodeSniffer\Util\Standards;
28
use PHP_CodeSniffer\Util\Timing;
29
use PHP_CodeSniffer\Util\Tokens;
30
use PHP_CodeSniffer\Util\Writers\StatusWriter;
31

32
class Runner
33
{
34

35
    /**
36
     * The config data for the run.
37
     *
38
     * @var \PHP_CodeSniffer\Config
39
     */
40
    public $config = null;
41

42
    /**
43
     * The ruleset used for the run.
44
     *
45
     * @var \PHP_CodeSniffer\Ruleset
46
     */
47
    public $ruleset = null;
48

49
    /**
50
     * The reporter used for generating reports after the run.
51
     *
52
     * @var \PHP_CodeSniffer\Reporter
53
     */
54
    public $reporter = null;
55

56

57
    /**
58
     * Run the PHPCS script.
59
     *
60
     * @return int
61
     */
62
    public function runPHPCS()
8✔
63
    {
64
        $this->registerOutOfMemoryShutdownMessage('phpcs');
8✔
65

66
        try {
67
            Timing::startTiming();
8✔
68

69
            if (defined('PHP_CODESNIFFER_CBF') === false) {
8✔
70
                define('PHP_CODESNIFFER_CBF', false);
×
71
            }
72

73
            // Creating the Config object populates it with all required settings
74
            // based on the CLI arguments provided to the script and any config
75
            // values the user has set.
76
            $this->config = new Config();
8✔
77

78
            // Init the run and load the rulesets to set additional config vars.
79
            $this->init();
8✔
80

81
            // Print a list of sniffs in each of the supplied standards.
82
            // We fudge the config here so that each standard is explained in isolation.
83
            if ($this->config->explain === true) {
8✔
84
                $standards = $this->config->standards;
3✔
85
                foreach ($standards as $standard) {
3✔
86
                    $this->config->standards = [$standard];
3✔
87
                    $ruleset = new Ruleset($this->config);
3✔
88
                    $ruleset->explain();
3✔
89
                }
90

91
                return 0;
3✔
92
            }
93

94
            // Generate documentation for each of the supplied standards.
95
            if ($this->config->generator !== null) {
5✔
96
                $standards = $this->config->standards;
5✔
97
                foreach ($standards as $standard) {
5✔
98
                    $this->config->standards = [$standard];
5✔
99
                    $ruleset   = new Ruleset($this->config);
5✔
100
                    $class     = 'PHP_CodeSniffer\Generators\\' . $this->config->generator;
5✔
101
                    $generator = new $class($ruleset);
5✔
102
                    $generator->generate();
5✔
103
                }
104

105
                return 0;
5✔
106
            }
107

108
            // Other report formats don't really make sense in interactive mode
109
            // so we hard-code the full report here and when outputting.
110
            // We also ensure parallel processing is off because we need to do one file at a time.
111
            if ($this->config->interactive === true) {
×
112
                $this->config->reports      = ['full' => null];
×
113
                $this->config->parallel     = 1;
×
114
                $this->config->showProgress = false;
×
115
            }
116

117
            // Disable caching if we are processing STDIN as we can't be 100%
118
            // sure where the file came from or if it will change in the future.
NEW
119
            if ($this->config->stdin === true) {
×
NEW
120
                $this->config->cache = false;
×
121
            }
122

NEW
123
            $this->run();
×
124

125
            // Print all the reports for this run.
126
            $this->reporter->printReports();
×
127

UNCOV
128
            if ($this->config->quiet === false) {
×
129
                Timing::printRunTime();
×
130
            }
131
        } catch (DeepExitException $e) {
×
132
            $exitCode = $e->getCode();
×
UNCOV
133
            $message  = $e->getMessage();
×
134
            if ($message !== '') {
×
135
                if ($exitCode === 0) {
×
136
                    echo $e->getMessage();
×
137
                } else {
138
                    StatusWriter::write($e->getMessage(), 0, 0);
×
139
                }
140
            }
141

UNCOV
142
            return $exitCode;
×
143
        }
144

145
        return ExitCode::calculate($this->reporter);
×
146
    }
147

148

149
    /**
150
     * Run the PHPCBF script.
151
     *
152
     * @return int
153
     */
UNCOV
154
    public function runPHPCBF()
×
155
    {
UNCOV
156
        $this->registerOutOfMemoryShutdownMessage('phpcbf');
×
157

UNCOV
158
        if (defined('PHP_CODESNIFFER_CBF') === false) {
×
159
            define('PHP_CODESNIFFER_CBF', true);
×
160
        }
161

162
        try {
UNCOV
163
            Timing::startTiming();
×
164

165
            // Creating the Config object populates it with all required settings
166
            // based on the CLI arguments provided to the script and any config
167
            // values the user has set.
UNCOV
168
            $this->config = new Config();
×
169

170
            // When processing STDIN, we can't output anything to the screen
171
            // or it will end up mixed in with the file output.
UNCOV
172
            if ($this->config->stdin === true) {
×
UNCOV
173
                $this->config->verbosity = 0;
×
174
            }
175

176
            // Init the run and load the rulesets to set additional config vars.
UNCOV
177
            $this->init();
×
178

179
            // When processing STDIN, we only process one file at a time and
180
            // we don't process all the way through, so we can't use the parallel
181
            // running system.
UNCOV
182
            if ($this->config->stdin === true) {
×
UNCOV
183
                $this->config->parallel = 1;
×
184
            }
185

186
            // Override some of the command line settings that might break the fixes.
UNCOV
187
            $this->config->generator    = null;
×
UNCOV
188
            $this->config->explain      = false;
×
UNCOV
189
            $this->config->interactive  = false;
×
190
            $this->config->cache        = false;
×
191
            $this->config->showSources  = false;
×
192
            $this->config->recordErrors = false;
×
193
            $this->config->reportFile   = null;
×
194

195
            // Only use the "Cbf" report, but allow for the Performance report as well.
196
            $originalReports = array_change_key_case($this->config->reports, CASE_LOWER);
×
UNCOV
197
            $newReports      = ['cbf' => null];
×
UNCOV
198
            if (array_key_exists('performance', $originalReports) === true) {
×
199
                $newReports['performance'] = $originalReports['performance'];
×
200
            }
201

202
            $this->config->reports = $newReports;
×
203

204
            // If a standard tries to set command line arguments itself, some
205
            // may be blocked because PHPCBF is running, so stop the script
206
            // dying if any are found.
UNCOV
207
            $this->config->dieOnUnknownArg = false;
×
208

UNCOV
209
            $this->run();
×
210
            $this->reporter->printReports();
×
211

212
            if ($this->config->quiet === false) {
×
213
                StatusWriter::writeNewline();
×
UNCOV
214
                Timing::printRunTime();
×
215
            }
216
        } catch (DeepExitException $e) {
×
217
            $exitCode = $e->getCode();
×
UNCOV
218
            $message  = $e->getMessage();
×
219
            if ($message !== '') {
×
220
                if ($exitCode === 0) {
×
221
                    echo $e->getMessage();
×
222
                } else {
223
                    StatusWriter::write($e->getMessage(), 0, 0);
×
224
                }
225
            }
226

UNCOV
227
            return $exitCode;
×
228
        }
229

230
        return ExitCode::calculate($this->reporter);
×
231
    }
232

233

234
    /**
235
     * Init the rulesets and other high-level settings.
236
     *
237
     * @return void
238
     * @throws \PHP_CodeSniffer\Exceptions\DeepExitException If a referenced standard is not installed.
239
     */
UNCOV
240
    public function init()
×
241
    {
UNCOV
242
        if (defined('PHP_CODESNIFFER_CBF') === false) {
×
243
            define('PHP_CODESNIFFER_CBF', false);
×
244
        }
245

246
        // Disable the PCRE JIT as this caused issues with parallel running.
UNCOV
247
        ini_set('pcre.jit', false);
×
248

249
        // Check that the standards are valid.
250
        foreach ($this->config->standards as $standard) {
×
UNCOV
251
            if (Standards::isInstalledStandard($standard) === false) {
×
252
                // They didn't select a valid coding standard, so help them
253
                // out by letting them know which standards are installed.
254
                $error  = 'ERROR: the "' . $standard . '" coding standard is not installed.' . PHP_EOL . PHP_EOL;
×
UNCOV
255
                $error .= Standards::prepareInstalledStandardsForDisplay() . PHP_EOL;
×
UNCOV
256
                throw new DeepExitException($error, ExitCode::PROCESS_ERROR);
×
257
            }
258
        }
259

260
        // Saves passing the Config object into other objects that only need
261
        // the verbosity flag for debug output.
UNCOV
262
        if (defined('PHP_CODESNIFFER_VERBOSITY') === false) {
×
UNCOV
263
            define('PHP_CODESNIFFER_VERBOSITY', $this->config->verbosity);
×
264
        }
265

266
        // Create this class so it is autoloaded and sets up a bunch
267
        // of PHP_CodeSniffer-specific token type constants.
UNCOV
268
        new Tokens();
×
269

270
        // Allow autoloading of custom files inside installed standards.
271
        $installedStandards = Standards::getInstalledStandardDetails();
×
UNCOV
272
        foreach ($installedStandards as $details) {
×
UNCOV
273
            Autoload::addSearchPath($details['path'], $details['namespace']);
×
274
        }
275

276
        // The ruleset contains all the information about how the files
277
        // should be checked and/or fixed.
278
        try {
UNCOV
279
            $this->ruleset = new Ruleset($this->config);
×
280

UNCOV
281
            if ($this->ruleset->hasSniffDeprecations() === true) {
×
282
                $this->ruleset->showSniffDeprecations();
×
283
            }
284
        } catch (RuntimeException $e) {
×
285
            $error  = rtrim($e->getMessage(), "\r\n") . PHP_EOL . PHP_EOL;
×
UNCOV
286
            $error .= $this->config->printShortUsage(true);
×
287
            throw new DeepExitException($error, ExitCode::PROCESS_ERROR);
×
288
        }
289
    }
290

291

292
    /**
293
     * Performs the run.
294
     *
295
     * @return void
296
     *
297
     * @throws \PHP_CodeSniffer\Exceptions\DeepExitException
298
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException
299
     */
300
    private function run()
12✔
301
    {
302
        // The class that manages all reporters for the run.
303
        $this->reporter = new Reporter($this->config);
12✔
304

305
        // Include bootstrap files.
306
        foreach ($this->config->bootstrap as $bootstrap) {
12✔
UNCOV
307
            include $bootstrap;
×
308
        }
309

310
        if ($this->config->stdin === true) {
12✔
UNCOV
311
            $fileContents = $this->config->stdinContent;
×
UNCOV
312
            if ($fileContents === null) {
×
UNCOV
313
                $handle = fopen('php://stdin', 'r');
×
314
                stream_set_blocking($handle, true);
×
315
                $fileContents = stream_get_contents($handle);
×
316
                fclose($handle);
×
317
            }
318

319
            $todo  = new FileList($this->config, $this->ruleset);
×
UNCOV
320
            $dummy = new DummyFile($fileContents, $this->ruleset, $this->config);
×
UNCOV
321
            $todo->addFile($dummy->path, $dummy);
×
322
        } else {
323
            if (empty($this->config->files) === true) {
12✔
324
                $error  = 'ERROR: You must supply at least one file or directory to process.' . PHP_EOL . PHP_EOL;
×
UNCOV
325
                $error .= $this->config->printShortUsage(true);
×
UNCOV
326
                throw new DeepExitException($error, ExitCode::PROCESS_ERROR);
×
327
            }
328

329
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
12✔
UNCOV
330
                StatusWriter::write('Creating file list... ', 0, 0);
×
331
            }
332

333
            $todo = new FileList($this->config, $this->ruleset);
12✔
334

335
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
12✔
UNCOV
336
                $numFiles = count($todo);
×
UNCOV
337
                StatusWriter::write("DONE ($numFiles files in queue)");
×
338
            }
339

340
            if ($this->config->cache === true) {
12✔
UNCOV
341
                if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
UNCOV
342
                    StatusWriter::write('Loading cache... ', 0, 0);
×
343
                }
344

345
                Cache::load($this->ruleset, $this->config);
×
346

UNCOV
347
                if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
348
                    $size = Cache::getSize();
×
UNCOV
349
                    StatusWriter::write("DONE ($size files in cache)");
×
350
                }
351
            }
352
        }
353

354
        $numFiles = count($todo);
12✔
355
        if ($numFiles === 0) {
12✔
356
            $error  = 'ERROR: No files were checked.' . PHP_EOL;
12✔
357
            $error .= 'All specified files were excluded or did not match filtering rules.' . PHP_EOL . PHP_EOL;
12✔
358
            throw new DeepExitException($error, ExitCode::PROCESS_ERROR);
12✔
359
        }
360

361
        // Turn all sniff errors into exceptions.
UNCOV
362
        set_error_handler([$this, 'handleErrors']);
×
363

364
        // If verbosity is too high, turn off parallelism so the
365
        // debug output is clean.
UNCOV
366
        if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
UNCOV
367
            $this->config->parallel = 1;
×
368
        }
369

370
        // If the PCNTL extension isn't installed, we can't fork.
UNCOV
371
        if (function_exists('pcntl_fork') === false) {
×
UNCOV
372
            $this->config->parallel = 1;
×
373
        }
374

375
        $lastDir = '';
×
376

UNCOV
377
        if ($this->config->parallel === 1) {
×
378
            // Running normally.
UNCOV
379
            $numProcessed = 0;
×
380
            foreach ($todo as $path => $file) {
×
UNCOV
381
                if ($file->ignored === false) {
×
382
                    $currDir = dirname($path);
×
383
                    if ($lastDir !== $currDir) {
×
384
                        if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
385
                            StatusWriter::write('Changing into directory ' . Common::stripBasepath($currDir, $this->config->basepath));
×
386
                        }
387

388
                        $lastDir = $currDir;
×
389
                    }
390

391
                    $this->processFile($file);
×
UNCOV
392
                } elseif (PHP_CODESNIFFER_VERBOSITY > 0) {
×
UNCOV
393
                    StatusWriter::write('Skipping ' . basename($file->path));
×
394
                }
395

396
                $numProcessed++;
×
UNCOV
397
                $this->printProgress($file, $numFiles, $numProcessed);
×
398
            }
399
        } else {
400
            // Batching and forking.
UNCOV
401
            $childProcs = [];
×
402

403
            // Distribute files round-robin in size order so each batch
404
            // ends up with a similar total workload.
NEW
405
            $sortedPaths = [];
×
NEW
406
            $todo->rewind();
×
NEW
407
            while ($todo->valid() === true) {
×
NEW
408
                $sortedPaths[] = $todo->key();
×
NEW
409
                $todo->next();
×
410
            }
411

NEW
412
            $sizes = [];
×
NEW
413
            foreach ($sortedPaths as $path) {
×
NEW
414
                $size = @filesize($path);
×
NEW
415
                if ($size === false) {
×
NEW
416
                    $size = 0;
×
417
                }
418

NEW
419
                $sizes[$path] = $size;
×
420
            }
421

NEW
422
            usort(
×
NEW
423
                $sortedPaths,
×
424
                static function ($a, $b) use ($sizes) {
NEW
425
                    return ($sizes[$b] - $sizes[$a]);
×
NEW
426
                }
×
427
            );
428

UNCOV
429
            for ($batch = 0; $batch < $this->config->parallel; $batch++) {
×
NEW
430
                if ($batch >= $numFiles) {
×
431
                    break;
×
432
                }
433

NEW
434
                $childOutFilename = tempnam(sys_get_temp_dir(), 'phpcs-child');
×
UNCOV
435
                $pid = pcntl_fork();
×
UNCOV
436
                if ($pid === -1) {
×
437
                    throw new RuntimeException('Failed to create child process');
×
438
                } elseif ($pid !== 0) {
×
439
                    $childProcs[$pid] = $childOutFilename;
×
440
                } else {
441
                    // Reset the reporter to make sure only figures from this
442
                    // file batch are recorded.
UNCOV
443
                    $this->reporter->totalFiles           = 0;
×
UNCOV
444
                    $this->reporter->totalErrors          = 0;
×
445
                    $this->reporter->totalWarnings        = 0;
×
446
                    $this->reporter->totalFixableErrors   = 0;
×
447
                    $this->reporter->totalFixableWarnings = 0;
×
448
                    $this->reporter->totalFixedErrors     = 0;
×
449
                    $this->reporter->totalFixedWarnings   = 0;
×
450

451
                    // Process the files.
452
                    $pathsProcessed = [];
×
UNCOV
453
                    ob_start();
×
UNCOV
454
                    for ($i = $batch; $i < $numFiles; $i += $this->config->parallel) {
×
455
                        $path = $sortedPaths[$i];
×
456
                        $file = new LocalFile($path, $this->ruleset, $this->config);
×
457

NEW
458
                        if ($file->ignored === true) {
×
NEW
459
                            continue;
×
460
                        }
461

462
                        $currDir = dirname($path);
×
463
                        if ($lastDir !== $currDir) {
×
UNCOV
464
                            if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
465
                                StatusWriter::write('Changing into directory ' . Common::stripBasepath($currDir, $this->config->basepath));
×
466
                            }
467

468
                            $lastDir = $currDir;
×
469
                        }
470

471
                        $this->processFile($file);
×
472

UNCOV
473
                        $pathsProcessed[] = $path;
×
474
                    }
475

476
                    $debugOutput = ob_get_contents();
×
477
                    ob_end_clean();
×
478

479
                    // Write information about the run to the filesystem
480
                    // so it can be picked up by the main process.
481
                    $childOutput = [
UNCOV
482
                        'totalFiles'           => $this->reporter->totalFiles,
×
UNCOV
483
                        'totalErrors'          => $this->reporter->totalErrors,
×
UNCOV
484
                        'totalWarnings'        => $this->reporter->totalWarnings,
×
485
                        'totalFixableErrors'   => $this->reporter->totalFixableErrors,
×
486
                        'totalFixableWarnings' => $this->reporter->totalFixableWarnings,
×
487
                        'totalFixedErrors'     => $this->reporter->totalFixedErrors,
×
488
                        'totalFixedWarnings'   => $this->reporter->totalFixedWarnings,
×
489
                    ];
490

491
                    $output  = '<' . '?php' . "\n" . ' $childOutput = ';
×
UNCOV
492
                    $output .= var_export($childOutput, true);
×
UNCOV
493
                    $output .= ";\n\$debugOutput = ";
×
494
                    $output .= var_export($debugOutput, true);
×
495

496
                    if ($this->config->cache === true) {
×
497
                        $childCache = [];
×
UNCOV
498
                        foreach ($pathsProcessed as $path) {
×
499
                            $childCache[$path] = Cache::get($path);
×
500
                        }
501

502
                        $output .= ";\n\$childCache = ";
×
UNCOV
503
                        $output .= var_export($childCache, true);
×
504
                    }
505

506
                    $output .= ";\n?" . '>';
×
UNCOV
507
                    file_put_contents($childOutFilename, $output);
×
UNCOV
508
                    exit();
×
509
                }
510
            }
511

UNCOV
512
            $success = $this->processChildProcs($childProcs);
×
UNCOV
513
            if ($success === false) {
×
UNCOV
514
                throw new RuntimeException('One or more child processes failed to run');
×
515
            }
516
        }
517

UNCOV
518
        restore_error_handler();
×
519

UNCOV
520
        if (PHP_CODESNIFFER_VERBOSITY === 0
×
521
            && $this->config->interactive === false
×
UNCOV
522
            && $this->config->showProgress === true
×
523
        ) {
524
            StatusWriter::writeNewline(2);
×
525
        }
526

527
        if ($this->config->cache === true) {
×
UNCOV
528
            Cache::save();
×
529
        }
530
    }
531

532

533
    /**
534
     * Converts all PHP errors into exceptions.
535
     *
536
     * This method forces a sniff to stop processing if it is not
537
     * able to handle a specific piece of code, instead of continuing
538
     * and potentially getting into a loop.
539
     *
540
     * @param int    $code    The level of error raised.
541
     * @param string $message The error message.
542
     * @param string $file    The path of the file that raised the error.
543
     * @param int    $line    The line number the error was raised at.
544
     *
545
     * @return bool
546
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException
547
     */
UNCOV
548
    public function handleErrors(int $code, string $message, string $file, int $line)
×
549
    {
UNCOV
550
        if ((error_reporting() & $code) === 0) {
×
551
            // This type of error is being muted.
UNCOV
552
            return true;
×
553
        }
554

555
        throw new RuntimeException("$message in $file on line $line");
×
556
    }
557

558

559
    /**
560
     * Processes a single file, including checking and fixing.
561
     *
562
     * @param \PHP_CodeSniffer\Files\File $file The file to be processed.
563
     *
564
     * @return void
565
     * @throws \PHP_CodeSniffer\Exceptions\DeepExitException
566
     */
UNCOV
567
    public function processFile(File $file)
×
568
    {
UNCOV
569
        if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
570
            $startTime = microtime(true);
×
UNCOV
571
            $newlines  = 0;
×
572
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
573
                $newlines = 1;
×
574
            }
575

576
            StatusWriter::write('Processing ' . basename($file->path) . ' ', 0, $newlines);
×
577
        }
578

579
        try {
UNCOV
580
            $file->process();
×
581

UNCOV
582
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
583
                StatusWriter::write('DONE in ' . Timing::getHumanReadableDuration(Timing::getDurationSince($startTime)), 0, 0);
×
584

585
                if (PHP_CODESNIFFER_CBF === true) {
×
586
                    $errors   = $file->getFixableErrorCount();
×
UNCOV
587
                    $warnings = $file->getFixableWarningCount();
×
588
                    StatusWriter::write(" ($errors fixable errors, $warnings fixable warnings)");
×
589
                } else {
590
                    $errors   = $file->getErrorCount();
×
591
                    $warnings = $file->getWarningCount();
×
UNCOV
592
                    StatusWriter::write(" ($errors errors, $warnings warnings)");
×
593
                }
594
            }
595
        } catch (Exception $e) {
×
UNCOV
596
            $error = 'An error occurred during processing; checking has been aborted. The error message was: ' . $e->getMessage();
×
597

598
            // Determine which sniff caused the error.
599
            $sniffStack = null;
×
UNCOV
600
            $nextStack  = null;
×
UNCOV
601
            foreach ($e->getTrace() as $step) {
×
602
                if (isset($step['file']) === false) {
×
603
                    continue;
×
604
                }
605

606
                if (empty($sniffStack) === false) {
×
UNCOV
607
                    $nextStack = $step;
×
UNCOV
608
                    break;
×
609
                }
610

611
                if (substr($step['file'], -9) === 'Sniff.php') {
×
UNCOV
612
                    $sniffStack = $step;
×
UNCOV
613
                    continue;
×
614
                }
615
            }
616

UNCOV
617
            if (empty($sniffStack) === false) {
×
UNCOV
618
                $sniffCode = '';
×
619
                try {
620
                    if (empty($nextStack) === false
×
621
                        && isset($nextStack['class']) === true
×
622
                    ) {
623
                        $sniffCode = 'the ' . Common::getSniffCode($nextStack['class']) . ' sniff';
×
624
                    }
UNCOV
625
                } catch (InvalidArgumentException $e) {
×
626
                    // Sniff code could not be determined. This may be an abstract sniff class.
627
                }
628

UNCOV
629
                if ($sniffCode === '') {
×
UNCOV
630
                    $sniffCode = substr(strrchr(str_replace('\\', '/', $sniffStack['file']), '/'), 1);
×
631
                }
632

633
                $error .= sprintf(PHP_EOL . 'The error originated in %s on line %s.', $sniffCode, $sniffStack['line']);
×
634
            }
635

636
            $file->addErrorOnLine($error, 1, 'Internal.Exception');
×
637
        }
638

639
        $this->reporter->cacheFileReport($file);
×
640

UNCOV
641
        if ($this->config->interactive === true) {
×
642
            /*
643
                Running interactively.
644
                Print the error report for the current file and then wait for user input.
645
            */
646

647
            // Get current violations and then clear the list to make sure
648
            // we only print violations for a single file each time.
UNCOV
649
            $numErrors = null;
×
UNCOV
650
            while ($numErrors !== 0) {
×
UNCOV
651
                $numErrors = ($file->getErrorCount() + $file->getWarningCount());
×
652
                if ($numErrors === 0) {
×
653
                    continue;
×
654
                }
655

656
                $this->reporter->printReport('full');
×
657

UNCOV
658
                echo '<ENTER> to recheck, [s] to skip or [q] to quit : ';
×
659
                $input = fgets(STDIN);
×
UNCOV
660
                $input = trim($input);
×
661

662
                switch ($input) {
663
                    case 's':
×
UNCOV
664
                        break(2);
×
UNCOV
665
                    case 'q':
×
666
                        // User request to "quit": exit code should be 0.
667
                        throw new DeepExitException('', ExitCode::OKAY);
×
668
                    default:
669
                        // Repopulate the sniffs because some of them save their state
670
                        // and only clear it when the file changes, but we are rechecking
671
                        // the same file.
UNCOV
672
                        $file->ruleset->populateTokenListeners();
×
UNCOV
673
                        $file->reloadContent();
×
UNCOV
674
                        $file->process();
×
675
                        $this->reporter->cacheFileReport($file);
×
676
                        break;
×
677
                }
678
            }
679
        }
680

681
        // Clean up the file to save (a lot of) memory.
UNCOV
682
        $file->cleanUp();
×
683
    }
684

685

686
    /**
687
     * Waits for child processes to complete and cleans up after them.
688
     *
689
     * The reporting information returned by each child process is merged
690
     * into the main reporter class.
691
     *
692
     * @param array<int, string> $childProcs An array of child processes to wait for.
693
     *
694
     * @return bool
695
     */
UNCOV
696
    private function processChildProcs(array $childProcs)
×
697
    {
UNCOV
698
        $numProcessed = 0;
×
699
        $totalBatches = count($childProcs);
×
700

701
        $success = true;
×
702

UNCOV
703
        while (count($childProcs) > 0) {
×
704
            $pid = pcntl_waitpid(0, $status);
×
UNCOV
705
            if ($pid <= 0 || isset($childProcs[$pid]) === false) {
×
706
                // No child or a child with an unmanaged PID was returned.
707
                continue;
×
708
            }
709

710
            $childProcessStatus = pcntl_wexitstatus($status);
×
UNCOV
711
            if ($childProcessStatus !== 0) {
×
UNCOV
712
                $success = false;
×
713
            }
714

715
            $out = $childProcs[$pid];
×
UNCOV
716
            unset($childProcs[$pid]);
×
UNCOV
717
            if (file_exists($out) === false) {
×
718
                continue;
×
719
            }
720

721
            include $out;
×
UNCOV
722
            unlink($out);
×
723

724
            $numProcessed++;
×
725

UNCOV
726
            if (isset($childOutput) === false) {
×
727
                // The child process died, so the run has failed.
UNCOV
728
                $file = new DummyFile('', $this->ruleset, $this->config);
×
729
                $file->setErrorCounts(1, 0, 0, 0, 0, 0);
×
UNCOV
730
                $this->printProgress($file, $totalBatches, $numProcessed);
×
731
                $success = false;
×
732
                continue;
×
733
            }
734

735
            $this->reporter->totalFiles           += $childOutput['totalFiles'];
×
UNCOV
736
            $this->reporter->totalErrors          += $childOutput['totalErrors'];
×
UNCOV
737
            $this->reporter->totalWarnings        += $childOutput['totalWarnings'];
×
738
            $this->reporter->totalFixableErrors   += $childOutput['totalFixableErrors'];
×
739
            $this->reporter->totalFixableWarnings += $childOutput['totalFixableWarnings'];
×
740
            $this->reporter->totalFixedErrors     += $childOutput['totalFixedErrors'];
×
741
            $this->reporter->totalFixedWarnings   += $childOutput['totalFixedWarnings'];
×
742

743
            if (isset($debugOutput) === true) {
×
744
                echo $debugOutput;
×
745
            }
746

747
            if (isset($childCache) === true) {
×
UNCOV
748
                foreach ($childCache as $path => $cache) {
×
UNCOV
749
                    Cache::set($path, $cache);
×
750
                }
751
            }
752

753
            // Fake a processed file so we can print progress output for the batch.
UNCOV
754
            $file = new DummyFile('', $this->ruleset, $this->config);
×
UNCOV
755
            $file->setErrorCounts(
×
UNCOV
756
                $childOutput['totalErrors'],
×
757
                $childOutput['totalWarnings'],
×
758
                $childOutput['totalFixableErrors'],
×
759
                $childOutput['totalFixableWarnings'],
×
760
                $childOutput['totalFixedErrors'],
×
761
                $childOutput['totalFixedWarnings']
×
762
            );
763
            $this->printProgress($file, $totalBatches, $numProcessed);
×
764
        }
765

766
        return $success;
×
767
    }
768

769

770
    /**
771
     * Print progress information for a single processed file.
772
     *
773
     * @param \PHP_CodeSniffer\Files\File $file         The file that was processed.
774
     * @param int                         $numFiles     The total number of files to process.
775
     * @param int                         $numProcessed The number of files that have been processed,
776
     *                                                  including this one.
777
     *
778
     * @return void
779
     */
780
    public function printProgress(File $file, int $numFiles, int $numProcessed)
63✔
781
    {
782
        if (PHP_CODESNIFFER_VERBOSITY > 0
63✔
783
            || $this->config->showProgress === false
63✔
784
        ) {
785
            return;
3✔
786
        }
787

788
        $showColors  = $this->config->colors;
60✔
789
        $colorOpen   = '';
60✔
790
        $progressDot = '.';
60✔
791
        $colorClose  = '';
60✔
792

793
        // Show progress information.
794
        if ($file->ignored === true) {
60✔
795
            $progressDot = 'S';
3✔
796
        } else {
797
            $errors   = $file->getErrorCount();
60✔
798
            $warnings = $file->getWarningCount();
60✔
799
            $fixable  = $file->getFixableCount();
60✔
800
            $fixed    = ($file->getFixedErrorCount() + $file->getFixedWarningCount());
60✔
801

802
            if (PHP_CODESNIFFER_CBF === true) {
60✔
803
                // Files with fixed errors or warnings are F (green).
804
                // Files with unfixable errors or warnings are E (red).
805
                // Files with no errors or warnings are . (black).
806
                if ($fixable > 0) {
18✔
807
                    $progressDot = 'E';
6✔
808

809
                    if ($showColors === true) {
6✔
810
                        $colorOpen  = "\033[31m";
3✔
811
                        $colorClose = "\033[0m";
4✔
812
                    }
813
                } elseif ($fixed > 0) {
12✔
814
                    $progressDot = 'F';
6✔
815

816
                    if ($showColors === true) {
6✔
817
                        $colorOpen  = "\033[32m";
3✔
818
                        $colorClose = "\033[0m";
8✔
819
                    }
820
                }
821
            } else {
822
                // Files with errors are E (red).
823
                // Files with fixable errors are E (green).
824
                // Files with warnings are W (yellow).
825
                // Files with fixable warnings are W (green).
826
                // Files with no errors or warnings are . (black).
827
                if ($errors > 0) {
42✔
828
                    $progressDot = 'E';
9✔
829

830
                    if ($showColors === true) {
9✔
831
                        if ($fixable > 0) {
6✔
832
                            $colorOpen = "\033[32m";
3✔
833
                        } else {
834
                            $colorOpen = "\033[31m";
3✔
835
                        }
836

837
                        $colorClose = "\033[0m";
7✔
838
                    }
839
                } elseif ($warnings > 0) {
33✔
840
                    $progressDot = 'W';
9✔
841

842
                    if ($showColors === true) {
9✔
843
                        if ($fixable > 0) {
6✔
844
                            $colorOpen = "\033[32m";
3✔
845
                        } else {
846
                            $colorOpen = "\033[33m";
3✔
847
                        }
848

849
                        $colorClose = "\033[0m";
6✔
850
                    }
851
                }
852
            }
853
        }
854

855
        StatusWriter::write($colorOpen . $progressDot . $colorClose, 0, 0);
60✔
856

857
        $numPerLine = 60;
60✔
858
        if ($numProcessed !== $numFiles && ($numProcessed % $numPerLine) !== 0) {
60✔
859
            return;
60✔
860
        }
861

862
        $percent = round(($numProcessed / $numFiles) * 100);
18✔
863
        $padding = (strlen($numFiles) - strlen($numProcessed));
18✔
864
        if ($numProcessed === $numFiles
18✔
865
            && $numFiles > $numPerLine
18✔
866
            && ($numProcessed % $numPerLine) !== 0
18✔
867
        ) {
868
            $padding += ($numPerLine - ($numFiles - (floor($numFiles / $numPerLine) * $numPerLine)));
9✔
869
        }
870

871
        StatusWriter::write(str_repeat(' ', $padding) . " $numProcessed / $numFiles ($percent%)");
18✔
872
    }
6✔
873

874

875
    /**
876
     * Registers a PHP shutdown function to provide a more informative out of memory error.
877
     *
878
     * @param string $command The command which was used to initiate the PHPCS run.
879
     *
880
     * @return void
881
     */
UNCOV
882
    private function registerOutOfMemoryShutdownMessage(string $command)
×
883
    {
884
        // Allocate all needed memory beforehand as much as possible.
885
        $errorMsg    = PHP_EOL . 'The PHP_CodeSniffer "%1$s" command ran out of memory.' . PHP_EOL;
×
UNCOV
886
        $errorMsg   .= 'Either raise the "memory_limit" of PHP in the php.ini file or raise the memory limit at runtime' . PHP_EOL;
×
UNCOV
887
        $errorMsg   .= 'using "%1$s -d memory_limit=512M" (replace 512M with the desired memory limit).' . PHP_EOL;
×
888
        $errorMsg    = sprintf($errorMsg, $command);
×
889
        $memoryError = 'Allowed memory size of';
×
890
        $errorArray  = [
891
            'type'    => 42,
×
892
            'message' => 'Some random dummy string to take up memory and take up some more memory and some more and more and more and more',
893
            'file'    => 'Another random string, which would be a filename this time. Should be relatively long to allow for deeply nested files',
894
            'line'    => 31427,
895
        ];
896

UNCOV
897
        register_shutdown_function(
×
898
            static function () use (
UNCOV
899
                $errorMsg,
×
900
                $memoryError,
×
UNCOV
901
                $errorArray
×
902
            ) {
903
                $errorArray = error_get_last();
×
904
                if (is_array($errorArray) === true && strpos($errorArray['message'], $memoryError) !== false) {
×
UNCOV
905
                    fwrite(STDERR, $errorMsg);
×
906
                }
907
            }
×
908
        );
909
    }
910
}
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