FixCommand.php 14 KB

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