Fix bin/publish: copy docs.dist from project root

Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-21 02:08:33 +00:00
commit f6fac6c4bc
79758 changed files with 10547827 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner\Event;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Event that is fired when Fixer starts analysis.
*
* @author Greg Korba <greg@codito.dev>
*
* @internal
*/
final class AnalysisStarted extends Event
{
public const NAME = 'fixer.analysis_started';
public const MODE_SEQUENTIAL = 'sequential';
public const MODE_PARALLEL = 'parallel';
/** @var self::MODE_* */
private string $mode;
private bool $dryRun;
/**
* @param self::MODE_* $mode
*/
public function __construct(string $mode, bool $dryRun)
{
$this->mode = $mode;
$this->dryRun = $dryRun;
}
public function getMode(): string
{
return $this->mode;
}
public function isDryRun(): bool
{
return $this->dryRun;
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner\Event;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Event that is fired when file was processed by Fixer.
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class FileProcessed extends Event
{
/**
* Event name.
*/
public const NAME = 'fixer.file_processed';
public const STATUS_INVALID = 1;
public const STATUS_SKIPPED = 2;
public const STATUS_NO_CHANGES = 3;
public const STATUS_FIXED = 4;
public const STATUS_EXCEPTION = 5;
public const STATUS_LINT = 6;
public const STATUS_NON_MONOLITHIC = 7;
/**
* @var self::STATUS_*
*/
private int $status;
private ?string $fileHash;
/**
* @param self::STATUS_* $status
*/
public function __construct(int $status, ?string $fileHash = null)
{
$this->status = $status;
$this->fileHash = $fileHash;
}
/**
* @return self::STATUS_*
*/
public function getStatus(): int
{
return $this->status;
}
public function getFileHash(): ?string
{
return $this->fileHash;
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner;
use PhpCsFixer\Linter\LinterInterface;
use PhpCsFixer\Linter\LintingResultInterface;
/**
* @internal
*
* @extends \CachingIterator<mixed, \SplFileInfo, \Iterator<mixed, \SplFileInfo>>
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class FileCachingLintingFileIterator extends \CachingIterator implements LintingResultAwareFileIteratorInterface
{
private LinterInterface $linter;
private ?LintingResultInterface $currentResult = null;
private ?LintingResultInterface $nextResult = null;
/**
* @param \Iterator<mixed, \SplFileInfo> $iterator
*/
public function __construct(\Iterator $iterator, LinterInterface $linter)
{
parent::__construct($iterator);
$this->linter = $linter;
}
public function currentLintingResult(): ?LintingResultInterface
{
return $this->currentResult;
}
public function next(): void
{
parent::next();
$this->currentResult = $this->nextResult;
if ($this->hasNext()) {
$this->nextResult = $this->handleItem($this->getInnerIterator()->current());
}
}
public function rewind(): void
{
parent::rewind();
if ($this->valid()) {
$this->currentResult = $this->handleItem($this->current());
}
if ($this->hasNext()) {
$this->nextResult = $this->handleItem($this->getInnerIterator()->current());
}
}
private function handleItem(\SplFileInfo $file): LintingResultInterface
{
return $this->linter->lintFile($file->getRealPath());
}
}

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner;
use PhpCsFixer\Cache\CacheManagerInterface;
use PhpCsFixer\FileReader;
use PhpCsFixer\Runner\Event\FileProcessed;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\EventDispatcher\Event;
/**
* @internal
*
* @extends \FilterIterator<mixed, \SplFileInfo, \Iterator<mixed, \SplFileInfo>>
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class FileFilterIterator extends \FilterIterator
{
private ?EventDispatcherInterface $eventDispatcher;
private CacheManagerInterface $cacheManager;
/**
* @var array<string, bool>
*/
private array $visitedElements = [];
/**
* @param \Traversable<\SplFileInfo> $iterator
*/
public function __construct(
\Traversable $iterator,
?EventDispatcherInterface $eventDispatcher,
CacheManagerInterface $cacheManager
) {
if (!$iterator instanceof \Iterator) {
$iterator = new \IteratorIterator($iterator);
}
parent::__construct($iterator);
$this->eventDispatcher = $eventDispatcher;
$this->cacheManager = $cacheManager;
}
public function accept(): bool
{
$file = $this->current();
if (!$file instanceof \SplFileInfo) {
throw new \RuntimeException(
\sprintf(
'Expected instance of "\SplFileInfo", got "%s".',
get_debug_type($file)
)
);
}
$path = $file->isLink() ? $file->getPathname() : $file->getRealPath();
if (isset($this->visitedElements[$path])) {
return false;
}
$this->visitedElements[$path] = true;
if (!$file->isFile() || $file->isLink()) {
return false;
}
$content = FileReader::createSingleton()->read($path);
// mark as skipped:
if (
// empty file
'' === $content
// file that does not need fixing due to cache
|| !$this->cacheManager->needFixing($file->getPathname(), $content)
) {
$this->dispatchEvent(FileProcessed::NAME, new FileProcessed(FileProcessed::STATUS_SKIPPED));
return false;
}
return true;
}
private function dispatchEvent(string $name, Event $event): void
{
if (null === $this->eventDispatcher) {
return;
}
$this->eventDispatcher->dispatch($event, $name);
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner;
use PhpCsFixer\Linter\LinterInterface;
use PhpCsFixer\Linter\LintingResultInterface;
/**
* @internal
*
* @extends \IteratorIterator<mixed, \SplFileInfo, \Traversable<\SplFileInfo>>
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class LintingFileIterator extends \IteratorIterator implements LintingResultAwareFileIteratorInterface
{
private ?LintingResultInterface $currentResult = null;
private LinterInterface $linter;
/**
* @param \Iterator<mixed, \SplFileInfo> $iterator
*/
public function __construct(\Iterator $iterator, LinterInterface $linter)
{
parent::__construct($iterator);
$this->linter = $linter;
}
public function currentLintingResult(): ?LintingResultInterface
{
return $this->currentResult;
}
public function next(): void
{
parent::next();
$this->currentResult = $this->valid() ? $this->handleItem($this->current()) : null;
}
public function rewind(): void
{
parent::rewind();
$this->currentResult = $this->valid() ? $this->handleItem($this->current()) : null;
}
private function handleItem(\SplFileInfo $file): LintingResultInterface
{
return $this->linter->lintFile($file->getRealPath());
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner;
use PhpCsFixer\Linter\LintingResultInterface;
/**
* @internal
*
* @extends \Iterator<mixed, \SplFileInfo>
*
* @author Greg Korba <greg@codito.dev>
*/
interface LintingResultAwareFileIteratorInterface extends \Iterator
{
public function currentLintingResult(): ?LintingResultInterface;
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner\Parallel;
/**
* @author Greg Korba <greg@codito.dev>
*
* @internal
*/
final class ParallelAction
{
// Actions executed by the runner (main process)
public const RUNNER_REQUEST_ANALYSIS = 'requestAnalysis';
public const RUNNER_THANK_YOU = 'thankYou';
// Actions executed by the worker
public const WORKER_ERROR_REPORT = 'errorReport';
public const WORKER_GET_FILE_CHUNK = 'getFileChunk';
public const WORKER_HELLO = 'hello';
public const WORKER_RESULT = 'result';
private function __construct() {}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner\Parallel;
/**
* @author Greg Korba <greg@codito.dev>
*
* @readonly
*/
final class ParallelConfig
{
/** @internal */
public const DEFAULT_FILES_PER_PROCESS = 10;
/** @internal */
public const DEFAULT_PROCESS_TIMEOUT = 120;
private int $filesPerProcess;
private int $maxProcesses;
private int $processTimeout;
/**
* @param positive-int $maxProcesses
* @param positive-int $filesPerProcess
* @param positive-int $processTimeout
*/
public function __construct(
int $maxProcesses = 2,
int $filesPerProcess = self::DEFAULT_FILES_PER_PROCESS,
int $processTimeout = self::DEFAULT_PROCESS_TIMEOUT
) {
if ($maxProcesses <= 0 || $filesPerProcess <= 0 || $processTimeout <= 0) {
throw new \InvalidArgumentException('Invalid parallelisation configuration: only positive integers are allowed');
}
$this->maxProcesses = $maxProcesses;
$this->filesPerProcess = $filesPerProcess;
$this->processTimeout = $processTimeout;
}
public function getFilesPerProcess(): int
{
return $this->filesPerProcess;
}
public function getMaxProcesses(): int
{
return $this->maxProcesses;
}
public function getProcessTimeout(): int
{
return $this->processTimeout;
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner\Parallel;
use Fidry\CpuCoreCounter\CpuCoreCounter;
use Fidry\CpuCoreCounter\Finder\DummyCpuCoreFinder;
use Fidry\CpuCoreCounter\Finder\FinderRegistry;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class ParallelConfigFactory
{
private static ?CpuCoreCounter $cpuDetector = null;
private function __construct() {}
public static function sequential(): ParallelConfig
{
return new ParallelConfig(1);
}
/**
* @param null|positive-int $filesPerProcess
* @param null|positive-int $processTimeout
* @param null|positive-int $maxProcesses
*/
public static function detect(
?int $filesPerProcess = null,
?int $processTimeout = null,
?int $maxProcesses = null
): ParallelConfig {
if (null === self::$cpuDetector) {
self::$cpuDetector = new CpuCoreCounter([
...FinderRegistry::getDefaultLogicalFinders(),
new DummyCpuCoreFinder(1),
]);
}
// Reserve 1 core for the main orchestrating process
$available = self::$cpuDetector->getAvailableForParallelisation(1, $maxProcesses);
return new ParallelConfig(
$available->availableCpus,
$filesPerProcess ?? ParallelConfig::DEFAULT_FILES_PER_PROCESS,
$processTimeout ?? ParallelConfig::DEFAULT_PROCESS_TIMEOUT
);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner\Parallel;
/**
* Common exception for all the errors related to parallelisation.
*
* @author Greg Korba <greg@codito.dev>
*
* @internal
*/
final class ParallelisationException extends \RuntimeException
{
public static function forUnknownIdentifier(ProcessIdentifier $identifier): self
{
return new self('Unknown process identifier: '.$identifier->toString());
}
}

View File

@@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner\Parallel;
use React\ChildProcess\Process as ReactProcess;
use React\EventLoop\LoopInterface;
use React\EventLoop\TimerInterface;
use React\Stream\ReadableStreamInterface;
use React\Stream\WritableStreamInterface;
/**
* Represents single process that is handled within parallel run.
* Inspired by:
* - https://github.com/phpstan/phpstan-src/blob/9ce425bca5337039fb52c0acf96a20a2b8ace490/src/Parallel/Process.php
* - https://github.com/phpstan/phpstan-src/blob/1477e752b4b5893f323b6d2c43591e68b3d85003/src/Process/ProcessHelper.php.
*
* @author Greg Korba <greg@codito.dev>
*
* @internal
*/
final class Process
{
// Properties required for process instantiation
private string $command;
private LoopInterface $loop;
private int $timeoutSeconds;
// Properties required for process execution
private ?ReactProcess $process = null;
private ?WritableStreamInterface $in = null;
/** @var resource */
private $stdErr;
/** @var resource */
private $stdOut;
/** @var callable(array<array-key, mixed>): void */
private $onData;
/** @var callable(\Throwable): void */
private $onError;
private ?TimerInterface $timer = null;
public function __construct(string $command, LoopInterface $loop, int $timeoutSeconds)
{
$this->command = $command;
$this->loop = $loop;
$this->timeoutSeconds = $timeoutSeconds;
}
/**
* @param callable(array<array-key, mixed> $json): void $onData callback to be called when data is received from the parallelisation operator
* @param callable(\Throwable $exception): void $onError callback to be called when an exception occurs
* @param callable(?int $exitCode, string $output): void $onExit callback to be called when the process exits
*/
public function start(callable $onData, callable $onError, callable $onExit): void
{
$stdOut = tmpfile();
if (false === $stdOut) {
throw new ParallelisationException('Failed creating temp file for stdOut.');
}
$this->stdOut = $stdOut;
$stdErr = tmpfile();
if (false === $stdErr) {
throw new ParallelisationException('Failed creating temp file for stdErr.');
}
$this->stdErr = $stdErr;
$this->onData = $onData;
$this->onError = $onError;
$this->process = new ReactProcess($this->command, null, null, [
1 => $this->stdOut,
2 => $this->stdErr,
]);
$this->process->start($this->loop);
$this->process->on('exit', function ($exitCode) use ($onExit): void {
$this->cancelTimer();
$output = '';
rewind($this->stdOut);
$stdOut = stream_get_contents($this->stdOut);
if (\is_string($stdOut)) {
$output .= $stdOut;
}
rewind($this->stdErr);
$stdErr = stream_get_contents($this->stdErr);
if (\is_string($stdErr)) {
$output .= $stdErr;
}
$onExit($exitCode, $output);
fclose($this->stdOut);
fclose($this->stdErr);
});
}
/**
* Handles requests from parallelisation operator to its worker (spawned process).
*
* @param array<array-key, mixed> $data
*/
public function request(array $data): void
{
$this->cancelTimer(); // Configured process timeout actually means "chunk timeout" (each request resets timer)
if (null === $this->in) {
throw new ParallelisationException(
'Process not connected with parallelisation operator, ensure `bindConnection()` was called'
);
}
$this->in->write($data);
$this->timer = $this->loop->addTimer($this->timeoutSeconds, function (): void {
($this->onError)(
new \Exception(
\sprintf(
'Child process timed out after %d seconds. Try making it longer using `ParallelConfig`.',
$this->timeoutSeconds
)
)
);
});
}
public function quit(): void
{
$this->cancelTimer();
if (null === $this->process || !$this->process->isRunning()) {
return;
}
foreach ($this->process->pipes as $pipe) {
$pipe->close();
}
if (null === $this->in) {
return;
}
$this->in->end();
}
public function bindConnection(ReadableStreamInterface $out, WritableStreamInterface $in): void
{
$this->in = $in;
$in->on('error', function (\Throwable $error): void {
($this->onError)($error);
});
$out->on('data', function (array $json): void {
$this->cancelTimer();
// Pass everything to the parallelisation operator, it should decide how to handle the data
($this->onData)($json);
});
$out->on('error', function (\Throwable $error): void {
($this->onError)($error);
});
}
private function cancelTimer(): void
{
if (null === $this->timer) {
return;
}
$this->loop->cancelTimer($this->timer);
$this->timer = null;
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner\Parallel;
use PhpCsFixer\Runner\RunnerConfig;
use React\EventLoop\LoopInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Process\PhpExecutableFinder;
/**
* @author Greg Korba <greg@codito.dev>
*
* @readonly
*
* @internal
*/
final class ProcessFactory
{
private InputInterface $input;
public function __construct(InputInterface $input)
{
$this->input = $input;
}
public function create(
LoopInterface $loop,
RunnerConfig $runnerConfig,
ProcessIdentifier $identifier,
int $serverPort
): Process {
$commandArgs = $this->getCommandArgs($serverPort, $identifier, $runnerConfig);
return new Process(
implode(' ', $commandArgs),
$loop,
$runnerConfig->getParallelConfig()->getProcessTimeout()
);
}
/**
* @private
*
* @return list<string>
*/
public function getCommandArgs(int $serverPort, ProcessIdentifier $identifier, RunnerConfig $runnerConfig): array
{
$phpBinary = (new PhpExecutableFinder())->find(false);
if (false === $phpBinary) {
throw new ParallelisationException('Cannot find PHP executable.');
}
$mainScript = realpath(__DIR__.'/../../../php-cs-fixer');
if (false === $mainScript
&& isset($_SERVER['argv'][0])
&& str_contains($_SERVER['argv'][0], 'php-cs-fixer')
) {
$mainScript = $_SERVER['argv'][0];
}
if (!is_file($mainScript)) {
throw new ParallelisationException('Cannot determine Fixer executable.');
}
$commandArgs = [
escapeshellarg($phpBinary),
escapeshellarg($mainScript),
'worker',
'--port',
(string) $serverPort,
'--identifier',
escapeshellarg($identifier->toString()),
];
if ($runnerConfig->isDryRun()) {
$commandArgs[] = '--dry-run';
}
if (filter_var($this->input->getOption('diff'), \FILTER_VALIDATE_BOOLEAN)) {
$commandArgs[] = '--diff';
}
if (filter_var($this->input->getOption('stop-on-violation'), \FILTER_VALIDATE_BOOLEAN)) {
$commandArgs[] = '--stop-on-violation';
}
foreach (['allow-risky', 'config', 'rules', 'using-cache', 'cache-file'] as $option) {
$optionValue = $this->input->getOption($option);
if (null !== $optionValue) {
$commandArgs[] = "--{$option}";
$commandArgs[] = escapeshellarg($optionValue);
}
}
return $commandArgs;
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner\Parallel;
/**
* Represents identifier of single process that is handled within parallel run.
*
* @author Greg Korba <greg@codito.dev>
*
* @readonly
*
* @internal
*/
final class ProcessIdentifier
{
private const IDENTIFIER_PREFIX = 'php-cs-fixer_parallel_';
private string $identifier;
private function __construct(string $identifier)
{
$this->identifier = $identifier;
}
public function toString(): string
{
return $this->identifier;
}
public static function create(): self
{
return new self(uniqid(self::IDENTIFIER_PREFIX, true));
}
public static function fromRaw(string $identifier): self
{
if (!str_starts_with($identifier, self::IDENTIFIER_PREFIX)) {
throw new ParallelisationException(\sprintf('Invalid process identifier "%s".', $identifier));
}
return new self($identifier);
}
}

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner\Parallel;
use React\Socket\ServerInterface;
/**
* Represents collection of active processes that are being run in parallel.
* Inspired by {@see https://github.com/phpstan/phpstan-src/blob/ed68345a82992775112acc2c2bd639d1bd3a1a02/src/Parallel/ProcessPool.php}.
*
* @author Greg Korba <greg@codito.dev>
*
* @internal
*/
final class ProcessPool
{
/**
* @readonly
*/
private ServerInterface $server;
/**
* @var null|(callable(): void)
*
* @readonly
*/
private $onServerClose;
/**
* @var array<string, Process>
*/
private array $processes = [];
/**
* @param null|(callable(): void) $onServerClose
*/
public function __construct(ServerInterface $server, ?callable $onServerClose = null)
{
$this->server = $server;
$this->onServerClose = $onServerClose;
}
public function getProcess(ProcessIdentifier $identifier): Process
{
if (!isset($this->processes[$identifier->toString()])) {
throw ParallelisationException::forUnknownIdentifier($identifier);
}
return $this->processes[$identifier->toString()];
}
public function addProcess(ProcessIdentifier $identifier, Process $process): void
{
$this->processes[$identifier->toString()] = $process;
}
public function endProcessIfKnown(ProcessIdentifier $identifier): void
{
if (!isset($this->processes[$identifier->toString()])) {
return;
}
$this->endProcess($identifier);
}
public function endAll(): void
{
foreach ($this->processes as $identifier => $process) {
$this->endProcessIfKnown(ProcessIdentifier::fromRaw($identifier));
}
}
private function endProcess(ProcessIdentifier $identifier): void
{
$this->getProcess($identifier)->quit();
unset($this->processes[$identifier->toString()]);
if (0 === \count($this->processes)) {
$this->server->close();
if (null !== $this->onServerClose) {
($this->onServerClose)();
}
}
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner\Parallel;
/**
* @author Greg Korba <gre@codito.dev>
*
* @internal
*/
final class WorkerException extends \RuntimeException
{
private string $originalTraceAsString;
private function __construct(string $message, int $code)
{
parent::__construct($message, $code);
}
/**
* @param array{
* class: class-string<\Throwable>,
* message: string,
* file: string,
* line: int,
* code: int,
* trace: string
* } $data
*/
public static function fromRaw(array $data): self
{
$exception = new self(
\sprintf('[%s] %s', $data['class'], $data['message']),
$data['code']
);
$exception->file = $data['file'];
$exception->line = $data['line'];
$exception->originalTraceAsString = \sprintf(
'## %s(%d)%s%s',
$data['file'],
$data['line'],
\PHP_EOL,
$data['trace']
);
return $exception;
}
public function getOriginalTraceAsString(): string
{
return $this->originalTraceAsString;
}
}

View File

@@ -0,0 +1,601 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner;
use Clue\React\NDJson\Decoder;
use Clue\React\NDJson\Encoder;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Cache\CacheManagerInterface;
use PhpCsFixer\Cache\Directory;
use PhpCsFixer\Cache\DirectoryInterface;
use PhpCsFixer\Console\Command\WorkerCommand;
use PhpCsFixer\Differ\DifferInterface;
use PhpCsFixer\Error\Error;
use PhpCsFixer\Error\ErrorsManager;
use PhpCsFixer\Error\SourceExceptionFactory;
use PhpCsFixer\FileReader;
use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\Linter\LinterInterface;
use PhpCsFixer\Linter\LintingException;
use PhpCsFixer\Linter\LintingResultInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Runner\Event\AnalysisStarted;
use PhpCsFixer\Runner\Event\FileProcessed;
use PhpCsFixer\Runner\Parallel\ParallelAction;
use PhpCsFixer\Runner\Parallel\ParallelConfig;
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
use PhpCsFixer\Runner\Parallel\ParallelisationException;
use PhpCsFixer\Runner\Parallel\ProcessFactory;
use PhpCsFixer\Runner\Parallel\ProcessIdentifier;
use PhpCsFixer\Runner\Parallel\ProcessPool;
use PhpCsFixer\Runner\Parallel\WorkerException;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Utils;
use React\EventLoop\StreamSelectLoop;
use React\Socket\ConnectionInterface;
use React\Socket\TcpServer;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Contracts\EventDispatcher\Event;
/**
* @phpstan-type _RunResult array<string, array{appliedFixers: list<string>, diff: string}>
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
* @author Greg Korba <greg@codito.dev>
*/
final class Runner
{
/**
* Buffer size used in the NDJSON decoder for communication between main process and workers.
*
* @see https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/pull/8068
*/
private const PARALLEL_BUFFER_SIZE = 16 * (1_024 * 1_024 /* 1MB */);
private DifferInterface $differ;
private DirectoryInterface $directory;
private ?EventDispatcherInterface $eventDispatcher;
private ErrorsManager $errorsManager;
private CacheManagerInterface $cacheManager;
private bool $isDryRun;
private LinterInterface $linter;
/**
* @var null|\Traversable<array-key, \SplFileInfo>
*/
private ?\Traversable $fileIterator = null;
private int $fileCount;
/**
* @var list<FixerInterface>
*/
private array $fixers;
private bool $stopOnViolation;
private ParallelConfig $parallelConfig;
private ?InputInterface $input;
private ?string $configFile;
/**
* @param null|\Traversable<array-key, \SplFileInfo> $fileIterator
* @param list<FixerInterface> $fixers
*/
public function __construct(
?\Traversable $fileIterator,
array $fixers,
DifferInterface $differ,
?EventDispatcherInterface $eventDispatcher,
ErrorsManager $errorsManager,
LinterInterface $linter,
bool $isDryRun,
CacheManagerInterface $cacheManager,
?DirectoryInterface $directory = null,
bool $stopOnViolation = false,
// @TODO Make these arguments required in 4.0
?ParallelConfig $parallelConfig = null,
?InputInterface $input = null,
?string $configFile = null
) {
// Required only for main process (calculating workers count)
$this->fileCount = null !== $fileIterator ? \count(iterator_to_array($fileIterator)) : 0;
$this->fileIterator = $fileIterator;
$this->fixers = $fixers;
$this->differ = $differ;
$this->eventDispatcher = $eventDispatcher;
$this->errorsManager = $errorsManager;
$this->linter = $linter;
$this->isDryRun = $isDryRun;
$this->cacheManager = $cacheManager;
$this->directory = $directory ?? new Directory('');
$this->stopOnViolation = $stopOnViolation;
$this->parallelConfig = $parallelConfig ?? ParallelConfigFactory::sequential();
$this->input = $input;
$this->configFile = $configFile;
}
/**
* @TODO consider to drop this method and make iterator parameter obligatory in constructor,
* more in https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/pull/7777/files#r1590447581
*
* @param \Traversable<array-key, \SplFileInfo> $fileIterator
*/
public function setFileIterator(iterable $fileIterator): void
{
$this->fileIterator = $fileIterator;
// Required only for main process (calculating workers count)
$this->fileCount = \count(iterator_to_array($fileIterator));
}
/**
* @return _RunResult
*/
public function fix(): array
{
if (0 === $this->fileCount) {
return [];
}
// @TODO 4.0: Remove condition and its body, as no longer needed when param will be required in the constructor.
// This is a fallback only in case someone calls `new Runner()` in a custom repo and does not provide v4-ready params in v3-codebase.
if (null === $this->input) {
return $this->fixSequential();
}
if (
1 === $this->parallelConfig->getMaxProcesses()
|| $this->fileCount <= $this->parallelConfig->getFilesPerProcess()
) {
return $this->fixSequential();
}
return $this->fixParallel();
}
/**
* Heavily inspired by {@see https://github.com/phpstan/phpstan-src/blob/9ce425bca5337039fb52c0acf96a20a2b8ace490/src/Parallel/ParallelAnalyser.php}.
*
* @return _RunResult
*/
private function fixParallel(): array
{
$this->dispatchEvent(AnalysisStarted::NAME, new AnalysisStarted(AnalysisStarted::MODE_PARALLEL, $this->isDryRun));
$changed = [];
$streamSelectLoop = new StreamSelectLoop();
$server = new TcpServer('127.0.0.1:0', $streamSelectLoop);
$serverPort = parse_url($server->getAddress() ?? '', \PHP_URL_PORT);
if (!is_numeric($serverPort)) {
throw new ParallelisationException(\sprintf(
'Unable to parse server port from "%s"',
$server->getAddress() ?? ''
));
}
$processPool = new ProcessPool($server);
$maxFilesPerProcess = $this->parallelConfig->getFilesPerProcess();
$fileIterator = $this->getFilteringFileIterator();
$fileIterator->rewind();
$getFileChunk = static function () use ($fileIterator, $maxFilesPerProcess): array {
$files = [];
while (\count($files) < $maxFilesPerProcess) {
$current = $fileIterator->current();
if (null === $current) {
break;
}
$files[] = $current->getPathname();
$fileIterator->next();
}
return $files;
};
// [REACT] Handle worker's handshake (init connection)
$server->on('connection', static function (ConnectionInterface $connection) use ($processPool, $getFileChunk): void {
$decoder = new Decoder(
$connection,
true,
512,
\JSON_INVALID_UTF8_IGNORE,
self::PARALLEL_BUFFER_SIZE
);
$encoder = new Encoder($connection, \JSON_INVALID_UTF8_IGNORE);
// [REACT] Bind connection when worker's process requests "hello" action (enables 2-way communication)
$decoder->on('data', static function (array $data) use ($processPool, $getFileChunk, $decoder, $encoder): void {
if (ParallelAction::WORKER_HELLO !== $data['action']) {
return;
}
$identifier = ProcessIdentifier::fromRaw($data['identifier']);
$process = $processPool->getProcess($identifier);
$process->bindConnection($decoder, $encoder);
$fileChunk = $getFileChunk();
if (0 === \count($fileChunk)) {
$process->request(['action' => ParallelAction::RUNNER_THANK_YOU]);
$processPool->endProcessIfKnown($identifier);
return;
}
$process->request(['action' => ParallelAction::RUNNER_REQUEST_ANALYSIS, 'files' => $fileChunk]);
});
});
$processesToSpawn = min(
$this->parallelConfig->getMaxProcesses(),
max(
1,
(int) ceil($this->fileCount / $this->parallelConfig->getFilesPerProcess()),
)
);
$processFactory = new ProcessFactory($this->input);
for ($i = 0; $i < $processesToSpawn; ++$i) {
$identifier = ProcessIdentifier::create();
$process = $processFactory->create(
$streamSelectLoop,
new RunnerConfig(
$this->isDryRun,
$this->stopOnViolation,
$this->parallelConfig,
$this->configFile
),
$identifier,
$serverPort,
);
$processPool->addProcess($identifier, $process);
$process->start(
// [REACT] Handle workers' responses (multiple actions possible)
function (array $workerResponse) use ($processPool, $process, $identifier, $getFileChunk, &$changed): void {
// File analysis result (we want close-to-realtime progress with frequent cache savings)
if (ParallelAction::WORKER_RESULT === $workerResponse['action']) {
// Dispatch an event for each file processed and dispatch its status (required for progress output)
$this->dispatchEvent(FileProcessed::NAME, new FileProcessed($workerResponse['status']));
if (isset($workerResponse['fileHash'])) {
$this->cacheManager->setFileHash($workerResponse['file'], $workerResponse['fileHash']);
}
foreach ($workerResponse['errors'] ?? [] as $error) {
$this->errorsManager->report(new Error(
$error['type'],
$error['filePath'],
null !== $error['source']
? SourceExceptionFactory::fromArray($error['source'])
: null,
$error['appliedFixers'],
$error['diff']
));
}
// Pass-back information about applied changes (only if there are any)
if (isset($workerResponse['fixInfo'])) {
$relativePath = $this->directory->getRelativePathTo($workerResponse['file']);
$changed[$relativePath] = $workerResponse['fixInfo'];
if ($this->stopOnViolation) {
$processPool->endAll();
return;
}
}
return;
}
if (ParallelAction::WORKER_GET_FILE_CHUNK === $workerResponse['action']) {
// Request another chunk of files, if still available
$fileChunk = $getFileChunk();
if (0 === \count($fileChunk)) {
$process->request(['action' => ParallelAction::RUNNER_THANK_YOU]);
$processPool->endProcessIfKnown($identifier);
return;
}
$process->request(['action' => ParallelAction::RUNNER_REQUEST_ANALYSIS, 'files' => $fileChunk]);
return;
}
if (ParallelAction::WORKER_ERROR_REPORT === $workerResponse['action']) {
throw WorkerException::fromRaw($workerResponse); // @phpstan-ignore-line
}
throw new ParallelisationException('Unsupported action: '.($workerResponse['action'] ?? 'n/a'));
},
// [REACT] Handle errors encountered during worker's execution
static function (\Throwable $error) use ($processPool): void {
$processPool->endAll();
throw new ParallelisationException($error->getMessage(), $error->getCode(), $error);
},
// [REACT] Handle worker's shutdown
static function ($exitCode, string $output) use ($processPool, $identifier): void {
$processPool->endProcessIfKnown($identifier);
if (0 === $exitCode || null === $exitCode) {
return;
}
$errorsReported = Preg::matchAll(
\sprintf('/^(?:%s)([^\n]+)+/m', WorkerCommand::ERROR_PREFIX),
$output,
$matches
);
if ($errorsReported > 0) {
throw WorkerException::fromRaw(json_decode($matches[1][0], true));
}
}
);
}
$streamSelectLoop->run();
return $changed;
}
/**
* @return _RunResult
*/
private function fixSequential(): array
{
$this->dispatchEvent(AnalysisStarted::NAME, new AnalysisStarted(AnalysisStarted::MODE_SEQUENTIAL, $this->isDryRun));
$changed = [];
$collection = $this->getLintingFileIterator();
foreach ($collection as $file) {
$fixInfo = $this->fixFile($file, $collection->currentLintingResult());
// we do not need Tokens to still caching just fixed file - so clear the cache
Tokens::clearCache();
if (null !== $fixInfo) {
$relativePath = $this->directory->getRelativePathTo($file->__toString());
$changed[$relativePath] = $fixInfo;
if ($this->stopOnViolation) {
break;
}
}
}
return $changed;
}
/**
* @return null|array{appliedFixers: list<string>, diff: string}
*/
private function fixFile(\SplFileInfo $file, LintingResultInterface $lintingResult): ?array
{
$filePathname = $file->getPathname();
try {
$lintingResult->check();
} catch (LintingException $e) {
$this->dispatchEvent(
FileProcessed::NAME,
new FileProcessed(FileProcessed::STATUS_INVALID)
);
$this->errorsManager->report(new Error(Error::TYPE_INVALID, $filePathname, $e));
return null;
}
$old = FileReader::createSingleton()->read($file->getRealPath());
$tokens = Tokens::fromCode($old);
if (
Utils::isFutureModeEnabled() // @TODO 4.0 drop this line
&& !filter_var(getenv('PHP_CS_FIXER_NON_MONOLITHIC'), \FILTER_VALIDATE_BOOL)
&& !$tokens->isMonolithicPhp()
) {
$this->dispatchEvent(
FileProcessed::NAME,
new FileProcessed(FileProcessed::STATUS_NON_MONOLITHIC)
);
return null;
}
$oldHash = $tokens->getCodeHash();
$new = $old;
$newHash = $oldHash;
$appliedFixers = [];
try {
foreach ($this->fixers as $fixer) {
// for custom fixers we don't know is it safe to run `->fix()` without checking `->supports()` and `->isCandidate()`,
// thus we need to check it and conditionally skip fixing
if (
!$fixer instanceof AbstractFixer
&& (!$fixer->supports($file) || !$fixer->isCandidate($tokens))
) {
continue;
}
$fixer->fix($file, $tokens);
if ($tokens->isChanged()) {
$tokens->clearEmptyTokens();
$tokens->clearChanged();
$appliedFixers[] = $fixer->getName();
}
}
} catch (\ParseError $e) {
$this->dispatchEvent(FileProcessed::NAME, new FileProcessed(FileProcessed::STATUS_LINT));
$this->errorsManager->report(new Error(Error::TYPE_LINT, $filePathname, $e));
return null;
} catch (\Throwable $e) {
$this->processException($filePathname, $e);
return null;
}
$fixInfo = null;
if ([] !== $appliedFixers) {
$new = $tokens->generateCode();
$newHash = $tokens->getCodeHash();
}
// We need to check if content was changed and then applied changes.
// But we can't simply check $appliedFixers, because one fixer may revert
// work of other and both of them will mark collection as changed.
// Therefore we need to check if code hashes changed.
if ($oldHash !== $newHash) {
$fixInfo = [
'appliedFixers' => $appliedFixers,
'diff' => $this->differ->diff($old, $new, $file),
];
try {
$this->linter->lintSource($new)->check();
} catch (LintingException $e) {
$this->dispatchEvent(FileProcessed::NAME, new FileProcessed(FileProcessed::STATUS_LINT));
$this->errorsManager->report(new Error(Error::TYPE_LINT, $filePathname, $e, $fixInfo['appliedFixers'], $fixInfo['diff']));
return null;
}
if (!$this->isDryRun) {
$fileRealPath = $file->getRealPath();
if (!file_exists($fileRealPath)) {
throw new IOException(
\sprintf('Failed to write file "%s" (no longer) exists.', $file->getPathname()),
0,
null,
$file->getPathname()
);
}
if (is_dir($fileRealPath)) {
throw new IOException(
\sprintf('Cannot write file "%s" as the location exists as directory.', $fileRealPath),
0,
null,
$fileRealPath
);
}
if (!is_writable($fileRealPath)) {
throw new IOException(
\sprintf('Cannot write to file "%s" as it is not writable.', $fileRealPath),
0,
null,
$fileRealPath
);
}
if (false === @file_put_contents($fileRealPath, $new)) {
$error = error_get_last();
throw new IOException(
\sprintf('Failed to write file "%s", "%s".', $fileRealPath, null !== $error ? $error['message'] : 'no reason available'),
0,
null,
$fileRealPath
);
}
}
}
$this->cacheManager->setFileHash($filePathname, $newHash);
$this->dispatchEvent(
FileProcessed::NAME,
new FileProcessed(null !== $fixInfo ? FileProcessed::STATUS_FIXED : FileProcessed::STATUS_NO_CHANGES, $newHash)
);
return $fixInfo;
}
/**
* Process an exception that occurred.
*/
private function processException(string $name, \Throwable $e): void
{
$this->dispatchEvent(FileProcessed::NAME, new FileProcessed(FileProcessed::STATUS_EXCEPTION));
$this->errorsManager->report(new Error(Error::TYPE_EXCEPTION, $name, $e));
}
private function dispatchEvent(string $name, Event $event): void
{
if (null === $this->eventDispatcher) {
return;
}
$this->eventDispatcher->dispatch($event, $name);
}
private function getLintingFileIterator(): LintingResultAwareFileIteratorInterface
{
$fileFilterIterator = $this->getFilteringFileIterator();
return $this->linter->isAsync()
? new FileCachingLintingFileIterator($fileFilterIterator, $this->linter)
: new LintingFileIterator($fileFilterIterator, $this->linter);
}
private function getFilteringFileIterator(): FileFilterIterator
{
if (null === $this->fileIterator) {
throw new \RuntimeException('File iterator is not configured. Pass paths during Runner initialisation or set them after with `setFileIterator()`.');
}
return new FileFilterIterator(
$this->fileIterator instanceof \IteratorAggregate
? $this->fileIterator->getIterator()
: $this->fileIterator,
$this->eventDispatcher,
$this->cacheManager
);
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Runner;
use PhpCsFixer\Runner\Parallel\ParallelConfig;
/**
* @author Greg Korba <greg@codito.dev>
*
* @readonly
*
* @internal
*/
final class RunnerConfig
{
private bool $isDryRun;
private bool $stopOnViolation;
private ParallelConfig $parallelConfig;
private ?string $configFile;
public function __construct(
bool $isDryRun,
bool $stopOnViolation,
ParallelConfig $parallelConfig,
?string $configFile = null
) {
$this->isDryRun = $isDryRun;
$this->stopOnViolation = $stopOnViolation;
$this->parallelConfig = $parallelConfig;
$this->configFile = $configFile;
}
public function isDryRun(): bool
{
return $this->isDryRun;
}
public function shouldStopOnViolation(): bool
{
return $this->stopOnViolation;
}
public function getParallelConfig(): ParallelConfig
{
return $this->parallelConfig;
}
public function getConfigFile(): ?string
{
return $this->configFile;
}
}