FixCommand.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  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\InputOption;
  14. use Symfony\Component\Console\Input\InputInterface;
  15. use Symfony\Component\Console\Output\OutputInterface;
  16. use Symfony\Component\Filesystem\Filesystem;
  17. use Symfony\CS\Fixer;
  18. use Symfony\CS\FixerInterface;
  19. use Symfony\CS\Config\Config;
  20. use Symfony\CS\ConfigInterface;
  21. use Symfony\CS\StdinFileInfo;
  22. /**
  23. * @author Fabien Potencier <fabien@symfony.com>
  24. */
  25. class FixCommand extends Command
  26. {
  27. protected $fixer;
  28. protected $defaultConfig;
  29. /**
  30. * @param Fixer $fixer
  31. * @param ConfigInterface $config
  32. */
  33. public function __construct(Fixer $fixer = null, ConfigInterface $config = null)
  34. {
  35. $this->fixer = $fixer ?: new Fixer();
  36. $this->fixer->registerBuiltInFixers();
  37. $this->fixer->registerBuiltInConfigs();
  38. $this->defaultConfig = $config ?: new Config();
  39. parent::__construct();
  40. }
  41. /**
  42. * @see Command
  43. */
  44. protected function configure()
  45. {
  46. $this
  47. ->setName('fix')
  48. ->setDefinition(array(
  49. new InputArgument('path', InputArgument::REQUIRED, 'The path'),
  50. new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The configuration name', null),
  51. new InputOption('config-file', '', InputOption::VALUE_OPTIONAL, 'The path to a .php_cs file ', null),
  52. new InputOption('dry-run', '', InputOption::VALUE_NONE, 'Only shows which files would have been modified'),
  53. new InputOption('level', '', InputOption::VALUE_REQUIRED, 'The level of fixes (can be psr0, psr1, psr2, or all)', null),
  54. new InputOption('fixers', '', InputOption::VALUE_REQUIRED, 'A list of fixers to run'),
  55. new InputOption('diff', '', InputOption::VALUE_NONE, 'Also produce diff for each file'),
  56. new InputOption('format', '', InputOption::VALUE_REQUIRED, 'To output results in other formats', 'txt')
  57. ))
  58. ->setDescription('Fixes a directory or a file')
  59. ->setHelp(<<<EOF
  60. The <info>%command.name%</info> command tries to fix as much coding standards
  61. problems as possible on a given file or directory:
  62. <info>php %command.full_name% /path/to/dir</info>
  63. <info>php %command.full_name% /path/to/file</info>
  64. The <comment>--level</comment> option limits the fixers to apply on the
  65. project:
  66. <info>php %command.full_name% /path/to/project --level=psr0</info>
  67. <info>php %command.full_name% /path/to/project --level=psr1</info>
  68. <info>php %command.full_name% /path/to/project --level=psr2</info>
  69. <info>php %command.full_name% /path/to/project --level=all</info>
  70. By default, all PSR-2 fixers and some additional ones are run.
  71. The <comment>--fixers</comment> option lets you choose the exact fixers to
  72. apply (the fixer names must be separated by a comma):
  73. <info>php %command.full_name% /path/to/dir --fixers=linefeed,short_tag,indentation</info>
  74. You can also blacklist the fixers you don't want if this is more convenient,
  75. using <comment>-name</comment>:
  76. <info>php %command.full_name% /path/to/dir --fixers=-short_tag,-indentation</info>
  77. A combination of <comment>--dry-run</comment>, <comment>--verbose</comment> and <comment>--diff</comment> will
  78. display summary of proposed fixes, leaving your files unchanged.
  79. The command can also read from standard input, in which case it won't
  80. automatically fix anything:
  81. <info>cat foo.php | php %command.full_name% -v --diff -</info>
  82. Choose from the list of available fixers:
  83. {$this->getFixersHelp()}
  84. The <comment>--config</comment> option customizes the files to analyse, based
  85. on some well-known directory structures:
  86. <comment># For the Symfony 2.3+ branch</comment>
  87. <info>php %command.full_name% /path/to/sf23 --config=sf23</info>
  88. Choose from the list of available configurations:
  89. {$this->getConfigsHelp()}
  90. The <comment>--dry-run</comment> option displays the files that need to be
  91. fixed but without actually modifying them:
  92. <info>php %command.full_name% /path/to/code --dry-run</info>
  93. Instead of using command line options to customize the fixer, you can save the
  94. configuration in a <comment>.php_cs</comment> file in the root directory of
  95. your project. The file must return an instance of
  96. `Symfony\CS\ConfigInterface`, which lets you configure the fixers, the files,
  97. and directories that need to be analyzed:
  98. <?php
  99. \$finder = Symfony\CS\Finder\DefaultFinder::create()
  100. ->exclude('somefile')
  101. ->in(__DIR__)
  102. ;
  103. return Symfony\CS\Config\Config::create()
  104. ->fixers(array('indentation', 'elseif'))
  105. ->finder(\$finder)
  106. ;
  107. You may also use a blacklist for the Fixers instead of the above shown whitelist approach.
  108. The following example shows how to use all Fixers but the `Psr0Fixer`.
  109. Note the additional <comment>-</comment> in front of the Fixer name.
  110. <?php
  111. \$finder = Symfony\CS\Finder\DefaultFinder::create()
  112. ->exclude('somefile')
  113. ->in(__DIR__)
  114. ;
  115. return Symfony\CS\Config\Config::create()
  116. ->fixers(array('-Psr0Fixer'))
  117. ->finder(\$finder)
  118. ;
  119. With the <comment>--config-file</comment> option you can specify the path to the
  120. <comment>.php_cs</comment> file.
  121. EOF
  122. );
  123. }
  124. /**
  125. * @see Command
  126. */
  127. protected function execute(InputInterface $input, OutputInterface $output)
  128. {
  129. $path = $input->getArgument('path');
  130. $stdin = false;
  131. if ('-' === $path) {
  132. $stdin = true;
  133. // Can't write to STDIN
  134. $input->setOption('dry-run', true);
  135. }
  136. $filesystem = new Filesystem();
  137. if (!$filesystem->isAbsolutePath($path)) {
  138. $path = getcwd().DIRECTORY_SEPARATOR.$path;
  139. }
  140. $addSuppliedPathFromCli = true;
  141. if ($input->getOption('config')) {
  142. $config = null;
  143. foreach ($this->fixer->getConfigs() as $c) {
  144. if ($c->getName() == $input->getOption('config')) {
  145. $config = $c;
  146. break;
  147. }
  148. }
  149. if (null === $config) {
  150. throw new \InvalidArgumentException(sprintf('The configuration "%s" is not defined', $input->getOption('config')));
  151. }
  152. } elseif ($input->getOption('config-file')) {
  153. $file = $input->getOption('config-file');
  154. $config = include $file;
  155. } elseif (file_exists($file = $path.'/.php_cs')) {
  156. $config = include $file;
  157. $addSuppliedPathFromCli = false;
  158. } else {
  159. $config = $this->defaultConfig;
  160. }
  161. if ($addSuppliedPathFromCli) {
  162. if (is_file($path)) {
  163. $config->finder(new \ArrayIterator(array(new \SplFileInfo($path))));
  164. } elseif ($stdin) {
  165. $config->finder(new \ArrayIterator(array(new StdinFileInfo())));
  166. } else {
  167. $config->setDir($path);
  168. }
  169. }
  170. // register custom fixers from config
  171. $this->fixer->registerCustomFixers($config->getCustomFixers());
  172. $allFixers = $this->fixer->getFixers();
  173. switch ($input->getOption('level')) {
  174. case 'psr0':
  175. $level = FixerInterface::PSR0_LEVEL;
  176. break;
  177. case 'psr1':
  178. $level = FixerInterface::PSR1_LEVEL;
  179. break;
  180. case 'psr2':
  181. $level = FixerInterface::PSR2_LEVEL;
  182. break;
  183. case 'all':
  184. $level = FixerInterface::ALL_LEVEL;
  185. break;
  186. case null:
  187. $fixerOption = $input->getOption('fixers');
  188. if (empty($fixerOption) || preg_match('{(^|,)-}', $fixerOption)) {
  189. $level = $config->getFixers();
  190. } else {
  191. $level = null;
  192. }
  193. break;
  194. default:
  195. throw new \InvalidArgumentException(sprintf('The level "%s" is not defined.', $input->getOption('level')));
  196. }
  197. // select base fixers for the given level
  198. $fixers = array();
  199. if (is_array($level)) {
  200. foreach ($allFixers as $fixer) {
  201. if (in_array($fixer->getName(), $level, true) || in_array($fixer, $level, true)) {
  202. $fixers[] = $fixer;
  203. }
  204. }
  205. } else {
  206. foreach ($allFixers as $fixer) {
  207. if ($fixer->getLevel() === ($fixer->getLevel() & $level)) {
  208. $fixers[] = $fixer;
  209. }
  210. }
  211. }
  212. // remove/add fixers based on the fixers option
  213. if (preg_match('{(^|,)-}', $input->getOption('fixers'))) {
  214. foreach ($fixers as $key => $fixer) {
  215. if (preg_match('{(^|,)-'.preg_quote($fixer->getName()).'}', $input->getOption('fixers'))) {
  216. unset($fixers[$key]);
  217. }
  218. }
  219. } elseif ($input->getOption('fixers')) {
  220. $names = array_map('trim', explode(',', $input->getOption('fixers')));
  221. foreach ($allFixers as $fixer) {
  222. if (in_array($fixer->getName(), $names) && !in_array($fixer, $fixers)) {
  223. $fixers[] = $fixer;
  224. }
  225. }
  226. }
  227. $config->fixers($fixers);
  228. $changed = $this->fixer->fix($config, $input->getOption('dry-run'), $input->getOption('diff'));
  229. $i = 1;
  230. switch ($input->getOption('format')) {
  231. case 'txt':
  232. foreach ($changed as $file => $fixResult) {
  233. $output->write(sprintf('%4d) %s', $i++, $file));
  234. if ($input->getOption('verbose')) {
  235. $output->write(sprintf(' (<comment>%s</comment>)', implode(', ', $fixResult['appliedFixers'])));
  236. if ($input->getOption('diff')) {
  237. $output->writeln('');
  238. $output->writeln('<comment> ---------- begin diff ----------</comment>');
  239. $output->writeln($fixResult['diff']);
  240. $output->writeln('<comment> ---------- end diff ----------</comment>');
  241. }
  242. }
  243. $output->writeln('');
  244. }
  245. break;
  246. case 'xml':
  247. $dom = new \DOMDocument('1.0', 'UTF-8');
  248. $dom->appendChild($filesXML = $dom->createElement('files'));
  249. foreach ($changed as $file => $fixResult) {
  250. $filesXML->appendChild($fileXML = $dom->createElement('file'));
  251. $fileXML->setAttribute('id', $i++);
  252. $fileXML->setAttribute('name', $file);
  253. if ($input->getOption('verbose')) {
  254. $fileXML->appendChild($appliedFixersXML = $dom->createElement('applied_fixers'));
  255. foreach ($fixResult['appliedFixers'] as $appliedFixer) {
  256. $appliedFixersXML->appendChild($appliedFixerXML = $dom->createElement('applied_fixer'));
  257. $appliedFixerXML->setAttribute('name', $appliedFixer);
  258. }
  259. if ($input->getOption('diff')) {
  260. $fileXML->appendChild($diffXML = $dom->createElement('diff'));
  261. $diffXML->appendChild($dom->createCDATASection($fixResult['diff']));
  262. }
  263. }
  264. }
  265. $dom->formatOutput = true;
  266. $output->write($dom->saveXML());
  267. break;
  268. default:
  269. throw new \InvalidArgumentException(sprintf('The format "%s" is not defined.', $input->getOption('format')));
  270. }
  271. return empty($changed) ? 0 : 1;
  272. }
  273. protected function getFixersHelp()
  274. {
  275. $fixers = '';
  276. $maxName = 0;
  277. foreach ($this->fixer->getFixers() as $fixer) {
  278. if (strlen($fixer->getName()) > $maxName) {
  279. $maxName = strlen($fixer->getName());
  280. }
  281. }
  282. $count = count($this->fixer->getFixers()) - 1;
  283. foreach ($this->fixer->getFixers() as $i => $fixer) {
  284. $chunks = explode("\n", wordwrap(sprintf('[%s] %s', $this->fixer->getLevelAsString($fixer), $fixer->getDescription()), 72 - $maxName, "\n"));
  285. $fixers .= sprintf(" * <comment>%s</comment>%s %s\n", $fixer->getName(), str_repeat(' ', $maxName - strlen($fixer->getName())), array_shift($chunks));
  286. while ($c = array_shift($chunks)) {
  287. $fixers .= str_repeat(' ', $maxName + 4).$c."\n";
  288. }
  289. if ($count != $i) {
  290. $fixers .= "\n";
  291. }
  292. }
  293. return $fixers;
  294. }
  295. protected function getConfigsHelp()
  296. {
  297. $configs = '';
  298. $maxName = 0;
  299. foreach ($this->fixer->getConfigs() as $config) {
  300. if (strlen($config->getName()) > $maxName) {
  301. $maxName = strlen($config->getName());
  302. }
  303. }
  304. $count = count($this->fixer->getConfigs()) - 1;
  305. foreach ($this->fixer->getConfigs() as $i => $config) {
  306. $chunks = explode("\n", wordwrap($config->getDescription(), 72 - $maxName, "\n"));
  307. $configs .= sprintf(" * <comment>%s</comment>%s %s\n", $config->getName(), str_repeat(' ', $maxName - strlen($config->getName())), array_shift($chunks));
  308. while ($c = array_shift($chunks)) {
  309. $configs .= str_repeat(' ', $maxName + 4).$c."\n";
  310. }
  311. if ($count != $i) {
  312. $configs .= "\n";
  313. }
  314. }
  315. return $configs;
  316. }
  317. }