FixCommand.php 17 KB

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