Skip to content

Fatal error when inadvertently passing the same file twice and using parallel execution #1113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
4 tasks done
rodrigoprimo opened this issue Jun 4, 2025 · 3 comments
Open
4 tasks done

Comments

@rodrigoprimo
Copy link
Contributor

Describe the bug

While testing #1112, I noticed that a fatal error occurs when parallel execution is used and the same file is passed to PHPCS twice.

To reproduce

Steps to reproduce the behavior:

  1. Run bin/phpcs test.php test.php --parallel=2
  2. See error message displayed
PHP Fatal error:  Uncaught PHP_CodeSniffer\Exceptions\RuntimeException: trim(): Passing null to parameter #1 ($string) of type string is deprecated in src/Files/LocalFile.php on line 32 in src/Runner.php:624
Stack trace:
#0 [internal function]: PHP_CodeSniffer\Runner->handleErrors()
#1 src/Files/LocalFile.php(32): trim()
#2 src/Files/FileList.php(197): PHP_CodeSniffer\Files\LocalFile->__construct()
#3 src/Runner.php(504): PHP_CodeSniffer\Files\FileList->current()
#4 src/Runner.php(120): PHP_CodeSniffer\Runner->run()
#5 bin/phpcs(14): PHP_CodeSniffer\Runner->runPHPCS()
#6 {main}
  thrown in src/Runner.php on line 624
EE 2 / 2 (100%)
PHP Fatal error:  Uncaught PHP_CodeSniffer\Exceptions\RuntimeException: One or more child processes failed to run in src/Runner.php:562
Stack trace:
#0 src/Runner.php(120): PHP_CodeSniffer\Runner->run()
#1 bin/phpcs(14): PHP_CodeSniffer\Runner->runPHPCS()
#2 {main}
  thrown in src/Runner.php on line 562

Expected behavior

I would expect PHPCS to check the file only once instead of failing with a fatal error.

Versions (please complete the following information)

Operating System Ubuntu 24.04
PHP version 8.3
PHP_CodeSniffer version master
Standard N/A
Install type git clone

Additional context

I believe the problem is in the FileList class. When the same file is passed twice, it adds it only once to the FileList::$files array, but increments the FileList::$numFiles twice:

foreach ($iterator as $path) {
$this->files[$path] = $file;
$this->numFiles++;
}

This causes problems when the Runner class uses the total number of files to calculate the number of files per batch and which files to run in which batch:

// Batching and forking.
$childProcs = [];
$numPerBatch = ceil($numFiles / $this->config->parallel);
for ($batch = 0; $batch < $this->config->parallel; $batch++) {
$startAt = ($batch * $numPerBatch);
if ($startAt >= $numFiles) {
break;
}
$endAt = ($startAt + $numPerBatch);
if ($endAt > $numFiles) {
$endAt = $numFiles;
}
$childOutFilename = tempnam(sys_get_temp_dir(), 'phpcs-child');
$pid = pcntl_fork();
if ($pid === -1) {
throw new RuntimeException('Failed to create child process');
} else if ($pid !== 0) {
$childProcs[$pid] = $childOutFilename;
} else {
// Move forward to the start of the batch.
$todo->rewind();
for ($i = 0; $i < $startAt; $i++) {
$todo->next();
}
// Reset the reporter to make sure only figures from this
// file batch are recorded.
$this->reporter->totalFiles = 0;
$this->reporter->totalErrors = 0;
$this->reporter->totalWarnings = 0;
$this->reporter->totalFixable = 0;
$this->reporter->totalFixed = 0;
// Process the files.
$pathsProcessed = [];
ob_start();
for ($i = $startAt; $i < $endAt; $i++) {
$path = $todo->key();
$file = $todo->current();
if ($file->ignored === true) {
$todo->next();
continue;
}
$currDir = dirname($path);
if ($lastDir !== $currDir) {
if (PHP_CODESNIFFER_VERBOSITY > 0) {
echo 'Changing into directory '.Common::stripBasepath($currDir, $this->config->basepath).PHP_EOL;
}
$lastDir = $currDir;
}
$this->processFile($file);
$pathsProcessed[] = $path;
$todo->next();
}//end for
$debugOutput = ob_get_contents();
ob_end_clean();
// Write information about the run to the filesystem
// so it can be picked up by the main process.
$childOutput = [
'totalFiles' => $this->reporter->totalFiles,
'totalErrors' => $this->reporter->totalErrors,
'totalWarnings' => $this->reporter->totalWarnings,
'totalFixable' => $this->reporter->totalFixable,
'totalFixed' => $this->reporter->totalFixed,
];
$output = '<'.'?php'."\n".' $childOutput = ';
$output .= var_export($childOutput, true);
$output .= ";\n\$debugOutput = ";
$output .= var_export($debugOutput, true);
if ($this->config->cache === true) {
$childCache = [];
foreach ($pathsProcessed as $path) {
$childCache[$path] = Cache::get($path);
}
$output .= ";\n\$childCache = ";
$output .= var_export($childCache, true);
}
$output .= ";\n?".'>';
file_put_contents($childOutFilename, $output);
exit();
}//end if
}//end for
$success = $this->processChildProcs($childProcs);
if ($success === false) {
throw new RuntimeException('One or more child processes failed to run');
}

Please confirm

  • I have searched the issue list and am not opening a duplicate issue.
  • I have read the Contribution Guidelines and this is not a support question.
  • I confirm that this bug is a bug in PHP_CodeSniffer and not in one of the external standards.
  • I have verified the issue still exists in the master branch of PHP_CodeSniffer.
@jrfnl
Copy link
Member

jrfnl commented Jun 4, 2025

@rodrigoprimo Thanks for finding this. While this is primarily an error in the user input, I do agree that preventing the fatal makes sense and could be as straight-forward as just checking via isset($this->files[$path]) === false before adding the file.

Looking at the FileList class, a similar code snippet is used in multiple places in the class, so this may need a critical look to see which of those need changing and/or whether it makes sense to add a (private) function to prevent duplicate code.

Also seems like something for which it should be quite straight-forward to add a test to the test suite.

@jrfnl
Copy link
Member

jrfnl commented Jun 4, 2025

For the record: this looks to be a duplicate of the following upstream issues, which look to never have been addressed, so will be nice to fix all these issues in one go 😉

Open PR squizlabs/PHP_CodeSniffer#3676 attempts to fix squizlabs/PHP_CodeSniffer#3677, but is not the correct fix.
The analysis by @rodrigoprimo above about where this originates from shows the right solution direction.

@jrfnl
Copy link
Member

jrfnl commented Jun 4, 2025

Brain fart - needs verification of viability - I've now started wondering if the numFiles property should even exist... and if it does need to exist (for performance reasons or something), if it shouldn't be set just once at the end of the __construct() method via a count($this->files) instead of doing the incrementing whenever a new file is being added... ? I'd expect that would solve the issue too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants