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

PHPCSStandards / PHP_CodeSniffer / 25052938176

28 Apr 2026 12:31PM UTC coverage: 78.697% (-0.3%) from 78.969%
25052938176

Pull #1419

github

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

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

1 existing line in 1 file now uncovered.

19875 of 25255 relevant lines covered (78.7%)

98.53 hits per line

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

17.58
/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
            // Also disable parallel processing — STDIN is a single file, and
120
            // the workers can't see the in-memory DummyFile that holds the
121
            // piped content.
122
            if ($this->config->stdin === true) {
×
NEW
123
                $this->config->cache    = false;
×
NEW
124
                $this->config->parallel = 1;
×
125
            }
126

127
            $this->run();
×
128

129
            // Print all the reports for this run.
130
            $this->reporter->printReports();
×
131

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

146
            return $exitCode;
×
147
        }
148

149
        return ExitCode::calculate($this->reporter);
×
150
    }
151

152

153
    /**
154
     * Run the PHPCBF script.
155
     *
156
     * @return int
157
     */
158
    public function runPHPCBF()
×
159
    {
160
        $this->registerOutOfMemoryShutdownMessage('phpcbf');
×
161

162
        if (defined('PHP_CODESNIFFER_CBF') === false) {
×
163
            define('PHP_CODESNIFFER_CBF', true);
×
164
        }
165

166
        try {
167
            Timing::startTiming();
×
168

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

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

180
            // Init the run and load the rulesets to set additional config vars.
181
            $this->init();
×
182

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

190
            // Override some of the command line settings that might break the fixes.
191
            $this->config->generator    = null;
×
192
            $this->config->explain      = false;
×
193
            $this->config->interactive  = false;
×
194
            $this->config->cache        = false;
×
195
            $this->config->showSources  = false;
×
196
            $this->config->recordErrors = false;
×
197
            $this->config->reportFile   = null;
×
198

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

206
            $this->config->reports = $newReports;
×
207

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

213
            $this->run();
×
214
            $this->reporter->printReports();
×
215

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

231
            return $exitCode;
×
232
        }
233

234
        return ExitCode::calculate($this->reporter);
×
235
    }
236

237

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

250
        // Disable the PCRE JIT as this caused issues with parallel running.
251
        ini_set('pcre.jit', false);
×
252

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

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

270
        // Create this class so it is autoloaded and sets up a bunch
271
        // of PHP_CodeSniffer-specific token type constants.
272
        new Tokens();
×
273

274
        // Allow autoloading of custom files inside installed standards.
275
        $installedStandards = Standards::getInstalledStandardDetails();
×
276
        foreach ($installedStandards as $details) {
×
277
            Autoload::addSearchPath($details['path'], $details['namespace']);
×
278
        }
279

280
        // The ruleset contains all the information about how the files
281
        // should be checked and/or fixed.
282
        try {
283
            $this->ruleset = new Ruleset($this->config);
×
284

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

295

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

309
        // Include bootstrap files.
310
        foreach ($this->config->bootstrap as $bootstrap) {
12✔
311
            include $bootstrap;
×
312
        }
313

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

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

333
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
12✔
334
                StatusWriter::write('Creating file list... ', 0, 0);
×
335
            }
336

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

339
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
12✔
340
                $numFiles = count($todo);
×
341
                StatusWriter::write("DONE ($numFiles files in queue)");
×
342
            }
343

344
            if ($this->config->cache === true) {
12✔
345
                if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
346
                    StatusWriter::write('Loading cache... ', 0, 0);
×
347
                }
348

349
                Cache::load($this->ruleset, $this->config);
×
350

351
                if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
352
                    $size = Cache::getSize();
×
353
                    StatusWriter::write("DONE ($size files in cache)");
×
354
                }
355
            }
356
        }
357

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

365
        // Turn all sniff errors into exceptions.
366
        set_error_handler([$this, 'handleErrors']);
×
367

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

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

379
        $lastDir = '';
×
380

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

392
                        $lastDir = $currDir;
×
393
                    }
394

395
                    $this->processFile($file);
×
396
                } elseif (PHP_CODESNIFFER_VERBOSITY > 0) {
×
397
                    StatusWriter::write('Skipping ' . basename($file->path));
×
398
                }
399

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

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

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

NEW
421
            $childProcs = [];
×
NEW
422
            $sockets    = [];
×
423

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

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

NEW
446
                    $this->processWorker($pair[1], $childOutFilename);
×
447
                    exit();
×
448
                }
449
            }
450

NEW
451
            $this->dispatchWork($queue, $sockets, $numFiles, $chunkSize);
×
452

NEW
453
            foreach ($sockets as $socket) {
×
NEW
454
                fclose($socket);
×
455
            }
456

457
            $success = $this->processChildProcs($childProcs);
×
458
            if ($success === false) {
×
459
                throw new RuntimeException('One or more child processes failed to run');
×
460
            }
461
        }
462

463
        restore_error_handler();
×
464

465
        if (PHP_CODESNIFFER_VERBOSITY === 0
×
466
            && $this->config->interactive === false
×
467
            && $this->config->showProgress === true
×
468
        ) {
469
            StatusWriter::writeNewline(2);
×
470
        }
471

472
        if ($this->config->cache === true) {
×
473
            Cache::save();
×
474
        }
475
    }
476

477

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

500
        throw new RuntimeException("$message in $file on line $line");
×
501
    }
502

503

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

521
            StatusWriter::write('Processing ' . basename($file->path) . ' ', 0, $newlines);
×
522
        }
523

524
        try {
525
            $file->process();
×
526

527
            if (PHP_CODESNIFFER_VERBOSITY > 0) {
×
528
                StatusWriter::write('DONE in ' . Timing::getHumanReadableDuration(Timing::getDurationSince($startTime)), 0, 0);
×
529

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

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

551
                if (empty($sniffStack) === false) {
×
552
                    $nextStack = $step;
×
553
                    break;
×
554
                }
555

556
                if (substr($step['file'], -9) === 'Sniff.php') {
×
557
                    $sniffStack = $step;
×
558
                    continue;
×
559
                }
560
            }
561

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

574
                if ($sniffCode === '') {
×
575
                    $sniffCode = substr(strrchr(str_replace('\\', '/', $sniffStack['file']), '/'), 1);
×
576
                }
577

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

581
            $file->addErrorOnLine($error, 1, 'Internal.Exception');
×
582
        }
583

584
        $this->reporter->cacheFileReport($file);
×
585

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

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

601
                $this->reporter->printReport('full');
×
602

603
                echo '<ENTER> to recheck, [s] to skip or [q] to quit : ';
×
604
                $input = fgets(STDIN);
×
605
                $input = trim($input);
×
606

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

626
        // Clean up the file to save (a lot of) memory.
627
        $file->cleanUp();
×
628
    }
629

630

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

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

652
            $childProcessStatus = pcntl_wexitstatus($status);
×
653
            if ($childProcessStatus !== 0) {
×
654
                $success = false;
×
655
            }
656

657
            $out = $childProcs[$pid];
×
658
            unset($childProcs[$pid]);
×
659
            if (file_exists($out) === false) {
×
660
                continue;
×
661
            }
662

663
            include $out;
×
664
            unlink($out);
×
665

666
            if (isset($childOutput) === false) {
×
667
                // The child process died, so the run has failed.
668
                $success = false;
×
669
                continue;
×
670
            }
671

672
            $this->reporter->totalFiles           += $childOutput['totalFiles'];
×
673
            $this->reporter->totalErrors          += $childOutput['totalErrors'];
×
674
            $this->reporter->totalWarnings        += $childOutput['totalWarnings'];
×
675
            $this->reporter->totalFixableErrors   += $childOutput['totalFixableErrors'];
×
676
            $this->reporter->totalFixableWarnings += $childOutput['totalFixableWarnings'];
×
677
            $this->reporter->totalFixedErrors     += $childOutput['totalFixedErrors'];
×
678
            $this->reporter->totalFixedWarnings   += $childOutput['totalFixedWarnings'];
×
679

680
            if (isset($debugOutput) === true) {
×
681
                echo $debugOutput;
×
682
            }
683

684
            if (isset($childCache) === true) {
×
685
                foreach ($childCache as $path => $cache) {
×
686
                    Cache::set($path, $cache);
×
687
                }
688
            }
689
        }
690

NEW
691
        return $success;
×
692
    }
693

694

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

NEW
714
        $pathsProcessed = [];
×
NEW
715
        $progressBuffer = [];
×
NEW
716
        $lastDir        = '';
×
717

NEW
718
        ob_start();
×
719

NEW
720
        while (true) {
×
NEW
721
            self::sendMessage(
×
NEW
722
                $socket,
×
723
                [
NEW
724
                    'type'     => 'ready',
×
NEW
725
                    'progress' => $progressBuffer,
×
726
                ]
727
            );
NEW
728
            $progressBuffer = [];
×
729

NEW
730
            $message = self::readMessage($socket);
×
NEW
731
            if ($message === null || $message['type'] === 'done') {
×
NEW
732
                break;
×
733
            }
734

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

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

NEW
745
                        $lastDir = $currDir;
×
746
                    }
747

NEW
748
                    $this->processFile($file);
×
749
                }
750

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

NEW
763
        $debugOutput = ob_get_contents();
×
NEW
764
        ob_end_clean();
×
765

766
        $childOutput = [
NEW
767
            'totalFiles'           => $this->reporter->totalFiles,
×
NEW
768
            'totalErrors'          => $this->reporter->totalErrors,
×
NEW
769
            'totalWarnings'        => $this->reporter->totalWarnings,
×
NEW
770
            'totalFixableErrors'   => $this->reporter->totalFixableErrors,
×
NEW
771
            'totalFixableWarnings' => $this->reporter->totalFixableWarnings,
×
NEW
772
            'totalFixedErrors'     => $this->reporter->totalFixedErrors,
×
NEW
773
            'totalFixedWarnings'   => $this->reporter->totalFixedWarnings,
×
774
        ];
775

NEW
776
        $output  = '<' . '?php' . "\n" . ' $childOutput = ';
×
NEW
777
        $output .= var_export($childOutput, true);
×
NEW
778
        $output .= ";\n\$debugOutput = ";
×
NEW
779
        $output .= var_export($debugOutput, true);
×
780

NEW
781
        if ($this->config->cache === true) {
×
NEW
782
            $childCache = [];
×
NEW
783
            foreach ($pathsProcessed as $path) {
×
NEW
784
                $childCache[$path] = Cache::get($path);
×
785
            }
786

NEW
787
            $output .= ";\n\$childCache = ";
×
NEW
788
            $output .= var_export($childCache, true);
×
789
        }
790

NEW
791
        $output .= ";\n?" . '>';
×
NEW
792
        file_put_contents($childOutFilename, $output);
×
793
    }
794

795

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

NEW
816
        while (count($active) > 0) {
×
NEW
817
            $read   = array_values($active);
×
NEW
818
            $write  = null;
×
NEW
819
            $except = null;
×
820

NEW
821
            $ready = @stream_select($read, $write, $except, null);
×
NEW
822
            if ($ready === false) {
×
823
                // Interrupted (e.g. SIGCHLD) — just retry.
NEW
824
                continue;
×
825
            }
826

NEW
827
            if ($ready === 0) {
×
NEW
828
                continue;
×
829
            }
830

NEW
831
            foreach ($read as $socket) {
×
NEW
832
                $pid = array_search($socket, $active, true);
×
NEW
833
                if ($pid === false) {
×
NEW
834
                    continue;
×
835
                }
836

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

NEW
844
                foreach ($message['progress'] as $progress) {
×
NEW
845
                    $numProcessed++;
×
NEW
846
                    $file          = new DummyFile('', $this->ruleset, $this->config);
×
NEW
847
                    $file->ignored = $progress['ignored'];
×
848

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

NEW
863
                if ($cursor >= $numFiles) {
×
NEW
864
                    self::sendMessage($socket, ['type' => 'done']);
×
NEW
865
                    unset($active[$pid]);
×
NEW
866
                    continue;
×
867
                }
868

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

882

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

NEW
899
        while ($offset < $total) {
×
NEW
900
            $written = fwrite($socket, substr($buffer, $offset));
×
NEW
901
            if ($written === false || $written === 0) {
×
NEW
902
                return;
×
903
            }
904

NEW
905
            $offset += $written;
×
906
        }
907
    }
908

909

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

NEW
924
        $length = unpack('N', $header)[1];
×
NEW
925
        if ($length === 0) {
×
NEW
926
            return null;
×
927
        }
928

NEW
929
        $body = self::readBytes($socket, $length);
×
NEW
930
        if ($body === null) {
×
NEW
931
            return null;
×
932
        }
933

NEW
934
        $value = @unserialize($body);
×
NEW
935
        if ($value === false && $body !== serialize(false)) {
×
NEW
936
            return null;
×
937
        }
938

NEW
939
        return $value;
×
940
    }
941

942

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

NEW
960
            $buffer .= $chunk;
×
961
        }
962

NEW
963
        return $buffer;
×
964
    }
965

966

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

985
        $showColors  = $this->config->colors;
60✔
986
        $colorOpen   = '';
60✔
987
        $progressDot = '.';
60✔
988
        $colorClose  = '';
60✔
989

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

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

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

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

1027
                    if ($showColors === true) {
9✔
1028
                        if ($fixable > 0) {
6✔
1029
                            $colorOpen = "\033[32m";
3✔
1030
                        } else {
1031
                            $colorOpen = "\033[31m";
3✔
1032
                        }
1033

1034
                        $colorClose = "\033[0m";
7✔
1035
                    }
1036
                } elseif ($warnings > 0) {
33✔
1037
                    $progressDot = 'W';
9✔
1038

1039
                    if ($showColors === true) {
9✔
1040
                        if ($fixable > 0) {
6✔
1041
                            $colorOpen = "\033[32m";
3✔
1042
                        } else {
1043
                            $colorOpen = "\033[33m";
3✔
1044
                        }
1045

1046
                        $colorClose = "\033[0m";
6✔
1047
                    }
1048
                }
1049
            }
1050
        }
1051

1052
        StatusWriter::write($colorOpen . $progressDot . $colorClose, 0, 0);
60✔
1053

1054
        $numPerLine = 60;
60✔
1055
        if ($numProcessed !== $numFiles && ($numProcessed % $numPerLine) !== 0) {
60✔
1056
            return;
60✔
1057
        }
1058

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

1068
        StatusWriter::write(str_repeat(' ', $padding) . " $numProcessed / $numFiles ($percent%)");
18✔
1069
    }
6✔
1070

1071

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

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