FixCommand.php 18 KB

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