Missing dependancies

This commit is contained in:
tokslaw7
2023-06-21 12:19:22 +00:00
parent fbf3f180c2
commit 421f25c80d
2356 changed files with 342670 additions and 4743 deletions
@@ -0,0 +1,142 @@
<?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\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\SelfUpdate\GithubClient;
use PhpCsFixer\Console\SelfUpdate\NewVersionChecker;
use PhpCsFixer\PharChecker;
use PhpCsFixer\ToolInfo;
use PhpCsFixer\Utils;
use Symfony\Component\Console\Application as BaseApplication;
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 VERSION = '3.13.0';
public const VERSION_CODENAME = 'Oliva';
private ToolInfo $toolInfo;
public function __construct()
{
parent::__construct('PHP CS Fixer', self::VERSION);
$this->toolInfo = new ToolInfo();
// in alphabetical order
$this->add(new DescribeCommand());
$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()
));
}
public static function getMajorVersion(): int
{
return (int) explode('.', self::VERSION)[0];
}
/**
* {@inheritdoc}
*/
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();
$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:</>' : 'Detected deprecations in use:');
foreach ($triggeredDeprecations as $deprecation) {
$stdErr->writeln(sprintf('- %s', $deprecation));
}
}
}
return $result;
}
/**
* {@inheritdoc}
*/
public function getLongVersion(): string
{
$commit = '@git-commit@';
$versionCommit = '';
if ('@'.'git-commit@' !== $commit) { /** @phpstan-ignore-line as `$commit` is replaced during phar building */
$versionCommit = substr($commit, 0, 7);
}
return implode('', [
parent::getLongVersion(),
$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> and <comment>Dariusz Ruminski</comment>.',
"\nPHP runtime: <info>".PHP_VERSION.'</info>',
]);
}
/**
* {@inheritdoc}
*/
protected function getDefaultCommands(): array
{
return [new HelpCommand(), new ListCommand()];
}
}
@@ -0,0 +1,428 @@
<?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\Differ\DiffConsoleFormatter;
use PhpCsFixer\Differ\FullDiffer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
use PhpCsFixer\Fixer\FixerInterface;
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\RuleSets;
use PhpCsFixer\StdinFileInfo;
use PhpCsFixer\Tokenizer\Tokens;
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\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
#[AsCommand(name: 'describe')]
final class DescribeCommand extends Command
{
/**
* @var string
*/
protected static $defaultName = 'describe';
/**
* @var string[]
*/
private $setNames;
private FixerFactory $fixerFactory;
/**
* @var array<string, FixerInterface>
*/
private $fixers;
public function __construct(?FixerFactory $fixerFactory = null)
{
parent::__construct();
if (null === $fixerFactory) {
$fixerFactory = new FixerFactory();
$fixerFactory->registerBuiltInFixers();
}
$this->fixerFactory = $fixerFactory;
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDefinition(
[
new InputArgument('name', InputArgument::REQUIRED, 'Name of rule / set.'),
]
)
->setDescription('Describe rule / ruleset.')
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity() && $output instanceof ConsoleOutputInterface) {
$stdErr = $output->getErrorOutput();
$stdErr->writeln($this->getApplication()->getLongVersion());
}
$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');
}
/** @var FixerInterface $fixer */
$fixer = $fixers[$name];
$definition = $fixer->getDefinition();
$summary = $definition->getSummary();
if ($fixer instanceof DeprecatedFixerInterface) {
$successors = $fixer->getSuccessorsNames();
$message = [] === $successors
? 'will be removed on next major version'
: sprintf('use %s instead', Utils::naturalLanguageJoinWithBackticks($successors));
$message = Preg::replace('/(`.+?`)/', '<info>$1</info>', $message);
$summary .= sprintf(' <error>DEPRECATED</error>: %s.', $message);
}
$output->writeln(sprintf('<info>Description of</info> %s <info>rule</info>.', $name));
if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
$output->writeln(sprintf('Fixer class: <comment>%s</comment>.', \get_class($fixer)));
}
$output->writeln($summary);
$description = $definition->getDescription();
if (null !== $description) {
$output->writeln($description);
}
$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 function ($value): string {
return $value instanceof AllowedValueSubset
? 'a subset of <comment>'.HelpCommand::toString($value->getAllowedValues()).'</comment>'
: '<comment>'.HelpCommand::toString($value).'</comment>';
}, $allowed);
}
$line .= ' ('.implode(', ', $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>',
HelpCommand::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('');
}
/** @var CodeSampleInterface[] $codeSamples */
$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($codeSamples)) {
$output->writeln([
'Fixing examples cannot be 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, HelpCommand::toString($codeSample->getConfiguration())));
}
} else {
$output->writeln(sprintf(' * Example #%d.', $index + 1));
}
$output->writeln([$diffFormatter->format($diff, ' %s'), '']);
}
}
}
private function describeSet(OutputInterface $output, string $name): void
{
if (!\in_array($name, $this->getSetNames(), true)) {
throw new DescribeNameNotFoundException($name, 'set');
}
$ruleSetDefinitions = RuleSets::getSetDefinitions();
$fixers = $this->getFixers();
$output->writeln(sprintf('<info>Description of the</info> %s <info>set.</info>', $ruleSetDefinitions[$name]->getName()));
$output->writeln($this->replaceRstLinks($ruleSetDefinitions[$name]->getDescription()));
if ($ruleSetDefinitions[$name]->isRisky()) {
$output->writeln('This set contains <error>risky</error> rules.');
}
$output->writeln('');
$help = '';
foreach ($ruleSetDefinitions[$name]->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;
}
/** @var FixerInterface $fixer */
$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", HelpCommand::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 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_VERY_VERBOSE) {
$describe = [
'sets' => $this->getSetNames(),
'rules' => $this->getFixers(),
];
} elseif ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
$describe = 'set' === $type ? ['sets' => $this->getSetNames()] : ['rules' => $this->getFixers()];
} else {
return;
}
/** @var string[] $items */
foreach ($describe as $list => $items) {
$output->writeln(sprintf('<comment>Defined %s:</comment>', $list));
foreach ($items as $name => $item) {
$output->writeln(sprintf('* <info>%s</info>', \is_string($name) ? $name : $item));
}
}
}
private function replaceRstLinks(string $content): string
{
return Preg::replaceCallback(
'/(`[^<]+<[^>]+>`_)/',
static function (array $matches) {
return Preg::replaceCallback(
'/`(.*)<(.*)>`_/',
static function (array $matches): string {
return $matches[1].'('.$matches[2].')';
},
$matches[1]
);
},
$content
);
}
}
@@ -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;
}
}
@@ -0,0 +1,128 @@
<?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\ListDocumentGenerator;
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;
use Symfony\Component\Finder\SplFileInfo;
/**
* @internal
*/
#[AsCommand(name: 'documentation')]
final class DocumentationCommand extends Command
{
/**
* @var string
*/
protected static $defaultName = 'documentation';
protected function configure(): void
{
$this
->setAliases(['doc'])
->setDescription('Dumps the documentation of the project into its "/doc" directory.')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$filesystem = new Filesystem();
$locator = new DocumentationLocator();
$fixerFactory = new FixerFactory();
$fixerFactory->registerBuiltInFixers();
$fixers = $fixerFactory->getFixers();
$setDefinitions = RuleSets::getSetDefinitions();
$fixerDocumentGenerator = new FixerDocumentGenerator($locator);
$ruleSetDocumentationGenerator = new RuleSetDocumentationGenerator($locator);
$listDocumentGenerator = new ListDocumentGenerator($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);
$filesystem->dumpFile(
$locator->getFixerDocumentationFilePath($fixer),
$fixerDocumentGenerator->generateFixerDocumentation($fixer)
);
}
/** @var SplFileInfo $file */
foreach (
(new Finder())->files()
->in($locator->getFixersDocumentationDirectoryPath())
->notPath($docForFixerRelativePaths) as $file
) {
$filesystem->remove($file->getPathname());
}
// Fixer doc. index
$filesystem->dumpFile(
$locator->getFixersDocumentationIndexFilePath(),
$fixerDocumentGenerator->generateFixersDocumentationIndex($fixers)
);
// RuleSet docs.
/** @var SplFileInfo $file */
foreach ((new Finder())->files()->in($locator->getRuleSetsDocumentationDirectoryPath()) as $file) {
$filesystem->remove($file->getPathname());
}
$paths = [];
foreach ($setDefinitions as $name => $definition) {
$path = $locator->getRuleSetsDocumentationFilePath($name);
$paths[$name] = $path;
$filesystem->dumpFile($path, $ruleSetDocumentationGenerator->generateRuleSetsDocumentation($definition, $fixers));
}
// RuleSet doc. index
$filesystem->dumpFile(
$locator->getRuleSetsDocumentationIndexFilePath(),
$ruleSetDocumentationGenerator->generateRuleSetsDocumentationIndex($paths)
);
// List file / Appendix
$filesystem->dumpFile(
$locator->getListingFilePath(),
$listDocumentGenerator->generateListingDocumentation($fixers)
);
$output->writeln('Docs updated.');
return 0;
}
}
@@ -0,0 +1,360 @@
<?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\ConfigurationResolver;
use PhpCsFixer\Console\Output\ErrorOutput;
use PhpCsFixer\Console\Output\NullOutput;
use PhpCsFixer\Console\Output\ProcessOutput;
use PhpCsFixer\Console\Report\FixReport\ReportSummary;
use PhpCsFixer\Error\ErrorsManager;
use PhpCsFixer\Runner\Runner;
use PhpCsFixer\ToolInfoInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
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>
*
* @internal
*/
#[AsCommand(name: 'fix')]
final class FixCommand extends Command
{
/**
* @var string
*/
protected static $defaultName = 'fix';
private EventDispatcherInterface $eventDispatcher;
private ErrorsManager $errorsManager;
private Stopwatch $stopwatch;
private ConfigInterface $defaultConfig;
private ToolInfoInterface $toolInfo;
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;
}
/**
* {@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 fix 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 `txt` (default one), `json`, `xml`, `checkstyle`, `junit` and `gitlab`.
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>`_
* `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>--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.
If the option is not provided, it defaults to <comment>dots</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 <command>--using-cache</command> 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 fix 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
;
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDefinition(
[
new InputArgument('path', InputArgument::IS_ARRAY, 'The path.'),
new InputOption('path-mode', '', InputOption::VALUE_REQUIRED, 'Specify path mode (can be override or intersection).', ConfigurationResolver::PATH_MODE_OVERRIDE),
new InputOption('allow-risky', '', InputOption::VALUE_REQUIRED, 'Are risky fixers allowed (can be yes or no).'),
new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a .php-cs-fixer.php file.'),
new InputOption('dry-run', '', InputOption::VALUE_NONE, 'Only shows which files would have been modified.'),
new InputOption('rules', '', InputOption::VALUE_REQUIRED, 'The rules.'),
new InputOption('using-cache', '', InputOption::VALUE_REQUIRED, 'Does cache should be used (can be yes or no).'),
new InputOption('cache-file', '', InputOption::VALUE_REQUIRED, 'The path to the cache file.'),
new InputOption('diff', '', InputOption::VALUE_NONE, 'Also produce diff for each file.'),
new InputOption('format', '', InputOption::VALUE_REQUIRED, 'To output results in other formats.'),
new InputOption('stop-on-violation', '', InputOption::VALUE_NONE, 'Stop execution on first violation.'),
new InputOption('show-progress', '', InputOption::VALUE_REQUIRED, 'Type of progress indicator (none, dots).'),
]
)
->setDescription('Fixes a directory or a file.')
;
}
/**
* {@inheritdoc}
*/
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' => $input->getOption('dry-run'),
'rules' => $passedRules,
'path' => $input->getArgument('path'),
'path-mode' => $input->getOption('path-mode'),
'using-cache' => $input->getOption('using-cache'),
'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'),
],
getcwd(),
$this->toolInfo
);
$reporter = $resolver->getReporter();
$stdErr = $output instanceof ConsoleOutputInterface
? $output->getErrorOutput()
: ('txt' === $reporter->getFormat() ? $output : null)
;
if (null !== $stdErr) {
if (OutputInterface::VERBOSITY_VERBOSE <= $verbosity) {
$stdErr->writeln($this->getApplication()->getLongVersion());
}
$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));
}
}
}
$progressType = $resolver->getProgress();
$finder = $resolver->getFinder();
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.')
);
}
if ('none' === $progressType || null === $stdErr) {
$progressOutput = new NullOutput();
} else {
$finder = new \ArrayIterator(iterator_to_array($finder));
$progressOutput = new ProcessOutput(
$stdErr,
$this->eventDispatcher,
(new Terminal())->getWidth(),
\count($finder)
);
}
$runner = new Runner(
$finder,
$resolver->getFixers(),
$resolver->getDiffer(),
'none' !== $progressType ? $this->eventDispatcher : null,
$this->errorsManager,
$resolver->getLinter(),
$resolver->isDryRun(),
$resolver->getCacheManager(),
$resolver->getDirectory(),
$resolver->shouldStopOnViolation()
);
$this->stopwatch->start('fixFiles');
$changed = $runner->fix();
$this->stopwatch->stop('fixFiles');
$progressOutput->printLegend();
$fixEvent = $this->stopwatch->getEvent('fixFiles');
$reportSummary = new ReportSummary(
$changed,
$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
);
}
}
@@ -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\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;
}
}
@@ -0,0 +1,131 @@
<?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\Preg;
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
{
/**
* @var string
*/
protected static $defaultName = 'help';
/**
* @param mixed $value
*/
public static function toString($value): string
{
return \is_array($value)
? static::arrayToString($value)
: static::scalarToString($value)
;
}
/**
* 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 function ($value): bool {
return !$value instanceof \Closure;
});
usort($allowed, static function ($valueA, $valueB): int {
if ($valueA instanceof AllowedValueSubset) {
return -1;
}
if ($valueB instanceof AllowedValueSubset) {
return 1;
}
return strcasecmp(
self::toString($valueA),
self::toString($valueB)
);
});
if (0 === \count($allowed)) {
$allowed = null;
}
}
return $allowed;
}
/**
* {@inheritdoc}
*/
protected function initialize(InputInterface $input, OutputInterface $output): void
{
$output->getFormatter()->setStyle('url', new OutputFormatterStyle('blue'));
}
/**
* @param mixed $value
*/
private static function scalarToString($value): string
{
$str = var_export($value, true);
return Preg::replace('/\bNULL\b/', 'null', $str);
}
/**
* @param array<mixed> $value
*/
private static function arrayToString(array $value): string
{
if (0 === \count($value)) {
return '[]';
}
$isHash = !array_is_list($value);
$str = '[';
foreach ($value as $k => $v) {
if ($isHash) {
$str .= static::scalarToString($k).' => ';
}
$str .= \is_array($v)
? static::arrayToString($v).', '
: static::scalarToString($v).', '
;
}
return substr($str, 0, -2).']';
}
}
@@ -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\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;
/**
* @author Markus Staab <markus.staab@redaxo.org>
*
* @internal
*/
#[AsCommand(name: 'list-files')]
final class ListFilesCommand extends Command
{
/**
* @var string
*/
protected static $defaultName = 'list-files';
private ConfigInterface $defaultConfig;
private ToolInfoInterface $toolInfo;
public function __construct(ToolInfoInterface $toolInfo)
{
parent::__construct();
$this->defaultConfig = new Config();
$this->toolInfo = $toolInfo;
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDefinition(
[
new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a .php-cs-fixer.php file.'),
]
)
->setDescription('List all files being fixed by the given config.')
;
}
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();
/** @var \SplFileInfo $file */
foreach ($finder as $file) {
if ($file->isFile()) {
$relativePath = str_replace($cwd, '.', $file->getRealPath());
// unify directory separators across operating system
$relativePath = str_replace('/', \DIRECTORY_SEPARATOR, $relativePath);
$output->writeln(escapeshellarg($relativePath));
}
}
return 0;
}
}
@@ -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 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')]
final class ListSetsCommand extends Command
{
/**
* @var string
*/
protected static $defaultName = 'list-sets';
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDefinition(
[
new InputOption('format', '', InputOption::VALUE_REQUIRED, 'To output results in other formats.', (new TextReporter())->getFormat()),
]
)
->setDescription('List all available RuleSets.')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$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, implode('", "', $formats)));
}
return $reporter;
}
}
@@ -0,0 +1,180 @@
<?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\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')]
final class SelfUpdateCommand extends Command
{
/**
* @var string
*/
protected static $defaultName = 'self-update';
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}
*/
protected function configure(): void
{
$this
->setAliases(['selfupdate'])
->setDefinition(
[
new InputOption('--force', '-f', InputOption::VALUE_NONE, 'Force update to next major version if available.'),
]
)
->setDescription('Update php-cs-fixer.phar to the latest stable version.')
->setHelp(
<<<'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
)
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity() && $output instanceof ConsoleOutputInterface) {
$stdErr = $output->getErrorOutput();
$stdErr->writeln($this->getApplication()->getLongVersion());
}
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 = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0];
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;
}
}
@@ -0,0 +1,961 @@
<?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\Command\HelpCommand;
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\RuleSet\RuleSet;
use PhpCsFixer\RuleSet\RuleSetInterface;
use PhpCsFixer\StdinFileInfo;
use PhpCsFixer\ToolInfoInterface;
use PhpCsFixer\Utils;
use PhpCsFixer\WhitespacesFixerConfig;
use PhpCsFixer\WordMatcher;
use Symfony\Component\Console\Output\OutputInterface;
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.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Katsuhiro Ogawa <ko.fivestar@gmail.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class ConfigurationResolver
{
public const PATH_MODE_OVERRIDE = 'override';
public const PATH_MODE_INTERSECTION = 'intersection';
/**
* @var null|bool
*/
private $allowRisky;
/**
* @var null|ConfigInterface
*/
private $config;
/**
* @var null|string
*/
private $configFile;
private string $cwd;
private ConfigInterface $defaultConfig;
/**
* @var null|ReporterInterface
*/
private $reporter;
/**
* @var null|bool
*/
private $isStdIn;
/**
* @var null|bool
*/
private $isDryRun;
/**
* @var null|FixerInterface[]
*/
private $fixers;
/**
* @var null|bool
*/
private $configFinderIsOverridden;
private ToolInfoInterface $toolInfo;
/**
* @var array<string, mixed>
*/
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,
'show-progress' => null,
'stop-on-violation' => null,
'using-cache' => null,
'verbosity' => null,
];
/**
* @var null|string
*/
private $cacheFile;
/**
* @var null|CacheManagerInterface
*/
private $cacheManager;
/**
* @var null|DifferInterface
*/
private $differ;
/**
* @var null|Directory
*/
private $directory;
/**
* @var null|iterable<\SplFileInfo>
*/
private ?iterable $finder = null;
private ?string $format = null;
/**
* @var null|Linter
*/
private $linter;
/**
* @var null|list<string>
*/
private ?array $path = null;
/**
* @var null|string
*/
private $progress;
/**
* @var null|RuleSet
*/
private $ruleSet;
/**
* @var null|bool
*/
private $usingCache;
/**
* @var FixerFactory
*/
private $fixerFactory;
/**
* @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);
$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 getConfigFile(): ?string
{
if (null === $this->configFile) {
$this->getConfig();
}
return $this->configFile;
}
public function getDiffer(): DifferInterface
{
if (null === $this->differ) {
if ($this->options['diff']) {
$this->differ = new UnifiedDiffer();
} else {
$this->differ = 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;
}
$this->directory = new Directory(\dirname($absolutePath));
}
return $this->directory;
}
/**
* @return FixerInterface[] An array of 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 function (FixerInterface $fixer): string {
return $fixer->getName();
},
array_filter(
$this->fixers,
static function (FixerInterface $fixer): bool {
return $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?', implode('", "', $riskyFixers)));
}
}
}
return $this->fixers;
}
public function getLinter(): LinterInterface
{
if (null === $this->linter) {
$this->linter = new Linter();
}
return $this->linter;
}
/**
* Returns path.
*
* @return 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;
}
/**
* @throws InvalidConfigurationException
*/
public function getProgress(): string
{
if (null === $this->progress) {
if (OutputInterface::VERBOSITY_VERBOSE <= $this->options['verbosity'] && 'txt' === $this->getFormat()) {
$progressType = $this->options['show-progress'];
$progressTypes = ['none', 'dots'];
if (null === $progressType) {
$progressType = $this->getConfig()->getHideProgress() ? 'none' : 'dots';
} elseif (!\in_array($progressType, $progressTypes, true)) {
throw new InvalidConfigurationException(sprintf(
'The progress type "%s" is not defined, supported are "%s".',
$progressType,
implode('", "', $progressTypes)
));
}
$this->progress = $progressType;
} else {
$this->progress = 'none';
}
}
return $this->progress;
}
public function getReporter(): ReporterInterface
{
if (null === $this->reporter) {
$reporterFactory = new ReporterFactory();
$reporterFactory->registerBuiltInReporters();
$format = $this->getFormat();
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, implode('", "', $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->toolInfo->isInstalledAsPhar() || $this->toolInfo->isInstalledByComposer());
return $this->usingCache;
}
/**
* @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 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 = $dirName ?: $path[0];
}
$candidates = [
$configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php',
$configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php',
$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';
$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 getFormat(): string
{
if (null === $this->format) {
$this->format = $this->options['format'] ?? $this->getConfig()->getFormat();
}
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<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<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 "'.HelpCommand::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()]);
}
$modes = [self::PATH_MODE_OVERRIDE, self::PATH_MODE_INTERSECTION];
if (!\in_array(
$this->options['path-mode'],
$modes,
true
)) {
throw new InvalidConfigurationException(sprintf(
'The path-mode "%s" is not defined, supported are "%s".',
$this->options['path-mode'],
implode('", "', $modes)
));
}
$isIntersectionPathMode = self::PATH_MODE_INTERSECTION === $this->options['path-mode'];
$paths = array_filter(array_map(
static function (string $path) {
return 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;
}
private function resolveOptionBooleanValue(string $optionName): bool
{
$value = $this->options[$optionName];
if (!\is_string($value)) {
throw new InvalidConfigurationException(sprintf('Expected boolean or string value for option "%s".', $optionName));
}
if ('yes' === $value) {
return true;
}
if ('no' === $value) {
return false;
}
throw new InvalidConfigurationException(sprintf('Expected "yes" or "no" for option "%s", got "%s".', $optionName, $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;
}
}
@@ -0,0 +1,156 @@
<?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\Formatter\OutputFormatter;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final class ErrorOutput
{
private OutputInterface $output;
/**
* @var bool
*/
private $isDecorated;
public function __construct(OutputInterface $output)
{
$this->output = $output;
$this->isDecorated = $output->isDecorated();
}
/**
* @param 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']) && \Symfony\Component\Console\Command\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 (!empty($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
;
}
}
@@ -0,0 +1,25 @@
<?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;
/**
* @internal
*/
final class NullOutput implements ProcessOutputInterface
{
public function printLegend(): void
{
}
}
@@ -0,0 +1,133 @@
<?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\FixerFileProcessedEvent;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Output writer to show the process of a FixCommand.
*
* @internal
*/
final class ProcessOutput implements ProcessOutputInterface
{
/**
* File statuses map.
*
* @var array<FixerFileProcessedEvent::STATUS_*, array{symbol: string, format: string, description: string}>
*/
private static array $eventStatusMap = [
FixerFileProcessedEvent::STATUS_NO_CHANGES => ['symbol' => '.', 'format' => '%s', 'description' => 'no changes'],
FixerFileProcessedEvent::STATUS_FIXED => ['symbol' => 'F', 'format' => '<fg=green>%s</fg=green>', 'description' => 'fixed'],
FixerFileProcessedEvent::STATUS_SKIPPED => ['symbol' => 'S', 'format' => '<fg=cyan>%s</fg=cyan>', 'description' => 'skipped (cached or empty file)'],
FixerFileProcessedEvent::STATUS_INVALID => ['symbol' => 'I', 'format' => '<bg=red>%s</bg=red>', 'description' => 'invalid file syntax (file ignored)'],
FixerFileProcessedEvent::STATUS_EXCEPTION => ['symbol' => 'E', 'format' => '<bg=red>%s</bg=red>', 'description' => 'error'],
FixerFileProcessedEvent::STATUS_LINT => ['symbol' => 'E', 'format' => '<bg=red>%s</bg=red>', 'description' => 'error'],
];
private OutputInterface $output;
private EventDispatcherInterface $eventDispatcher;
private int $files;
private int $processedFiles = 0;
/**
* @var int
*/
private $symbolsPerLine;
public function __construct(OutputInterface $output, EventDispatcherInterface $dispatcher, int $width, int $nbFiles)
{
$this->output = $output;
$this->eventDispatcher = $dispatcher;
$this->eventDispatcher->addListener(FixerFileProcessedEvent::NAME, [$this, 'onFixerFileProcessed']);
$this->files = $nbFiles;
// 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, $width - \strlen((string) $this->files) * 2 - 11);
}
public function __destruct()
{
$this->eventDispatcher->removeListener(FixerFileProcessedEvent::NAME, [$this, 'onFixerFileProcessed']);
}
/**
* This class is not intended to be serialized,
* and cannot be deserialized (see __wakeup method).
*/
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__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 '.__CLASS__);
}
public function onFixerFileProcessed(FixerFileProcessedEvent $event): void
{
$status = self::$eventStatusMap[$event->getStatus()];
$this->output->write($this->output->isDecorated() ? sprintf($status['format'], $status['symbol']) : $status['symbol']);
++$this->processedFiles;
$symbolsOnCurrentLine = $this->processedFiles % $this->symbolsPerLine;
$isLast = $this->processedFiles === $this->files;
if (0 === $symbolsOnCurrentLine || $isLast) {
$this->output->write(sprintf(
'%s %'.\strlen((string) $this->files).'d / %d (%3d%%)',
$isLast && 0 !== $symbolsOnCurrentLine ? str_repeat(' ', $this->symbolsPerLine - $symbolsOnCurrentLine) : '',
$this->processedFiles,
$this->files,
round($this->processedFiles / $this->files * 100)
));
if (!$isLast) {
$this->output->writeln('');
}
}
}
public function printLegend(): void
{
$symbols = [];
foreach (self::$eventStatusMap as $status) {
$symbol = $status['symbol'];
if ('' === $symbol || isset($symbols[$symbol])) {
continue;
}
$symbols[$symbol] = sprintf('%s-%s', $this->output->isDecorated() ? sprintf($status['format'], $symbol) : $symbol, $status['description']);
}
$this->output->write(sprintf("\nLegend: %s\n", implode(', ', $symbols)));
}
}
@@ -0,0 +1,23 @@
<?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;
/**
* @internal
*/
interface ProcessOutputInterface
{
public function printLegend(): void;
}
@@ -0,0 +1,71 @@
<?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\Console\Formatter\OutputFormatter;
/**
* @author Kévin Gomez <contact@kevingomez.fr>
*
* @internal
*/
final class CheckstyleReporter implements ReporterInterface
{
/**
* {@inheritdoc}
*/
public function getFormat(): string
{
return 'checkstyle';
}
/**
* {@inheritdoc}
*/
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');
$checkstyles = $dom->appendChild($dom->createElement('checkstyle'));
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;
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($dom->saveXML()) : $dom->saveXML();
}
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;
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Console\Report\FixReport;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* Generates a report according to gitlabs subset of codeclimate json files.
*
* @see https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#data-types
*
* @author Hans-Christian Otto <c.otto@suora.com>
*
* @internal
*/
final class GitlabReporter implements ReporterInterface
{
public function getFormat(): string
{
return 'gitlab';
}
/**
* Process changed files array. Returns generated report.
*/
public function generate(ReportSummary $reportSummary): string
{
$report = [];
foreach ($reportSummary->getChanged() as $fileName => $change) {
foreach ($change['appliedFixers'] as $fixerName) {
$report[] = [
'description' => $fixerName,
'fingerprint' => md5($fileName.$fixerName),
'severity' => 'minor',
'location' => [
'path' => $fileName,
'lines' => [
'begin' => 0, // line numbers are required in the format, but not available to reports
],
],
];
}
}
$jsonString = json_encode($report);
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($jsonString) : $jsonString;
}
}
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Console\Report\FixReport;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @internal
*/
final class JsonReporter implements ReporterInterface
{
/**
* {@inheritdoc}
*/
public function getFormat(): string
{
return 'json';
}
/**
* {@inheritdoc}
*/
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 = [
'files' => $jsonFiles,
'time' => [
'total' => round($reportSummary->getTime() / 1000, 3),
],
'memory' => round($reportSummary->getMemory() / 1024 / 1024, 3),
];
$json = json_encode($json);
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($json) : $json;
}
}
@@ -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\Preg;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @internal
*/
final class JunitReporter implements ReporterInterface
{
/**
* {@inheritdoc}
*/
public function getFormat(): string
{
return 'junit';
}
/**
* {@inheritdoc}
*/
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');
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() / 1000
)
);
}
$dom->formatOutput = true;
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($dom->saveXML()) : $dom->saveXML();
}
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;
}
}
@@ -0,0 +1,92 @@
<?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>
*
* @internal
*/
final class ReportSummary
{
/**
* @var array<string, array{appliedFixers: list<string>, diff: string}>
*/
private array $changed;
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 $time,
int $memory,
bool $addAppliedFixers,
bool $isDryRun,
bool $isDecoratedOutput
) {
$this->changed = $changed;
$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 shouldAddAppliedFixers(): bool
{
return $this->addAppliedFixers;
}
}
@@ -0,0 +1,92 @@
<?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];
}
}
@@ -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;
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Console\Report\FixReport;
use PhpCsFixer\Differ\DiffConsoleFormatter;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @internal
*/
final class TextReporter implements ReporterInterface
{
/**
* {@inheritdoc}
*/
public function getFormat(): string
{
return 'txt';
}
/**
* {@inheritdoc}
*/
public function generate(ReportSummary $reportSummary): string
{
$output = '';
$i = 0;
foreach ($reportSummary->getChanged() as $file => $fixResult) {
++$i;
$output .= sprintf('%4d) %s', $i, $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(), $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 $memory, bool $isDryRun): string
{
if (0 === $time || 0 === $memory) {
return '';
}
return PHP_EOL.sprintf(
'%s all files in %.3f seconds, %.3f MB memory used'.PHP_EOL,
$isDryRun ? 'Checked' : 'Fixed',
$time / 1000,
$memory / 1024 / 1024
);
}
}
@@ -0,0 +1,129 @@
<?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\Console\Formatter\OutputFormatter;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @internal
*/
final class XmlReporter implements ReporterInterface
{
/**
* {@inheritdoc}
*/
public function getFormat(): string
{
return 'xml';
}
/**
* {@inheritdoc}
*/
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);
$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;
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($dom->saveXML()) : $dom->saveXML();
}
/**
* @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 / 1000, 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 / 1024 / 1024, 3);
$memoryXML = $dom->createElement('memory');
$memoryXML->setAttribute('value', (string) $memory);
$memoryXML->setAttribute('unit', 'MB');
return $memoryXML;
}
}
@@ -0,0 +1,58 @@
<?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>
*
* @internal
*/
final class JsonReporter implements ReporterInterface
{
/**
* {@inheritdoc}
*/
public function getFormat(): string
{
return 'json';
}
/**
* {@inheritdoc}
*/
public function generate(ReportSummary $reportSummary): string
{
$sets = $reportSummary->getSets();
usort($sets, static function (RuleSetDescriptionInterface $a, RuleSetDescriptionInterface $b): int {
return strcmp($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_PRETTY_PRINT);
}
}
@@ -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\Report\ListSetsReport;
use PhpCsFixer\RuleSet\RuleSetDescriptionInterface;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @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;
}
}
@@ -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];
}
}
@@ -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;
}
@@ -0,0 +1,57 @@
<?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>
*
* @internal
*/
final class TextReporter implements ReporterInterface
{
/**
* {@inheritdoc}
*/
public function getFormat(): string
{
return 'txt';
}
/**
* {@inheritdoc}
*/
public function generate(ReportSummary $reportSummary): string
{
$sets = $reportSummary->getSets();
usort($sets, static function (RuleSetDescriptionInterface $a, RuleSetDescriptionInterface $b): int {
return strcmp($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;
}
}
@@ -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\SelfUpdate;
/**
* @internal
*/
final class GithubClient implements GithubClientInterface
{
/**
* {@inheritdoc}
*/
public function getTags(): array
{
$url = 'https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/tags';
$result = @file_get_contents(
$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".', $url));
}
$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.',
$url,
json_last_error_msg()
));
}
return $result;
}
}
@@ -0,0 +1,31 @@
<?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<array{
* name: string,
* zipball_url: string,
* tarball_url: string,
* commit: array{sha: string, url: string},
* }>
*/
public function getTags(): array;
}
@@ -0,0 +1,110 @@
<?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|string[]
*/
private $availableVersions;
public function __construct(GithubClientInterface $githubClient)
{
$this->githubClient = $githubClient;
$this->versionParser = new VersionParser();
}
/**
* {@inheritdoc}
*/
public function getLatestVersion(): string
{
$this->retrieveAvailableVersions();
return $this->availableVersions[0];
}
/**
* {@inheritdoc}
*/
public function getLatestVersionOfMajor(int $majorVersion): ?string
{
$this->retrieveAvailableVersions();
$semverConstraint = '^'.$majorVersion;
foreach ($this->availableVersions as $availableVersion) {
if (Semver::satisfies($availableVersion, $semverConstraint)) {
return $availableVersion;
}
}
return null;
}
/**
* {@inheritdoc}
*/
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 $tag) {
$version = $tag['name'];
try {
$this->versionParser->normalize($version);
if ('stable' === VersionParser::parseStability($version)) {
$this->availableVersions[] = $version;
}
} catch (\UnexpectedValueException $exception) {
// not a valid version tag
}
}
$this->availableVersions = Semver::rsort($this->availableVersions);
}
}
@@ -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;
}
@@ -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;
use PhpCsFixer\ToolInfo;
use PhpCsFixer\ToolInfoInterface;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class WarningsDetector
{
private ToolInfoInterface $toolInfo;
/**
* @var 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
);
}
}
}
/**
* @return string[]
*/
public function getWarnings(): array
{
if (0 === \count($this->warnings)) {
return [];
}
return array_unique(array_merge(
$this->warnings,
['If you need help while solving warnings, ask at https://gitter.im/PHP-CS-Fixer, we will help you!']
));
}
}