FixCommand.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of PHP CS Fixer.
  5. *
  6. * (c) Fabien Potencier <fabien@symfony.com>
  7. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  8. *
  9. * This source file is subject to the MIT license that is bundled
  10. * with this source code in the file LICENSE.
  11. */
  12. namespace PhpCsFixer\Console\Command;
  13. use PhpCsFixer\Config;
  14. use PhpCsFixer\ConfigInterface;
  15. use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
  16. use PhpCsFixer\Console\ConfigurationResolver;
  17. use PhpCsFixer\Console\Output\ErrorOutput;
  18. use PhpCsFixer\Console\Output\OutputContext;
  19. use PhpCsFixer\Console\Output\Progress\ProgressOutputFactory;
  20. use PhpCsFixer\Console\Output\Progress\ProgressOutputType;
  21. use PhpCsFixer\Console\Report\FixReport\ReportSummary;
  22. use PhpCsFixer\Error\ErrorsManager;
  23. use PhpCsFixer\FixerFileProcessedEvent;
  24. use PhpCsFixer\Runner\Runner;
  25. use PhpCsFixer\ToolInfoInterface;
  26. use Symfony\Component\Console\Attribute\AsCommand;
  27. use Symfony\Component\Console\Command\Command;
  28. use Symfony\Component\Console\Input\InputArgument;
  29. use Symfony\Component\Console\Input\InputInterface;
  30. use Symfony\Component\Console\Input\InputOption;
  31. use Symfony\Component\Console\Output\ConsoleOutputInterface;
  32. use Symfony\Component\Console\Output\OutputInterface;
  33. use Symfony\Component\Console\Terminal;
  34. use Symfony\Component\EventDispatcher\EventDispatcher;
  35. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  36. use Symfony\Component\Stopwatch\Stopwatch;
  37. /**
  38. * @author Fabien Potencier <fabien@symfony.com>
  39. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  40. *
  41. * @final
  42. *
  43. * @internal
  44. */
  45. #[AsCommand(name: 'fix', description: 'Fixes a directory or a file.')]
  46. /* final */ class FixCommand extends Command
  47. {
  48. protected static $defaultName = 'fix';
  49. protected static $defaultDescription = 'Fixes a directory or a file.';
  50. private EventDispatcherInterface $eventDispatcher;
  51. private ErrorsManager $errorsManager;
  52. private Stopwatch $stopwatch;
  53. private ConfigInterface $defaultConfig;
  54. private ToolInfoInterface $toolInfo;
  55. private ProgressOutputFactory $progressOutputFactory;
  56. public function __construct(ToolInfoInterface $toolInfo)
  57. {
  58. parent::__construct();
  59. $this->eventDispatcher = new EventDispatcher();
  60. $this->errorsManager = new ErrorsManager();
  61. $this->stopwatch = new Stopwatch();
  62. $this->defaultConfig = new Config();
  63. $this->toolInfo = $toolInfo;
  64. $this->progressOutputFactory = new ProgressOutputFactory();
  65. }
  66. /**
  67. * {@inheritdoc}
  68. *
  69. * Override here to only generate the help copy when used.
  70. */
  71. public function getHelp(): string
  72. {
  73. return <<<'EOF'
  74. The <info>%command.name%</info> command tries to %command.name% as much coding standards
  75. problems as possible on a given file or files in a given directory and its subdirectories:
  76. <info>$ php %command.full_name% /path/to/dir</info>
  77. <info>$ php %command.full_name% /path/to/file</info>
  78. 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
  79. command arguments, then the paths provided to a `Finder` in config file will be ignored. You can use <comment>--path-mode=intersection</comment>
  80. to merge paths from the config file and from the argument:
  81. <info>$ php %command.full_name% --path-mode=intersection /path/to/dir</info>
  82. The <comment>--format</comment> option for the output format. Supported formats are `txt` (default one), `json`, `xml`, `checkstyle`, `junit` and `gitlab`.
  83. NOTE: the output for the following formats are generated in accordance with schemas
  84. * `checkstyle` follows the common `"checkstyle" XML schema </doc/schemas/fix/checkstyle.xsd>`_
  85. * `gitlab` follows the `codeclimate JSON schema </doc/schemas/fix/codeclimate.json>`_
  86. * `json` follows the `own JSON schema </doc/schemas/fix/schema.json>`_
  87. * `junit` follows the `JUnit XML schema from Jenkins </doc/schemas/fix/junit-10.xsd>`_
  88. * `xml` follows the `own XML schema </doc/schemas/fix/xml.xsd>`_
  89. The <comment>--quiet</comment> Do not output any message.
  90. The <comment>--verbose</comment> option will show the applied rules. When using the `txt` format it will also display progress notifications.
  91. 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
  92. * `-v`: verbose
  93. * `-vv`: very verbose
  94. * `-vvv`: debug
  95. The <comment>--rules</comment> option limits the rules to apply to the
  96. project:
  97. EOF. /* @TODO: 4.0 - change to @PER */ <<<'EOF'
  98. <info>$ php %command.full_name% /path/to/project --rules=@PSR12</info>
  99. By default the PSR-12 rules are used.
  100. The <comment>--rules</comment> option lets you choose the exact rules to
  101. apply (the rule names must be separated by a comma):
  102. <info>$ php %command.full_name% /path/to/dir --rules=line_ending,full_opening_tag,indentation_type</info>
  103. 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,
  104. using <comment>-name_of_fixer</comment>:
  105. <info>$ php %command.full_name% /path/to/dir --rules=-full_opening_tag,-indentation_type</info>
  106. When using combinations of exact and exclude rules, applying exact rules along with above excluded results:
  107. <info>$ php %command.full_name% /path/to/project --rules=@Symfony,-@PSR1,-blank_line_before_statement,strict_comparison</info>
  108. Complete configuration for rules can be supplied using a `json` formatted string.
  109. <info>$ php %command.full_name% /path/to/project --rules='{"concat_space": {"spacing": "none"}}'</info>
  110. The <comment>--dry-run</comment> flag will run the fixer without making changes to your files.
  111. The <comment>--diff</comment> flag can be used to let the fixer output all the changes it makes.
  112. 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.
  113. A rule is considered risky if it could change code behaviour. By default no risky rules are run.
  114. The <comment>--stop-on-violation</comment> flag stops the execution upon first file that needs to be fixed.
  115. The <comment>--show-progress</comment> option allows you to choose the way process progress is rendered:
  116. * <comment>none</comment>: disables progress output;
  117. * <comment>dots</comment>: multiline progress output with number of files and percentage on each line.
  118. * <comment>bar</comment>: single line progress output with number of files and calculated percentage.
  119. If the option is not provided, it defaults to <comment>bar</comment> unless a config file that disables output is used, in which case it defaults to <comment>none</comment>. This option has no effect if the verbosity of the command is less than <comment>verbose</comment>.
  120. <info>$ php %command.full_name% --verbose --show-progress=dots</info>
  121. By using <comment>--using-cache</comment> option with `yes` or `no` you can set if the caching
  122. mechanism should be used.
  123. The command can also read from standard input, in which case it won't
  124. automatically fix anything:
  125. <info>$ cat foo.php | php %command.full_name% --diff -</info>
  126. Finally, if you don't need BC kept on CLI level, you might use `PHP_CS_FIXER_FUTURE_MODE` to start using options that
  127. would be default in next MAJOR release and to forbid using deprecated configuration:
  128. <info>$ PHP_CS_FIXER_FUTURE_MODE=1 php %command.full_name% -v --diff</info>
  129. Exit code
  130. ---------
  131. Exit code of the `%command.name%` command is built using following bit flags:
  132. * 0 - OK.
  133. * 1 - General error (or PHP minimal requirement not matched).
  134. * 4 - Some files have invalid syntax (only in dry-run mode).
  135. * 8 - Some files need fixing (only in dry-run mode).
  136. * 16 - Configuration error of the application.
  137. * 32 - Configuration error of a Fixer.
  138. * 64 - Exception raised within the application.
  139. EOF;
  140. }
  141. protected function configure(): void
  142. {
  143. $this->setDefinition(
  144. [
  145. new InputArgument('path', InputArgument::IS_ARRAY, 'The path(s) that rules will be run against (each path can be a file or directory).'),
  146. new InputOption('path-mode', '', InputOption::VALUE_REQUIRED, 'Specify path mode (can be `override` or `intersection`).', ConfigurationResolver::PATH_MODE_OVERRIDE),
  147. new InputOption('allow-risky', '', InputOption::VALUE_REQUIRED, 'Are risky fixers allowed (can be `yes` or `no`).'),
  148. new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a config file.'),
  149. new InputOption('dry-run', '', InputOption::VALUE_NONE, 'Only shows which files would have been modified.'),
  150. new InputOption('rules', '', InputOption::VALUE_REQUIRED, 'List of rules that should be run against configured paths.'),
  151. new InputOption('using-cache', '', InputOption::VALUE_REQUIRED, 'Does cache should be used (can be `yes` or `no`).'),
  152. new InputOption('cache-file', '', InputOption::VALUE_REQUIRED, 'The path to the cache file.'),
  153. new InputOption('diff', '', InputOption::VALUE_NONE, 'Prints diff for each file.'),
  154. new InputOption('format', '', InputOption::VALUE_REQUIRED, 'To output results in other formats.'),
  155. new InputOption('stop-on-violation', '', InputOption::VALUE_NONE, 'Stop execution on first violation.'),
  156. new InputOption('show-progress', '', InputOption::VALUE_REQUIRED, 'Type of progress indicator (none, dots).'),
  157. ]
  158. );
  159. }
  160. protected function execute(InputInterface $input, OutputInterface $output): int
  161. {
  162. $verbosity = $output->getVerbosity();
  163. $passedConfig = $input->getOption('config');
  164. $passedRules = $input->getOption('rules');
  165. if (null !== $passedConfig && null !== $passedRules) {
  166. throw new InvalidConfigurationException('Passing both `--config` and `--rules` options is not allowed.');
  167. }
  168. $resolver = new ConfigurationResolver(
  169. $this->defaultConfig,
  170. [
  171. 'allow-risky' => $input->getOption('allow-risky'),
  172. 'config' => $passedConfig,
  173. 'dry-run' => $this->isDryRun($input),
  174. 'rules' => $passedRules,
  175. 'path' => $input->getArgument('path'),
  176. 'path-mode' => $input->getOption('path-mode'),
  177. 'using-cache' => $input->getOption('using-cache'),
  178. 'cache-file' => $input->getOption('cache-file'),
  179. 'format' => $input->getOption('format'),
  180. 'diff' => $input->getOption('diff'),
  181. 'stop-on-violation' => $input->getOption('stop-on-violation'),
  182. 'verbosity' => $verbosity,
  183. 'show-progress' => $input->getOption('show-progress'),
  184. ],
  185. getcwd(),
  186. $this->toolInfo
  187. );
  188. $reporter = $resolver->getReporter();
  189. $stdErr = $output instanceof ConsoleOutputInterface
  190. ? $output->getErrorOutput()
  191. : ('txt' === $reporter->getFormat() ? $output : null);
  192. if (null !== $stdErr) {
  193. if (OutputInterface::VERBOSITY_VERBOSE <= $verbosity) {
  194. $stdErr->writeln($this->getApplication()->getLongVersion());
  195. }
  196. $configFile = $resolver->getConfigFile();
  197. $stdErr->writeln(sprintf('Loaded config <comment>%s</comment>%s.', $resolver->getConfig()->getName(), null === $configFile ? '' : ' from "'.$configFile.'"'));
  198. if ($resolver->getUsingCache()) {
  199. $cacheFile = $resolver->getCacheFile();
  200. if (is_file($cacheFile)) {
  201. $stdErr->writeln(sprintf('Using cache file "%s".', $cacheFile));
  202. }
  203. }
  204. }
  205. $finder = new \ArrayIterator(iterator_to_array($resolver->getFinder()));
  206. if (null !== $stdErr && $resolver->configFinderIsOverridden()) {
  207. $stdErr->writeln(
  208. sprintf($stdErr->isDecorated() ? '<bg=yellow;fg=black;>%s</>' : '%s', 'Paths from configuration file have been overridden by paths provided as command arguments.')
  209. );
  210. }
  211. $progressType = $resolver->getProgressType();
  212. $progressOutput = $this->progressOutputFactory->create(
  213. $progressType,
  214. new OutputContext(
  215. $stdErr,
  216. (new Terminal())->getWidth(),
  217. \count($finder)
  218. )
  219. );
  220. $runner = new Runner(
  221. $finder,
  222. $resolver->getFixers(),
  223. $resolver->getDiffer(),
  224. ProgressOutputType::NONE !== $progressType ? $this->eventDispatcher : null,
  225. $this->errorsManager,
  226. $resolver->getLinter(),
  227. $resolver->isDryRun(),
  228. $resolver->getCacheManager(),
  229. $resolver->getDirectory(),
  230. $resolver->shouldStopOnViolation()
  231. );
  232. $this->eventDispatcher->addListener(FixerFileProcessedEvent::NAME, [$progressOutput, 'onFixerFileProcessed']);
  233. $this->stopwatch->start('fixFiles');
  234. $changed = $runner->fix();
  235. $this->stopwatch->stop('fixFiles');
  236. $this->eventDispatcher->removeListener(FixerFileProcessedEvent::NAME, [$progressOutput, 'onFixerFileProcessed']);
  237. $progressOutput->printLegend();
  238. $fixEvent = $this->stopwatch->getEvent('fixFiles');
  239. $reportSummary = new ReportSummary(
  240. $changed,
  241. \count($finder),
  242. $fixEvent->getDuration(),
  243. $fixEvent->getMemory(),
  244. OutputInterface::VERBOSITY_VERBOSE <= $verbosity,
  245. $resolver->isDryRun(),
  246. $output->isDecorated()
  247. );
  248. $output->isDecorated()
  249. ? $output->write($reporter->generate($reportSummary))
  250. : $output->write($reporter->generate($reportSummary), false, OutputInterface::OUTPUT_RAW);
  251. $invalidErrors = $this->errorsManager->getInvalidErrors();
  252. $exceptionErrors = $this->errorsManager->getExceptionErrors();
  253. $lintErrors = $this->errorsManager->getLintErrors();
  254. if (null !== $stdErr) {
  255. $errorOutput = new ErrorOutput($stdErr);
  256. if (\count($invalidErrors) > 0) {
  257. $errorOutput->listErrors('linting before fixing', $invalidErrors);
  258. }
  259. if (\count($exceptionErrors) > 0) {
  260. $errorOutput->listErrors('fixing', $exceptionErrors);
  261. }
  262. if (\count($lintErrors) > 0) {
  263. $errorOutput->listErrors('linting after fixing', $lintErrors);
  264. }
  265. }
  266. $exitStatusCalculator = new FixCommandExitStatusCalculator();
  267. return $exitStatusCalculator->calculate(
  268. $resolver->isDryRun(),
  269. \count($changed) > 0,
  270. \count($invalidErrors) > 0,
  271. \count($exceptionErrors) > 0,
  272. \count($lintErrors) > 0
  273. );
  274. }
  275. protected function isDryRun(InputInterface $input): bool
  276. {
  277. return $input->getOption('dry-run');
  278. }
  279. }