FixCommand.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. <?php
  2. /*
  3. * This file is part of PHP CS Fixer.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  7. *
  8. * This source file is subject to the MIT license that is bundled
  9. * with this source code in the file LICENSE.
  10. */
  11. namespace PhpCsFixer\Console\Command;
  12. use PhpCsFixer\Config;
  13. use PhpCsFixer\ConfigInterface;
  14. use PhpCsFixer\Console\ConfigurationResolver;
  15. use PhpCsFixer\Console\Output\NullOutput;
  16. use PhpCsFixer\Console\Output\ProcessOutput;
  17. use PhpCsFixer\Differ\NullDiffer;
  18. use PhpCsFixer\Differ\SebastianBergmannDiffer;
  19. use PhpCsFixer\Error\Error;
  20. use PhpCsFixer\Error\ErrorsManager;
  21. use PhpCsFixer\FixerFactory;
  22. use PhpCsFixer\FixerInterface;
  23. use PhpCsFixer\Linter\Linter;
  24. use PhpCsFixer\Linter\NullLinter;
  25. use PhpCsFixer\Linter\UnavailableLinterException;
  26. use PhpCsFixer\Report\ReporterFactory;
  27. use PhpCsFixer\Report\ReportSummary;
  28. use PhpCsFixer\RuleSet;
  29. use PhpCsFixer\Runner\Runner;
  30. use Symfony\Component\Console\Command\Command;
  31. use Symfony\Component\Console\Input\InputArgument;
  32. use Symfony\Component\Console\Input\InputInterface;
  33. use Symfony\Component\Console\Input\InputOption;
  34. use Symfony\Component\Console\Output\ConsoleOutputInterface;
  35. use Symfony\Component\Console\Output\OutputInterface;
  36. use Symfony\Component\EventDispatcher\EventDispatcher;
  37. use Symfony\Component\Stopwatch\Stopwatch;
  38. /**
  39. * @author Fabien Potencier <fabien@symfony.com>
  40. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  41. *
  42. * @internal
  43. */
  44. final class FixCommand extends Command
  45. {
  46. const EXIT_STATUS_FLAG_HAS_INVALID_FILES = 4;
  47. const EXIT_STATUS_FLAG_HAS_CHANGED_FILES = 8;
  48. const EXIT_STATUS_FLAG_HAS_INVALID_CONFIG = 16;
  49. const EXIT_STATUS_FLAG_HAS_INVALID_FIXER_CONFIG = 32;
  50. const EXIT_STATUS_FLAG_EXCEPTION_IN_APP = 64;
  51. /**
  52. * EventDispatcher instance.
  53. *
  54. * @var EventDispatcher
  55. */
  56. protected $eventDispatcher;
  57. /**
  58. * ErrorsManager instance.
  59. *
  60. * @var ErrorsManager
  61. */
  62. protected $errorsManager;
  63. /**
  64. * Stopwatch instance.
  65. *
  66. * @var Stopwatch
  67. */
  68. protected $stopwatch;
  69. /**
  70. * Config instance.
  71. *
  72. * @var ConfigInterface
  73. */
  74. protected $defaultConfig;
  75. public function __construct()
  76. {
  77. parent::__construct();
  78. $this->defaultConfig = new Config();
  79. $this->errorsManager = new ErrorsManager();
  80. $this->eventDispatcher = new EventDispatcher();
  81. $this->stopwatch = new Stopwatch();
  82. }
  83. /**
  84. * {@inheritdoc}
  85. */
  86. protected function configure()
  87. {
  88. $this
  89. ->setName('fix')
  90. ->setDefinition(
  91. array(
  92. new InputArgument('path', InputArgument::IS_ARRAY, 'The path', null),
  93. new InputOption('path-mode', '', InputOption::VALUE_REQUIRED, 'Specify path mode (can be override or intersection)', 'override'),
  94. new InputOption('allow-risky', '', InputOption::VALUE_REQUIRED, 'Are risky fixers allowed (can be yes or no)', null),
  95. new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a .php_cs file ', null),
  96. new InputOption('dry-run', '', InputOption::VALUE_NONE, 'Only shows which files would have been modified'),
  97. new InputOption('rules', '', InputOption::VALUE_REQUIRED, 'The rules', null),
  98. new InputOption('using-cache', '', InputOption::VALUE_REQUIRED, 'Does cache should be used (can be yes or no)', null),
  99. new InputOption('cache-file', '', InputOption::VALUE_REQUIRED, 'The path to the cache file'),
  100. new InputOption('diff', '', InputOption::VALUE_NONE, 'Also produce diff for each file'),
  101. new InputOption('format', '', InputOption::VALUE_REQUIRED, 'To output results in other formats', 'txt'),
  102. )
  103. )
  104. ->setDescription('Fixes a directory or a file')
  105. ->setHelp(<<<EOF
  106. The <info>%command.name%</info> command tries to fix as much coding standards
  107. problems as possible on a given file or files in a given directory and its subdirectories:
  108. <info>php %command.full_name% /path/to/dir</info>
  109. <info>php %command.full_name% /path/to/file</info>
  110. The <comment>--format</comment> option for the output format. Supported formats are ``txt`` (default one), ``json`` and ``xml``.
  111. The <comment>--verbose</comment> option will show the applied fixers. When using the ``txt`` format it will also displays progress notifications.
  112. The <comment>--rules</comment> option limits the rules to apply on the
  113. project:
  114. <info>php %command.full_name% /path/to/project --rules=@PSR2</info>
  115. By default, all PSR fixers are run.
  116. The <comment>--rules</comment> option lets you choose the exact fixers to
  117. apply (the fixer names must be separated by a comma):
  118. <info>php %command.full_name% /path/to/dir --rules=unix_line_endings,full_opening_tag,no_tab_indentation</info>
  119. You can also blacklist the fixers you don't want by placing a dash in front of the fixer name, if this is more convenient,
  120. using <comment>-name_of_fixer</comment>:
  121. <info>php %command.full_name% /path/to/dir --rules=-full_opening_tag,-no_tab_indentation</info>
  122. When using combinations of exact and blacklist fixers, applying exact fixers along with above blacklisted results:
  123. <info>php %command.full_name% /path/to/project --rules=@Symfony,-@PSR1,-return,strict</info>
  124. A combination of <comment>--dry-run</comment> and <comment>--diff</comment> will
  125. display a summary of proposed fixes, leaving your files unchanged.
  126. The <comment>--allow-risky</comment> option allows you to set whether riskys fixer may run. Default value is taken from config file.
  127. Risky fixer is a fixer, which could change code behaviour. By default no risky fixers are run.
  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. Choose from the list of available fixers:
  132. {$this->getFixersHelp()}
  133. The <comment>--dry-run</comment> option displays the files that need to be
  134. fixed but without actually modifying them:
  135. <info>php %command.full_name% /path/to/code --dry-run</info>
  136. Instead of using command line options to customize the fixer, you can save the
  137. project configuration in a <comment>.php_cs.dist</comment> file in the root directory
  138. of your project. The file must return an instance of ``PhpCsFixer\ConfigInterface``,
  139. which lets you configure the rules, the files and directories that
  140. need to be analyzed. You may also create <comment>.php_cs</comment> file, which is
  141. the local configuration that will be used instead of the project configuration. It
  142. is a good practice to add that file into your <comment>.gitignore</comment> file.
  143. With the <comment>--config</comment> option you can specify the path to the
  144. <comment>.php_cs</comment> file.
  145. The example below will add two fixers to the default list of PSR2 set fixers:
  146. <?php
  147. \$finder = PhpCsFixer\Finder::create()
  148. ->exclude('somedir')
  149. ->notPath('src/Symfony/Component/Translation/Tests/fixtures/resources.php')
  150. ->in(__DIR__)
  151. ;
  152. return PhpCsFixer\Config::create()
  153. ->setRules(array(
  154. '@PSR2' => true,
  155. 'strict_param' => true,
  156. 'short_array_syntax' => true,
  157. ))
  158. ->finder(\$finder)
  159. ;
  160. ?>
  161. **NOTE**: ``exclude`` will work only for directories, so if you need to exclude file, try ``notPath``.
  162. See `Symfony\\\\Finder <http://symfony.com/doc/current/components/finder.html>`_
  163. online documentation for other `Finder` methods.
  164. You may also use a blacklist for the Fixers instead of the above shown whitelist approach.
  165. The following example shows how to use all ``Symfony`` Fixers but the ``full_opening_tag`` Fixer.
  166. <?php
  167. \$finder = PhpCsFixer\Finder::create()
  168. ->exclude('somedir')
  169. ->in(__DIR__)
  170. ;
  171. return PhpCsFixer\Config::create()
  172. ->setRules(array(
  173. '@Symfony' => true,
  174. 'full_opening_tag' => false,
  175. ))
  176. ->finder(\$finder)
  177. ;
  178. ?>
  179. By using ``--using-cache`` option with yes or no you can set if the caching
  180. mechanism should be used.
  181. Caching
  182. -------
  183. The caching mechanism is enabled by default. This will speed up further runs by
  184. fixing only files that were modified since the last run. The tool will fix all
  185. files if the tool version has changed or the list of fixers has changed.
  186. Cache is supported only for tool downloaded as phar file or installed via
  187. composer.
  188. Cache can be disabled via ``--using-cache`` option or config file:
  189. <?php
  190. return PhpCsFixer\Config::create()
  191. ->setUsingCache(false)
  192. ;
  193. ?>
  194. Cache file can be specified via ``--cache-file`` option or config file:
  195. <?php
  196. return PhpCsFixer\Config::create()
  197. ->setCacheFile(__DIR__.'/.php_cs.cache')
  198. ;
  199. ?>
  200. Using PHP CS Fixer on CI
  201. ------------------------
  202. Require ``fabpot/php-cs-fixer`` as a `dev`` dependency:
  203. $ ./composer.phar require --dev fabpot/php-cs-fixer
  204. Then, add the following command to your CI:
  205. $ vendor/bin/php-cs-fixer fix --config=.php_cs.dist --path-mode=intersection `git diff --name-only \$COMMIT_RANGE`
  206. Where ``\$COMMIT_RANGE`` is your range of commits, eg ``\$TRAVIS_COMMIT_RANGE`` or ``HEAD~..HEAD``.
  207. Exit codes
  208. ----------
  209. Exit code is build using following bit flags:
  210. * 0 OK
  211. * 4 Some files have invalid syntax (only in dry-run mode)
  212. * 8 Some files need fixing (only in dry-run mode)
  213. * 16 Configuration error of the application
  214. * 32 Configuration error of a Fixer
  215. * 64 Exception raised within the application
  216. EOF
  217. );
  218. }
  219. /**
  220. * @see Command
  221. */
  222. protected function execute(InputInterface $input, OutputInterface $output)
  223. {
  224. $verbosity = $output->getVerbosity();
  225. $resolver = new ConfigurationResolver();
  226. $resolver
  227. ->setCwd(getcwd())
  228. ->setDefaultConfig($this->defaultConfig)
  229. ->setOptions(array(
  230. 'allow-risky' => $input->getOption('allow-risky'),
  231. 'config' => $input->getOption('config'),
  232. 'dry-run' => $input->getOption('dry-run'),
  233. 'rules' => $input->getOption('rules'),
  234. 'path' => $input->getArgument('path'),
  235. 'path-mode' => $input->getOption('path-mode'),
  236. 'progress' => (OutputInterface::VERBOSITY_VERBOSE <= $verbosity) && 'txt' === $input->getOption('format'),
  237. 'using-cache' => $input->getOption('using-cache'),
  238. 'cache-file' => $input->getOption('cache-file'),
  239. 'format' => $input->getOption('format'),
  240. ))
  241. ->resolve()
  242. ;
  243. $reporter = ReporterFactory::create()
  244. ->registerBuiltInReporters()
  245. ->getReporter($resolver->getFormat())
  246. ;
  247. $stdErr = $output instanceof ConsoleOutputInterface
  248. ? $output->getErrorOutput()
  249. : ('txt' === $reporter->getFormat() ? $output : null)
  250. ;
  251. if (null !== $stdErr && extension_loaded('xdebug')) {
  252. $stdErr->writeln(sprintf($stdErr->isDecorated() ? '<bg=yellow;fg=black;>%s</>' : '%s', 'You are running php-cs-fixer with xdebug enabled. This has a major impact on runtime performance.'));
  253. }
  254. $config = $resolver->getConfig();
  255. $configFile = $resolver->getConfigFile();
  256. if (null !== $stdErr && $configFile) {
  257. $stdErr->writeln(sprintf('Loaded config from "%s".', $configFile));
  258. }
  259. $linter = new NullLinter();
  260. if ($config->usingLinter()) {
  261. try {
  262. $linter = new Linter($config->getPhpExecutable());
  263. } catch (UnavailableLinterException $e) {
  264. if (null !== $stdErr && $configFile) {
  265. $stdErr->writeln('Unable to use linter, can not find PHP executable.');
  266. }
  267. }
  268. }
  269. if (null !== $stdErr && $config->usingCache()) {
  270. $cacheFile = $config->getCacheFile();
  271. if (is_file($cacheFile)) {
  272. $stdErr->writeln(sprintf('Using cache file "%s".', $cacheFile));
  273. }
  274. }
  275. $showProgress = $resolver->getProgress();
  276. $runner = new Runner(
  277. $config,
  278. $input->getOption('diff') ? new SebastianBergmannDiffer() : new NullDiffer(),
  279. $showProgress ? $this->eventDispatcher : null,
  280. $this->errorsManager,
  281. $linter,
  282. $resolver->isDryRun()
  283. );
  284. $progressOutput = $showProgress && $stdErr
  285. ? new ProcessOutput($stdErr, $this->eventDispatcher)
  286. : new NullOutput()
  287. ;
  288. $this->stopwatch->start('fixFiles');
  289. $changed = $runner->fix();
  290. $this->stopwatch->stop('fixFiles');
  291. $progressOutput->printLegend();
  292. $fixEvent = $this->stopwatch->getEvent('fixFiles');
  293. $reportSummary = ReportSummary::create()
  294. ->setChanged($changed)
  295. ->setAddAppliedFixers(OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity())
  296. ->setIsDecoratedOutput($output->isDecorated())
  297. ->setIsDryRun($resolver->isDryRun())
  298. ->setMemory($fixEvent->getMemory())
  299. ->setTime($fixEvent->getDuration())
  300. ;
  301. $output->write(
  302. $reporter->generate($reportSummary)
  303. );
  304. $invalidErrors = $this->errorsManager->getInvalidErrors();
  305. $exceptionErrors = $this->errorsManager->getExceptionErrors();
  306. $lintErrors = $this->errorsManager->getLintErrors();
  307. if (null !== $stdErr) {
  308. if (count($invalidErrors) > 0) {
  309. $this->listErrors($stdErr, 'linting before fixing', $invalidErrors);
  310. }
  311. if (count($exceptionErrors) > 0) {
  312. $this->listErrors($stdErr, 'fixing', $exceptionErrors);
  313. }
  314. if (count($lintErrors) > 0) {
  315. $this->listErrors($stdErr, 'linting after fixing', $lintErrors);
  316. }
  317. }
  318. return $this->calculateExitStatus(
  319. $resolver->isDryRun(),
  320. count($changed) > 0,
  321. count($invalidErrors) > 0,
  322. count($exceptionErrors) > 0
  323. );
  324. }
  325. /**
  326. * @param bool $isDryRun
  327. * @param bool $hasChangedFiles
  328. * @param bool $hasInvalidErrors
  329. * @param bool $hasExceptionErrors
  330. *
  331. * @return int
  332. */
  333. private function calculateExitStatus($isDryRun, $hasChangedFiles, $hasInvalidErrors, $hasExceptionErrors)
  334. {
  335. $exitStatus = 0;
  336. if ($isDryRun) {
  337. if ($hasChangedFiles) {
  338. $exitStatus |= self::EXIT_STATUS_FLAG_HAS_CHANGED_FILES;
  339. }
  340. if ($hasInvalidErrors) {
  341. $exitStatus |= self::EXIT_STATUS_FLAG_HAS_INVALID_FILES;
  342. }
  343. }
  344. if ($hasExceptionErrors) {
  345. $exitStatus |= self::EXIT_STATUS_FLAG_EXCEPTION_IN_APP;
  346. }
  347. return $exitStatus;
  348. }
  349. /**
  350. * @param OutputInterface $output
  351. * @param string $process
  352. * @param Error[] $errors
  353. */
  354. private function listErrors(OutputInterface $output, $process, array $errors)
  355. {
  356. $output->writeln('');
  357. $output->writeln(sprintf(
  358. 'Files that were not fixed due to errors reported during %s:',
  359. $process
  360. ));
  361. foreach ($errors as $i => $error) {
  362. $output->writeln(sprintf('%4d) %s', $i + 1, $error->getFilePath()));
  363. }
  364. }
  365. protected function getFixersHelp()
  366. {
  367. $help = '';
  368. $maxName = 0;
  369. $fixerFactory = new FixerFactory();
  370. $fixers = $fixerFactory->registerBuiltInFixers()->getFixers();
  371. // sort fixers by name
  372. usort(
  373. $fixers,
  374. function (FixerInterface $a, FixerInterface $b) {
  375. return strcmp($a->getName(), $b->getName());
  376. }
  377. );
  378. foreach ($fixers as $fixer) {
  379. if (strlen($fixer->getName()) > $maxName) {
  380. $maxName = strlen($fixer->getName());
  381. }
  382. }
  383. $ruleSets = array();
  384. foreach (RuleSet::create()->getSetDefinitionNames() as $setName) {
  385. $ruleSets[$setName] = new RuleSet(array($setName => true));
  386. }
  387. $getSetsWithRule = function ($rule) use ($ruleSets) {
  388. $sets = array();
  389. foreach ($ruleSets as $setName => $ruleSet) {
  390. if ($ruleSet->hasRule($rule)) {
  391. $sets[] = $setName;
  392. }
  393. }
  394. return $sets;
  395. };
  396. $count = count($fixers) - 1;
  397. foreach ($fixers as $i => $fixer) {
  398. $sets = $getSetsWithRule($fixer->getName());
  399. $description = $fixer->getDescription();
  400. if ($fixer->isRisky()) {
  401. $description .= ' (Risky fixer!)';
  402. }
  403. if (!empty($sets)) {
  404. $chunks = explode("\n", wordwrap(sprintf("[%s]\n%s", implode(', ', $sets), $description), 72 - $maxName, "\n"));
  405. $help .= sprintf(" * <comment>%s</comment>%s %s\n", $fixer->getName(), str_repeat(' ', $maxName - strlen($fixer->getName())), array_shift($chunks));
  406. } else {
  407. $chunks = explode("\n", wordwrap(sprintf("\n%s", $description), 72 - $maxName, "\n"));
  408. $help .= sprintf(" * <comment>%s</comment>%s\n", $fixer->getName(), array_shift($chunks));
  409. }
  410. while ($c = array_shift($chunks)) {
  411. $help .= str_repeat(' ', $maxName + 4).$c."\n";
  412. }
  413. if ($count !== $i) {
  414. $help .= "\n";
  415. }
  416. }
  417. return $help;
  418. }
  419. }