Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
tipuloidea
/
back
/
vendor
/
friendsofphp
/
php-cs-fixer
/
src
/
Console
/
Command
:
DescribeCommand.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?php declare(strict_types=1); /* * This file is part of PHP CS Fixer. * * (c) Fabien Potencier <fabien@symfony.com> * Dariusz Rumiński <dariusz.ruminski@gmail.com> * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace PhpCsFixer\Console\Command; use PhpCsFixer\Config; use PhpCsFixer\Console\Application; use PhpCsFixer\Console\ConfigurationResolver; use PhpCsFixer\Differ\DiffConsoleFormatter; use PhpCsFixer\Differ\FullDiffer; use PhpCsFixer\Documentation\DocumentationTag; use PhpCsFixer\Documentation\DocumentationTagGenerator; use PhpCsFixer\Documentation\DocumentationTagType; use PhpCsFixer\Documentation\FixerDocumentGenerator; use PhpCsFixer\Fixer\ConfigurableFixerInterface; 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\Future; use PhpCsFixer\Preg; use PhpCsFixer\RuleSet\AutomaticRuleSetDefinitionInterface; use PhpCsFixer\RuleSet\DeprecatedRuleSetDefinitionInterface; use PhpCsFixer\RuleSet\RuleSet; use PhpCsFixer\RuleSet\RuleSetDefinitionInterface; use PhpCsFixer\RuleSet\RuleSets; use PhpCsFixer\StdinFileInfo; use PhpCsFixer\Tokenizer\Tokens; use PhpCsFixer\ToolInfo; use PhpCsFixer\Utils; use PhpCsFixer\WordMatcher; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\TreeHelper; use Symfony\Component\Console\Helper\TreeNode; use Symfony\Component\Console\Input\ArrayInput; 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\Style\SymfonyStyle; /** * @author Dariusz Rumiński <dariusz.ruminski@gmail.com> * * @internal * * @no-named-arguments Parameter names are not covered by the backward compatibility promise. */ #[AsCommand(name: 'describe', description: 'Describe rule / ruleset.')] final class DescribeCommand extends Command { private const SET_ALIAS_TO_DESCRIBE_CONFIG = '@'; private const SET_ALIAS_TO_DESCRIBE_RULES_WITHOUT_SET = '@-'; /** @TODO PHP 8.0 - remove the property */ protected static $defaultName = 'describe'; /** @TODO PHP 8.0 - remove the property */ protected static $defaultDescription = 'Describe rule / ruleset.'; /** * @var ?list<string> */ private ?array $setNames = null; private FixerFactory $fixerFactory; /** * @var null|array<string, FixerInterface> */ private ?array $fixers = null; public function __construct(?FixerFactory $fixerFactory = null) { parent::__construct(); if (null === $fixerFactory) { $fixerFactory = new FixerFactory(); $fixerFactory->registerBuiltInFixers(); } $this->fixerFactory = $fixerFactory; } protected function configure(): void { $this->setDefinition( [ new InputArgument('name', InputArgument::OPTIONAL, 'Name of rule / set.', null, fn () => array_merge($this->getSetNames(), array_keys($this->getFixers()))), new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a .php-cs-fixer.php file.'), new InputOption('expand', '', InputOption::VALUE_NONE, 'Shall nested sets be expanded into nested rules.'), new InputOption('format', '', InputOption::VALUE_REQUIRED, 'To output results in other formats (txt, tree).', 'txt', ['txt', 'tree']), ], ); } protected function execute(InputInterface $input, OutputInterface $output): int { if ($output instanceof ConsoleOutputInterface) { $stdErr = $output->getErrorOutput(); $stdErr->writeln(Application::getAboutWithRuntime(true)); } $resolver = new ConfigurationResolver( new Config(), ['config' => $input->getOption('config')], getcwd(), // @phpstan-ignore argument.type new ToolInfo(), ); $this->fixerFactory->registerCustomFixers($resolver->getConfig()->getCustomFixers()); /** @var ?string $name */ $name = $input->getArgument('name'); $expand = $input->getOption('expand'); $format = $input->getOption('format'); if (null === $name) { if (false === $input->isInteractive()) { throw new RuntimeException('Not enough arguments (missing: "name") when not running interactively.'); } $io = new SymfonyStyle($input, $output); $shallDescribeConfigInUse = 'yes' === $io->choice( 'Do you want to describe used configuration? (alias:`@`', ['yes', 'no'], 'yes', ); if ($shallDescribeConfigInUse) { $name = self::SET_ALIAS_TO_DESCRIBE_CONFIG; } else { $name = $io->choice( 'Please select rule / set to describe', array_merge($this->getSetNames(), array_keys($this->getFixers())), ); } } if ('tree' === $format) { if (!str_starts_with($name, '@')) { throw new \InvalidArgumentException( 'The "--format=tree" option is available only when describing a set (name starting with "@").', ); } if (!class_exists(TreeHelper::class)) { throw new \RuntimeException('The "--format=tree" option requires symfony/console 7.3+.'); } } if (!str_starts_with($name, '@')) { if (true === $expand) { throw new \InvalidArgumentException( 'The "--expand" option is available only when describing a set (name starting with "@").', ); } } try { if (str_starts_with($name, '@')) { $this->describeSet($input, $output, $name, $resolver); return 0; } $this->describeRule($output, $name); } catch (DescribeNameNotFoundException $e) { $matcher = new WordMatcher( 'set' === $e->getType() ? $this->getSetNames() : array_keys($this->getFixers()), ); $alternative = $matcher->match($name); $this->describeList($output, $e->getType()); throw new \InvalidArgumentException(\sprintf( '%s "%s" not found.%s', ucfirst($e->getType()), $name, null === $alternative ? '' : ' Did you mean "'.$alternative.'"?', )); } return 0; } private function describeRule(OutputInterface $output, string $name): void { $fixers = $this->getFixers(); if (!isset($fixers[$name])) { throw new DescribeNameNotFoundException($name, 'rule'); } $fixer = $fixers[$name]; $definition = $fixer->getDefinition(); $output->writeln(\sprintf('<fg=blue>Description of the <info>`%s`</info> rule.</>', $name)); $output->writeln(''); if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { $output->writeln(\sprintf('Fixer class: <comment>%s</comment>.', \get_class($fixer))); $output->writeln(''); } $output->writeln($definition->getSummary()); $description = $definition->getDescription(); if (null !== $description) { $output->writeln($description); } $output->writeln(''); $tags = DocumentationTagGenerator::analyseRule($fixer); foreach ($tags as $tag) { if (DocumentationTagType::DEPRECATED === $tag->type) { Future::triggerDeprecation(new \RuntimeException(str_replace( '`', '"', \sprintf( '%s%s', str_replace('This rule', \sprintf('Rule "%s"', $name), $tag->title), null !== $tag->description ? '. '.$tag->description : '', ), ))); } elseif (DocumentationTagType::CONFIGURABLE === $tag->type) { continue; // skip, handled later } $output->writeln(\sprintf('<error>%s</error>', $tag->title)); $tagDescription = $tag->description; if (null !== $tagDescription) { $tagDescription = Preg::replace('/(`[^`]+`)/', '<info>$1</info>', $tagDescription); $output->writeln($tagDescription); } $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) { $allowedTypes = $option->getAllowedTypes(); if (null !== $allowedTypes) { $allowed = array_map( static fn (string $type): string => '<comment>'.$type.'</comment>', $allowedTypes, ); } } else { $allowed = array_map(static fn ($value): string => $value instanceof AllowedValueSubset ? 'a subset of <comment>'.Utils::toString($value->getAllowedValues()).'</comment>' : '<comment>'.Utils::toString($value).'</comment>', $allowed); } if (null !== $allowed) { $line .= ' ('.Utils::naturalLanguageJoin($allowed, '').')'; } $description = Preg::replace('/(`.+?`)/', '<info>$1</info>', OutputFormatter::escape($option->getDescription())); $line .= ': '.lcfirst(Preg::replace('/\.$/', '', $description)).'; '; if ($option->hasDefault()) { $line .= \sprintf( 'defaults to <comment>%s</comment>', Utils::toString($option->getDefault()), ); } else { $line .= '<comment>required</comment>'; } if ($option instanceof DeprecatedFixerOption) { $line .= '. <error>DEPRECATED</error>: '.Preg::replace( '/(`.+?`)/', '<info>$1</info>', OutputFormatter::escape(lcfirst($option->getDeprecationMessage())), ); } if ($option instanceof AliasedFixerOption) { $line .= '; <error>DEPRECATED</error> alias: <comment>'.$option->getAlias().'</comment>'; } $output->writeln($line); } $output->writeln(''); } $codeSamples = array_filter($definition->getCodeSamples(), static function (CodeSampleInterface $codeSample): bool { if ($codeSample instanceof VersionSpecificCodeSampleInterface) { return $codeSample->isSuitableFor(\PHP_VERSION_ID); } return true; }); if (0 === \count($definition->getCodeSamples())) { $output->writeln([ 'Fixing examples are not available for this rule.', '', ]); } elseif (0 === \count($codeSamples)) { $output->writeln([ 'Fixing examples <error>cannot be</error> demonstrated on the current PHP version.', '', ]); } else { $output->writeln('Fixing examples:'); $differ = new FullDiffer(); $diffFormatter = new DiffConsoleFormatter( $output->isDecorated(), \sprintf( '<comment> ---------- begin diff ----------</comment>%s%%s%s<comment> ----------- end diff -----------</comment>', \PHP_EOL, \PHP_EOL, ), ); foreach ($codeSamples as $index => $codeSample) { $old = $codeSample->getCode(); $tokens = Tokens::fromCode($old); $configuration = $codeSample->getConfiguration(); if ($fixer instanceof ConfigurableFixerInterface) { $fixer->configure($configuration ?? []); } $file = $codeSample instanceof FileSpecificCodeSampleInterface ? $codeSample->getSplFileInfo() : new StdinFileInfo(); $fixer->fix($file, $tokens); $diff = $differ->diff($old, $tokens->generateCode()); if ($fixer instanceof ConfigurableFixerInterface) { if (null === $configuration) { $output->writeln(\sprintf(' * Example #%d. Fixing with the <comment>default</comment> configuration.', $index + 1)); } else { $output->writeln(\sprintf(' * Example #%d. Fixing with configuration: <comment>%s</comment>.', $index + 1, Utils::toString($codeSample->getConfiguration()))); } } else { $output->writeln(\sprintf(' * Example #%d.', $index + 1)); } $output->writeln([$diffFormatter->format($diff, ' %s'), '']); } } $ruleSetConfigs = FixerDocumentGenerator::getSetsOfRule($name); if ([] !== $ruleSetConfigs) { ksort($ruleSetConfigs); $plural = 1 !== \count($ruleSetConfigs) ? 's' : ''; $output->writeln("The fixer is part of the following rule set{$plural}:"); $ruleSetDefinitions = RuleSets::getSetDefinitions(); foreach ($ruleSetConfigs as $set => $config) { \assert(isset($ruleSetDefinitions[$set])); $ruleSetDefinition = $ruleSetDefinitions[$set]; if ($ruleSetDefinition instanceof AutomaticRuleSetDefinitionInterface) { continue; } $deprecatedDesc = ($ruleSetDefinition instanceof DeprecatedRuleSetDefinitionInterface) ? ' *(deprecated)*' : ''; if (null !== $config) { $output->writeln(\sprintf('* <info>%s</info> with config: <comment>%s</comment>', $set.$deprecatedDesc, Utils::toString($config))); } else { $output->writeln(\sprintf('* <info>%s</info> with <comment>default</comment> config', $set.$deprecatedDesc)); } } $output->writeln(''); } } private function describeSet(InputInterface $input, OutputInterface $output, string $name, ConfigurationResolver $resolver): void { if ( !\in_array($name, [self::SET_ALIAS_TO_DESCRIBE_CONFIG, self::SET_ALIAS_TO_DESCRIBE_RULES_WITHOUT_SET], true) && !\in_array($name, $this->getSetNames(), true)) { throw new DescribeNameNotFoundException($name, 'set'); } if (self::SET_ALIAS_TO_DESCRIBE_CONFIG === $name) { $aliasedRuleSetDefinition = $this->createRuleSetDefinition( null, [], [ 'getDescription' => null === $resolver->getConfigFile() ? 'Default rules, no config file.' : 'Rules defined in used config.', 'getName' => \sprintf('@ - %s', $resolver->getConfig()->getName()), 'getRules' => $resolver->getConfig()->getRules(), 'isRisky' => $resolver->getRiskyAllowed(), ], ); } elseif (self::SET_ALIAS_TO_DESCRIBE_RULES_WITHOUT_SET === $name) { $rulesWithoutSet = array_filter( $this->getFixers(), static fn (string $name): bool => [] === FixerDocumentGenerator::getSetsOfRule($name), \ARRAY_FILTER_USE_KEY, ); $aliasedRuleSetDefinition = $this->createRuleSetDefinition( null, [], [ 'getDescription' => 'Rules that are not part of any set.', 'getName' => '@- - rules without set', 'getRules' => array_combine( array_map( static fn (FixerInterface $fixer): string => $fixer->getName(), $rulesWithoutSet, ), array_fill(0, \count($rulesWithoutSet), true), ), 'isRisky' => array_any( $rulesWithoutSet, static fn (FixerInterface $fixer): bool => $fixer->isRisky(), ), ], ); } $ruleSetDefinitions = RuleSets::getSetDefinitions(); $ruleSetDefinition = $aliasedRuleSetDefinition ?? $ruleSetDefinitions[$name]; $fixers = $this->getFixers(); if (true === $input->getOption('expand')) { $ruleSetDefinition = $this->createRuleSetDefinition($ruleSetDefinition, ['expand'], []); } else { $output->writeln("You may the '--expand' option to see nested sets expanded into nested rules."); } $output->writeln(\sprintf('<fg=blue>Description of the <info>`%s`</info> set.</>', $ruleSetDefinition->getName())); $output->writeln(''); $output->writeln($this->replaceRstLinks($ruleSetDefinition->getDescription())); $output->writeln(''); $tags = DocumentationTagGenerator::analyseRuleSet($ruleSetDefinition); foreach ($tags as $tag) { if (DocumentationTagType::DEPRECATED === $tag->type) { Future::triggerDeprecation(new \RuntimeException(str_replace( '`', '"', \sprintf( '%s%s', str_replace('This rule set', \sprintf('Rule set "%s"', $name), $tag->title), null !== $tag->description ? '. '.$tag->description : '', ), ))); } $output->writeln(\sprintf('<error>%s</error>', $tag->title)); $tagDescription = $tag->description; if (null !== $tagDescription) { $tagDescription = Preg::replace('/(`[^`]+`)/', '<info>$1</info>', $tagDescription); $output->writeln($tagDescription); } $output->writeln(''); } if ('tree' === $input->getOption('format')) { $this->describeSetContentAsTree($output, $ruleSetDefinition, $ruleSetDefinitions, $fixers); } else { $this->describeSetContentAsTxt($output, $ruleSetDefinition, $ruleSetDefinitions, $fixers); } } /** * @param array<string, RuleSetDefinitionInterface> $ruleSetDefinitions * @param array<string, FixerInterface> $fixers */ private function createTreeNode(RuleSetDefinitionInterface $ruleSetDefinition, array $ruleSetDefinitions, array $fixers): TreeNode { $tags = DocumentationTagGenerator::analyseRuleSet($ruleSetDefinition); $extra = [] !== $tags ? ' '.implode(' ', array_map( static fn (DocumentationTag $tag): string => "<error>{$tag->type}</error>", $tags, )) : ''; $node = new TreeNode($ruleSetDefinition->getName().$extra); $rules = $ruleSetDefinition->getRules(); $rulesKeys = array_keys($rules); natcasesort($rulesKeys); foreach ($rulesKeys as $rule) { \assert(isset($rules[$rule])); $config = $rules[$rule]; if (str_starts_with($rule, '@')) { $child = $this->createTreeNode($ruleSetDefinitions[$rule], $ruleSetDefinitions, $fixers); } else { $fixer = $fixers[$rule]; $tags = DocumentationTagGenerator::analyseRule($fixer); $extra = [] !== $tags ? ' '.implode(' ', array_map( static fn (DocumentationTag $tag): string => "<error>{$tag->type}</error>", $tags, )) : ''; if (false === $config) { $extra = \sprintf(' | <error>Configuration: %s</>', Utils::toString($config)); } elseif (true !== $config) { $extra = \sprintf(' | <comment>Configuration: %s</>', Utils::toString($config)); } $child = new TreeNode($rule.$extra); } $node->addChild($child); } return $node; } /** * @param array<string, RuleSetDefinitionInterface> $ruleSetDefinitions * @param array<string, FixerInterface> $fixers */ private function describeSetContentAsTree(OutputInterface $output, RuleSetDefinitionInterface $ruleSetDefinition, array $ruleSetDefinitions, array $fixers): void { $io = new SymfonyStyle( new ArrayInput([]), $output, ); $root = $this->createTreeNode($ruleSetDefinition, $ruleSetDefinitions, $fixers); $tree = TreeHelper::createTree($io, $root); $tree->render(); } /** * @param array<string, RuleSetDefinitionInterface> $ruleSetDefinitions * @param array<string, FixerInterface> $fixers */ private function describeSetContentAsTxt(OutputInterface $output, RuleSetDefinitionInterface $ruleSetDefinition, array $ruleSetDefinitions, array $fixers): void { $help = ''; foreach ($ruleSetDefinition->getRules() as $rule => $config) { if (str_starts_with($rule, '@')) { \assert(isset($ruleSetDefinitions[$rule])); $set = $ruleSetDefinitions[$rule]; $tags = DocumentationTagGenerator::analyseRuleSet($set); $help .= \sprintf( " * <info>%s</info>%s%s\n | %s\n\n", $rule, [] !== $tags ? ' ' : '', implode(' ', array_map( static fn (DocumentationTag $tag): string => "<error>{$tag->type}</error>", $tags, )), $this->replaceRstLinks($set->getDescription()), ); continue; } \assert(isset($fixers[$rule])); $fixer = $fixers[$rule]; $tags = DocumentationTagGenerator::analyseRule($fixer); $definition = $fixer->getDefinition(); $help .= \sprintf( " * <info>%s</info>%s%s\n | %s\n%s\n", $rule, [] !== $tags ? ' ' : '', implode(' ', array_map( static fn (DocumentationTag $tag): string => "<error>{$tag->type}</error>", $tags, )), $definition->getSummary(), true !== $config ? \sprintf(" <comment>| Configuration: %s</comment>\n", Utils::toString($config)) : '', ); } $output->write($help); } /** * @return array<string, FixerInterface> */ private function getFixers(): array { if (null !== $this->fixers) { return $this->fixers; } $fixers = []; foreach ($this->fixerFactory->getFixers() as $fixer) { $fixers[$fixer->getName()] = $fixer; } $this->fixers = $fixers; ksort($this->fixers); return $this->fixers; } /** * @return list<string> */ private function getSetNames(): array { if (null !== $this->setNames) { return $this->setNames; } $this->setNames = RuleSets::getSetDefinitionNames(); return $this->setNames; } /** * @param string $type 'rule'|'set' */ private function describeList(OutputInterface $output, string $type): void { if ($output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) { return; } if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE || 'set' === $type) { $output->writeln('<comment>Defined sets:</comment>'); $items = $this->getSetNames(); foreach ($items as $item) { $output->writeln(\sprintf('* <info>%s</info>', $item)); } } if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE || 'rule' === $type) { $output->writeln('<comment>Defined rules:</comment>'); $items = array_keys($this->getFixers()); foreach ($items as $item) { $output->writeln(\sprintf('* <info>%s</info>', $item)); } } } private function replaceRstLinks(string $content): string { return Preg::replaceCallback( '/(`[^<]+<[^>]+>`_)/', static fn (array $matches) => Preg::replaceCallback( '/`(.*)<(.*)>`_/', static fn (array $matches): string => $matches[1].'('.$matches[2].')', $matches[1], ), $content, ); } /** * @param list<'expand'> $adjustments * @param array{getDescription?: string, getName?: string, getRules?: array<string, array<string, mixed>|bool>, isRisky?: bool} $overrides */ private function createRuleSetDefinition(?RuleSetDefinitionInterface $ruleSetDefinition, array $adjustments, array $overrides): RuleSetDefinitionInterface { return new class($ruleSetDefinition, $adjustments, $overrides) implements RuleSetDefinitionInterface { private ?RuleSetDefinitionInterface $original; /** @var list<'expand'> */ private array $adjustments; /** @var array{getDescription?: string, getName?: string, getRules?: array<string, array<string, mixed>|bool>, isRisky?: bool} */ private array $overrides; /** * @param list<'expand'> $adjustments * @param array{getDescription?: string, getName?: string, getRules?: array<string, array<string, mixed>|bool>, isRisky?: bool} $overrides */ public function __construct( ?RuleSetDefinitionInterface $original, array $adjustments, array $overrides ) { $this->original = $original; $this->adjustments = $adjustments; $this->overrides = $overrides; } public function getDescription(): string { return $this->overrides[__FUNCTION__] ?? (null !== $this->original ? $this->original->{__FUNCTION__}() : 'unknown description'); // @phpstan-ignore method.dynamicName } public function getName(): string { $value = $this->overrides[__FUNCTION__] ?? (null !== $this->original ? $this->original->{__FUNCTION__}() : 'unknown name'); // @phpstan-ignore method.dynamicName if (\in_array('expand', $this->adjustments, true)) { $value .= ' (expanded)'; } return $value; } public function getRules(): array { $value = $this->overrides[__FUNCTION__] ?? (null !== $this->original ? $this->original->{__FUNCTION__}() : null); // @phpstan-ignore method.dynamicName if (null === $value) { throw new \LogicException('Cannot get rules from unknown original rule set and missing overrides.'); } if (\in_array('expand', $this->adjustments, true)) { $value = (new RuleSet($value))->getRules(); } return $value; } public function isRisky(): bool { $value = $this->overrides[__FUNCTION__] ?? (null !== $this->original ? $this->original->{__FUNCTION__}() : null); // @phpstan-ignore method.dynamicName if (null === $value) { throw new \LogicException('Cannot get isRisky from unknown original rule set and missing overrides.'); } return $value; } }; } }