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

PHPCSStandards / PHP_CodeSniffer / 25051759558

28 Apr 2026 12:05PM UTC coverage: 78.7% (-0.3%) from 78.969%
25051759558

Pull #1419

github

web-flow
Merge b6c69d0a2 into c3eb74d77
Pull Request #1419: Use work-stealing scheduler for parallel runs

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

1 existing line in 1 file now uncovered.

19875 of 25254 relevant lines covered (78.7%)

98.53 hits per line

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

17.61
/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.
119
            if ($this->config->stdin === true) {
×
120
                $this->config->cache = false;
×
121
            }
122

123
            $this->run();
×
124

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

128
            if ($this->config->quiet === false) {
×
129
                Timing::printRunTime();
×
130
            }
131
        } catch (DeepExitException $e) {
×
132
            $exitCode = $e->getCode();
×
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

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
     */
154
    public function runPHPCBF()
×
155
    {
156
        $this->registerOutOfMemoryShutdownMessage('phpcbf');
×
157

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

162
        try {
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.
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.
172
            if ($this->config->stdin === true) {
×
173
                $this->config->verbosity = 0;
×
174
            }
175

176
            // Init the run and load the rulesets to set additional config vars.
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.
182
            if ($this->config->stdin === true) {
×
183
                $this->config->parallel = 1;
×
184
            }
185

186
            // Override some of the command line settings that might break the fixes.
187
            $this->config->generator    = null;
×
188
            $this->config->explain      = false;
×
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);
×
197
            $newReports      = ['cbf' => null];
×
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.
207
            $this->config->dieOnUnknownArg = false;
×
208

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

212
            if ($this->config->quiet === false) {
×
213
                StatusWriter::writeNewline();
×
214
                Timing::printRunTime();
×
215
            }
216
        } catch (DeepExitException $e) {
×
217
            $exitCode = $e->getCode();
×
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

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
     */
240
    public function init()
×
241
    {
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.
247
        ini_set('pcre.jit', false);
×
248

249
        // Check that the standards are valid.
250
        foreach ($this->config->standards as $standard) {
×
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;
×
255
                $error .= Standards::prepareInstalledStandardsForDisplay() . PHP_EOL;
×
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.
262
        if (defined('PHP_CODESNIFFER_VERBOSITY') === false) {
×
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.
268
        new Tokens();
×
269

270
        // Allow autoloading of custom files inside installed standards.
271
        $installedStandards = Standards::getInstalledStandardDetails();
×
272
        foreach ($installedStandards as $details) {
×
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 {
279
            $this->ruleset = new Ruleset($this->config);
×
280

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;
×
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✔
307
            include $bootstrap;
×
308
        }
309

310
        if ($this->config->stdin === true) {
12✔
311
            $fileContents = $this->config->stdinContent;
×
312
            if ($fileContents === null) {
×
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);
×
320
            $dummy = new DummyFile($fileContents, $this->ruleset, $this->config);
×
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;
×
325
                $error .= $this->config->printShortUsage(true);
×
326
                throw new DeepExitException($error, ExitCode::PROCESS_ERROR);
×
327
            }
328

329
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
12✔
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✔
336
                $numFiles = count($todo);
×
337
                StatusWriter::write("DONE ($numFiles files in queue)");
×
338
            }
339

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

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

347
                if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
348
                    $size = Cache::getSize();
×
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.
362
        set_error_handler([$this, 'handleErrors']);
×
363

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

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

375
        $lastDir = '';
×
376

377
        if ($this->config->parallel === 1) {
×
378
            // Running normally.
379
            $numProcessed = 0;
×
380
            foreach ($todo as $path => $file) {
×
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);
×
392
                } elseif (PHP_CODESNIFFER_VERBOSITY > 0) {
×
393
                    StatusWriter::write('Skipping ' . basename($file->path));
×
394
                }
395

396
                $numProcessed++;
×
397
                $this->printProgress($file, $numFiles, $numProcessed);
×
398
            }
399
        } else {
400
            // Work-stealing parallel processing: workers ask the master for
401
            // chunks of files as they finish, so faster workers (or workers
402
            // that drew an easier slice) automatically pick up more work.
NEW
403
            $queue = [];
×
NEW
404
            $todo->rewind();
×
NEW
405
            while ($todo->valid() === true) {
×
NEW
406
                $queue[] = $todo->key();
×
NEW
407
                $todo->next();
×
408
            }
409

NEW
410
            $numWorkers = min($this->config->parallel, $numFiles);
×
411

412
            // Same default as PHPStan's parallel scheduler — small enough that
413
            // a fast worker keeps coming back for more, large enough to amortize
414
            // the IPC round-trip per file.
NEW
415
            $chunkSize = 20;
×
416

NEW
417
            $childProcs = [];
×
NEW
418
            $sockets    = [];
×
419

NEW
420
            for ($worker = 0; $worker < $numWorkers; $worker++) {
×
NEW
421
                $pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
×
NEW
422
                if ($pair === false) {
×
NEW
423
                    throw new RuntimeException('Failed to create socket pair for worker');
×
424
                }
425

426
                $childOutFilename = tempnam(sys_get_temp_dir(), 'phpcs-child');
×
427
                $pid = pcntl_fork();
×
428
                if ($pid === -1) {
×
429
                    throw new RuntimeException('Failed to create child process');
×
430
                } elseif ($pid !== 0) {
×
NEW
431
                    fclose($pair[1]);
×
UNCOV
432
                    $childProcs[$pid] = $childOutFilename;
×
NEW
433
                    $sockets[$pid]    = $pair[0];
×
434
                } else {
NEW
435
                    fclose($pair[0]);
×
436
                    // Don't leak previously-created parent-side sockets into
437
                    // sibling workers — only this child's socket should remain.
NEW
438
                    foreach ($sockets as $inheritedSocket) {
×
NEW
439
                        fclose($inheritedSocket);
×
440
                    }
441

NEW
442
                    $this->processWorker($pair[1], $childOutFilename);
×
443
                    exit();
×
444
                }
445
            }
446

NEW
447
            $this->dispatchWork($queue, $sockets, $numFiles, $chunkSize);
×
448

NEW
449
            foreach ($sockets as $socket) {
×
NEW
450
                fclose($socket);
×
451
            }
452

453
            $success = $this->processChildProcs($childProcs);
×
454
            if ($success === false) {
×
455
                throw new RuntimeException('One or more child processes failed to run');
×
456
            }
457
        }
458

459
        restore_error_handler();
×
460

461
        if (PHP_CODESNIFFER_VERBOSITY === 0
×
462
            && $this->config->interactive === false
×
463
            && $this->config->showProgress === true
×
464
        ) {
465
            StatusWriter::writeNewline(2);
×
466
        }
467

468
        if ($this->config->cache === true) {
×
469
            Cache::save();
×
470
        }
471
    }
472

473

474
    /**
475
     * Converts all PHP errors into exceptions.
476
     *
477
     * This method forces a sniff to stop processing if it is not
478
     * able to handle a specific piece of code, instead of continuing
479
     * and potentially getting into a loop.
480
     *
481
     * @param int    $code    The level of error raised.
482
     * @param string $message The error message.
483
     * @param string $file    The path of the file that raised the error.
484
     * @param int    $line    The line number the error was raised at.
485
     *
486
     * @return bool
487
     * @throws \PHP_CodeSniffer\Exceptions\RuntimeException
488
     */
489
    public function handleErrors(int $code, string $message, string $file, int $line)
×
490
    {
491
        if ((error_reporting() & $code) === 0) {
×
492
            // This type of error is being muted.
493
            return true;
×
494
        }
495

496
        throw new RuntimeException("$message in $file on line $line");
×
497
    }
498

499

500
    /**
501
     * Processes a single file, including checking and fixing.
502
     *
503
     * @param \PHP_CodeSniffer\Files\File $file The file to be processed.
504
     *
505
     * @return void
506
     * @throws \PHP_CodeSniffer\Exceptions\DeepExitException
507
     */
508
    public function processFile(File $file)
×
509
    {
510
        if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
511
            $startTime = microtime(true);
×
512
            $newlines  = 0;
×
513
            if (PHP_CODESNIFFER_VERBOSITY > 1) {
×
514
                $newlines = 1;
×
515
            }
516

517
            StatusWriter::write('Processing ' . basename($file->path) . ' ', 0, $newlines);
×
518
        }
519

520
        try {
521
            $file->process();
×
522

523
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
524
                StatusWriter::write('DONE in ' . Timing::getHumanReadableDuration(Timing::getDurationSince($startTime)), 0, 0);
×
525

526
                if (PHP_CODESNIFFER_CBF === true) {
×
527
                    $errors   = $file->getFixableErrorCount();
×
528
                    $warnings = $file->getFixableWarningCount();
×
529
                    StatusWriter::write(" ($errors fixable errors, $warnings fixable warnings)");
×
530
                } else {
531
                    $errors   = $file->getErrorCount();
×
532
                    $warnings = $file->getWarningCount();
×
533
                    StatusWriter::write(" ($errors errors, $warnings warnings)");
×
534
                }
535
            }
536
        } catch (Exception $e) {
×
537
            $error = 'An error occurred during processing; checking has been aborted. The error message was: ' . $e->getMessage();
×
538

539
            // Determine which sniff caused the error.
540
            $sniffStack = null;
×
541
            $nextStack  = null;
×
542
            foreach ($e->getTrace() as $step) {
×
543
                if (isset($step['file']) === false) {
×
544
                    continue;
×
545
                }
546

547
                if (empty($sniffStack) === false) {
×
548
                    $nextStack = $step;
×
549
                    break;
×
550
                }
551

552
                if (substr($step['file'], -9) === 'Sniff.php') {
×
553
                    $sniffStack = $step;
×
554
                    continue;
×
555
                }
556
            }
557

558
            if (empty($sniffStack) === false) {
×
559
                $sniffCode = '';
×
560
                try {
561
                    if (empty($nextStack) === false
×
562
                        && isset($nextStack['class']) === true
×
563
                    ) {
564
                        $sniffCode = 'the ' . Common::getSniffCode($nextStack['class']) . ' sniff';
×
565
                    }
566
                } catch (InvalidArgumentException $e) {
×
567
                    // Sniff code could not be determined. This may be an abstract sniff class.
568
                }
569

570
                if ($sniffCode === '') {
×
571
                    $sniffCode = substr(strrchr(str_replace('\\', '/', $sniffStack['file']), '/'), 1);
×
572
                }
573

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

577
            $file->addErrorOnLine($error, 1, 'Internal.Exception');
×
578
        }
579

580
        $this->reporter->cacheFileReport($file);
×
581

582
        if ($this->config->interactive === true) {
×
583
            /*
584
                Running interactively.
585
                Print the error report for the current file and then wait for user input.
586
            */
587

588
            // Get current violations and then clear the list to make sure
589
            // we only print violations for a single file each time.
590
            $numErrors = null;
×
591
            while ($numErrors !== 0) {
×
592
                $numErrors = ($file->getErrorCount() + $file->getWarningCount());
×
593
                if ($numErrors === 0) {
×
594
                    continue;
×
595
                }
596

597
                $this->reporter->printReport('full');
×
598

599
                echo '<ENTER> to recheck, [s] to skip or [q] to quit : ';
×
600
                $input = fgets(STDIN);
×
601
                $input = trim($input);
×
602

603
                switch ($input) {
604
                    case 's':
×
605
                        break(2);
×
606
                    case 'q':
×
607
                        // User request to "quit": exit code should be 0.
608
                        throw new DeepExitException('', ExitCode::OKAY);
×
609
                    default:
610
                        // Repopulate the sniffs because some of them save their state
611
                        // and only clear it when the file changes, but we are rechecking
612
                        // the same file.
613
                        $file->ruleset->populateTokenListeners();
×
614
                        $file->reloadContent();
×
615
                        $file->process();
×
616
                        $this->reporter->cacheFileReport($file);
×
617
                        break;
×
618
                }
619
            }
620
        }
621

622
        // Clean up the file to save (a lot of) memory.
623
        $file->cleanUp();
×
624
    }
625

626

627
    /**
628
     * Waits for child processes to complete and cleans up after them.
629
     *
630
     * The reporting information returned by each child process is merged
631
     * into the main reporter class.
632
     *
633
     * @param array<int, string> $childProcs An array of child processes to wait for.
634
     *
635
     * @return bool
636
     */
637
    private function processChildProcs(array $childProcs)
×
638
    {
639
        $success = true;
×
640

641
        while (count($childProcs) > 0) {
×
642
            $pid = pcntl_waitpid(0, $status);
×
643
            if ($pid <= 0 || isset($childProcs[$pid]) === false) {
×
644
                // No child or a child with an unmanaged PID was returned.
645
                continue;
×
646
            }
647

648
            $childProcessStatus = pcntl_wexitstatus($status);
×
649
            if ($childProcessStatus !== 0) {
×
650
                $success = false;
×
651
            }
652

653
            $out = $childProcs[$pid];
×
654
            unset($childProcs[$pid]);
×
655
            if (file_exists($out) === false) {
×
656
                continue;
×
657
            }
658

659
            include $out;
×
660
            unlink($out);
×
661

662
            if (isset($childOutput) === false) {
×
663
                // The child process died, so the run has failed.
664
                $success = false;
×
665
                continue;
×
666
            }
667

668
            $this->reporter->totalFiles           += $childOutput['totalFiles'];
×
669
            $this->reporter->totalErrors          += $childOutput['totalErrors'];
×
670
            $this->reporter->totalWarnings        += $childOutput['totalWarnings'];
×
671
            $this->reporter->totalFixableErrors   += $childOutput['totalFixableErrors'];
×
672
            $this->reporter->totalFixableWarnings += $childOutput['totalFixableWarnings'];
×
673
            $this->reporter->totalFixedErrors     += $childOutput['totalFixedErrors'];
×
674
            $this->reporter->totalFixedWarnings   += $childOutput['totalFixedWarnings'];
×
675

676
            if (isset($debugOutput) === true) {
×
677
                echo $debugOutput;
×
678
            }
679

680
            if (isset($childCache) === true) {
×
681
                foreach ($childCache as $path => $cache) {
×
682
                    Cache::set($path, $cache);
×
683
                }
684
            }
685
        }
686

NEW
687
        return $success;
×
688
    }
689

690

691
    /**
692
     * Run a worker loop that asks the master for chunks of files until told to stop.
693
     *
694
     * @param resource $socket           The Unix socket connecting back to the master.
695
     * @param string   $childOutFilename The temp file used to return totals and cache.
696
     *
697
     * @return void
698
     */
NEW
699
    private function processWorker($socket, string $childOutFilename)
×
700
    {
701
        // Reset the reporter so this worker only accumulates its own files.
NEW
702
        $this->reporter->totalFiles           = 0;
×
NEW
703
        $this->reporter->totalErrors          = 0;
×
NEW
704
        $this->reporter->totalWarnings        = 0;
×
NEW
705
        $this->reporter->totalFixableErrors   = 0;
×
NEW
706
        $this->reporter->totalFixableWarnings = 0;
×
NEW
707
        $this->reporter->totalFixedErrors     = 0;
×
NEW
708
        $this->reporter->totalFixedWarnings   = 0;
×
709

NEW
710
        $pathsProcessed = [];
×
NEW
711
        $progressBuffer = [];
×
NEW
712
        $lastDir        = '';
×
713

NEW
714
        ob_start();
×
715

NEW
716
        while (true) {
×
NEW
717
            self::sendMessage(
×
NEW
718
                $socket,
×
719
                [
NEW
720
                    'type'     => 'ready',
×
NEW
721
                    'progress' => $progressBuffer,
×
722
                ]
723
            );
NEW
724
            $progressBuffer = [];
×
725

NEW
726
            $message = self::readMessage($socket);
×
NEW
727
            if ($message === null || $message['type'] === 'done') {
×
NEW
728
                break;
×
729
            }
730

NEW
731
            foreach ($message['paths'] as $path) {
×
NEW
732
                $file = new LocalFile($path, $this->ruleset, $this->config);
×
733

NEW
734
                if ($file->ignored === false) {
×
NEW
735
                    $currDir = dirname($path);
×
NEW
736
                    if ($lastDir !== $currDir) {
×
NEW
737
                        if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
NEW
738
                            StatusWriter::write('Changing into directory ' . Common::stripBasepath($currDir, $this->config->basepath));
×
739
                        }
740

NEW
741
                        $lastDir = $currDir;
×
742
                    }
743

NEW
744
                    $this->processFile($file);
×
745
                }
746

NEW
747
                $pathsProcessed[] = $path;
×
NEW
748
                $progressBuffer[] = [
×
NEW
749
                    'ignored'       => $file->ignored,
×
NEW
750
                    'errors'        => $file->getErrorCount(),
×
NEW
751
                    'warnings'      => $file->getWarningCount(),
×
NEW
752
                    'fixable'       => $file->getFixableCount(),
×
NEW
753
                    'fixedErrors'   => $file->getFixedErrorCount(),
×
NEW
754
                    'fixedWarnings' => $file->getFixedWarningCount(),
×
755
                ];
756
            }
757
        }
758

NEW
759
        $debugOutput = ob_get_contents();
×
NEW
760
        ob_end_clean();
×
761

762
        $childOutput = [
NEW
763
            'totalFiles'           => $this->reporter->totalFiles,
×
NEW
764
            'totalErrors'          => $this->reporter->totalErrors,
×
NEW
765
            'totalWarnings'        => $this->reporter->totalWarnings,
×
NEW
766
            'totalFixableErrors'   => $this->reporter->totalFixableErrors,
×
NEW
767
            'totalFixableWarnings' => $this->reporter->totalFixableWarnings,
×
NEW
768
            'totalFixedErrors'     => $this->reporter->totalFixedErrors,
×
NEW
769
            'totalFixedWarnings'   => $this->reporter->totalFixedWarnings,
×
770
        ];
771

NEW
772
        $output  = '<' . '?php' . "\n" . ' $childOutput = ';
×
NEW
773
        $output .= var_export($childOutput, true);
×
NEW
774
        $output .= ";\n\$debugOutput = ";
×
NEW
775
        $output .= var_export($debugOutput, true);
×
776

NEW
777
        if ($this->config->cache === true) {
×
NEW
778
            $childCache = [];
×
NEW
779
            foreach ($pathsProcessed as $path) {
×
NEW
780
                $childCache[$path] = Cache::get($path);
×
781
            }
782

NEW
783
            $output .= ";\n\$childCache = ";
×
NEW
784
            $output .= var_export($childCache, true);
×
785
        }
786

NEW
787
        $output .= ";\n?" . '>';
×
NEW
788
        file_put_contents($childOutFilename, $output);
×
789
    }
790

791

792
    /**
793
     * Hand out chunks of files to workers as they request more work.
794
     *
795
     * Reads "ready" messages on the worker sockets and sends back either the
796
     * next slice of paths or a "done" signal once the queue is exhausted.
797
     * Per-file progress reported by workers is rendered as it arrives.
798
     *
799
     * @param array<int, string>   $queue     Ordered list of file paths to dispatch.
800
     * @param array<int, resource> $sockets   PID-keyed master-side sockets.
801
     * @param int                  $numFiles  Total file count, used for progress percentage.
802
     * @param int                  $chunkSize Max files per dispatch.
803
     *
804
     * @return void
805
     */
NEW
806
    private function dispatchWork(array $queue, array $sockets, int $numFiles, int $chunkSize)
×
807
    {
NEW
808
        $cursor       = 0;
×
NEW
809
        $numProcessed = 0;
×
NEW
810
        $active       = $sockets;
×
811

NEW
812
        while (count($active) > 0) {
×
NEW
813
            $read   = array_values($active);
×
NEW
814
            $write  = null;
×
NEW
815
            $except = null;
×
816

NEW
817
            $ready = @stream_select($read, $write, $except, null);
×
NEW
818
            if ($ready === false) {
×
819
                // Interrupted (e.g. SIGCHLD) — just retry.
NEW
820
                continue;
×
821
            }
822

NEW
823
            if ($ready === 0) {
×
NEW
824
                continue;
×
825
            }
826

NEW
827
            foreach ($read as $socket) {
×
NEW
828
                $pid = array_search($socket, $active, true);
×
NEW
829
                if ($pid === false) {
×
NEW
830
                    continue;
×
831
                }
832

NEW
833
                $message = self::readMessage($socket);
×
NEW
834
                if ($message === null) {
×
835
                    // Worker closed the socket unexpectedly — let processChildProcs report it.
NEW
836
                    unset($active[$pid]);
×
NEW
837
                    continue;
×
838
                }
839

NEW
840
                foreach ($message['progress'] as $progress) {
×
NEW
841
                    $numProcessed++;
×
NEW
842
                    $file          = new DummyFile('', $this->ruleset, $this->config);
×
NEW
843
                    $file->ignored = $progress['ignored'];
×
844

845
                    // The combined fixable count is the only fixable value
846
                    // printProgress reads (via getFixableCount), so park it in
847
                    // the error slot and leave the warning slot at zero.
NEW
848
                    $file->setErrorCounts(
×
NEW
849
                        $progress['errors'],
×
NEW
850
                        $progress['warnings'],
×
NEW
851
                        $progress['fixable'],
×
NEW
852
                        0,
×
NEW
853
                        $progress['fixedErrors'],
×
NEW
854
                        $progress['fixedWarnings']
×
855
                    );
NEW
856
                    $this->printProgress($file, $numFiles, $numProcessed);
×
857
                }
858

NEW
859
                if ($cursor >= $numFiles) {
×
NEW
860
                    self::sendMessage($socket, ['type' => 'done']);
×
NEW
861
                    unset($active[$pid]);
×
NEW
862
                    continue;
×
863
                }
864

NEW
865
                $chunk   = array_slice($queue, $cursor, $chunkSize);
×
NEW
866
                $cursor += count($chunk);
×
NEW
867
                self::sendMessage(
×
NEW
868
                    $socket,
×
869
                    [
NEW
870
                        'type'  => 'work',
×
NEW
871
                        'paths' => $chunk,
×
872
                    ]
873
                );
874
            }
875
        }
876
    }
877

878

879
    /**
880
     * Send a length-prefixed serialized message over a worker socket.
881
     *
882
     * @param resource $socket  The socket to write to.
883
     * @param mixed    $payload Any serializable value.
884
     *
885
     * @return void
886
     */
NEW
887
    private static function sendMessage($socket, $payload)
×
888
    {
NEW
889
        $data   = serialize($payload);
×
NEW
890
        $header = pack('N', strlen($data));
×
NEW
891
        $buffer = $header . $data;
×
NEW
892
        $offset = 0;
×
NEW
893
        $total  = strlen($buffer);
×
894

NEW
895
        while ($offset < $total) {
×
NEW
896
            $written = fwrite($socket, substr($buffer, $offset));
×
NEW
897
            if ($written === false || $written === 0) {
×
NEW
898
                return;
×
899
            }
900

NEW
901
            $offset += $written;
×
902
        }
903
    }
904

905

906
    /**
907
     * Read a length-prefixed serialized message from a worker socket.
908
     *
909
     * @param resource $socket The socket to read from.
910
     *
911
     * @return mixed|null The decoded payload, or null on EOF / error.
912
     */
NEW
913
    private static function readMessage($socket)
×
914
    {
NEW
915
        $header = self::readBytes($socket, 4);
×
NEW
916
        if ($header === null) {
×
NEW
917
            return null;
×
918
        }
919

NEW
920
        $length = unpack('N', $header)[1];
×
NEW
921
        if ($length === 0) {
×
NEW
922
            return null;
×
923
        }
924

NEW
925
        $body = self::readBytes($socket, $length);
×
NEW
926
        if ($body === null) {
×
NEW
927
            return null;
×
928
        }
929

NEW
930
        $value = @unserialize($body);
×
NEW
931
        if ($value === false && $body !== serialize(false)) {
×
NEW
932
            return null;
×
933
        }
934

NEW
935
        return $value;
×
936
    }
937

938

939
    /**
940
     * Read exactly $length bytes from a stream, looping over short reads.
941
     *
942
     * @param resource $socket The socket to read from.
943
     * @param int      $length Number of bytes required.
944
     *
945
     * @return string|null The bytes read, or null on EOF / error.
946
     */
NEW
947
    private static function readBytes($socket, int $length)
×
948
    {
NEW
949
        $buffer = '';
×
NEW
950
        while (strlen($buffer) < $length) {
×
NEW
951
            $chunk = fread($socket, ($length - strlen($buffer)));
×
NEW
952
            if ($chunk === false || $chunk === '') {
×
NEW
953
                return null;
×
954
            }
955

NEW
956
            $buffer .= $chunk;
×
957
        }
958

NEW
959
        return $buffer;
×
960
    }
961

962

963
    /**
964
     * Print progress information for a single processed file.
965
     *
966
     * @param \PHP_CodeSniffer\Files\File $file         The file that was processed.
967
     * @param int                         $numFiles     The total number of files to process.
968
     * @param int                         $numProcessed The number of files that have been processed,
969
     *                                                  including this one.
970
     *
971
     * @return void
972
     */
973
    public function printProgress(File $file, int $numFiles, int $numProcessed)
63✔
974
    {
975
        if (PHP_CODESNIFFER_VERBOSITY > 0
63✔
976
            || $this->config->showProgress === false
63✔
977
        ) {
978
            return;
3✔
979
        }
980

981
        $showColors  = $this->config->colors;
60✔
982
        $colorOpen   = '';
60✔
983
        $progressDot = '.';
60✔
984
        $colorClose  = '';
60✔
985

986
        // Show progress information.
987
        if ($file->ignored === true) {
60✔
988
            $progressDot = 'S';
3✔
989
        } else {
990
            $errors   = $file->getErrorCount();
60✔
991
            $warnings = $file->getWarningCount();
60✔
992
            $fixable  = $file->getFixableCount();
60✔
993
            $fixed    = ($file->getFixedErrorCount() + $file->getFixedWarningCount());
60✔
994

995
            if (PHP_CODESNIFFER_CBF === true) {
60✔
996
                // Files with fixed errors or warnings are F (green).
997
                // Files with unfixable errors or warnings are E (red).
998
                // Files with no errors or warnings are . (black).
999
                if ($fixable > 0) {
18✔
1000
                    $progressDot = 'E';
6✔
1001

1002
                    if ($showColors === true) {
6✔
1003
                        $colorOpen  = "\033[31m";
3✔
1004
                        $colorClose = "\033[0m";
4✔
1005
                    }
1006
                } elseif ($fixed > 0) {
12✔
1007
                    $progressDot = 'F';
6✔
1008

1009
                    if ($showColors === true) {
6✔
1010
                        $colorOpen  = "\033[32m";
3✔
1011
                        $colorClose = "\033[0m";
8✔
1012
                    }
1013
                }
1014
            } else {
1015
                // Files with errors are E (red).
1016
                // Files with fixable errors are E (green).
1017
                // Files with warnings are W (yellow).
1018
                // Files with fixable warnings are W (green).
1019
                // Files with no errors or warnings are . (black).
1020
                if ($errors > 0) {
42✔
1021
                    $progressDot = 'E';
9✔
1022

1023
                    if ($showColors === true) {
9✔
1024
                        if ($fixable > 0) {
6✔
1025
                            $colorOpen = "\033[32m";
3✔
1026
                        } else {
1027
                            $colorOpen = "\033[31m";
3✔
1028
                        }
1029

1030
                        $colorClose = "\033[0m";
7✔
1031
                    }
1032
                } elseif ($warnings > 0) {
33✔
1033
                    $progressDot = 'W';
9✔
1034

1035
                    if ($showColors === true) {
9✔
1036
                        if ($fixable > 0) {
6✔
1037
                            $colorOpen = "\033[32m";
3✔
1038
                        } else {
1039
                            $colorOpen = "\033[33m";
3✔
1040
                        }
1041

1042
                        $colorClose = "\033[0m";
6✔
1043
                    }
1044
                }
1045
            }
1046
        }
1047

1048
        StatusWriter::write($colorOpen . $progressDot . $colorClose, 0, 0);
60✔
1049

1050
        $numPerLine = 60;
60✔
1051
        if ($numProcessed !== $numFiles && ($numProcessed % $numPerLine) !== 0) {
60✔
1052
            return;
60✔
1053
        }
1054

1055
        $percent = round(($numProcessed / $numFiles) * 100);
18✔
1056
        $padding = (strlen($numFiles) - strlen($numProcessed));
18✔
1057
        if ($numProcessed === $numFiles
18✔
1058
            && $numFiles > $numPerLine
18✔
1059
            && ($numProcessed % $numPerLine) !== 0
18✔
1060
        ) {
1061
            $padding += ($numPerLine - ($numFiles - (floor($numFiles / $numPerLine) * $numPerLine)));
9✔
1062
        }
1063

1064
        StatusWriter::write(str_repeat(' ', $padding) . " $numProcessed / $numFiles ($percent%)");
18✔
1065
    }
6✔
1066

1067

1068
    /**
1069
     * Registers a PHP shutdown function to provide a more informative out of memory error.
1070
     *
1071
     * @param string $command The command which was used to initiate the PHPCS run.
1072
     *
1073
     * @return void
1074
     */
1075
    private function registerOutOfMemoryShutdownMessage(string $command)
×
1076
    {
1077
        // Allocate all needed memory beforehand as much as possible.
1078
        $errorMsg    = PHP_EOL . 'The PHP_CodeSniffer "%1$s" command ran out of memory.' . PHP_EOL;
×
1079
        $errorMsg   .= 'Either raise the "memory_limit" of PHP in the php.ini file or raise the memory limit at runtime' . PHP_EOL;
×
1080
        $errorMsg   .= 'using "%1$s -d memory_limit=512M" (replace 512M with the desired memory limit).' . PHP_EOL;
×
1081
        $errorMsg    = sprintf($errorMsg, $command);
×
1082
        $memoryError = 'Allowed memory size of';
×
1083
        $errorArray  = [
1084
            'type'    => 42,
×
1085
            'message' => 'Some random dummy string to take up memory and take up some more memory and some more and more and more and more',
1086
            'file'    => 'Another random string, which would be a filename this time. Should be relatively long to allow for deeply nested files',
1087
            'line'    => 31427,
1088
        ];
1089

1090
        register_shutdown_function(
×
1091
            static function () use (
1092
                $errorMsg,
×
1093
                $memoryError,
×
1094
                $errorArray
×
1095
            ) {
1096
                $errorArray = error_get_last();
×
1097
                if (is_array($errorArray) === true && strpos($errorArray['message'], $memoryError) !== false) {
×
1098
                    fwrite(STDERR, $errorMsg);
×
1099
                }
1100
            }
×
1101
        );
1102
    }
1103
}
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