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,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\Console\Command;
use PhpCsFixer\ToolInfoInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
/**
* @author Greg Korba <greg@codito.dev>
*
* @internal
*/
#[AsCommand(name: 'check', description: 'Checks if configured files/directories comply with configured rules.')]
final class CheckCommand extends FixCommand
{
/** @TODO PHP 8.0 - remove the property */
protected static $defaultName = 'check'; // @phpstan-ignore property.parentPropertyFinalByPhpDoc
/** @TODO PHP 8.0 - remove the property */
protected static $defaultDescription = 'Checks if configured files/directories comply with configured rules.'; // @phpstan-ignore property.parentPropertyFinalByPhpDoc
public function __construct(ToolInfoInterface $toolInfo)
{
parent::__construct($toolInfo);
}
public function getHelp(): string
{
$help = explode('<comment>--dry-run</comment>', parent::getHelp());
return substr($help[0], 0, strrpos($help[0], "\n") - 1)
.substr($help[1], strpos($help[1], "\n"));
}
protected function configure(): void
{
parent::configure();
$this->setDefinition([
...array_values($this->getDefinition()->getArguments()),
...array_values(array_filter(
$this->getDefinition()->getOptions(),
static fn (InputOption $option): bool => 'dry-run' !== $option->getName()
)),
]);
}
protected function isDryRun(InputInterface $input): bool
{
return true;
}
}

View File

@@ -0,0 +1,493 @@
<?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\Console\Command;
use PhpCsFixer\Config;
use PhpCsFixer\Console\Application;
use PhpCsFixer\Console\ConfigurationResolver;
use PhpCsFixer\Differ\DiffConsoleFormatter;
use PhpCsFixer\Differ\FullDiffer;
use PhpCsFixer\Documentation\FixerDocumentGenerator;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
use PhpCsFixer\Fixer\ExperimentalFixerInterface;
use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\Fixer\InternalFixerInterface;
use PhpCsFixer\FixerConfiguration\AliasedFixerOption;
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
use PhpCsFixer\FixerConfiguration\DeprecatedFixerOption;
use PhpCsFixer\FixerDefinition\CodeSampleInterface;
use PhpCsFixer\FixerDefinition\FileSpecificCodeSampleInterface;
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSampleInterface;
use PhpCsFixer\FixerFactory;
use PhpCsFixer\Preg;
use PhpCsFixer\RuleSet\DeprecatedRuleSetDescriptionInterface;
use PhpCsFixer\RuleSet\RuleSets;
use PhpCsFixer\StdinFileInfo;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\ToolInfo;
use PhpCsFixer\Utils;
use PhpCsFixer\WordMatcher;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
#[AsCommand(name: 'describe', description: 'Describe rule / ruleset.')]
final class DescribeCommand extends Command
{
/** @TODO PHP 8.0 - remove the property */
protected static $defaultName = 'describe';
/** @TODO PHP 8.0 - remove the property */
protected static $defaultDescription = 'Describe rule / ruleset.';
/**
* @var ?list<string>
*/
private ?array $setNames = null;
private FixerFactory $fixerFactory;
/**
* @var null|array<string, FixerInterface>
*/
private ?array $fixers = null;
public function __construct(?FixerFactory $fixerFactory = null)
{
parent::__construct();
if (null === $fixerFactory) {
$fixerFactory = new FixerFactory();
$fixerFactory->registerBuiltInFixers();
}
$this->fixerFactory = $fixerFactory;
}
protected function configure(): void
{
$this->setDefinition(
[
new InputArgument('name', InputArgument::REQUIRED, 'Name of rule / set.', null, fn () => array_merge($this->getSetNames(), array_keys($this->getFixers()))),
new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a .php-cs-fixer.php file.'),
]
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($output instanceof ConsoleOutputInterface) {
$stdErr = $output->getErrorOutput();
$stdErr->writeln(Application::getAboutWithRuntime(true));
}
$resolver = new ConfigurationResolver(
new Config(),
['config' => $input->getOption('config')],
getcwd(),
new ToolInfo()
);
$this->fixerFactory->registerCustomFixers($resolver->getConfig()->getCustomFixers());
$name = $input->getArgument('name');
try {
if (str_starts_with($name, '@')) {
$this->describeSet($output, $name);
return 0;
}
$this->describeRule($output, $name);
} catch (DescribeNameNotFoundException $e) {
$matcher = new WordMatcher(
'set' === $e->getType() ? $this->getSetNames() : array_keys($this->getFixers())
);
$alternative = $matcher->match($name);
$this->describeList($output, $e->getType());
throw new \InvalidArgumentException(\sprintf(
'%s "%s" not found.%s',
ucfirst($e->getType()),
$name,
null === $alternative ? '' : ' Did you mean "'.$alternative.'"?'
));
}
return 0;
}
private function describeRule(OutputInterface $output, string $name): void
{
$fixers = $this->getFixers();
if (!isset($fixers[$name])) {
throw new DescribeNameNotFoundException($name, 'rule');
}
$fixer = $fixers[$name];
$definition = $fixer->getDefinition();
$output->writeln(\sprintf('<fg=blue>Description of the <info>`%s`</info> rule.</>', $name));
$output->writeln('');
if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
$output->writeln(\sprintf('Fixer class: <comment>%s</comment>.', \get_class($fixer)));
$output->writeln('');
}
if ($fixer instanceof DeprecatedFixerInterface) {
$successors = $fixer->getSuccessorsNames();
$message = [] === $successors
? \sprintf('it will be removed in version %d.0', Application::getMajorVersion() + 1)
: \sprintf('use %s instead', Utils::naturalLanguageJoinWithBackticks($successors));
$endMessage = '. '.ucfirst($message);
Utils::triggerDeprecation(new \RuntimeException(str_replace('`', '"', "Rule \"{$name}\" is deprecated{$endMessage}.")));
$message = Preg::replace('/(`[^`]+`)/', '<info>$1</info>', $message);
$output->writeln(\sprintf('<error>DEPRECATED</error>: %s.', $message));
$output->writeln('');
}
$output->writeln($definition->getSummary());
$description = $definition->getDescription();
if (null !== $description) {
$output->writeln($description);
}
$output->writeln('');
if ($fixer instanceof ExperimentalFixerInterface) {
$output->writeln('<error>Fixer applying this rule is EXPERIMENTAL.</error>.');
$output->writeln('It is not covered with backward compatibility promise and may produce unstable or unexpected results.');
$output->writeln('');
}
if ($fixer instanceof InternalFixerInterface) {
$output->writeln('<error>Fixer applying this rule is INTERNAL.</error>.');
$output->writeln('It is expected to be used only on PHP CS Fixer project itself.');
$output->writeln('');
}
if ($fixer->isRisky()) {
$output->writeln('<error>Fixer applying this rule is RISKY.</error>');
$riskyDescription = $definition->getRiskyDescription();
if (null !== $riskyDescription) {
$output->writeln($riskyDescription);
}
$output->writeln('');
}
if ($fixer instanceof ConfigurableFixerInterface) {
$configurationDefinition = $fixer->getConfigurationDefinition();
$options = $configurationDefinition->getOptions();
$output->writeln(\sprintf('Fixer is configurable using following option%s:', 1 === \count($options) ? '' : 's'));
foreach ($options as $option) {
$line = '* <info>'.OutputFormatter::escape($option->getName()).'</info>';
$allowed = HelpCommand::getDisplayableAllowedValues($option);
if (null === $allowed) {
$allowed = array_map(
static fn (string $type): string => '<comment>'.$type.'</comment>',
$option->getAllowedTypes(),
);
} else {
$allowed = array_map(static fn ($value): string => $value instanceof AllowedValueSubset
? 'a subset of <comment>'.Utils::toString($value->getAllowedValues()).'</comment>'
: '<comment>'.Utils::toString($value).'</comment>', $allowed);
}
$line .= ' ('.Utils::naturalLanguageJoin($allowed, '').')';
$description = Preg::replace('/(`.+?`)/', '<info>$1</info>', OutputFormatter::escape($option->getDescription()));
$line .= ': '.lcfirst(Preg::replace('/\.$/', '', $description)).'; ';
if ($option->hasDefault()) {
$line .= \sprintf(
'defaults to <comment>%s</comment>',
Utils::toString($option->getDefault())
);
} else {
$line .= '<comment>required</comment>';
}
if ($option instanceof DeprecatedFixerOption) {
$line .= '. <error>DEPRECATED</error>: '.Preg::replace(
'/(`.+?`)/',
'<info>$1</info>',
OutputFormatter::escape(lcfirst($option->getDeprecationMessage()))
);
}
if ($option instanceof AliasedFixerOption) {
$line .= '; <error>DEPRECATED</error> alias: <comment>'.$option->getAlias().'</comment>';
}
$output->writeln($line);
}
$output->writeln('');
}
$codeSamples = array_filter($definition->getCodeSamples(), static function (CodeSampleInterface $codeSample): bool {
if ($codeSample instanceof VersionSpecificCodeSampleInterface) {
return $codeSample->isSuitableFor(\PHP_VERSION_ID);
}
return true;
});
if (0 === \count($definition->getCodeSamples())) {
$output->writeln([
'Fixing examples are not available for this rule.',
'',
]);
} elseif (0 === \count($codeSamples)) {
$output->writeln([
'Fixing examples <error>cannot be</error> demonstrated on the current PHP version.',
'',
]);
} else {
$output->writeln('Fixing examples:');
$differ = new FullDiffer();
$diffFormatter = new DiffConsoleFormatter(
$output->isDecorated(),
\sprintf(
'<comment> ---------- begin diff ----------</comment>%s%%s%s<comment> ----------- end diff -----------</comment>',
\PHP_EOL,
\PHP_EOL
)
);
foreach ($codeSamples as $index => $codeSample) {
$old = $codeSample->getCode();
$tokens = Tokens::fromCode($old);
$configuration = $codeSample->getConfiguration();
if ($fixer instanceof ConfigurableFixerInterface) {
$fixer->configure($configuration ?? []);
}
$file = $codeSample instanceof FileSpecificCodeSampleInterface
? $codeSample->getSplFileInfo()
: new StdinFileInfo();
$fixer->fix($file, $tokens);
$diff = $differ->diff($old, $tokens->generateCode());
if ($fixer instanceof ConfigurableFixerInterface) {
if (null === $configuration) {
$output->writeln(\sprintf(' * Example #%d. Fixing with the <comment>default</comment> configuration.', $index + 1));
} else {
$output->writeln(\sprintf(' * Example #%d. Fixing with configuration: <comment>%s</comment>.', $index + 1, Utils::toString($codeSample->getConfiguration())));
}
} else {
$output->writeln(\sprintf(' * Example #%d.', $index + 1));
}
$output->writeln([$diffFormatter->format($diff, ' %s'), '']);
}
}
$ruleSetConfigs = FixerDocumentGenerator::getSetsOfRule($name);
if ([] !== $ruleSetConfigs) {
ksort($ruleSetConfigs);
$plural = 1 !== \count($ruleSetConfigs) ? 's' : '';
$output->writeln("Fixer is part of the following rule set{$plural}:");
$ruleSetDefinitions = RuleSets::getSetDefinitions();
foreach ($ruleSetConfigs as $set => $config) {
\assert(isset($ruleSetDefinitions[$set]));
$ruleSetDescription = $ruleSetDefinitions[$set];
$deprecatedDesc = ($ruleSetDescription instanceof DeprecatedRuleSetDescriptionInterface) ? ' *(deprecated)*' : '';
if (null !== $config) {
$output->writeln(\sprintf('* <info>%s</info> with config: <comment>%s</comment>', $set.$deprecatedDesc, Utils::toString($config)));
} else {
$output->writeln(\sprintf('* <info>%s</info> with <comment>default</comment> config', $set.$deprecatedDesc));
}
}
$output->writeln('');
}
}
private function describeSet(OutputInterface $output, string $name): void
{
if (!\in_array($name, $this->getSetNames(), true)) {
throw new DescribeNameNotFoundException($name, 'set');
}
$ruleSetDefinitions = RuleSets::getSetDefinitions();
$ruleSetDescription = $ruleSetDefinitions[$name];
$fixers = $this->getFixers();
$output->writeln(\sprintf('<fg=blue>Description of the <info>`%s`</info> set.</>', $ruleSetDescription->getName()));
$output->writeln('');
$output->writeln($this->replaceRstLinks($ruleSetDescription->getDescription()));
$output->writeln('');
if ($ruleSetDescription instanceof DeprecatedRuleSetDescriptionInterface) {
$successors = $ruleSetDescription->getSuccessorsNames();
$message = [] === $successors
? \sprintf('it will be removed in version %d.0', Application::getMajorVersion() + 1)
: \sprintf('use %s instead', Utils::naturalLanguageJoinWithBackticks($successors));
Utils::triggerDeprecation(new \RuntimeException(str_replace('`', '"', "Set \"{$name}\" is deprecated, {$message}.")));
$message = Preg::replace('/(`[^`]+`)/', '<info>$1</info>', $message);
$output->writeln(\sprintf('<error>DEPRECATED</error>: %s.', $message));
$output->writeln('');
}
if ($ruleSetDescription->isRisky()) {
$output->writeln('<error>This set contains risky rules.</error>');
$output->writeln('');
}
$help = '';
foreach ($ruleSetDescription->getRules() as $rule => $config) {
if (str_starts_with($rule, '@')) {
$set = $ruleSetDefinitions[$rule];
$help .= \sprintf(
" * <info>%s</info>%s\n | %s\n\n",
$rule,
$set->isRisky() ? ' <error>risky</error>' : '',
$this->replaceRstLinks($set->getDescription())
);
continue;
}
$fixer = $fixers[$rule];
$definition = $fixer->getDefinition();
$help .= \sprintf(
" * <info>%s</info>%s\n | %s\n%s\n",
$rule,
$fixer->isRisky() ? ' <error>risky</error>' : '',
$definition->getSummary(),
true !== $config ? \sprintf(" <comment>| Configuration: %s</comment>\n", Utils::toString($config)) : ''
);
}
$output->write($help);
}
/**
* @return array<string, FixerInterface>
*/
private function getFixers(): array
{
if (null !== $this->fixers) {
return $this->fixers;
}
$fixers = [];
foreach ($this->fixerFactory->getFixers() as $fixer) {
$fixers[$fixer->getName()] = $fixer;
}
$this->fixers = $fixers;
ksort($this->fixers);
return $this->fixers;
}
/**
* @return list<string>
*/
private function getSetNames(): array
{
if (null !== $this->setNames) {
return $this->setNames;
}
$this->setNames = RuleSets::getSetDefinitionNames();
return $this->setNames;
}
/**
* @param string $type 'rule'|'set'
*/
private function describeList(OutputInterface $output, string $type): void
{
if ($output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
return;
}
if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE || 'set' === $type) {
$output->writeln('<comment>Defined sets:</comment>');
$items = $this->getSetNames();
foreach ($items as $item) {
$output->writeln(\sprintf('* <info>%s</info>', $item));
}
}
if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE || 'rule' === $type) {
$output->writeln('<comment>Defined rules:</comment>');
$items = array_keys($this->getFixers());
foreach ($items as $item) {
$output->writeln(\sprintf('* <info>%s</info>', $item));
}
}
}
private function replaceRstLinks(string $content): string
{
return Preg::replaceCallback(
'/(`[^<]+<[^>]+>`_)/',
static fn (array $matches) => Preg::replaceCallback(
'/`(.*)<(.*)>`_/',
static fn (array $matches): string => $matches[1].'('.$matches[2].')',
$matches[1]
),
$content
);
}
}

View File

@@ -0,0 +1,46 @@
<?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\Console\Command;
/**
* @internal
*/
final class DescribeNameNotFoundException extends \InvalidArgumentException
{
private string $name;
/**
* 'rule'|'set'.
*/
private string $type;
public function __construct(string $name, string $type)
{
$this->name = $name;
$this->type = $type;
parent::__construct();
}
public function getName(): string
{
return $this->name;
}
public function getType(): string
{
return $this->type;
}
}

View File

@@ -0,0 +1,121 @@
<?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\Console\Command;
use PhpCsFixer\Documentation\DocumentationLocator;
use PhpCsFixer\Documentation\FixerDocumentGenerator;
use PhpCsFixer\Documentation\RuleSetDocumentationGenerator;
use PhpCsFixer\FixerFactory;
use PhpCsFixer\RuleSet\RuleSets;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
/**
* @internal
*/
#[AsCommand(name: 'documentation', description: 'Dumps the documentation of the project into its "/doc" directory.')]
final class DocumentationCommand extends Command
{
/** @TODO PHP 8.0 - remove the property */
protected static $defaultName = 'documentation';
/** @TODO PHP 8.0 - remove the property */
protected static $defaultDescription = 'Dumps the documentation of the project into its "/doc" directory.';
private Filesystem $filesystem;
public function __construct(Filesystem $filesystem)
{
parent::__construct();
$this->filesystem = $filesystem;
}
protected function configure(): void
{
$this->setAliases(['doc']);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$locator = new DocumentationLocator();
$fixerFactory = new FixerFactory();
$fixerFactory->registerBuiltInFixers();
$fixers = $fixerFactory->getFixers();
$setDefinitions = RuleSets::getSetDefinitions();
$fixerDocumentGenerator = new FixerDocumentGenerator($locator);
$ruleSetDocumentationGenerator = new RuleSetDocumentationGenerator($locator);
// Array of existing fixer docs.
// We first override existing files, and then we will delete files that are no longer needed.
// We cannot remove all files first, as generation of docs is re-using existing docs to extract code-samples for
// VersionSpecificCodeSample under incompatible PHP version.
$docForFixerRelativePaths = [];
foreach ($fixers as $fixer) {
$docForFixerRelativePaths[] = $locator->getFixerDocumentationFileRelativePath($fixer);
$this->filesystem->dumpFile(
$locator->getFixerDocumentationFilePath($fixer),
$fixerDocumentGenerator->generateFixerDocumentation($fixer)
);
}
foreach (
(new Finder())->files()
->in($locator->getFixersDocumentationDirectoryPath())
->notPath($docForFixerRelativePaths) as $file
) {
$this->filesystem->remove($file->getPathname());
}
// Fixer doc. index
$this->filesystem->dumpFile(
$locator->getFixersDocumentationIndexFilePath(),
$fixerDocumentGenerator->generateFixersDocumentationIndex($fixers)
);
// RuleSet docs.
foreach ((new Finder())->files()->in($locator->getRuleSetsDocumentationDirectoryPath()) as $file) {
$this->filesystem->remove($file->getPathname());
}
$paths = [];
foreach ($setDefinitions as $name => $definition) {
$path = $locator->getRuleSetsDocumentationFilePath($name);
$paths[$path] = $definition;
$this->filesystem->dumpFile($path, $ruleSetDocumentationGenerator->generateRuleSetsDocumentation($definition, $fixers));
}
// RuleSet doc. index
$this->filesystem->dumpFile(
$locator->getRuleSetsDocumentationIndexFilePath(),
$ruleSetDocumentationGenerator->generateRuleSetsDocumentationIndex($paths)
);
$output->writeln('Docs updated.');
return 0;
}
}

View File

@@ -0,0 +1,449 @@
<?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\Console\Command;
use PhpCsFixer\Config;
use PhpCsFixer\ConfigInterface;
use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
use PhpCsFixer\Console\Application;
use PhpCsFixer\Console\ConfigurationResolver;
use PhpCsFixer\Console\Output\ErrorOutput;
use PhpCsFixer\Console\Output\OutputContext;
use PhpCsFixer\Console\Output\Progress\ProgressOutputFactory;
use PhpCsFixer\Console\Output\Progress\ProgressOutputType;
use PhpCsFixer\Console\Report\FixReport\ReporterFactory;
use PhpCsFixer\Console\Report\FixReport\ReportSummary;
use PhpCsFixer\Error\ErrorsManager;
use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\FixerFactory;
use PhpCsFixer\RuleSet\RuleSets;
use PhpCsFixer\Runner\Event\FileProcessed;
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
use PhpCsFixer\Runner\Runner;
use PhpCsFixer\ToolInfoInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Terminal;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Stopwatch\Stopwatch;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @final
*
* @internal
*/
#[AsCommand(name: 'fix', description: 'Fixes a directory or a file.')]
/* final */ class FixCommand extends Command
{
/** @TODO PHP 8.0 - remove the property */
protected static $defaultName = 'fix';
/** @TODO PHP 8.0 - remove the property */
protected static $defaultDescription = 'Fixes a directory or a file.';
private EventDispatcherInterface $eventDispatcher;
private ErrorsManager $errorsManager;
private Stopwatch $stopwatch;
private ConfigInterface $defaultConfig;
private ToolInfoInterface $toolInfo;
private ProgressOutputFactory $progressOutputFactory;
public function __construct(ToolInfoInterface $toolInfo)
{
parent::__construct();
$this->eventDispatcher = new EventDispatcher();
$this->errorsManager = new ErrorsManager();
$this->stopwatch = new Stopwatch();
$this->defaultConfig = new Config();
$this->toolInfo = $toolInfo;
$this->progressOutputFactory = new ProgressOutputFactory();
}
/**
* {@inheritdoc}
*
* Override here to only generate the help copy when used.
*/
public function getHelp(): string
{
return <<<'EOF'
The <info>%command.name%</info> command tries to %command.name% as much coding standards
problems as possible on a given file or files in a given directory and its subdirectories:
<info>$ php %command.full_name% /path/to/dir</info>
<info>$ php %command.full_name% /path/to/file</info>
By default <comment>--path-mode</comment> is set to `override`, which means, that if you specify the path to a file or a directory via
command arguments, then the paths provided to a `Finder` in config file will be ignored. You can use <comment>--path-mode=intersection</comment>
to merge paths from the config file and from the argument:
<info>$ php %command.full_name% --path-mode=intersection /path/to/dir</info>
The <comment>--format</comment> option for the output format. Supported formats are `@auto` (default one on v4+), `txt` (default one on v3), `json`, `xml`, `checkstyle`, `junit` and `gitlab`.
* `@auto` aims to auto-select best reporter for given CI or local execution (resolution into best format is outside of BC promise and is future-ready)
* `gitlab` for GitLab
* `@auto,{format}` takes `@auto` under CI, and {format} otherwise
NOTE: the output for the following formats are generated in accordance with schemas
* `checkstyle` follows the common `"checkstyle" XML schema </doc/schemas/fix/checkstyle.xsd>`_
* `gitlab` follows the `codeclimate JSON schema </doc/schemas/fix/codeclimate.json>`_
* `json` follows the `own JSON schema </doc/schemas/fix/schema.json>`_
* `junit` follows the `JUnit XML schema from Jenkins </doc/schemas/fix/junit-10.xsd>`_
* `xml` follows the `own XML schema </doc/schemas/fix/xml.xsd>`_
The <comment>--quiet</comment> Do not output any message.
The <comment>--verbose</comment> option will show the applied rules. When using the `txt` format it will also display progress notifications.
NOTE: if there is an error like "errors reported during linting after fixing", you can use this to be even more verbose for debugging purpose
* `-v`: verbose
* `-vv`: very verbose
* `-vvv`: debug
The <comment>--rules</comment> option limits the rules to apply to the
project:
EOF. /* @TODO: 4.0 - change to @PER */ <<<'EOF'
<info>$ php %command.full_name% /path/to/project --rules=@PSR12</info>
By default the PSR-12 rules are used.
The <comment>--rules</comment> option lets you choose the exact rules to
apply (the rule names must be separated by a comma):
<info>$ php %command.full_name% /path/to/dir --rules=line_ending,full_opening_tag,indentation_type</info>
You can also exclude the rules you don't want by placing a dash in front of the rule name, if this is more convenient,
using <comment>-name_of_fixer</comment>:
<info>$ php %command.full_name% /path/to/dir --rules=-full_opening_tag,-indentation_type</info>
When using combinations of exact and exclude rules, applying exact rules along with above excluded results:
<info>$ php %command.full_name% /path/to/project --rules=@Symfony,-@PSR1,-blank_line_before_statement,strict_comparison</info>
Complete configuration for rules can be supplied using a `json` formatted string.
<info>$ php %command.full_name% /path/to/project --rules='{"concat_space": {"spacing": "none"}}'</info>
The <comment>--dry-run</comment> flag will run the fixer without making changes to your files.
The <comment>--sequential</comment> flag will enforce sequential analysis even if parallel config is provided.
The <comment>--diff</comment> flag can be used to let the fixer output all the changes it makes.
The <comment>--allow-risky</comment> option (pass `yes` or `no`) allows you to set whether risky rules may run. Default value is taken from config file.
A rule is considered risky if it could change code behaviour. By default no risky rules are run.
The <comment>--stop-on-violation</comment> flag stops the execution upon first file that needs to be fixed.
The <comment>--show-progress</comment> option allows you to choose the way process progress is rendered:
* <comment>none</comment>: disables progress output;
* <comment>dots</comment>: multiline progress output with number of files and percentage on each line.
* <comment>bar</comment>: single line progress output with number of files and calculated percentage.
If the option is not provided, it defaults to <comment>bar</comment> unless a config file that disables output is used, in which case it defaults to <comment>none</comment>. This option has no effect if the verbosity of the command is less than <comment>verbose</comment>.
<info>$ php %command.full_name% --verbose --show-progress=dots</info>
By using <comment>--using-cache</comment> option with `yes` or `no` you can set if the caching
mechanism should be used.
The command can also read from standard input, in which case it won't
automatically fix anything:
<info>$ cat foo.php | php %command.full_name% --diff -</info>
Finally, if you don't need BC kept on CLI level, you might use `PHP_CS_FIXER_FUTURE_MODE` to start using options that
would be default in next MAJOR release and to forbid using deprecated configuration:
<info>$ PHP_CS_FIXER_FUTURE_MODE=1 php %command.full_name% -v --diff</info>
Exit code
---------
Exit code of the `%command.name%` command is built using following bit flags:
* 0 - OK.
* 1 - General error (or PHP minimal requirement not matched).
* 4 - Some files have invalid syntax (only in dry-run mode).
* 8 - Some files need fixing (only in dry-run mode).
* 16 - Configuration error of the application.
* 32 - Configuration error of a Fixer.
* 64 - Exception raised within the application.
EOF;
}
protected function configure(): void
{
$reporterFactory = new ReporterFactory();
$reporterFactory->registerBuiltInReporters();
$formats = $reporterFactory->getFormats();
array_unshift($formats, '@auto', '@auto,txt');
$progessOutputTypes = ProgressOutputType::all();
$this->setDefinition(
[
new InputArgument('path', InputArgument::IS_ARRAY, 'The path(s) that rules will be run against (each path can be a file or directory).'),
new InputOption('path-mode', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('Specify path mode (%s).', ConfigurationResolver::PATH_MODE_VALUES), ConfigurationResolver::PATH_MODE_OVERRIDE, ConfigurationResolver::PATH_MODE_VALUES),
new InputOption('allow-risky', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('Are risky fixers allowed (%s).', ConfigurationResolver::BOOL_VALUES), null, ConfigurationResolver::BOOL_VALUES),
new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a config file.'),
new InputOption('dry-run', '', InputOption::VALUE_NONE, 'Only shows which files would have been modified.'),
new InputOption('rules', '', InputOption::VALUE_REQUIRED, 'List of rules that should be run against configured paths.', null, static function () {
$fixerFactory = new FixerFactory();
$fixerFactory->registerBuiltInFixers();
$fixers = array_map(static fn (FixerInterface $fixer) => $fixer->getName(), $fixerFactory->getFixers());
return array_merge(RuleSets::getSetDefinitionNames(), $fixers);
}),
new InputOption('using-cache', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('Should cache be used (%s).', ConfigurationResolver::BOOL_VALUES), null, ConfigurationResolver::BOOL_VALUES),
new InputOption('allow-unsupported-php-version', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('Should the command refuse to run on unsupported PHP version (%s).', ConfigurationResolver::BOOL_VALUES), null, ConfigurationResolver::BOOL_VALUES),
new InputOption('cache-file', '', InputOption::VALUE_REQUIRED, 'The path to the cache file.'),
new InputOption('diff', '', InputOption::VALUE_NONE, 'Prints diff for each file.'),
new InputOption('format', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('To output results in other formats (%s).', $formats), null, $formats),
new InputOption('stop-on-violation', '', InputOption::VALUE_NONE, 'Stop execution on first violation.'),
new InputOption('show-progress', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('Type of progress indicator (%s).', $progessOutputTypes), null, $progessOutputTypes),
new InputOption('sequential', '', InputOption::VALUE_NONE, 'Enforce sequential analysis.'),
]
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$verbosity = $output->getVerbosity();
$passedConfig = $input->getOption('config');
$passedRules = $input->getOption('rules');
if (null !== $passedConfig && null !== $passedRules) {
throw new InvalidConfigurationException('Passing both `--config` and `--rules` options is not allowed.');
}
$resolver = new ConfigurationResolver(
$this->defaultConfig,
[
'allow-risky' => $input->getOption('allow-risky'),
'config' => $passedConfig,
'dry-run' => $this->isDryRun($input),
'rules' => $passedRules,
'path' => $input->getArgument('path'),
'path-mode' => $input->getOption('path-mode'),
'using-cache' => $input->getOption('using-cache'),
'allow-unsupported-php-version' => $input->getOption('allow-unsupported-php-version'),
'cache-file' => $input->getOption('cache-file'),
'format' => $input->getOption('format'),
'diff' => $input->getOption('diff'),
'stop-on-violation' => $input->getOption('stop-on-violation'),
'verbosity' => $verbosity,
'show-progress' => $input->getOption('show-progress'),
'sequential' => $input->getOption('sequential'),
],
getcwd(),
$this->toolInfo
);
$reporter = $resolver->getReporter();
$stdErr = $output instanceof ConsoleOutputInterface
? $output->getErrorOutput()
: ('txt' === $reporter->getFormat() ? $output : null);
if (null !== $stdErr) {
$stdErr->writeln(Application::getAboutWithRuntime(true));
if (version_compare(\PHP_VERSION, ConfigInterface::PHP_VERSION_SYNTAX_SUPPORTED.'.99', '>')) {
$message = \sprintf(
'PHP CS Fixer currently supports PHP syntax only up to PHP %s, current PHP version: %s.',
ConfigInterface::PHP_VERSION_SYNTAX_SUPPORTED,
\PHP_VERSION
);
if (!$resolver->getUnsupportedPhpVersionAllowed()) {
$message .= ' Add Config::setUnsupportedPhpVersionAllowed(true) to allow executions on unsupported PHP versions. Such execution may be unstable and you may experience code modified in a wrong way.';
$stdErr->writeln(\sprintf(
$stdErr->isDecorated() ? '<bg=red;fg=white;>%s</>' : '%s',
$message
));
return 1;
}
$message .= ' Execution may be unstable. You may experience code modified in a wrong way. Please report such cases at https://github.com/PHP-CS-Fixer/PHP-CS-Fixer. Remove Config::setUnsupportedPhpVersionAllowed(true) to allow executions only on supported PHP versions.';
$stdErr->writeln(\sprintf(
$stdErr->isDecorated() ? '<bg=yellow;fg=black;>%s</>' : '%s',
$message
));
}
$isParallel = $resolver->getParallelConfig()->getMaxProcesses() > 1;
$stdErr->writeln(\sprintf(
'Running analysis on %d core%s.',
$resolver->getParallelConfig()->getMaxProcesses(),
$isParallel ? \sprintf(
's with %d file%s per process',
$resolver->getParallelConfig()->getFilesPerProcess(),
$resolver->getParallelConfig()->getFilesPerProcess() > 1 ? 's' : ''
) : ' sequentially'
));
/** @TODO v4 remove warnings related to parallel runner */
$availableMaxProcesses = ParallelConfigFactory::detect()->getMaxProcesses();
if ($isParallel || $availableMaxProcesses > 1) {
$usageDocs = 'https://cs.symfony.com/doc/usage.html';
$stdErr->writeln(\sprintf(
$stdErr->isDecorated() ? '<bg=yellow;fg=black;>%s</>' : '%s',
$isParallel
? 'Parallel runner is an experimental feature and may be unstable, use it at your own risk. Feedback highly appreciated!'
: \sprintf(
'You can enable parallel runner and speed up the analysis! Please see %s for more information.',
$stdErr->isDecorated()
? \sprintf('<href=%s;bg=yellow;fg=red;bold>usage docs</>', OutputFormatter::escape($usageDocs))
: $usageDocs
)
));
}
$configFile = $resolver->getConfigFile();
$stdErr->writeln(\sprintf('Loaded config <comment>%s</comment>%s.', $resolver->getConfig()->getName(), null === $configFile ? '' : ' from "'.$configFile.'"'));
if ($resolver->getUsingCache()) {
$cacheFile = $resolver->getCacheFile();
if (is_file($cacheFile)) {
$stdErr->writeln(\sprintf('Using cache file "%s".', $cacheFile));
}
}
}
$finder = new \ArrayIterator(array_filter(
iterator_to_array($resolver->getFinder()),
static fn (\SplFileInfo $fileInfo) => false !== $fileInfo->getRealPath(),
));
if (null !== $stdErr && $resolver->configFinderIsOverridden()) {
$stdErr->writeln(
\sprintf($stdErr->isDecorated() ? '<bg=yellow;fg=black;>%s</>' : '%s', 'Paths from configuration file have been overridden by paths provided as command arguments.')
);
}
$progressType = $resolver->getProgressType();
$progressOutput = $this->progressOutputFactory->create(
$progressType,
new OutputContext(
$stdErr,
(new Terminal())->getWidth(),
\count($finder)
)
);
$runner = new Runner(
$finder,
$resolver->getFixers(),
$resolver->getDiffer(),
ProgressOutputType::NONE !== $progressType ? $this->eventDispatcher : null,
$this->errorsManager,
$resolver->getLinter(),
$resolver->isDryRun(),
$resolver->getCacheManager(),
$resolver->getDirectory(),
$resolver->shouldStopOnViolation(),
$resolver->getParallelConfig(),
$input,
$resolver->getConfigFile()
);
$this->eventDispatcher->addListener(FileProcessed::NAME, [$progressOutput, 'onFixerFileProcessed']);
$this->stopwatch->start('fixFiles');
$changed = $runner->fix();
$this->stopwatch->stop('fixFiles');
$this->eventDispatcher->removeListener(FileProcessed::NAME, [$progressOutput, 'onFixerFileProcessed']);
$progressOutput->printLegend();
$fixEvent = $this->stopwatch->getEvent('fixFiles');
$reportSummary = new ReportSummary(
$changed,
\count($finder),
$fixEvent->getDuration(),
$fixEvent->getMemory(),
OutputInterface::VERBOSITY_VERBOSE <= $verbosity,
$resolver->isDryRun(),
$output->isDecorated()
);
$output->isDecorated()
? $output->write($reporter->generate($reportSummary))
: $output->write($reporter->generate($reportSummary), false, OutputInterface::OUTPUT_RAW);
$invalidErrors = $this->errorsManager->getInvalidErrors();
$exceptionErrors = $this->errorsManager->getExceptionErrors();
$lintErrors = $this->errorsManager->getLintErrors();
if (null !== $stdErr) {
$errorOutput = new ErrorOutput($stdErr);
if (\count($invalidErrors) > 0) {
$errorOutput->listErrors('linting before fixing', $invalidErrors);
}
if (\count($exceptionErrors) > 0) {
$errorOutput->listErrors('fixing', $exceptionErrors);
}
if (\count($lintErrors) > 0) {
$errorOutput->listErrors('linting after fixing', $lintErrors);
}
}
$exitStatusCalculator = new FixCommandExitStatusCalculator();
return $exitStatusCalculator->calculate(
$resolver->isDryRun(),
\count($changed) > 0,
\count($invalidErrors) > 0,
\count($exceptionErrors) > 0,
\count($lintErrors) > 0
);
}
protected function isDryRun(InputInterface $input): bool
{
return $input->getOption('dry-run'); // @phpstan-ignore symfonyConsole.optionNotFound (Because PHPStan doesn't recognise the method is overridden in the child class and this parameter is _not_ used in the child class.)
}
}

View File

@@ -0,0 +1,56 @@
<?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\Console\Command;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class FixCommandExitStatusCalculator
{
// Exit status 1 is reserved for environment constraints not matched.
public const EXIT_STATUS_FLAG_HAS_INVALID_FILES = 4;
public const EXIT_STATUS_FLAG_HAS_CHANGED_FILES = 8;
public const EXIT_STATUS_FLAG_HAS_INVALID_CONFIG = 16;
public const EXIT_STATUS_FLAG_HAS_INVALID_FIXER_CONFIG = 32;
public const EXIT_STATUS_FLAG_EXCEPTION_IN_APP = 64;
public function calculate(
bool $isDryRun,
bool $hasChangedFiles,
bool $hasInvalidErrors,
bool $hasExceptionErrors,
bool $hasLintErrorsAfterFixing
): int {
$exitStatus = 0;
if ($isDryRun) {
if ($hasChangedFiles) {
$exitStatus |= self::EXIT_STATUS_FLAG_HAS_CHANGED_FILES;
}
if ($hasInvalidErrors) {
$exitStatus |= self::EXIT_STATUS_FLAG_HAS_INVALID_FILES;
}
}
if ($hasExceptionErrors || $hasLintErrorsAfterFixing) {
$exitStatus |= self::EXIT_STATUS_FLAG_EXCEPTION_IN_APP;
}
return $exitStatus;
}
}

View File

@@ -0,0 +1,90 @@
<?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\Console\Command;
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
use PhpCsFixer\FixerConfiguration\FixerOptionInterface;
use PhpCsFixer\Utils;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\HelpCommand as BaseHelpCommand;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
#[AsCommand(name: 'help')]
final class HelpCommand extends BaseHelpCommand
{
/** @TODO PHP 8.0 - remove the property */
protected static $defaultName = 'help';
/**
* Formats the description of an option to include its allowed values.
*
* @param string $description description with a single `%s` placeholder for the allowed values
* @param non-empty-list<string> $allowedValues
*/
public static function getDescriptionWithAllowedValues(string $description, array $allowedValues): string
{
$allowedValues = Utils::naturalLanguageJoinWithBackticks($allowedValues, 'or');
return \sprintf($description, 'can be '.$allowedValues);
}
/**
* Returns the allowed values of the given option that can be converted to a string.
*
* @return null|list<AllowedValueSubset|mixed>
*/
public static function getDisplayableAllowedValues(FixerOptionInterface $option): ?array
{
$allowed = $option->getAllowedValues();
if (null !== $allowed) {
$allowed = array_filter($allowed, static fn ($value): bool => !$value instanceof \Closure);
usort($allowed, static function ($valueA, $valueB): int {
if ($valueA instanceof AllowedValueSubset) {
return -1;
}
if ($valueB instanceof AllowedValueSubset) {
return 1;
}
return strcasecmp(
Utils::toString($valueA),
Utils::toString($valueB)
);
});
if (0 === \count($allowed)) {
$allowed = null;
}
}
return $allowed;
}
protected function initialize(InputInterface $input, OutputInterface $output): void
{
$output->getFormatter()->setStyle('url', new OutputFormatterStyle('blue'));
}
}

View File

@@ -0,0 +1,91 @@
<?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\Console\Command;
use PhpCsFixer\Config;
use PhpCsFixer\ConfigInterface;
use PhpCsFixer\Console\ConfigurationResolver;
use PhpCsFixer\ToolInfoInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Path;
/**
* @author Markus Staab <markus.staab@redaxo.org>
*
* @internal
*/
#[AsCommand(name: 'list-files', description: 'List all files being fixed by the given config.')]
final class ListFilesCommand extends Command
{
/** @TODO PHP 8.0 - remove the property */
protected static $defaultName = 'list-files';
/** @TODO PHP 8.0 - remove the property */
protected static $defaultDescription = 'List all files being fixed by the given config.';
private ConfigInterface $defaultConfig;
private ToolInfoInterface $toolInfo;
public function __construct(ToolInfoInterface $toolInfo)
{
parent::__construct();
$this->defaultConfig = new Config();
$this->toolInfo = $toolInfo;
}
protected function configure(): void
{
$this->setDefinition(
[
new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a .php-cs-fixer.php file.'),
]
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$passedConfig = $input->getOption('config');
$cwd = getcwd();
$resolver = new ConfigurationResolver(
$this->defaultConfig,
[
'config' => $passedConfig,
],
$cwd,
$this->toolInfo
);
$finder = $resolver->getFinder();
foreach ($finder as $file) {
if ($file->isFile()) {
$relativePath = './'.Path::makeRelative($file->getRealPath(), $cwd);
// unify directory separators across operating system
$relativePath = str_replace('/', \DIRECTORY_SEPARATOR, $relativePath);
$output->writeln(escapeshellarg($relativePath));
}
}
return 0;
}
}

View File

@@ -0,0 +1,93 @@
<?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\Console\Command;
use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
use PhpCsFixer\Console\Report\ListSetsReport\ReporterFactory;
use PhpCsFixer\Console\Report\ListSetsReport\ReporterInterface;
use PhpCsFixer\Console\Report\ListSetsReport\ReportSummary;
use PhpCsFixer\Console\Report\ListSetsReport\TextReporter;
use PhpCsFixer\RuleSet\RuleSets;
use PhpCsFixer\Utils;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
#[AsCommand(name: 'list-sets', description: 'List all available RuleSets.')]
final class ListSetsCommand extends Command
{
/** @TODO PHP 8.0 - remove the property */
protected static $defaultName = 'list-sets';
/** @TODO PHP 8.0 - remove the property */
protected static $defaultDescription = 'List all available RuleSets.';
protected function configure(): void
{
$reporterFactory = new ReporterFactory();
$reporterFactory->registerBuiltInReporters();
$formats = $reporterFactory->getFormats();
\assert([] !== $formats);
$this->setDefinition(
[
new InputOption('format', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('To output results in other formats (%s).', $formats), (new TextReporter())->getFormat(), $formats),
]
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$reporter = $this->resolveReporterWithFactory(
$input->getOption('format'),
new ReporterFactory()
);
$reportSummary = new ReportSummary(
array_values(RuleSets::getSetDefinitions())
);
$report = $reporter->generate($reportSummary);
$output->isDecorated()
? $output->write(OutputFormatter::escape($report))
: $output->write($report, false, OutputInterface::OUTPUT_RAW);
return 0;
}
private function resolveReporterWithFactory(string $format, ReporterFactory $factory): ReporterInterface
{
try {
$factory->registerBuiltInReporters();
$reporter = $factory->getReporter($format);
} catch (\UnexpectedValueException $e) {
$formats = $factory->getFormats();
sort($formats);
throw new InvalidConfigurationException(\sprintf('The format "%s" is not defined, supported are %s.', $format, Utils::naturalLanguageJoin($formats)));
}
return $reporter;
}
}

View File

@@ -0,0 +1,186 @@
<?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\Console\Command;
use PhpCsFixer\Console\Application;
use PhpCsFixer\Console\SelfUpdate\NewVersionCheckerInterface;
use PhpCsFixer\PharCheckerInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\ToolInfoInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Igor Wiedler <igor@wiedler.ch>
* @author Stephane PY <py.stephane1@gmail.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
#[AsCommand(name: 'self-update', description: 'Update php-cs-fixer.phar to the latest stable version.')]
final class SelfUpdateCommand extends Command
{
/** @TODO PHP 8.0 - remove the property */
protected static $defaultName = 'self-update';
/** @TODO PHP 8.0 - remove the property */
protected static $defaultDescription = 'Update php-cs-fixer.phar to the latest stable version.';
private NewVersionCheckerInterface $versionChecker;
private ToolInfoInterface $toolInfo;
private PharCheckerInterface $pharChecker;
public function __construct(
NewVersionCheckerInterface $versionChecker,
ToolInfoInterface $toolInfo,
PharCheckerInterface $pharChecker
) {
parent::__construct();
$this->versionChecker = $versionChecker;
$this->toolInfo = $toolInfo;
$this->pharChecker = $pharChecker;
}
/**
* {@inheritdoc}
*
* Override here to only generate the help copy when used.
*/
public function getHelp(): string
{
return <<<'EOT'
The <info>%command.name%</info> command replace your php-cs-fixer.phar by the
latest version released on:
<comment>https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases</comment>
<info>$ php php-cs-fixer.phar %command.name%</info>
EOT;
}
protected function configure(): void
{
$this
->setAliases(['selfupdate'])
->setDefinition(
[
new InputOption('--force', '-f', InputOption::VALUE_NONE, 'Force update to next major version if available.'),
]
)
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($output instanceof ConsoleOutputInterface) {
$stdErr = $output->getErrorOutput();
$stdErr->writeln(Application::getAboutWithRuntime(true));
}
if (!$this->toolInfo->isInstalledAsPhar()) {
$output->writeln('<error>Self-update is available only for PHAR version.</error>');
return 1;
}
$currentVersion = $this->getApplication()->getVersion();
Preg::match('/^v?(?<major>\d+)\./', $currentVersion, $matches);
$currentMajor = (int) $matches['major'];
try {
$latestVersion = $this->versionChecker->getLatestVersion();
$latestVersionOfCurrentMajor = $this->versionChecker->getLatestVersionOfMajor($currentMajor);
} catch (\Exception $exception) {
$output->writeln(\sprintf(
'<error>Unable to determine newest version: %s</error>',
$exception->getMessage()
));
return 1;
}
if (1 !== $this->versionChecker->compareVersions($latestVersion, $currentVersion)) {
$output->writeln('<info>PHP CS Fixer is already up-to-date.</info>');
return 0;
}
$remoteTag = $latestVersion;
if (
0 !== $this->versionChecker->compareVersions($latestVersionOfCurrentMajor, $latestVersion)
&& true !== $input->getOption('force')
) {
$output->writeln(\sprintf('<info>A new major version of PHP CS Fixer is available</info> (<comment>%s</comment>)', $latestVersion));
$output->writeln(\sprintf('<info>Before upgrading please read</info> https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/%s/UPGRADE-v%s.md', $latestVersion, $currentMajor + 1));
$output->writeln('<info>If you are ready to upgrade run this command with</info> <comment>-f</comment>');
$output->writeln('<info>Checking for new minor/patch version...</info>');
if (1 !== $this->versionChecker->compareVersions($latestVersionOfCurrentMajor, $currentVersion)) {
$output->writeln('<info>No minor update for PHP CS Fixer.</info>');
return 0;
}
$remoteTag = $latestVersionOfCurrentMajor;
}
$localFilename = $_SERVER['argv'][0];
$realPath = realpath($localFilename);
if (false !== $realPath) {
$localFilename = $realPath;
}
if (!is_writable($localFilename)) {
$output->writeln(\sprintf('<error>No permission to update</error> "%s" <error>file.</error>', $localFilename));
return 1;
}
$tempFilename = \dirname($localFilename).'/'.basename($localFilename, '.phar').'-tmp.phar';
$remoteFilename = $this->toolInfo->getPharDownloadUri($remoteTag);
if (false === @copy($remoteFilename, $tempFilename)) {
$output->writeln(\sprintf('<error>Unable to download new version</error> %s <error>from the server.</error>', $remoteTag));
return 1;
}
chmod($tempFilename, 0777 & ~umask());
$pharInvalidityReason = $this->pharChecker->checkFileValidity($tempFilename);
if (null !== $pharInvalidityReason) {
unlink($tempFilename);
$output->writeln(\sprintf('<error>The download of</error> %s <error>is corrupt (%s).</error>', $remoteTag, $pharInvalidityReason));
$output->writeln('<error>Please re-run the "self-update" command to try again.</error>');
return 1;
}
rename($tempFilename, $localFilename);
$output->writeln(\sprintf('<info>PHP CS Fixer updated</info> (<comment>%s</comment> -> <comment>%s</comment>)', $currentVersion, $remoteTag));
return 0;
}
}

View File

@@ -0,0 +1,244 @@
<?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\Console\Command;
use Clue\React\NDJson\Decoder;
use Clue\React\NDJson\Encoder;
use PhpCsFixer\Cache\NullCacheManager;
use PhpCsFixer\Config;
use PhpCsFixer\Console\ConfigurationResolver;
use PhpCsFixer\Error\ErrorsManager;
use PhpCsFixer\Runner\Event\FileProcessed;
use PhpCsFixer\Runner\Parallel\ParallelAction;
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
use PhpCsFixer\Runner\Parallel\ParallelisationException;
use PhpCsFixer\Runner\Runner;
use PhpCsFixer\ToolInfoInterface;
use React\EventLoop\StreamSelectLoop;
use React\Socket\ConnectionInterface;
use React\Socket\TcpConnector;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* @author Greg Korba <greg@codito.dev>
*
* @internal
*/
#[AsCommand(name: 'worker', description: 'Internal command for running fixers in parallel', hidden: true)]
final class WorkerCommand extends Command
{
/** @var string Prefix used before JSON-encoded error printed in the worker's process */
public const ERROR_PREFIX = 'WORKER_ERROR::';
/** @TODO PHP 8.0 - remove the property */
protected static $defaultName = 'worker';
/** @TODO PHP 8.0 - remove the property */
protected static $defaultDescription = 'Internal command for running fixers in parallel';
private ToolInfoInterface $toolInfo;
private ConfigurationResolver $configurationResolver;
private ErrorsManager $errorsManager;
private EventDispatcherInterface $eventDispatcher;
/** @var list<FileProcessed> */
private array $events;
public function __construct(ToolInfoInterface $toolInfo)
{
parent::__construct();
$this->setHidden(true);
$this->toolInfo = $toolInfo;
$this->errorsManager = new ErrorsManager();
$this->eventDispatcher = new EventDispatcher();
}
protected function configure(): void
{
$this->setDefinition(
[
new InputOption('port', null, InputOption::VALUE_REQUIRED, 'Specifies parallelisation server\'s port.'),
new InputOption('identifier', null, InputOption::VALUE_REQUIRED, 'Specifies parallelisation process\' identifier.'),
new InputOption('allow-risky', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('Are risky fixers allowed (%s).', ConfigurationResolver::BOOL_VALUES), null, ConfigurationResolver::BOOL_VALUES),
new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a config file.'),
new InputOption('dry-run', '', InputOption::VALUE_NONE, 'Only shows which files would have been modified.'),
new InputOption('rules', '', InputOption::VALUE_REQUIRED, 'List of rules that should be run against configured paths.'),
new InputOption('using-cache', '', InputOption::VALUE_REQUIRED, HelpCommand::getDescriptionWithAllowedValues('Should cache be used (%s).', ConfigurationResolver::BOOL_VALUES), null, ConfigurationResolver::BOOL_VALUES),
new InputOption('cache-file', '', InputOption::VALUE_REQUIRED, 'The path to the cache file.'),
new InputOption('diff', '', InputOption::VALUE_NONE, 'Prints diff for each file.'),
new InputOption('stop-on-violation', '', InputOption::VALUE_NONE, 'Stop execution on first violation.'),
]
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$errorOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output;
$identifier = $input->getOption('identifier');
$port = $input->getOption('port');
if (null === $identifier || !is_numeric($port)) {
throw new ParallelisationException('Missing parallelisation options');
}
try {
$runner = $this->createRunner($input);
} catch (\Throwable $e) {
throw new ParallelisationException('Unable to create runner: '.$e->getMessage(), 0, $e);
}
$loop = new StreamSelectLoop();
$tcpConnector = new TcpConnector($loop);
$tcpConnector
->connect(\sprintf('127.0.0.1:%d', $port))
->then(
/** @codeCoverageIgnore */
function (ConnectionInterface $connection) use ($loop, $runner, $identifier): void {
$out = new Encoder($connection, \JSON_INVALID_UTF8_IGNORE);
$in = new Decoder($connection, true, 512, \JSON_INVALID_UTF8_IGNORE);
// [REACT] Initialise connection with the parallelisation operator
$out->write(['action' => ParallelAction::WORKER_HELLO, 'identifier' => $identifier]);
$handleError = static function (\Throwable $error) use ($out): void {
$out->write([
'action' => ParallelAction::WORKER_ERROR_REPORT,
'class' => \get_class($error),
'message' => $error->getMessage(),
'file' => $error->getFile(),
'line' => $error->getLine(),
'code' => $error->getCode(),
'trace' => $error->getTraceAsString(),
]);
};
$out->on('error', $handleError);
$in->on('error', $handleError);
// [REACT] Listen for messages from the parallelisation operator (analysis requests)
$in->on('data', function (array $json) use ($loop, $runner, $out): void {
$action = $json['action'] ?? null;
// Parallelisation operator does not have more to do, let's close the connection
if (ParallelAction::RUNNER_THANK_YOU === $action) {
$loop->stop();
return;
}
if (ParallelAction::RUNNER_REQUEST_ANALYSIS !== $action) {
// At this point we only expect analysis requests, if any other action happen, we need to fix the code.
throw new \LogicException(\sprintf('Unexpected action ParallelAction::%s.', $action));
}
/** @var iterable<int, string> $files */
$files = $json['files'];
foreach ($files as $path) {
// Reset events because we want to collect only those coming from analysed files chunk
$this->events = [];
$runner->setFileIterator(new \ArrayIterator([new \SplFileInfo($path)]));
$analysisResult = $runner->fix();
if (1 !== \count($this->events)) {
throw new ParallelisationException('Runner did not report a fixing event or reported too many.');
}
if (1 < \count($analysisResult)) {
throw new ParallelisationException('Runner returned more analysis results than expected.');
}
$out->write([
'action' => ParallelAction::WORKER_RESULT,
'file' => $path,
'fileHash' => $this->events[0]->getFileHash(),
'status' => $this->events[0]->getStatus(),
'fixInfo' => array_pop($analysisResult),
'errors' => $this->errorsManager->forPath($path),
]);
}
// Request another file chunk (if available, the parallelisation operator will request new "run" action)
$out->write(['action' => ParallelAction::WORKER_GET_FILE_CHUNK]);
});
},
static function (\Throwable $error) use ($errorOutput): void {
// @TODO Verify onRejected behaviour → https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/pull/7777#discussion_r1590399285
$errorOutput->writeln($error->getMessage());
}
)
;
$loop->run();
return Command::SUCCESS;
}
private function createRunner(InputInterface $input): Runner
{
$passedConfig = $input->getOption('config');
$passedRules = $input->getOption('rules');
if (null !== $passedConfig && null !== $passedRules) {
throw new \RuntimeException('Passing both `--config` and `--rules` options is not allowed');
}
// There's no one single source of truth when it comes to fixing single file, we need to collect statuses from events.
$this->eventDispatcher->addListener(FileProcessed::NAME, function (FileProcessed $event): void {
$this->events[] = $event;
});
$this->configurationResolver = new ConfigurationResolver(
new Config(),
[
'allow-risky' => $input->getOption('allow-risky'),
'config' => $passedConfig,
'dry-run' => $input->getOption('dry-run'),
'rules' => $passedRules,
'path' => [],
'path-mode' => ConfigurationResolver::PATH_MODE_OVERRIDE, // IMPORTANT! WorkerCommand is called with file that already passed filtering, so here we can rely on PATH_MODE_OVERRIDE.
'using-cache' => $input->getOption('using-cache'),
'cache-file' => $input->getOption('cache-file'),
'diff' => $input->getOption('diff'),
'stop-on-violation' => $input->getOption('stop-on-violation'),
],
getcwd(), // @phpstan-ignore-line
$this->toolInfo
);
return new Runner(
null, // Paths are known when parallelisation server requests new chunk, not now
$this->configurationResolver->getFixers(),
$this->configurationResolver->getDiffer(),
$this->eventDispatcher,
$this->errorsManager,
$this->configurationResolver->getLinter(),
$this->configurationResolver->isDryRun(),
new NullCacheManager(), // IMPORTANT! We pass null cache, as cache is read&write in main process and we do not need to do it again.
$this->configurationResolver->getDirectory(),
$this->configurationResolver->shouldStopOnViolation(),
ParallelConfigFactory::sequential(), // IMPORTANT! Worker must run in sequential mode.
null,
$this->configurationResolver->getConfigFile()
);
}
}