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,225 @@
<?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;
use PhpCsFixer\Console\Command\CheckCommand;
use PhpCsFixer\Console\Command\DescribeCommand;
use PhpCsFixer\Console\Command\FixCommand;
use PhpCsFixer\Console\Command\HelpCommand;
use PhpCsFixer\Console\Command\ListFilesCommand;
use PhpCsFixer\Console\Command\ListSetsCommand;
use PhpCsFixer\Console\Command\SelfUpdateCommand;
use PhpCsFixer\Console\Command\WorkerCommand;
use PhpCsFixer\Console\SelfUpdate\GithubClient;
use PhpCsFixer\Console\SelfUpdate\NewVersionChecker;
use PhpCsFixer\PharChecker;
use PhpCsFixer\Runner\Parallel\WorkerException;
use PhpCsFixer\ToolInfo;
use PhpCsFixer\Utils;
use Symfony\Component\Console\Application as BaseApplication;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\CompleteCommand;
use Symfony\Component\Console\Command\DumpCompletionCommand;
use Symfony\Component\Console\Command\ListCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class Application extends BaseApplication
{
public const NAME = 'PHP CS Fixer';
public const VERSION = '3.86.0';
public const VERSION_CODENAME = 'Alexander';
/**
* @readonly
*/
private ToolInfo $toolInfo;
private ?Command $executedCommand = null;
public function __construct()
{
parent::__construct(self::NAME, self::VERSION);
$this->toolInfo = new ToolInfo();
// in alphabetical order
$this->add(new DescribeCommand());
$this->add(new CheckCommand($this->toolInfo));
$this->add(new FixCommand($this->toolInfo));
$this->add(new ListFilesCommand($this->toolInfo));
$this->add(new ListSetsCommand());
$this->add(new SelfUpdateCommand(
new NewVersionChecker(new GithubClient()),
$this->toolInfo,
new PharChecker()
));
$this->add(new WorkerCommand($this->toolInfo));
}
// polyfill for `add` method, as it is not available in Symfony 8.0
public function add(Command $command): ?Command
{
if (method_exists($this, 'addCommand')) { // @phpstan-ignore function.impossibleType
return $this->addCommand($command);
}
return parent::add($command);
}
public static function getMajorVersion(): int
{
return (int) explode('.', self::VERSION)[0];
}
public function doRun(InputInterface $input, OutputInterface $output): int
{
$stdErr = $output instanceof ConsoleOutputInterface
? $output->getErrorOutput()
: ($input->hasParameterOption('--format', true) && 'txt' !== $input->getParameterOption('--format', null, true) ? null : $output);
if (null !== $stdErr) {
$warningsDetector = new WarningsDetector($this->toolInfo);
$warningsDetector->detectOldVendor();
$warningsDetector->detectOldMajor();
$warningsDetector->detectNonMonolithic();
$warnings = $warningsDetector->getWarnings();
if (\count($warnings) > 0) {
foreach ($warnings as $warning) {
$stdErr->writeln(\sprintf($stdErr->isDecorated() ? '<bg=yellow;fg=black;>%s</>' : '%s', $warning));
}
$stdErr->writeln('');
}
}
$result = parent::doRun($input, $output);
if (
null !== $stdErr
&& $output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE
) {
$triggeredDeprecations = Utils::getTriggeredDeprecations();
if (\count($triggeredDeprecations) > 0) {
$stdErr->writeln('');
$stdErr->writeln($stdErr->isDecorated() ? '<bg=yellow;fg=black;>Detected deprecations in use (they will stop working in next major release):</>' : 'Detected deprecations in use (they will stop working in next major release):');
foreach ($triggeredDeprecations as $deprecation) {
$stdErr->writeln(\sprintf('- %s', $deprecation));
}
}
}
return $result;
}
/**
* @internal
*/
public static function getAbout(bool $decorated = false): string
{
$longVersion = \sprintf('%s <info>%s</info>', self::NAME, self::VERSION);
$commit = '@git-commit@';
$versionCommit = '';
if ('@'.'git-commit@' !== $commit) { /** @phpstan-ignore-line as `$commit` is replaced during phar building */
$versionCommit = substr($commit, 0, 7);
}
$about = implode('', [
$longVersion,
$versionCommit ? \sprintf(' <info>(%s)</info>', $versionCommit) : '', // @phpstan-ignore-line to avoid `Ternary operator condition is always true|false.`
self::VERSION_CODENAME ? \sprintf(' <info>%s</info>', self::VERSION_CODENAME) : '', // @phpstan-ignore-line to avoid `Ternary operator condition is always true|false.`
' by <comment>Fabien Potencier</comment>, <comment>Dariusz Ruminski</comment> and <comment>contributors</comment>.',
]);
if (false === $decorated) {
return strip_tags($about);
}
return $about;
}
/**
* @internal
*/
public static function getAboutWithRuntime(bool $decorated = false): string
{
$about = self::getAbout(true)."\nPHP runtime: <info>".\PHP_VERSION.'</info>';
if (false === $decorated) {
return strip_tags($about);
}
return $about;
}
public function getLongVersion(): string
{
return self::getAboutWithRuntime(true);
}
protected function getDefaultCommands(): array
{
return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()];
}
/**
* @throws \Throwable
*/
protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output): int
{
$this->executedCommand = $command;
return parent::doRunCommand($command, $input, $output);
}
protected function doRenderThrowable(\Throwable $e, OutputInterface $output): void
{
// Since parallel analysis utilises child processes, and they have their own output,
// we need to capture the output of the child process to determine it there was an exception.
// Default render format is not machine-friendly, so we need to override it for `worker` command,
// in order to be able to easily parse exception data for further displaying on main process' side.
if ($this->executedCommand instanceof WorkerCommand) {
$output->writeln(WorkerCommand::ERROR_PREFIX.json_encode(
[
'class' => \get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'code' => $e->getCode(),
'trace' => $e->getTraceAsString(),
]
));
return;
}
parent::doRenderThrowable($e, $output);
if ($output->isVeryVerbose() && $e instanceof WorkerException) {
$output->writeln('<comment>Original trace from worker:</comment>');
$output->writeln('');
$output->writeln($e->getOriginalTraceAsString());
$output->writeln('');
}
}
}

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()
);
}
}

View File

@@ -0,0 +1,997 @@
<?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;
use PhpCsFixer\Cache\CacheManagerInterface;
use PhpCsFixer\Cache\Directory;
use PhpCsFixer\Cache\DirectoryInterface;
use PhpCsFixer\Cache\FileCacheManager;
use PhpCsFixer\Cache\FileHandler;
use PhpCsFixer\Cache\NullCacheManager;
use PhpCsFixer\Cache\Signature;
use PhpCsFixer\ConfigInterface;
use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
use PhpCsFixer\Console\Output\Progress\ProgressOutputType;
use PhpCsFixer\Console\Report\FixReport\ReporterFactory;
use PhpCsFixer\Console\Report\FixReport\ReporterInterface;
use PhpCsFixer\Differ\DifferInterface;
use PhpCsFixer\Differ\NullDiffer;
use PhpCsFixer\Differ\UnifiedDiffer;
use PhpCsFixer\Finder;
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\FixerFactory;
use PhpCsFixer\Linter\Linter;
use PhpCsFixer\Linter\LinterInterface;
use PhpCsFixer\ParallelAwareConfigInterface;
use PhpCsFixer\RuleSet\RuleSet;
use PhpCsFixer\RuleSet\RuleSetInterface;
use PhpCsFixer\Runner\Parallel\ParallelConfig;
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
use PhpCsFixer\StdinFileInfo;
use PhpCsFixer\ToolInfoInterface;
use PhpCsFixer\UnsupportedPhpVersionAllowedConfigInterface;
use PhpCsFixer\Utils;
use PhpCsFixer\WhitespacesFixerConfig;
use PhpCsFixer\WordMatcher;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder as SymfonyFinder;
/**
* The resolver that resolves configuration to use by command line options and config.
*
* @internal
*
* @phpstan-type _Options array{
* allow-risky: null|string,
* cache-file: null|string,
* config: null|string,
* diff: null|string,
* dry-run: null|bool,
* format: null|string,
* path: list<string>,
* path-mode: value-of<self::PATH_MODE_VALUES>,
* rules: null|string,
* sequential: null|string,
* show-progress: null|string,
* stop-on-violation: null|bool,
* using-cache: null|string,
* allow-unsupported-php-version: null|bool,
* verbosity: null|string,
* }
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Katsuhiro Ogawa <ko.fivestar@gmail.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class ConfigurationResolver
{
public const PATH_MODE_OVERRIDE = 'override';
public const PATH_MODE_INTERSECTION = 'intersection';
public const PATH_MODE_VALUES = [
self::PATH_MODE_OVERRIDE,
self::PATH_MODE_INTERSECTION,
];
public const BOOL_YES = 'yes';
public const BOOL_NO = 'no';
public const BOOL_VALUES = [
self::BOOL_YES,
self::BOOL_NO,
];
private ?bool $allowRisky = null;
private ?ConfigInterface $config = null;
private ?string $configFile = null;
private string $cwd;
private ConfigInterface $defaultConfig;
private ?ReporterInterface $reporter = null;
private ?bool $isStdIn = null;
private ?bool $isDryRun = null;
/**
* @var null|list<FixerInterface>
*/
private ?array $fixers = null;
private ?bool $configFinderIsOverridden = null;
private ToolInfoInterface $toolInfo;
/**
* @var _Options
*/
private array $options = [
'allow-risky' => null,
'cache-file' => null,
'config' => null,
'diff' => null,
'dry-run' => null,
'format' => null,
'path' => [],
'path-mode' => self::PATH_MODE_OVERRIDE,
'rules' => null,
'sequential' => null,
'show-progress' => null,
'stop-on-violation' => null,
'using-cache' => null,
'allow-unsupported-php-version' => null,
'verbosity' => null,
];
private ?string $cacheFile = null;
private ?CacheManagerInterface $cacheManager = null;
private ?DifferInterface $differ = null;
private ?Directory $directory = null;
/**
* @var null|iterable<\SplFileInfo>
*/
private ?iterable $finder = null;
private ?string $format = null;
private ?Linter $linter = null;
/**
* @var null|list<string>
*/
private ?array $path = null;
/**
* @var null|ProgressOutputType::*
*/
private $progress;
private ?RuleSet $ruleSet = null;
private ?bool $usingCache = null;
private ?bool $isUnsupportedPhpVersionAllowed = null;
private ?FixerFactory $fixerFactory = null;
/**
* @param array<string, mixed> $options
*/
public function __construct(
ConfigInterface $config,
array $options,
string $cwd,
ToolInfoInterface $toolInfo
) {
$this->defaultConfig = $config;
$this->cwd = $cwd;
$this->toolInfo = $toolInfo;
foreach ($options as $name => $value) {
$this->setOption($name, $value);
}
}
public function getCacheFile(): ?string
{
if (!$this->getUsingCache()) {
return null;
}
if (null === $this->cacheFile) {
if (null === $this->options['cache-file']) {
$this->cacheFile = $this->getConfig()->getCacheFile();
} else {
$this->cacheFile = $this->options['cache-file'];
}
}
return $this->cacheFile;
}
public function getCacheManager(): CacheManagerInterface
{
if (null === $this->cacheManager) {
$cacheFile = $this->getCacheFile();
if (null === $cacheFile) {
$this->cacheManager = new NullCacheManager();
} else {
$this->cacheManager = new FileCacheManager(
new FileHandler($cacheFile),
new Signature(
\PHP_VERSION,
$this->toolInfo->getVersion(),
$this->getConfig()->getIndent(),
$this->getConfig()->getLineEnding(),
$this->getRules()
),
$this->isDryRun(),
$this->getDirectory()
);
}
}
return $this->cacheManager;
}
public function getConfig(): ConfigInterface
{
if (null === $this->config) {
foreach ($this->computeConfigFiles() as $configFile) {
if (!file_exists($configFile)) {
continue;
}
$configFileBasename = basename($configFile);
/** @TODO v4 drop handling (triggering error) for v2 config names */
$deprecatedConfigs = [
'.php_cs' => '.php-cs-fixer.php',
'.php_cs.dist' => '.php-cs-fixer.dist.php',
];
if (isset($deprecatedConfigs[$configFileBasename])) {
throw new InvalidConfigurationException("Configuration file `{$configFileBasename}` is outdated, rename to `{$deprecatedConfigs[$configFileBasename]}`.");
}
$this->config = self::separatedContextLessInclude($configFile);
$this->configFile = $configFile;
break;
}
if (null === $this->config) {
$this->config = $this->defaultConfig;
}
}
return $this->config;
}
public function getParallelConfig(): ParallelConfig
{
$config = $this->getConfig();
return true !== $this->options['sequential'] && $config instanceof ParallelAwareConfigInterface
? $config->getParallelConfig()
: ParallelConfigFactory::sequential();
}
public function getConfigFile(): ?string
{
if (null === $this->configFile) {
$this->getConfig();
}
return $this->configFile;
}
public function getDiffer(): DifferInterface
{
if (null === $this->differ) {
$this->differ = (true === $this->options['diff']) ? new UnifiedDiffer() : new NullDiffer();
}
return $this->differ;
}
public function getDirectory(): DirectoryInterface
{
if (null === $this->directory) {
$path = $this->getCacheFile();
if (null === $path) {
$absolutePath = $this->cwd;
} else {
$filesystem = new Filesystem();
$absolutePath = $filesystem->isAbsolutePath($path)
? $path
: $this->cwd.\DIRECTORY_SEPARATOR.$path;
$absolutePath = \dirname($absolutePath);
}
$this->directory = new Directory($absolutePath);
}
return $this->directory;
}
/**
* @return list<FixerInterface>
*/
public function getFixers(): array
{
if (null === $this->fixers) {
$this->fixers = $this->createFixerFactory()
->useRuleSet($this->getRuleSet())
->setWhitespacesConfig(new WhitespacesFixerConfig($this->config->getIndent(), $this->config->getLineEnding()))
->getFixers()
;
if (false === $this->getRiskyAllowed()) {
$riskyFixers = array_map(
static fn (FixerInterface $fixer): string => $fixer->getName(),
array_filter(
$this->fixers,
static fn (FixerInterface $fixer): bool => $fixer->isRisky()
)
);
if (\count($riskyFixers) > 0) {
throw new InvalidConfigurationException(\sprintf('The rules contain risky fixers (%s), but they are not allowed to run. Perhaps you forget to use --allow-risky=yes option?', Utils::naturalLanguageJoin($riskyFixers)));
}
}
}
return $this->fixers;
}
public function getLinter(): LinterInterface
{
if (null === $this->linter) {
$this->linter = new Linter();
}
return $this->linter;
}
/**
* Returns path.
*
* @return list<string>
*/
public function getPath(): array
{
if (null === $this->path) {
$filesystem = new Filesystem();
$cwd = $this->cwd;
if (1 === \count($this->options['path']) && '-' === $this->options['path'][0]) {
$this->path = $this->options['path'];
} else {
$this->path = array_map(
static function (string $rawPath) use ($cwd, $filesystem): string {
$path = trim($rawPath);
if ('' === $path) {
throw new InvalidConfigurationException("Invalid path: \"{$rawPath}\".");
}
$absolutePath = $filesystem->isAbsolutePath($path)
? $path
: $cwd.\DIRECTORY_SEPARATOR.$path;
if (!file_exists($absolutePath)) {
throw new InvalidConfigurationException(\sprintf(
'The path "%s" is not readable.',
$path
));
}
return $absolutePath;
},
$this->options['path']
);
}
}
return $this->path;
}
/**
* @return ProgressOutputType::*
*
* @throws InvalidConfigurationException
*/
public function getProgressType(): string
{
if (null === $this->progress) {
if ('txt' === $this->resolveFormat()) {
$progressType = $this->options['show-progress'];
if (null === $progressType) {
$progressType = $this->getConfig()->getHideProgress()
? ProgressOutputType::NONE
: ProgressOutputType::BAR;
} elseif (!\in_array($progressType, ProgressOutputType::all(), true)) {
throw new InvalidConfigurationException(\sprintf(
'The progress type "%s" is not defined, supported are %s.',
$progressType,
Utils::naturalLanguageJoin(ProgressOutputType::all())
));
}
$this->progress = $progressType;
} else {
$this->progress = ProgressOutputType::NONE;
}
}
return $this->progress;
}
public function getReporter(): ReporterInterface
{
if (null === $this->reporter) {
$reporterFactory = new ReporterFactory();
$reporterFactory->registerBuiltInReporters();
$format = $this->resolveFormat();
try {
$this->reporter = $reporterFactory->getReporter($format);
} catch (\UnexpectedValueException $e) {
$formats = $reporterFactory->getFormats();
sort($formats);
throw new InvalidConfigurationException(\sprintf('The format "%s" is not defined, supported are %s.', $format, Utils::naturalLanguageJoin($formats)));
}
}
return $this->reporter;
}
public function getRiskyAllowed(): bool
{
if (null === $this->allowRisky) {
if (null === $this->options['allow-risky']) {
$this->allowRisky = $this->getConfig()->getRiskyAllowed();
} else {
$this->allowRisky = $this->resolveOptionBooleanValue('allow-risky');
}
}
return $this->allowRisky;
}
/**
* Returns rules.
*
* @return array<string, array<string, mixed>|bool>
*/
public function getRules(): array
{
return $this->getRuleSet()->getRules();
}
public function getUsingCache(): bool
{
if (null === $this->usingCache) {
if (null === $this->options['using-cache']) {
$this->usingCache = $this->getConfig()->getUsingCache();
} else {
$this->usingCache = $this->resolveOptionBooleanValue('using-cache');
}
}
$this->usingCache = $this->usingCache && $this->isCachingAllowedForRuntime();
return $this->usingCache;
}
public function getUnsupportedPhpVersionAllowed(): bool
{
if (null === $this->isUnsupportedPhpVersionAllowed) {
if (null === $this->options['allow-unsupported-php-version']) {
$config = $this->getConfig();
$this->isUnsupportedPhpVersionAllowed = $config instanceof UnsupportedPhpVersionAllowedConfigInterface
? $config->getUnsupportedPhpVersionAllowed()
: false;
} else {
$this->isUnsupportedPhpVersionAllowed = $this->resolveOptionBooleanValue('allow-unsupported-php-version');
}
}
return $this->isUnsupportedPhpVersionAllowed;
}
/**
* @return iterable<\SplFileInfo>
*/
public function getFinder(): iterable
{
if (null === $this->finder) {
$this->finder = $this->resolveFinder();
}
return $this->finder;
}
/**
* Returns dry-run flag.
*/
public function isDryRun(): bool
{
if (null === $this->isDryRun) {
if ($this->isStdIn()) {
// Can't write to STDIN
$this->isDryRun = true;
} else {
$this->isDryRun = $this->options['dry-run'];
}
}
return $this->isDryRun;
}
public function shouldStopOnViolation(): bool
{
return $this->options['stop-on-violation'];
}
public function configFinderIsOverridden(): bool
{
if (null === $this->configFinderIsOverridden) {
$this->resolveFinder();
}
return $this->configFinderIsOverridden;
}
/**
* Compute file candidates for config file.
*
* @return list<string>
*/
private function computeConfigFiles(): array
{
$configFile = $this->options['config'];
if (null !== $configFile) {
if (false === file_exists($configFile) || false === is_readable($configFile)) {
throw new InvalidConfigurationException(\sprintf('Cannot read config file "%s".', $configFile));
}
return [$configFile];
}
$path = $this->getPath();
if ($this->isStdIn() || 0 === \count($path)) {
$configDir = $this->cwd;
} elseif (1 < \count($path)) {
throw new InvalidConfigurationException('For multiple paths config parameter is required.');
} elseif (!is_file($path[0])) {
$configDir = $path[0];
} else {
$dirName = pathinfo($path[0], \PATHINFO_DIRNAME);
$configDir = is_dir($dirName) ? $dirName : $path[0];
}
$candidates = [
$configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php',
$configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php',
// @TODO v4 drop handling (triggering error) for v2 config names
$configDir.\DIRECTORY_SEPARATOR.'.php_cs', // old v2 config, present here only to throw nice error message later
$configDir.\DIRECTORY_SEPARATOR.'.php_cs.dist', // old v2 config, present here only to throw nice error message later
];
if ($configDir !== $this->cwd) {
$candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php';
$candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php';
// @TODO v4 drop handling (triggering error) for v2 config names
$candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php_cs'; // old v2 config, present here only to throw nice error message later
$candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php_cs.dist'; // old v2 config, present here only to throw nice error message later
}
return $candidates;
}
private function createFixerFactory(): FixerFactory
{
if (null === $this->fixerFactory) {
$fixerFactory = new FixerFactory();
$fixerFactory->registerBuiltInFixers();
$fixerFactory->registerCustomFixers($this->getConfig()->getCustomFixers());
$this->fixerFactory = $fixerFactory;
}
return $this->fixerFactory;
}
private function resolveFormat(): string
{
if (null === $this->format) {
$formatCandidate = $this->options['format'] ?? $this->getConfig()->getFormat();
$parts = explode(',', $formatCandidate);
if (\count($parts) > 2) {
throw new InvalidConfigurationException(\sprintf('The format "%s" is invalid.', $formatCandidate));
}
$this->format = $parts[0];
if ('@auto' === $this->format) {
$this->format = $parts[1] ?? 'txt';
if (filter_var(getenv('GITLAB_CI'), \FILTER_VALIDATE_BOOL)) {
$this->format = 'gitlab';
}
}
}
return $this->format;
}
private function getRuleSet(): RuleSetInterface
{
if (null === $this->ruleSet) {
$rules = $this->parseRules();
$this->validateRules($rules);
$this->ruleSet = new RuleSet($rules);
}
return $this->ruleSet;
}
private function isStdIn(): bool
{
if (null === $this->isStdIn) {
$this->isStdIn = 1 === \count($this->options['path']) && '-' === $this->options['path'][0];
}
return $this->isStdIn;
}
/**
* @template T
*
* @param iterable<T> $iterable
*
* @return \Traversable<T>
*/
private function iterableToTraversable(iterable $iterable): \Traversable
{
return \is_array($iterable) ? new \ArrayIterator($iterable) : $iterable;
}
/**
* @return array<string, mixed>
*/
private function parseRules(): array
{
if (null === $this->options['rules']) {
return $this->getConfig()->getRules();
}
$rules = trim($this->options['rules']);
if ('' === $rules) {
throw new InvalidConfigurationException('Empty rules value is not allowed.');
}
if (str_starts_with($rules, '{')) {
$rules = json_decode($rules, true);
if (\JSON_ERROR_NONE !== json_last_error()) {
throw new InvalidConfigurationException(\sprintf('Invalid JSON rules input: "%s".', json_last_error_msg()));
}
return $rules;
}
$rules = [];
foreach (explode(',', $this->options['rules']) as $rule) {
$rule = trim($rule);
if ('' === $rule) {
throw new InvalidConfigurationException('Empty rule name is not allowed.');
}
if (str_starts_with($rule, '-')) {
$rules[substr($rule, 1)] = false;
} else {
$rules[$rule] = true;
}
}
return $rules;
}
/**
* @param array<string, mixed> $rules
*
* @throws InvalidConfigurationException
*/
private function validateRules(array $rules): void
{
/**
* Create a ruleset that contains all configured rules, even when they originally have been disabled.
*
* @see RuleSet::resolveSet()
*/
$ruleSet = [];
foreach ($rules as $key => $value) {
if (\is_int($key)) {
throw new InvalidConfigurationException(\sprintf('Missing value for "%s" rule/set.', $value));
}
$ruleSet[$key] = true;
}
$ruleSet = new RuleSet($ruleSet);
$configuredFixers = array_keys($ruleSet->getRules());
$fixers = $this->createFixerFactory()->getFixers();
$availableFixers = array_map(static fn (FixerInterface $fixer): string => $fixer->getName(), $fixers);
$unknownFixers = array_diff($configuredFixers, $availableFixers);
if (\count($unknownFixers) > 0) {
$renamedRules = [
'blank_line_before_return' => [
'new_name' => 'blank_line_before_statement',
'config' => ['statements' => ['return']],
],
'final_static_access' => [
'new_name' => 'self_static_accessor',
],
'hash_to_slash_comment' => [
'new_name' => 'single_line_comment_style',
'config' => ['comment_types' => ['hash']],
],
'lowercase_constants' => [
'new_name' => 'constant_case',
'config' => ['case' => 'lower'],
],
'no_extra_consecutive_blank_lines' => [
'new_name' => 'no_extra_blank_lines',
],
'no_multiline_whitespace_before_semicolons' => [
'new_name' => 'multiline_whitespace_before_semicolons',
],
'no_short_echo_tag' => [
'new_name' => 'echo_tag_syntax',
'config' => ['format' => 'long'],
],
'php_unit_ordered_covers' => [
'new_name' => 'phpdoc_order_by_value',
'config' => ['annotations' => ['covers']],
],
'phpdoc_inline_tag' => [
'new_name' => 'general_phpdoc_tag_rename, phpdoc_inline_tag_normalizer and phpdoc_tag_type',
],
'pre_increment' => [
'new_name' => 'increment_style',
'config' => ['style' => 'pre'],
],
'psr0' => [
'new_name' => 'psr_autoloading',
'config' => ['dir' => 'x'],
],
'psr4' => [
'new_name' => 'psr_autoloading',
],
'silenced_deprecation_error' => [
'new_name' => 'error_suppression',
],
'trailing_comma_in_multiline_array' => [
'new_name' => 'trailing_comma_in_multiline',
'config' => ['elements' => ['arrays']],
],
];
$message = 'The rules contain unknown fixers: ';
$hasOldRule = false;
foreach ($unknownFixers as $unknownFixer) {
if (isset($renamedRules[$unknownFixer])) { // Check if present as old renamed rule
$hasOldRule = true;
$message .= \sprintf(
'"%s" is renamed (did you mean "%s"?%s), ',
$unknownFixer,
$renamedRules[$unknownFixer]['new_name'],
isset($renamedRules[$unknownFixer]['config']) ? ' (note: use configuration "'.Utils::toString($renamedRules[$unknownFixer]['config']).'")' : ''
);
} else { // Go to normal matcher if it is not a renamed rule
$matcher = new WordMatcher($availableFixers);
$alternative = $matcher->match($unknownFixer);
$message .= \sprintf(
'"%s"%s, ',
$unknownFixer,
null === $alternative ? '' : ' (did you mean "'.$alternative.'"?)'
);
}
}
$message = substr($message, 0, -2).'.';
if ($hasOldRule) {
$message .= "\nFor more info about updating see: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/v3.0.0/UPGRADE-v3.md#renamed-ruless.";
}
throw new InvalidConfigurationException($message);
}
foreach ($fixers as $fixer) {
$fixerName = $fixer->getName();
if (isset($rules[$fixerName]) && $fixer instanceof DeprecatedFixerInterface) {
$successors = $fixer->getSuccessorsNames();
$messageEnd = [] === $successors
? \sprintf(' and will be removed in version %d.0.', Application::getMajorVersion() + 1)
: \sprintf('. Use %s instead.', str_replace('`', '"', Utils::naturalLanguageJoinWithBackticks($successors)));
Utils::triggerDeprecation(new \RuntimeException("Rule \"{$fixerName}\" is deprecated{$messageEnd}"));
}
}
}
/**
* Apply path on config instance.
*
* @return iterable<\SplFileInfo>
*/
private function resolveFinder(): iterable
{
$this->configFinderIsOverridden = false;
if ($this->isStdIn()) {
return new \ArrayIterator([new StdinFileInfo()]);
}
if (!\in_array(
$this->options['path-mode'],
self::PATH_MODE_VALUES,
true
)) {
throw new InvalidConfigurationException(\sprintf(
'The path-mode "%s" is not defined, supported are %s.',
$this->options['path-mode'],
Utils::naturalLanguageJoin(self::PATH_MODE_VALUES)
));
}
$isIntersectionPathMode = self::PATH_MODE_INTERSECTION === $this->options['path-mode'];
$paths = array_map(
static fn (string $path) => realpath($path),
$this->getPath()
);
if (0 === \count($paths)) {
if ($isIntersectionPathMode) {
return new \ArrayIterator([]);
}
return $this->iterableToTraversable($this->getConfig()->getFinder());
}
$pathsByType = [
'file' => [],
'dir' => [],
];
foreach ($paths as $path) {
if (is_file($path)) {
$pathsByType['file'][] = $path;
} else {
$pathsByType['dir'][] = $path.\DIRECTORY_SEPARATOR;
}
}
$nestedFinder = null;
$currentFinder = $this->iterableToTraversable($this->getConfig()->getFinder());
try {
$nestedFinder = $currentFinder instanceof \IteratorAggregate ? $currentFinder->getIterator() : $currentFinder;
} catch (\Exception $e) {
}
if ($isIntersectionPathMode) {
if (null === $nestedFinder) {
throw new InvalidConfigurationException(
'Cannot create intersection with not-fully defined Finder in configuration file.'
);
}
return new \CallbackFilterIterator(
new \IteratorIterator($nestedFinder),
static function (\SplFileInfo $current) use ($pathsByType): bool {
$currentRealPath = $current->getRealPath();
if (\in_array($currentRealPath, $pathsByType['file'], true)) {
return true;
}
foreach ($pathsByType['dir'] as $path) {
if (str_starts_with($currentRealPath, $path)) {
return true;
}
}
return false;
}
);
}
if (null !== $this->getConfigFile() && null !== $nestedFinder) {
$this->configFinderIsOverridden = true;
}
if ($currentFinder instanceof SymfonyFinder && null === $nestedFinder) {
// finder from configuration Symfony finder and it is not fully defined, we may fulfill it
return $currentFinder->in($pathsByType['dir'])->append($pathsByType['file']);
}
return Finder::create()->in($pathsByType['dir'])->append($pathsByType['file']);
}
/**
* Set option that will be resolved.
*
* @param mixed $value
*/
private function setOption(string $name, $value): void
{
if (!\array_key_exists($name, $this->options)) {
throw new InvalidConfigurationException(\sprintf('Unknown option name: "%s".', $name));
}
$this->options[$name] = $value;
}
/**
* @param key-of<_Options> $optionName
*/
private function resolveOptionBooleanValue(string $optionName): bool
{
$value = $this->options[$optionName];
if (self::BOOL_YES === $value) {
return true;
}
if (self::BOOL_NO === $value) {
return false;
}
throw new InvalidConfigurationException(\sprintf('Expected "%s" or "%s" for option "%s", got "%s".', self::BOOL_YES, self::BOOL_NO, $optionName, \is_object($value) ? \get_class($value) : (\is_scalar($value) ? $value : \gettype($value))));
}
private static function separatedContextLessInclude(string $path): ConfigInterface
{
$config = include $path;
// verify that the config has an instance of Config
if (!$config instanceof ConfigInterface) {
throw new InvalidConfigurationException(\sprintf('The config file: "%s" does not return a "PhpCsFixer\ConfigInterface" instance. Got: "%s".', $path, \is_object($config) ? \get_class($config) : \gettype($config)));
}
return $config;
}
private function isCachingAllowedForRuntime(): bool
{
return $this->toolInfo->isInstalledAsPhar()
|| $this->toolInfo->isInstalledByComposer()
|| $this->toolInfo->isRunInsideDocker()
|| filter_var(getenv('PHP_CS_FIXER_ENFORCE_CACHE'), \FILTER_VALIDATE_BOOL);
}
}

View File

@@ -0,0 +1,155 @@
<?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\Output;
use PhpCsFixer\Differ\DiffConsoleFormatter;
use PhpCsFixer\Error\Error;
use PhpCsFixer\Linter\LintingException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @readonly
*
* @internal
*/
final class ErrorOutput
{
private OutputInterface $output;
private bool $isDecorated;
public function __construct(OutputInterface $output)
{
$this->output = $output;
$this->isDecorated = $output->isDecorated();
}
/**
* @param list<Error> $errors
*/
public function listErrors(string $process, array $errors): void
{
$this->output->writeln(['', \sprintf(
'Files that were not fixed due to errors reported during %s:',
$process
)]);
$showDetails = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE;
$showTrace = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG;
foreach ($errors as $i => $error) {
$this->output->writeln(\sprintf('%4d) %s', $i + 1, $error->getFilePath()));
$e = $error->getSource();
if (!$showDetails || null === $e) {
continue;
}
$class = \sprintf('[%s]', \get_class($e));
$message = $e->getMessage();
$code = $e->getCode();
if (0 !== $code) {
$message .= " ({$code})";
}
$length = max(\strlen($class), \strlen($message));
$lines = [
'',
$class,
$message,
'',
];
$this->output->writeln('');
foreach ($lines as $line) {
if (\strlen($line) < $length) {
$line .= str_repeat(' ', $length - \strlen($line));
}
$this->output->writeln(\sprintf(' <error> %s </error>', $this->prepareOutput($line)));
}
if ($showTrace && !$e instanceof LintingException) { // stack trace of lint exception is of no interest
$this->output->writeln('');
$stackTrace = $e->getTrace();
foreach ($stackTrace as $trace) {
if (isset($trace['class']) && Command::class === $trace['class'] && 'run' === $trace['function']) {
$this->output->writeln(' [ ... ]');
break;
}
$this->outputTrace($trace);
}
}
if (Error::TYPE_LINT === $error->getType() && 0 < \count($error->getAppliedFixers())) {
$this->output->writeln('');
$this->output->writeln(\sprintf(' Applied fixers: <comment>%s</comment>', implode(', ', $error->getAppliedFixers())));
$diff = $error->getDiff();
if (null !== $diff) {
$diffFormatter = new DiffConsoleFormatter(
$this->isDecorated,
\sprintf(
'<comment> ---------- begin diff ----------</comment>%s%%s%s<comment> ----------- end diff -----------</comment>',
\PHP_EOL,
\PHP_EOL
)
);
$this->output->writeln($diffFormatter->format($diff));
}
}
}
}
/**
* @param array{
* function?: string,
* line?: int,
* file?: string,
* class?: class-string,
* type?: '->'|'::',
* args?: mixed[],
* object?: object,
* } $trace
*/
private function outputTrace(array $trace): void
{
if (isset($trace['class'], $trace['type'], $trace['function'])) {
$this->output->writeln(\sprintf(
' <comment>%s</comment>%s<comment>%s()</comment>',
$this->prepareOutput($trace['class']),
$this->prepareOutput($trace['type']),
$this->prepareOutput($trace['function'])
));
} elseif (isset($trace['function'])) {
$this->output->writeln(\sprintf(' <comment>%s()</comment>', $this->prepareOutput($trace['function'])));
}
if (isset($trace['file'])) {
$this->output->writeln(\sprintf(' in <info>%s</info> at line <info>%d</info>', $this->prepareOutput($trace['file']), $trace['line']));
}
}
private function prepareOutput(string $string): string
{
return $this->isDecorated
? OutputFormatter::escape($string)
: $string;
}
}

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\Console\Output;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @readonly
*
* @internal
*/
final class OutputContext
{
private ?OutputInterface $output;
private int $terminalWidth;
private int $filesCount;
public function __construct(
?OutputInterface $output,
int $terminalWidth,
int $filesCount
) {
$this->output = $output;
$this->terminalWidth = $terminalWidth;
$this->filesCount = $filesCount;
}
public function getOutput(): ?OutputInterface
{
return $this->output;
}
public function getTerminalWidth(): int
{
return $this->terminalWidth;
}
public function getFilesCount(): int
{
return $this->filesCount;
}
}

View File

@@ -0,0 +1,125 @@
<?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\Output\Progress;
use PhpCsFixer\Console\Output\OutputContext;
use PhpCsFixer\Runner\Event\FileProcessed;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Output writer to show the progress of a FixCommand using dots and meaningful letters.
*
* @internal
*/
final class DotsOutput implements ProgressOutputInterface
{
/**
* File statuses map.
*
* @var array<FileProcessed::STATUS_*, array{symbol: string, format: string, description: string}>
*/
private const EVENT_STATUS_MAP = [
FileProcessed::STATUS_NO_CHANGES => ['symbol' => '.', 'format' => '%s', 'description' => 'no changes'],
FileProcessed::STATUS_FIXED => ['symbol' => 'F', 'format' => '<fg=green>%s</fg=green>', 'description' => 'fixed'],
FileProcessed::STATUS_SKIPPED => ['symbol' => 'S', 'format' => '<fg=cyan>%s</fg=cyan>', 'description' => 'skipped (cached or empty file)'],
FileProcessed::STATUS_NON_MONOLITHIC => ['symbol' => 'M', 'format' => '<bg=magenta>%s</bg=magenta>', 'description' => 'skipped (non-monolithic)'],
FileProcessed::STATUS_INVALID => ['symbol' => 'I', 'format' => '<bg=red>%s</bg=red>', 'description' => 'invalid file syntax (file ignored)'],
FileProcessed::STATUS_EXCEPTION => ['symbol' => 'E', 'format' => '<bg=red>%s</bg=red>', 'description' => 'error'],
FileProcessed::STATUS_LINT => ['symbol' => 'E', 'format' => '<bg=red>%s</bg=red>', 'description' => 'error'],
];
/** @readonly */
private OutputContext $context;
private int $processedFiles = 0;
private int $symbolsPerLine;
public function __construct(OutputContext $context)
{
$this->context = $context;
// max number of characters per line
// - total length x 2 (e.g. " 1 / 123" => 6 digits and padding spaces)
// - 11 (extra spaces, parentheses and percentage characters, e.g. " x / x (100%)")
$this->symbolsPerLine = max(1, $context->getTerminalWidth() - \strlen((string) $context->getFilesCount()) * 2 - 11);
}
/**
* This class is not intended to be serialized,
* and cannot be deserialized (see __wakeup method).
*/
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.self::class);
}
/**
* Disable the deserialization of the class to prevent attacker executing
* code by leveraging the __destruct method.
*
* @see https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
*/
public function __wakeup(): void
{
throw new \BadMethodCallException('Cannot unserialize '.self::class);
}
public function onFixerFileProcessed(FileProcessed $event): void
{
$status = self::EVENT_STATUS_MAP[$event->getStatus()];
$this->getOutput()->write($this->getOutput()->isDecorated() ? \sprintf($status['format'], $status['symbol']) : $status['symbol']);
++$this->processedFiles;
$symbolsOnCurrentLine = $this->processedFiles % $this->symbolsPerLine;
$isLast = $this->processedFiles === $this->context->getFilesCount();
if (0 === $symbolsOnCurrentLine || $isLast) {
$this->getOutput()->write(\sprintf(
'%s %'.\strlen((string) $this->context->getFilesCount()).'d / %d (%3d%%)',
$isLast && 0 !== $symbolsOnCurrentLine ? str_repeat(' ', $this->symbolsPerLine - $symbolsOnCurrentLine) : '',
$this->processedFiles,
$this->context->getFilesCount(),
round($this->processedFiles / $this->context->getFilesCount() * 100)
));
if (!$isLast) {
$this->getOutput()->writeln('');
}
}
}
public function printLegend(): void
{
$symbols = [];
foreach (self::EVENT_STATUS_MAP as $status) {
$symbol = $status['symbol'];
if (isset($symbols[$symbol])) {
continue;
}
$symbols[$symbol] = \sprintf('%s-%s', $this->getOutput()->isDecorated() ? \sprintf($status['format'], $symbol) : $symbol, $status['description']);
}
$this->getOutput()->write(\sprintf("\nLegend: %s\n", implode(', ', $symbols)));
}
private function getOutput(): OutputInterface
{
return $this->context->getOutput();
}
}

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\Console\Output\Progress;
use PhpCsFixer\Runner\Event\FileProcessed;
/**
* @readonly
*
* @internal
*/
final class NullOutput implements ProgressOutputInterface
{
public function printLegend(): void {}
public function onFixerFileProcessed(FileProcessed $event): void {}
}

View File

@@ -0,0 +1,78 @@
<?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\Output\Progress;
use PhpCsFixer\Console\Output\OutputContext;
use PhpCsFixer\Runner\Event\FileProcessed;
use Symfony\Component\Console\Helper\ProgressBar;
/**
* Output writer to show the progress of a FixCommand using progress bar (percentage).
*
* @readonly
*
* @internal
*/
final class PercentageBarOutput implements ProgressOutputInterface
{
/** @readonly */
private OutputContext $context;
private ProgressBar $progressBar;
public function __construct(OutputContext $context)
{
$this->context = $context;
$this->progressBar = new ProgressBar($context->getOutput(), $this->context->getFilesCount());
$this->progressBar->setBarCharacter('▓'); // dark shade character \u2593
$this->progressBar->setEmptyBarCharacter('░'); // light shade character \u2591
$this->progressBar->setProgressCharacter('');
$this->progressBar->setFormat('normal');
$this->progressBar->start();
}
/**
* This class is not intended to be serialized,
* and cannot be deserialized (see __wakeup method).
*/
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.self::class);
}
/**
* Disable the deserialization of the class to prevent attacker executing
* code by leveraging the __destruct method.
*
* @see https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
*/
public function __wakeup(): void
{
throw new \BadMethodCallException('Cannot unserialize '.self::class);
}
public function onFixerFileProcessed(FileProcessed $event): void
{
$this->progressBar->advance(1);
if ($this->progressBar->getProgress() === $this->progressBar->getMaxSteps()) {
$this->context->getOutput()->write("\n\n");
}
}
public function printLegend(): void {}
}

View File

@@ -0,0 +1,63 @@
<?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\Output\Progress;
use PhpCsFixer\Console\Output\OutputContext;
/**
* @readonly
*
* @internal
*/
final class ProgressOutputFactory
{
/**
* @var array<ProgressOutputType::*, class-string<ProgressOutputInterface>>
*/
private const OUTPUT_TYPE_MAP = [
ProgressOutputType::NONE => NullOutput::class,
ProgressOutputType::DOTS => DotsOutput::class,
ProgressOutputType::BAR => PercentageBarOutput::class,
];
/**
* @param ProgressOutputType::* $outputType
*/
public function create(string $outputType, OutputContext $context): ProgressOutputInterface
{
if (null === $context->getOutput()) {
$outputType = ProgressOutputType::NONE;
}
if (!$this->isBuiltInType($outputType)) {
throw new \InvalidArgumentException(
\sprintf(
'Something went wrong, "%s" output type is not supported',
$outputType
)
);
}
$outputClass = self::OUTPUT_TYPE_MAP[$outputType];
// @phpstan-ignore-next-line new.noConstructor
return new $outputClass($context);
}
private function isBuiltInType(string $outputType): bool
{
return \in_array($outputType, ProgressOutputType::all(), true);
}
}

View File

@@ -0,0 +1,27 @@
<?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\Output\Progress;
use PhpCsFixer\Runner\Event\FileProcessed;
/**
* @internal
*/
interface ProgressOutputInterface
{
public function printLegend(): void;
public function onFixerFileProcessed(FileProcessed $event): void;
}

View File

@@ -0,0 +1,37 @@
<?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\Output\Progress;
/**
* @internal
*/
final class ProgressOutputType
{
public const NONE = 'none';
public const DOTS = 'dots';
public const BAR = 'bar';
/**
* @return non-empty-list<ProgressOutputType::*>
*/
public static function all(): array
{
return [
self::BAR,
self::DOTS,
self::NONE,
];
}
}

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\Console\Report\FixReport;
use PhpCsFixer\Console\Application;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* @author Kévin Gomez <contact@kevingomez.fr>
*
* @readonly
*
* @internal
*/
final class CheckstyleReporter implements ReporterInterface
{
public function getFormat(): string
{
return 'checkstyle';
}
public function generate(ReportSummary $reportSummary): string
{
if (!\extension_loaded('dom')) {
throw new \RuntimeException('Cannot generate report! `ext-dom` is not available!');
}
$dom = new \DOMDocument('1.0', 'UTF-8');
/** @var \DOMElement $checkstyles */
$checkstyles = $dom->appendChild($dom->createElement('checkstyle'));
$checkstyles->setAttribute('version', Application::getAbout());
foreach ($reportSummary->getChanged() as $filePath => $fixResult) {
/** @var \DOMElement $file */
$file = $checkstyles->appendChild($dom->createElement('file'));
$file->setAttribute('name', $filePath);
foreach ($fixResult['appliedFixers'] as $appliedFixer) {
$error = $this->createError($dom, $appliedFixer);
$file->appendChild($error);
}
}
$dom->formatOutput = true;
$result = $dom->saveXML();
if (false === $result) {
throw new \RuntimeException('Failed to generate XML output');
}
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($result) : $result;
}
private function createError(\DOMDocument $dom, string $appliedFixer): \DOMElement
{
$error = $dom->createElement('error');
$error->setAttribute('severity', 'warning');
$error->setAttribute('source', 'PHP-CS-Fixer.'.$appliedFixer);
$error->setAttribute('message', 'Found violation(s) of type: '.$appliedFixer);
return $error;
}
}

View File

@@ -0,0 +1,96 @@
<?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\Report\FixReport;
use PhpCsFixer\Console\Application;
use SebastianBergmann\Diff\Chunk;
use SebastianBergmann\Diff\Diff;
use SebastianBergmann\Diff\Parser;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* Generates a report according to gitlabs subset of codeclimate json files.
*
* @author Hans-Christian Otto <c.otto@suora.com>
*
* @see https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#data-types
*
* @readonly
*
* @internal
*/
final class GitlabReporter implements ReporterInterface
{
private Parser $diffParser;
public function __construct()
{
$this->diffParser = new Parser();
}
public function getFormat(): string
{
return 'gitlab';
}
/**
* Process changed files array. Returns generated report.
*/
public function generate(ReportSummary $reportSummary): string
{
$about = Application::getAbout();
$report = [];
foreach ($reportSummary->getChanged() as $fileName => $change) {
foreach ($change['appliedFixers'] as $fixerName) {
$report[] = [
'check_name' => 'PHP-CS-Fixer.'.$fixerName,
'description' => 'PHP-CS-Fixer.'.$fixerName.' by '.$about,
'categories' => ['Style'],
'fingerprint' => md5($fileName.$fixerName),
'severity' => 'minor',
'location' => [
'path' => $fileName,
'lines' => self::getLines($this->diffParser->parse($change['diff'])),
],
];
}
}
$jsonString = json_encode($report, \JSON_THROW_ON_ERROR);
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($jsonString) : $jsonString;
}
/**
* @param list<Diff> $diffs
*
* @return array{begin: int, end: int}
*/
private static function getLines(array $diffs): array
{
if (isset($diffs[0])) {
$firstDiff = $diffs[0];
$firstChunk = \Closure::bind(static fn (Diff $diff) => array_shift($diff->chunks), null, $firstDiff)($firstDiff);
if ($firstChunk instanceof Chunk) {
return \Closure::bind(static fn (Chunk $chunk): array => ['begin' => $chunk->start, 'end' => $chunk->startRange], null, $firstChunk)($firstChunk);
}
}
return ['begin' => 0, 'end' => 0];
}
}

View File

@@ -0,0 +1,65 @@
<?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\Report\FixReport;
use PhpCsFixer\Console\Application;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @readonly
*
* @internal
*/
final class JsonReporter implements ReporterInterface
{
public function getFormat(): string
{
return 'json';
}
public function generate(ReportSummary $reportSummary): string
{
$jsonFiles = [];
foreach ($reportSummary->getChanged() as $file => $fixResult) {
$jsonFile = ['name' => $file];
if ($reportSummary->shouldAddAppliedFixers()) {
$jsonFile['appliedFixers'] = $fixResult['appliedFixers'];
}
if ('' !== $fixResult['diff']) {
$jsonFile['diff'] = $fixResult['diff'];
}
$jsonFiles[] = $jsonFile;
}
$json = [
'about' => Application::getAbout(),
'files' => $jsonFiles,
'time' => [
'total' => round($reportSummary->getTime() / 1_000, 3),
],
'memory' => round($reportSummary->getMemory() / 1_024 / 1_024, 3),
];
$json = json_encode($json, \JSON_THROW_ON_ERROR);
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($json) : $json;
}
}

View File

@@ -0,0 +1,150 @@
<?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\Report\FixReport;
use PhpCsFixer\Console\Application;
use PhpCsFixer\Preg;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @readonly
*
* @internal
*/
final class JunitReporter implements ReporterInterface
{
public function getFormat(): string
{
return 'junit';
}
public function generate(ReportSummary $reportSummary): string
{
if (!\extension_loaded('dom')) {
throw new \RuntimeException('Cannot generate report! `ext-dom` is not available!');
}
$dom = new \DOMDocument('1.0', 'UTF-8');
$testsuites = $dom->appendChild($dom->createElement('testsuites'));
/** @var \DOMElement $testsuite */
$testsuite = $testsuites->appendChild($dom->createElement('testsuite'));
$testsuite->setAttribute('name', 'PHP CS Fixer');
$properties = $dom->createElement('properties');
$property = $dom->createElement('property');
$property->setAttribute('name', 'about');
$property->setAttribute('value', Application::getAbout());
$properties->appendChild($property);
$testsuite->appendChild($properties);
if (\count($reportSummary->getChanged()) > 0) {
$this->createFailedTestCases($dom, $testsuite, $reportSummary);
} else {
$this->createSuccessTestCase($dom, $testsuite);
}
if ($reportSummary->getTime() > 0) {
$testsuite->setAttribute(
'time',
\sprintf(
'%.3f',
$reportSummary->getTime() / 1_000
)
);
}
$dom->formatOutput = true;
$result = $dom->saveXML();
if (false === $result) {
throw new \RuntimeException('Failed to generate XML output');
}
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($result) : $result;
}
private function createSuccessTestCase(\DOMDocument $dom, \DOMElement $testsuite): void
{
$testcase = $dom->createElement('testcase');
$testcase->setAttribute('name', 'All OK');
$testcase->setAttribute('assertions', '1');
$testsuite->appendChild($testcase);
$testsuite->setAttribute('tests', '1');
$testsuite->setAttribute('assertions', '1');
$testsuite->setAttribute('failures', '0');
$testsuite->setAttribute('errors', '0');
}
private function createFailedTestCases(\DOMDocument $dom, \DOMElement $testsuite, ReportSummary $reportSummary): void
{
$assertionsCount = 0;
foreach ($reportSummary->getChanged() as $file => $fixResult) {
$testcase = $this->createFailedTestCase(
$dom,
$file,
$fixResult,
$reportSummary->shouldAddAppliedFixers()
);
$testsuite->appendChild($testcase);
$assertionsCount += (int) $testcase->getAttribute('assertions');
}
$testsuite->setAttribute('tests', (string) \count($reportSummary->getChanged()));
$testsuite->setAttribute('assertions', (string) $assertionsCount);
$testsuite->setAttribute('failures', (string) $assertionsCount);
$testsuite->setAttribute('errors', '0');
}
/**
* @param array{appliedFixers: list<string>, diff: string} $fixResult
*/
private function createFailedTestCase(\DOMDocument $dom, string $file, array $fixResult, bool $shouldAddAppliedFixers): \DOMElement
{
$appliedFixersCount = \count($fixResult['appliedFixers']);
$testName = str_replace('.', '_DOT_', Preg::replace('@\.'.pathinfo($file, \PATHINFO_EXTENSION).'$@', '', $file));
$testcase = $dom->createElement('testcase');
$testcase->setAttribute('name', $testName);
$testcase->setAttribute('file', $file);
$testcase->setAttribute('assertions', (string) $appliedFixersCount);
$failure = $dom->createElement('failure');
$failure->setAttribute('type', 'code_style');
$testcase->appendChild($failure);
if ($shouldAddAppliedFixers) {
$failureContent = "applied fixers:\n---------------\n";
foreach ($fixResult['appliedFixers'] as $appliedFixer) {
$failureContent .= "* {$appliedFixer}\n";
}
} else {
$failureContent = "Wrong code style\n";
}
if ('' !== $fixResult['diff']) {
$failureContent .= "\nDiff:\n---------------\n\n".$fixResult['diff'];
}
$failure->appendChild($dom->createCDATASection(trim($failureContent)));
return $testcase;
}
}

View File

@@ -0,0 +1,103 @@
<?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\Report\FixReport;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @readonly
*
* @internal
*/
final class ReportSummary
{
/**
* @var array<string, array{appliedFixers: list<string>, diff: string}>
*/
private array $changed;
private int $filesCount;
private int $time;
private int $memory;
private bool $addAppliedFixers;
private bool $isDryRun;
private bool $isDecoratedOutput;
/**
* @param array<string, array{appliedFixers: list<string>, diff: string}> $changed
* @param int $time duration in milliseconds
* @param int $memory memory usage in bytes
*/
public function __construct(
array $changed,
int $filesCount,
int $time,
int $memory,
bool $addAppliedFixers,
bool $isDryRun,
bool $isDecoratedOutput
) {
$this->changed = $changed;
$this->filesCount = $filesCount;
$this->time = $time;
$this->memory = $memory;
$this->addAppliedFixers = $addAppliedFixers;
$this->isDryRun = $isDryRun;
$this->isDecoratedOutput = $isDecoratedOutput;
}
public function isDecoratedOutput(): bool
{
return $this->isDecoratedOutput;
}
public function isDryRun(): bool
{
return $this->isDryRun;
}
/**
* @return array<string, array{appliedFixers: list<string>, diff: string}>
*/
public function getChanged(): array
{
return $this->changed;
}
public function getMemory(): int
{
return $this->memory;
}
public function getTime(): int
{
return $this->time;
}
public function getFilesCount(): int
{
return $this->filesCount;
}
public function shouldAddAppliedFixers(): bool
{
return $this->addAppliedFixers;
}
}

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\Report\FixReport;
use Symfony\Component\Finder\Finder as SymfonyFinder;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @internal
*/
final class ReporterFactory
{
/** @var array<string, ReporterInterface> */
private array $reporters = [];
public function registerBuiltInReporters(): self
{
/** @var null|list<string> $builtInReporters */
static $builtInReporters;
if (null === $builtInReporters) {
$builtInReporters = [];
foreach (SymfonyFinder::create()->files()->name('*Reporter.php')->in(__DIR__) as $file) {
$relativeNamespace = $file->getRelativePath();
$builtInReporters[] = \sprintf(
'%s\%s%s',
__NAMESPACE__,
'' !== $relativeNamespace ? $relativeNamespace.'\\' : '',
$file->getBasename('.php')
);
}
}
foreach ($builtInReporters as $reporterClass) {
$this->registerReporter(new $reporterClass());
}
return $this;
}
/**
* @return $this
*/
public function registerReporter(ReporterInterface $reporter): self
{
$format = $reporter->getFormat();
if (isset($this->reporters[$format])) {
throw new \UnexpectedValueException(\sprintf('Reporter for format "%s" is already registered.', $format));
}
$this->reporters[$format] = $reporter;
return $this;
}
/**
* @return list<string>
*/
public function getFormats(): array
{
$formats = array_keys($this->reporters);
sort($formats);
return $formats;
}
public function getReporter(string $format): ReporterInterface
{
if (!isset($this->reporters[$format])) {
throw new \UnexpectedValueException(\sprintf('Reporter for format "%s" is not registered.', $format));
}
return $this->reporters[$format];
}
}

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\Console\Report\FixReport;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @internal
*/
interface ReporterInterface
{
public function getFormat(): string;
/**
* Process changed files array. Returns generated report.
*/
public function generate(ReportSummary $reportSummary): string;
}

View File

@@ -0,0 +1,104 @@
<?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\Report\FixReport;
use PhpCsFixer\Differ\DiffConsoleFormatter;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @readonly
*
* @internal
*/
final class TextReporter implements ReporterInterface
{
public function getFormat(): string
{
return 'txt';
}
public function generate(ReportSummary $reportSummary): string
{
$output = '';
$identifiedFiles = 0;
foreach ($reportSummary->getChanged() as $file => $fixResult) {
++$identifiedFiles;
$output .= \sprintf('%4d) %s', $identifiedFiles, $file);
if ($reportSummary->shouldAddAppliedFixers()) {
$output .= $this->getAppliedFixers(
$reportSummary->isDecoratedOutput(),
$fixResult['appliedFixers'],
);
}
$output .= $this->getDiff($reportSummary->isDecoratedOutput(), $fixResult['diff']);
$output .= \PHP_EOL;
}
return $output.$this->getFooter(
$reportSummary->getTime(),
$identifiedFiles,
$reportSummary->getFilesCount(),
$reportSummary->getMemory(),
$reportSummary->isDryRun()
);
}
/**
* @param list<string> $appliedFixers
*/
private function getAppliedFixers(bool $isDecoratedOutput, array $appliedFixers): string
{
return \sprintf(
$isDecoratedOutput ? ' (<comment>%s</comment>)' : ' (%s)',
implode(', ', $appliedFixers)
);
}
private function getDiff(bool $isDecoratedOutput, string $diff): string
{
if ('' === $diff) {
return '';
}
$diffFormatter = new DiffConsoleFormatter($isDecoratedOutput, \sprintf(
'<comment> ---------- begin diff ----------</comment>%s%%s%s<comment> ----------- end diff -----------</comment>',
\PHP_EOL,
\PHP_EOL
));
return \PHP_EOL.$diffFormatter->format($diff).\PHP_EOL;
}
private function getFooter(int $time, int $identifiedFiles, int $files, int $memory, bool $isDryRun): string
{
if (0 === $time || 0 === $memory) {
return '';
}
return \PHP_EOL.\sprintf(
'%s %d of %d %s in %.3f seconds, %.2f MB memory used'.\PHP_EOL,
$isDryRun ? 'Found' : 'Fixed',
$identifiedFiles,
$files,
$isDryRun ? 'files that can be fixed' : 'files',
$time / 1_000,
$memory / 1_024 / 1_024
);
}
}

View File

@@ -0,0 +1,141 @@
<?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\Report\FixReport;
use PhpCsFixer\Console\Application;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @readonly
*
* @internal
*/
final class XmlReporter implements ReporterInterface
{
public function getFormat(): string
{
return 'xml';
}
public function generate(ReportSummary $reportSummary): string
{
if (!\extension_loaded('dom')) {
throw new \RuntimeException('Cannot generate report! `ext-dom` is not available!');
}
$dom = new \DOMDocument('1.0', 'UTF-8');
// new nodes should be added to this or existing children
$root = $dom->createElement('report');
$dom->appendChild($root);
$root->appendChild($this->createAboutElement($dom, Application::getAbout()));
$filesXML = $dom->createElement('files');
$root->appendChild($filesXML);
$i = 1;
foreach ($reportSummary->getChanged() as $file => $fixResult) {
$fileXML = $dom->createElement('file');
$fileXML->setAttribute('id', (string) $i++);
$fileXML->setAttribute('name', $file);
$filesXML->appendChild($fileXML);
if ($reportSummary->shouldAddAppliedFixers()) {
$fileXML->appendChild(
$this->createAppliedFixersElement($dom, $fixResult['appliedFixers']),
);
}
if ('' !== $fixResult['diff']) {
$fileXML->appendChild($this->createDiffElement($dom, $fixResult['diff']));
}
}
if (0 !== $reportSummary->getTime()) {
$root->appendChild($this->createTimeElement($reportSummary->getTime(), $dom));
}
if (0 !== $reportSummary->getMemory()) {
$root->appendChild($this->createMemoryElement($reportSummary->getMemory(), $dom));
}
$dom->formatOutput = true;
$result = $dom->saveXML();
if (false === $result) {
throw new \RuntimeException('Failed to generate XML output');
}
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($result) : $result;
}
/**
* @param list<string> $appliedFixers
*/
private function createAppliedFixersElement(\DOMDocument $dom, array $appliedFixers): \DOMElement
{
$appliedFixersXML = $dom->createElement('applied_fixers');
foreach ($appliedFixers as $appliedFixer) {
$appliedFixerXML = $dom->createElement('applied_fixer');
$appliedFixerXML->setAttribute('name', $appliedFixer);
$appliedFixersXML->appendChild($appliedFixerXML);
}
return $appliedFixersXML;
}
private function createDiffElement(\DOMDocument $dom, string $diff): \DOMElement
{
$diffXML = $dom->createElement('diff');
$diffXML->appendChild($dom->createCDATASection($diff));
return $diffXML;
}
private function createTimeElement(float $time, \DOMDocument $dom): \DOMElement
{
$time = round($time / 1_000, 3);
$timeXML = $dom->createElement('time');
$timeXML->setAttribute('unit', 's');
$timeTotalXML = $dom->createElement('total');
$timeTotalXML->setAttribute('value', (string) $time);
$timeXML->appendChild($timeTotalXML);
return $timeXML;
}
private function createMemoryElement(float $memory, \DOMDocument $dom): \DOMElement
{
$memory = round($memory / 1_024 / 1_024, 3);
$memoryXML = $dom->createElement('memory');
$memoryXML->setAttribute('value', (string) $memory);
$memoryXML->setAttribute('unit', 'MB');
return $memoryXML;
}
private function createAboutElement(\DOMDocument $dom, string $about): \DOMElement
{
$xml = $dom->createElement('about');
$xml->setAttribute('value', $about);
return $xml;
}
}

View File

@@ -0,0 +1,52 @@
<?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\Report\ListSetsReport;
use PhpCsFixer\RuleSet\RuleSetDescriptionInterface;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @readonly
*
* @internal
*/
final class JsonReporter implements ReporterInterface
{
public function getFormat(): string
{
return 'json';
}
public function generate(ReportSummary $reportSummary): string
{
$sets = $reportSummary->getSets();
usort($sets, static fn (RuleSetDescriptionInterface $a, RuleSetDescriptionInterface $b): int => $a->getName() <=> $b->getName());
$json = ['sets' => []];
foreach ($sets as $set) {
$setName = $set->getName();
$json['sets'][$setName] = [
'description' => $set->getDescription(),
'isRisky' => $set->isRisky(),
'name' => $setName,
];
}
return json_encode($json, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT);
}
}

View File

@@ -0,0 +1,48 @@
<?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\Report\ListSetsReport;
use PhpCsFixer\RuleSet\RuleSetDescriptionInterface;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @readonly
*
* @internal
*/
final class ReportSummary
{
/**
* @var list<RuleSetDescriptionInterface>
*/
private array $sets;
/**
* @param list<RuleSetDescriptionInterface> $sets
*/
public function __construct(array $sets)
{
$this->sets = $sets;
}
/**
* @return list<RuleSetDescriptionInterface>
*/
public function getSets(): array
{
return $this->sets;
}
}

View File

@@ -0,0 +1,89 @@
<?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\Report\ListSetsReport;
use Symfony\Component\Finder\Finder as SymfonyFinder;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @internal
*/
final class ReporterFactory
{
/**
* @var array<string, ReporterInterface>
*/
private array $reporters = [];
public function registerBuiltInReporters(): self
{
/** @var null|list<string> $builtInReporters */
static $builtInReporters;
if (null === $builtInReporters) {
$builtInReporters = [];
foreach (SymfonyFinder::create()->files()->name('*Reporter.php')->in(__DIR__) as $file) {
$relativeNamespace = $file->getRelativePath();
$builtInReporters[] = \sprintf(
'%s\%s%s',
__NAMESPACE__,
'' !== $relativeNamespace ? $relativeNamespace.'\\' : '',
$file->getBasename('.php')
);
}
}
foreach ($builtInReporters as $reporterClass) {
$this->registerReporter(new $reporterClass());
}
return $this;
}
public function registerReporter(ReporterInterface $reporter): self
{
$format = $reporter->getFormat();
if (isset($this->reporters[$format])) {
throw new \UnexpectedValueException(\sprintf('Reporter for format "%s" is already registered.', $format));
}
$this->reporters[$format] = $reporter;
return $this;
}
/**
* @return list<string>
*/
public function getFormats(): array
{
$formats = array_keys($this->reporters);
sort($formats);
return $formats;
}
public function getReporter(string $format): ReporterInterface
{
if (!isset($this->reporters[$format])) {
throw new \UnexpectedValueException(\sprintf('Reporter for format "%s" is not registered.', $format));
}
return $this->reporters[$format];
}
}

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\Console\Report\ListSetsReport;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
interface ReporterInterface
{
public function getFormat(): string;
/**
* Process changed files array. Returns generated report.
*/
public function generate(ReportSummary $reportSummary): string;
}

View File

@@ -0,0 +1,51 @@
<?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\Report\ListSetsReport;
use PhpCsFixer\RuleSet\RuleSetDescriptionInterface;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @readonly
*
* @internal
*/
final class TextReporter implements ReporterInterface
{
public function getFormat(): string
{
return 'txt';
}
public function generate(ReportSummary $reportSummary): string
{
$sets = $reportSummary->getSets();
usort($sets, static fn (RuleSetDescriptionInterface $a, RuleSetDescriptionInterface $b): int => $a->getName() <=> $b->getName());
$output = '';
foreach ($sets as $i => $set) {
$output .= \sprintf('%2d) %s', $i + 1, $set->getName()).\PHP_EOL.' '.$set->getDescription().\PHP_EOL;
if ($set->isRisky()) {
$output .= ' Set contains risky rules.'.\PHP_EOL;
}
}
return $output;
}
}

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\Console\SelfUpdate;
/**
* @readonly
*
* @internal
*/
final class GithubClient implements GithubClientInterface
{
private string $url;
public function __construct(string $url = 'https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/tags')
{
$this->url = $url;
}
public function getTags(): array
{
$result = @file_get_contents(
$this->url,
false,
stream_context_create([
'http' => [
'header' => 'User-Agent: PHP-CS-Fixer/PHP-CS-Fixer',
],
])
);
if (false === $result) {
throw new \RuntimeException(\sprintf('Failed to load tags at "%s".', $this->url));
}
/**
* @var list<array{
* name: string,
* zipball_url: string,
* tarball_url: string,
* commit: array{sha: string, url: string},
* }>
*/
$result = json_decode($result, true);
if (\JSON_ERROR_NONE !== json_last_error()) {
throw new \RuntimeException(\sprintf(
'Failed to read response from "%s" as JSON: %s.',
$this->url,
json_last_error_msg()
));
}
return array_map(
static fn (array $tagData): string => $tagData['name'],
$result
);
}
}

View File

@@ -0,0 +1,26 @@
<?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\SelfUpdate;
/**
* @internal
*/
interface GithubClientInterface
{
/**
* @return list<string>
*/
public function getTags(): array;
}

View File

@@ -0,0 +1,102 @@
<?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\SelfUpdate;
use Composer\Semver\Comparator;
use Composer\Semver\Semver;
use Composer\Semver\VersionParser;
/**
* @internal
*/
final class NewVersionChecker implements NewVersionCheckerInterface
{
private GithubClientInterface $githubClient;
private VersionParser $versionParser;
/**
* @var null|list<string>
*/
private ?array $availableVersions = null;
public function __construct(GithubClientInterface $githubClient)
{
$this->githubClient = $githubClient;
$this->versionParser = new VersionParser();
}
public function getLatestVersion(): string
{
$this->retrieveAvailableVersions();
return $this->availableVersions[0];
}
public function getLatestVersionOfMajor(int $majorVersion): ?string
{
$this->retrieveAvailableVersions();
$semverConstraint = '^'.$majorVersion;
foreach ($this->availableVersions as $availableVersion) {
if (Semver::satisfies($availableVersion, $semverConstraint)) {
return $availableVersion;
}
}
return null;
}
public function compareVersions(string $versionA, string $versionB): int
{
$versionA = $this->versionParser->normalize($versionA);
$versionB = $this->versionParser->normalize($versionB);
if (Comparator::lessThan($versionA, $versionB)) {
return -1;
}
if (Comparator::greaterThan($versionA, $versionB)) {
return 1;
}
return 0;
}
private function retrieveAvailableVersions(): void
{
if (null !== $this->availableVersions) {
return;
}
foreach ($this->githubClient->getTags() as $version) {
try {
$this->versionParser->normalize($version);
if ('stable' === VersionParser::parseStability($version)) {
$this->availableVersions[] = $version;
}
} catch (\UnexpectedValueException $exception) {
// not a valid version tag
}
}
$versions = Semver::rsort($this->availableVersions);
\assert(array_is_list($versions)); // Semver::rsort provides soft `array` type, let's validate and ensure proper type for SCA
$this->availableVersions = $versions;
}
}

View File

@@ -0,0 +1,37 @@
<?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\SelfUpdate;
/**
* @internal
*/
interface NewVersionCheckerInterface
{
/**
* Returns the tag of the latest version.
*/
public function getLatestVersion(): string;
/**
* Returns the tag of the latest minor/patch version of the given major version.
*/
public function getLatestVersionOfMajor(int $majorVersion): ?string;
/**
* Returns -1, 0, or 1 if the first version is respectively less than,
* equal to, or greater than the second.
*/
public function compareVersions(string $versionA, string $versionB): int;
}

View File

@@ -0,0 +1,83 @@
<?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;
use PhpCsFixer\ToolInfo;
use PhpCsFixer\ToolInfoInterface;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class WarningsDetector
{
private ToolInfoInterface $toolInfo;
/**
* @var list<string>
*/
private array $warnings = [];
public function __construct(ToolInfoInterface $toolInfo)
{
$this->toolInfo = $toolInfo;
}
public function detectOldMajor(): void
{
// @TODO 3.99 to be activated with new MAJOR release 4.0
// $currentMajorVersion = \intval(explode('.', Application::VERSION)[0], 10);
// $nextMajorVersion = $currentMajorVersion + 1;
// $this->warnings[] = "You are running PHP CS Fixer v{$currentMajorVersion}, which is not maintained anymore. Please update to v{$nextMajorVersion}.";
// $this->warnings[] = "You may find an UPGRADE guide at https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/v{$nextMajorVersion}.0.0/UPGRADE-v{$nextMajorVersion}.md .";
}
public function detectOldVendor(): void
{
if ($this->toolInfo->isInstalledByComposer()) {
$details = $this->toolInfo->getComposerInstallationDetails();
if (ToolInfo::COMPOSER_LEGACY_PACKAGE_NAME === $details['name']) {
$this->warnings[] = \sprintf(
'You are running PHP CS Fixer installed with old vendor `%s`. Please update to `%s`.',
ToolInfo::COMPOSER_LEGACY_PACKAGE_NAME,
ToolInfo::COMPOSER_PACKAGE_NAME
);
}
}
}
public function detectNonMonolithic(): void
{
if (filter_var(getenv('PHP_CS_FIXER_NON_MONOLITHIC'), \FILTER_VALIDATE_BOOL)) {
$this->warnings[] = 'Processing non-monolithic files enabled, because `PHP_CS_FIXER_NON_MONOLITHIC` is set. Execution result may be unpredictable - non-monolithic files are not officially supported.';
}
}
/**
* @return list<string>
*/
public function getWarnings(): array
{
if (0 === \count($this->warnings)) {
return [];
}
return array_values(array_unique(array_merge(
$this->warnings,
['If you need help while solving warnings, ask at https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/discussions/, we will help you!']
)));
}
}