FixCommand.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. <?php
  2. /*
  3. * This file is part of the PHP CS utility.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * This source file is subject to the MIT license that is bundled
  8. * with this source code in the file LICENSE.
  9. */
  10. namespace Symfony\CS\Console\Command;
  11. use Symfony\Component\Console\Command\Command;
  12. use Symfony\Component\Console\Input\InputArgument;
  13. use Symfony\Component\Console\Input\InputInterface;
  14. use Symfony\Component\Console\Input\InputOption;
  15. use Symfony\Component\Console\Output\ConsoleOutputInterface;
  16. use Symfony\Component\Console\Output\OutputInterface;
  17. use Symfony\Component\EventDispatcher\EventDispatcher;
  18. use Symfony\Component\Filesystem\Filesystem;
  19. use Symfony\Component\Stopwatch\Stopwatch;
  20. use Symfony\CS\Config\Config;
  21. use Symfony\CS\ConfigInterface;
  22. use Symfony\CS\ConfigurationException\InvalidConfigurationException;
  23. use Symfony\CS\ConfigurationResolver;
  24. use Symfony\CS\ErrorsManager;
  25. use Symfony\CS\Fixer;
  26. use Symfony\CS\FixerFileProcessedEvent;
  27. use Symfony\CS\FixerInterface;
  28. use Symfony\CS\LintManager;
  29. use Symfony\CS\StdinFileInfo;
  30. use Symfony\CS\Utils;
  31. /**
  32. * @author Fabien Potencier <fabien@symfony.com>
  33. */
  34. class FixCommand extends Command
  35. {
  36. const EXIT_STATUS_FLAG_HAS_INVALID_CONFIG = 16;
  37. const EXIT_STATUS_FLAG_HAS_INVALID_FIXER_CONFIG = 32;
  38. /**
  39. * EventDispatcher instance.
  40. *
  41. * @var EventDispatcher
  42. */
  43. protected $eventDispatcher;
  44. /**
  45. * ErrorsManager instance.
  46. *
  47. * @var ErrorsManager
  48. */
  49. protected $errorsManager;
  50. /**
  51. * Stopwatch instance.
  52. *
  53. * @var Stopwatch
  54. */
  55. protected $stopwatch;
  56. /**
  57. * Fixer instance.
  58. *
  59. * @var Fixer
  60. */
  61. protected $fixer;
  62. /**
  63. * Config instance.
  64. *
  65. * @var ConfigInterface
  66. */
  67. protected $defaultConfig;
  68. /**
  69. * @param Fixer|null $fixer
  70. * @param ConfigInterface|null $config
  71. */
  72. public function __construct(Fixer $fixer = null, ConfigInterface $config = null)
  73. {
  74. $this->defaultConfig = $config ?: new Config();
  75. $this->eventDispatcher = new EventDispatcher();
  76. $this->errorsManager = new ErrorsManager();
  77. $this->stopwatch = new Stopwatch();
  78. $this->fixer = $fixer ?: new Fixer();
  79. $this->fixer->registerBuiltInFixers();
  80. $this->fixer->registerBuiltInConfigs();
  81. $this->fixer->setStopwatch($this->stopwatch);
  82. $this->fixer->setErrorsManager($this->errorsManager);
  83. parent::__construct();
  84. }
  85. /**
  86. * @see Command
  87. */
  88. protected function configure()
  89. {
  90. $this
  91. ->setName('fix')
  92. ->setDefinition(
  93. array(
  94. new InputArgument('path', InputArgument::OPTIONAL, 'The path', null),
  95. new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The configuration name', null),
  96. new InputOption('config-file', '', InputOption::VALUE_OPTIONAL, '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('level', '', InputOption::VALUE_REQUIRED, 'The level of fixes (can be psr0, psr1, psr2, or symfony (formerly all))', null),
  99. new InputOption('fixers', '', InputOption::VALUE_REQUIRED, 'A list of fixers to run'),
  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>--level</comment> option limits the fixers to apply on the
  113. project:
  114. <info>php %command.full_name% /path/to/project --level=psr0</info>
  115. <info>php %command.full_name% /path/to/project --level=psr1</info>
  116. <info>php %command.full_name% /path/to/project --level=psr2</info>
  117. <info>php %command.full_name% /path/to/project --level=symfony</info>
  118. By default, all PSR-2 fixers and some additional ones are run. The "contrib
  119. level" fixers cannot be enabled via this option; you should instead set them
  120. manually by their name via the <comment>--fixers</comment> option.
  121. The <comment>--fixers</comment> option lets you choose the exact fixers to
  122. apply (the fixer names must be separated by a comma):
  123. <info>php %command.full_name% /path/to/dir --fixers=linefeed,short_tag,indentation</info>
  124. 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,
  125. using <comment>-name_of_fixer</comment>:
  126. <info>php %command.full_name% /path/to/dir --fixers=-short_tag,-indentation</info>
  127. When using combination with exact and blacklist fixers, apply exact fixers along with above blacklisted result:
  128. <info>php php-cs-fixer.phar fix /path/to/dir --fixers=linefeed,-short_tag</info>
  129. A combination of <comment>--dry-run</comment> and <comment>--diff</comment> will
  130. display summary of proposed fixes, leaving your files unchanged.
  131. The command can also read from standard input, in which case it won't
  132. automatically fix anything:
  133. <info>cat foo.php | php %command.full_name% --diff -</info>
  134. Choose from the list of available fixers:
  135. {$this->getFixersHelp()}
  136. The <comment>--config</comment> option customizes the files to analyse, based
  137. on some well-known directory structures:
  138. <comment># For the Symfony 2.3+ branch</comment>
  139. <info>php %command.full_name% /path/to/sf23 --config=sf23</info>
  140. Choose from the list of available configurations:
  141. {$this->getConfigsHelp()}
  142. The <comment>--dry-run</comment> option displays the files that need to be
  143. fixed but without actually modifying them:
  144. <info>php %command.full_name% /path/to/code --dry-run</info>
  145. Instead of using command line options to customize the fixer, you can save the
  146. configuration in a <comment>.php_cs</comment> file in the root directory of
  147. your project. The file must return an instance of
  148. ``Symfony\CS\ConfigInterface``, which lets you configure the fixers, the level, the files,
  149. and directories that need to be analyzed. The example below will add two contrib fixers
  150. to the default list of symfony-level fixers:
  151. <?php
  152. \$finder = Symfony\CS\Finder\DefaultFinder::create()
  153. ->exclude('somedir')
  154. ->in(__DIR__)
  155. ;
  156. return Symfony\CS\Config\Config::create()
  157. ->fixers(array('strict_param', 'short_array_syntax'))
  158. ->finder(\$finder)
  159. ;
  160. ?>
  161. If you want complete control over which fixers you use, you may use the empty level and
  162. then specify all fixers to be used:
  163. <?php
  164. \$finder = Symfony\CS\Finder\DefaultFinder::create()
  165. ->in(__DIR__)
  166. ;
  167. return Symfony\CS\Config\Config::create()
  168. ->level(Symfony\CS\FixerInterface::NONE_LEVEL)
  169. ->fixers(array('trailing_spaces', 'encoding'))
  170. ->finder(\$finder)
  171. ;
  172. ?>
  173. You may also use a blacklist for the Fixers instead of the above shown whitelist approach.
  174. The following example shows how to use all ``symfony`` Fixers but the ``psr0`` fixer.
  175. Note the additional <comment>-</comment> in front of the Fixer name.
  176. <?php
  177. \$finder = Symfony\CS\Finder\DefaultFinder::create()
  178. ->exclude('somedir')
  179. ->in(__DIR__)
  180. ;
  181. return Symfony\CS\Config\Config::create()
  182. ->fixers(array('-psr0'))
  183. ->finder(\$finder)
  184. ;
  185. ?>
  186. The ``symfony`` level is set by default, you can also change the default level:
  187. <?php
  188. return Symfony\CS\Config\Config::create()
  189. ->level(Symfony\CS\FixerInterface::PSR2_LEVEL)
  190. ;
  191. ?>
  192. In combination with these config and command line options, you can choose various usage.
  193. For example, default level is ``symfony``, but if you also don't want to use
  194. the ``psr0`` fixer, you can specify the ``--fixers="-psr0"`` option.
  195. But if you use the ``--fixers`` option with only exact fixers,
  196. only those exact fixers are enabled whether or not level is set.
  197. With the <comment>--config-file</comment> option you can specify the path to the
  198. <comment>.php_cs</comment> file.
  199. Caching
  200. -------
  201. You can enable caching by returning a custom config with caching enabled. This will
  202. speed up further runs.
  203. <?php
  204. return Symfony\CS\Config\Config::create()
  205. ->setUsingCache(true)
  206. ;
  207. ?>
  208. Exit codes
  209. ----------
  210. * 0 OK
  211. * 1 No changes made
  212. * 16 Configuration error of the application
  213. * 32 Configuration error of a Fixer
  214. EOF
  215. );
  216. }
  217. /**
  218. * @see Command
  219. */
  220. protected function execute(InputInterface $input, OutputInterface $output)
  221. {
  222. // setup output
  223. $stdErr = ($output instanceof ConsoleOutputInterface) ? $output->getErrorOutput() : null;
  224. if ($stdErr && extension_loaded('xdebug')) {
  225. $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.'));
  226. }
  227. $verbosity = $output->getVerbosity();
  228. // setup input
  229. $path = $input->getArgument('path');
  230. $stdin = false;
  231. if ('-' === $path) {
  232. $stdin = true;
  233. // Can't write to STDIN
  234. $input->setOption('dry-run', true);
  235. }
  236. if (null !== $path) {
  237. $filesystem = new Filesystem();
  238. if (!$filesystem->isAbsolutePath($path)) {
  239. $path = getcwd().DIRECTORY_SEPARATOR.$path;
  240. }
  241. }
  242. // setup configuration location
  243. $configFile = $input->getOption('config-file');
  244. if (null === $configFile) {
  245. $configDir = $path;
  246. if (is_file($path) && $dirName = pathinfo($path, PATHINFO_DIRNAME)) {
  247. $configDir = $dirName;
  248. } elseif ($stdin || null === $path) {
  249. $configDir = getcwd();
  250. // path is directory
  251. }
  252. $configFile = $configDir.DIRECTORY_SEPARATOR.'.php_cs';
  253. }
  254. if ($input->getOption('config')) {
  255. $config = null;
  256. foreach ($this->fixer->getConfigs() as $c) {
  257. if ($c->getName() === $input->getOption('config')) {
  258. $config = $c;
  259. break;
  260. }
  261. }
  262. if (null === $config) {
  263. throw new InvalidConfigurationException(sprintf('The configuration "%s" is not defined.', $input->getOption('config')));
  264. }
  265. } elseif (file_exists($configFile)) {
  266. $config = include $configFile;
  267. // verify that the config has an instance of Config
  268. if (!$config instanceof Config) {
  269. throw new InvalidConfigurationException(sprintf('The config file "%s" does not return a "Symfony\CS\Config\Config" instance. Got: "%s".', $configFile, is_object($config) ? get_class($config) : gettype($config)));
  270. }
  271. if ('txt' === $input->getOption('format')) {
  272. $output->writeln(sprintf('Loaded config from "%s"', $configFile));
  273. }
  274. } else {
  275. $config = $this->defaultConfig;
  276. }
  277. // setup location of source(s) to fix
  278. if (is_file($path)) {
  279. $config->finder(new \ArrayIterator(array(new \SplFileInfo($path))));
  280. } elseif ($stdin) {
  281. $config->finder(new \ArrayIterator(array(new StdinFileInfo())));
  282. } elseif (null !== $path) {
  283. $config->setDir($path);
  284. }
  285. // setup Linter
  286. if ($config->usingLinter()) {
  287. $this->fixer->setLintManager(new LintManager());
  288. }
  289. // register custom fixers from config
  290. $this->fixer->registerCustomFixers($config->getCustomFixers());
  291. $resolver = new ConfigurationResolver();
  292. $resolver
  293. ->setAllFixers($this->fixer->getFixers())
  294. ->setConfig($config)
  295. ->setOptions(array(
  296. 'level' => $input->getOption('level'),
  297. 'fixers' => $input->getOption('fixers'),
  298. 'progress' => (OutputInterface::VERBOSITY_VERBOSE <= $verbosity) && 'txt' === $input->getOption('format'),
  299. 'format' => $input->getOption('format'),
  300. ))
  301. ->resolve();
  302. $config->fixers($resolver->getFixers());
  303. $showProgress = $resolver->getProgress();
  304. if ($showProgress) {
  305. $fileProcessedEventListener = function (FixerFileProcessedEvent $event) use ($output) {
  306. $output->write($event->getStatusAsString());
  307. };
  308. $this->fixer->setEventDispatcher($this->eventDispatcher);
  309. $this->eventDispatcher->addListener(FixerFileProcessedEvent::NAME, $fileProcessedEventListener);
  310. }
  311. $this->stopwatch->start('fixFiles');
  312. $changed = $this->fixer->fix($config, $input->getOption('dry-run'), $input->getOption('diff'));
  313. $this->stopwatch->stop('fixFiles');
  314. if ($showProgress) {
  315. $this->fixer->setEventDispatcher(null);
  316. $this->eventDispatcher->removeListener(FixerFileProcessedEvent::NAME, $fileProcessedEventListener);
  317. $output->writeln('');
  318. $legend = array();
  319. foreach (FixerFileProcessedEvent::getStatusMap() as $status) {
  320. if ($status['symbol'] && $status['description']) {
  321. $legend[] = $status['symbol'].'-'.$status['description'];
  322. }
  323. }
  324. $output->writeln('Legend: '.implode(', ', array_unique($legend)));
  325. }
  326. $i = 1;
  327. switch ($resolver->getFormat()) {
  328. case 'txt':
  329. $fixerDetailLine = false;
  330. if (OutputInterface::VERBOSITY_VERBOSE <= $verbosity) {
  331. $fixerDetailLine = $output->isDecorated() ? ' (<comment>%s</comment>)' : ' %s';
  332. }
  333. foreach ($changed as $file => $fixResult) {
  334. $output->write(sprintf('%4d) %s', $i++, $file));
  335. if ($fixerDetailLine) {
  336. $output->write(sprintf($fixerDetailLine, implode(', ', $fixResult['appliedFixers'])));
  337. }
  338. if ($input->getOption('diff')) {
  339. $output->writeln('');
  340. $output->writeln('<comment> ---------- begin diff ----------</comment>');
  341. $output->writeln($fixResult['diff']);
  342. $output->writeln('<comment> ---------- end diff ----------</comment>');
  343. }
  344. $output->writeln('');
  345. }
  346. if (OutputInterface::VERBOSITY_DEBUG <= $verbosity) {
  347. $output->writeln('Fixing time per file:');
  348. foreach ($this->stopwatch->getSectionEvents('fixFile') as $file => $event) {
  349. if ('__section__' === $file) {
  350. continue;
  351. }
  352. $output->writeln(sprintf('[%.3f s] %s', $event->getDuration() / 1000, $file));
  353. }
  354. $output->writeln('');
  355. }
  356. $fixEvent = $this->stopwatch->getEvent('fixFiles');
  357. $output->writeln(sprintf('%s all files in %.3f seconds, %.3f MB memory used', $input->getOption('dry-run') ? 'Checked' : 'Fixed', $fixEvent->getDuration() / 1000, $fixEvent->getMemory() / 1024 / 1024));
  358. break;
  359. case 'xml':
  360. $dom = new \DOMDocument('1.0', 'UTF-8');
  361. $filesXML = $dom->createElement('files');
  362. $dom->appendChild($filesXML);
  363. foreach ($changed as $file => $fixResult) {
  364. $fileXML = $dom->createElement('file');
  365. $fileXML->setAttribute('id', $i++);
  366. $fileXML->setAttribute('name', $file);
  367. $filesXML->appendChild($fileXML);
  368. if (OutputInterface::VERBOSITY_VERBOSE <= $verbosity) {
  369. $appliedFixersXML = $dom->createElement('applied_fixers');
  370. $fileXML->appendChild($appliedFixersXML);
  371. foreach ($fixResult['appliedFixers'] as $appliedFixer) {
  372. $appliedFixerXML = $dom->createElement('applied_fixer');
  373. $appliedFixerXML->setAttribute('name', $appliedFixer);
  374. $appliedFixersXML->appendChild($appliedFixerXML);
  375. }
  376. }
  377. if ($input->getOption('diff')) {
  378. $diffXML = $dom->createElement('diff');
  379. $diffXML->appendChild($dom->createCDATASection($fixResult['diff']));
  380. $fileXML->appendChild($diffXML);
  381. }
  382. }
  383. $dom->formatOutput = true;
  384. $output->write($dom->saveXML());
  385. break;
  386. case 'json':
  387. $jFiles = array();
  388. foreach ($changed as $file => $fixResult) {
  389. $jfile = array('name' => $file);
  390. if (OutputInterface::VERBOSITY_VERBOSE <= $verbosity) {
  391. $jfile['appliedFixers'] = $fixResult['appliedFixers'];
  392. }
  393. if ($input->getOption('diff')) {
  394. $jfile['diff'] = $fixResult['diff'];
  395. }
  396. $jFiles[] = $jfile;
  397. }
  398. $fixEvent = $this->stopwatch->getEvent('fixFiles');
  399. $json = array(
  400. 'files' => $jFiles,
  401. 'memory' => round($fixEvent->getMemory() / 1024 / 1024, 3),
  402. 'time' => array(
  403. 'total' => round($fixEvent->getDuration() / 1000, 3),
  404. ),
  405. );
  406. if (OutputInterface::VERBOSITY_DEBUG <= $verbosity) {
  407. $jFileTime = array();
  408. foreach ($this->stopwatch->getSectionEvents('fixFile') as $file => $event) {
  409. if ('__section__' === $file) {
  410. continue;
  411. }
  412. $jFileTime[$file] = round($event->getDuration() / 1000, 3);
  413. }
  414. $json['time']['files'] = $jFileTime;
  415. }
  416. $output->write(json_encode($json));
  417. break;
  418. }
  419. if (!$this->errorsManager->isEmpty()) {
  420. $output->writeln('');
  421. $output->writeln('Files that were not fixed due to internal error:');
  422. foreach ($this->errorsManager->getErrors() as $i => $error) {
  423. $output->writeln(sprintf('%4d) %s', $i + 1, $error['filepath']));
  424. }
  425. }
  426. return empty($changed) ? 0 : 1;
  427. }
  428. protected function getFixersHelp()
  429. {
  430. $help = '';
  431. $maxName = 0;
  432. $fixers = $this->fixer->getFixers();
  433. // sort fixers by level and name
  434. usort(
  435. $fixers,
  436. function (FixerInterface $a, FixerInterface $b) {
  437. $cmp = Utils::cmpInt($a->getLevel(), $b->getLevel());
  438. if (0 !== $cmp) {
  439. return $cmp;
  440. }
  441. return strcmp($a->getName(), $b->getName());
  442. }
  443. );
  444. foreach ($fixers as $fixer) {
  445. if (strlen($fixer->getName()) > $maxName) {
  446. $maxName = strlen($fixer->getName());
  447. }
  448. }
  449. $count = count($fixers) - 1;
  450. foreach ($fixers as $i => $fixer) {
  451. $chunks = explode("\n", wordwrap(sprintf("[%s]\n%s", $this->fixer->getLevelAsString($fixer), $fixer->getDescription()), 72 - $maxName, "\n"));
  452. $help .= sprintf(" * <comment>%s</comment>%s %s\n", $fixer->getName(), str_repeat(' ', $maxName - strlen($fixer->getName())), array_shift($chunks));
  453. while ($c = array_shift($chunks)) {
  454. $help .= str_repeat(' ', $maxName + 4).$c."\n";
  455. }
  456. if ($count !== $i) {
  457. $help .= "\n";
  458. }
  459. }
  460. return $help;
  461. }
  462. protected function getConfigsHelp()
  463. {
  464. $help = '';
  465. $maxName = 0;
  466. $configs = $this->fixer->getConfigs();
  467. usort(
  468. $configs,
  469. function (ConfigInterface $a, ConfigInterface $b) {
  470. return strcmp($a->getName(), $b->getName());
  471. }
  472. );
  473. foreach ($configs as $config) {
  474. if (strlen($config->getName()) > $maxName) {
  475. $maxName = strlen($config->getName());
  476. }
  477. }
  478. $count = count($this->fixer->getConfigs()) - 1;
  479. foreach ($configs as $i => $config) {
  480. $chunks = explode("\n", wordwrap($config->getDescription(), 72 - $maxName, "\n"));
  481. $help .= sprintf(" * <comment>%s</comment>%s %s\n", $config->getName(), str_repeat(' ', $maxName - strlen($config->getName())), array_shift($chunks));
  482. while ($c = array_shift($chunks)) {
  483. $help .= str_repeat(' ', $maxName + 4).$c."\n";
  484. }
  485. if ($count !== $i) {
  486. $help .= "\n";
  487. }
  488. }
  489. return $help;
  490. }
  491. }