DescribeCommandTest.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  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\Tests\Console\Command;
  13. use PhpCsFixer\AbstractFixer;
  14. use PhpCsFixer\Console\Application;
  15. use PhpCsFixer\Console\Command\DescribeCommand;
  16. use PhpCsFixer\Fixer\ConfigurableFixerInterface;
  17. use PhpCsFixer\Fixer\DeprecatedFixerInterface;
  18. use PhpCsFixer\Fixer\FixerInterface;
  19. use PhpCsFixer\Fixer\Operator\BinaryOperatorSpacesFixer;
  20. use PhpCsFixer\FixerConfiguration\AliasedFixerOptionBuilder;
  21. use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
  22. use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
  23. use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
  24. use PhpCsFixer\FixerDefinition\CodeSample;
  25. use PhpCsFixer\FixerDefinition\CodeSampleInterface;
  26. use PhpCsFixer\FixerDefinition\FixerDefinition;
  27. use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
  28. use PhpCsFixer\FixerDefinition\VersionSpecification;
  29. use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
  30. use PhpCsFixer\FixerFactory;
  31. use PhpCsFixer\Tests\Fixtures\DescribeCommand\DescribeFixtureFixer;
  32. use PhpCsFixer\Tests\TestCase;
  33. use PhpCsFixer\Tokenizer\Token;
  34. use PhpCsFixer\Tokenizer\Tokens;
  35. use Symfony\Component\Console\Output\OutputInterface;
  36. use Symfony\Component\Console\Tester\CommandTester;
  37. /**
  38. * @internal
  39. *
  40. * @group legacy
  41. *
  42. * @covers \PhpCsFixer\Console\Command\DescribeCommand
  43. */
  44. final class DescribeCommandTest extends TestCase
  45. {
  46. /**
  47. * @dataProvider provideExecuteOutputCases
  48. */
  49. public function testExecuteOutput(string $expected, bool $expectedIsRegEx, bool $decorated, FixerInterface $fixer): void
  50. {
  51. if ($fixer instanceof DeprecatedFixerInterface) {
  52. $this->expectDeprecation(\sprintf('Rule "%s" is deprecated. Use "%s" instead.', $fixer->getName(), implode('", "', $fixer->getSuccessorsNames())));
  53. }
  54. // @TODO 4.0 Remove these expectations:
  55. $this->expectDeprecation('Rule set "@PER" is deprecated. Use "@PER-CS" instead.');
  56. $this->expectDeprecation('Rule set "@PER:risky" is deprecated. Use "@PER-CS:risky" instead.');
  57. $actual = $this->execute($fixer->getName(), $decorated, $fixer)->getDisplay(true);
  58. if (true === $expectedIsRegEx) {
  59. self::assertMatchesRegularExpression($expected, $actual);
  60. } else {
  61. self::assertSame($expected, $actual);
  62. }
  63. }
  64. /**
  65. * @return iterable<string, array{string, bool, bool, FixerInterface}>
  66. */
  67. public static function provideExecuteOutputCases(): iterable
  68. {
  69. yield 'rule is configurable, risky and deprecated' => [
  70. "Description of the `Foo/bar` rule.
  71. DEPRECATED: use `Foo/baz` instead.
  72. Fixes stuff.
  73. Replaces bad stuff with good stuff.
  74. Fixer applying this rule is RISKY.
  75. Can break stuff.
  76. Fixer is configurable using following options:
  77. * deprecated_option (bool): a deprecated option; defaults to false. DEPRECATED: use option `functions` instead.
  78. * functions (a subset of ['foo', 'test']): list of `function` names to fix; defaults to ['foo', 'test']; DEPRECATED alias: funcs
  79. Fixing examples:
  80. * Example #1. Fixing with the default configuration.
  81. ---------- begin diff ----------
  82. --- Original
  83. +++ New
  84. @@ -1,1 +1,1 @@
  85. -<?php echo 'bad stuff and bad thing';
  86. +<?php echo 'good stuff and bad thing';
  87. "."
  88. ----------- end diff -----------
  89. * Example #2. Fixing with configuration: ['functions' => ['foo', 'bar']].
  90. ---------- begin diff ----------
  91. --- Original
  92. +++ New
  93. @@ -1,1 +1,1 @@
  94. -<?php echo 'bad stuff and bad thing';
  95. +<?php echo 'good stuff and good thing';
  96. ".'
  97. ----------- end diff -----------
  98. ',
  99. false,
  100. false,
  101. self::createConfigurableDeprecatedFixerDouble(),
  102. ];
  103. yield 'rule is configurable, risky and deprecated [with decoration]' => [
  104. "\033[34mDescription of the \033[39m\033[32m`Foo/bar`\033[39m\033[34m rule.\033[39m
  105. \033[37;41mDEPRECATED\033[39;49m: use \033[32m`Foo/baz`\033[39m instead.
  106. Fixes stuff.
  107. Replaces bad stuff with good stuff.
  108. \033[37;41mFixer applying this rule is RISKY.\033[39;49m
  109. Can break stuff.
  110. Fixer is configurable using following options:
  111. * \033[32mdeprecated_option\033[39m (\033[33mbool\033[39m): a deprecated option; defaults to \e[33mfalse\e[39m. \033[37;41mDEPRECATED\033[39;49m: use option \e[32m`functions`\e[39m instead.
  112. * \033[32mfunctions\033[39m (a subset of \e[33m['foo', 'test']\e[39m): list of \033[32m`function`\033[39m names to fix; defaults to \033[33m['foo', 'test']\033[39m; \e[37;41mDEPRECATED\e[39;49m alias: \033[33mfuncs\033[39m
  113. Fixing examples:
  114. * Example #1. Fixing with the \033[33mdefault\033[39m configuration.
  115. \033[33m ---------- begin diff ----------\033[39m
  116. \033[31m--- Original\033[39m
  117. \033[32m+++ New\033[39m
  118. \033[36m@@ -1,1 +1,1 @@\033[39m
  119. \033[31m-<?php echo 'bad stuff and bad thing';\033[39m
  120. \033[32m+<?php echo 'good stuff and bad thing';\033[39m
  121. "."
  122. \033[33m ----------- end diff -----------\033[39m
  123. * Example #2. Fixing with configuration: \033[33m['functions' => ['foo', 'bar']]\033[39m.
  124. \033[33m ---------- begin diff ----------\033[39m
  125. \033[31m--- Original\033[39m
  126. \033[32m+++ New\033[39m
  127. \033[36m@@ -1,1 +1,1 @@\033[39m
  128. \033[31m-<?php echo 'bad stuff and bad thing';\033[39m
  129. \033[32m+<?php echo 'good stuff and good thing';\033[39m
  130. "."
  131. \033[33m ----------- end diff -----------\033[39m
  132. ",
  133. false,
  134. true,
  135. self::createConfigurableDeprecatedFixerDouble(),
  136. ];
  137. yield 'rule without code samples' => [
  138. 'Description of the `Foo/samples` rule.
  139. Summary of the rule.
  140. Description of the rule.
  141. Fixing examples are not available for this rule.
  142. ',
  143. false,
  144. false,
  145. self::createFixerWithSamplesDouble([]),
  146. ];
  147. yield 'rule with code samples' => [
  148. "Description of the `Foo/samples` rule.
  149. Summary of the rule.
  150. Description of the rule.
  151. Fixing examples:
  152. * Example #1.
  153. ---------- begin diff ----------
  154. --- Original
  155. +++ New
  156. @@ -1,1 +1,1 @@
  157. -<?php echo 'BEFORE';
  158. +<?php echo 'AFTER';
  159. "."
  160. ----------- end diff -----------
  161. * Example #2.
  162. ---------- begin diff ----------
  163. --- Original
  164. +++ New
  165. @@ -1,1 +1,1 @@
  166. -<?php echo 'BEFORE'.'-B';
  167. +<?php echo 'AFTER'.'-B';
  168. ".'
  169. ----------- end diff -----------
  170. ',
  171. false,
  172. false,
  173. self::createFixerWithSamplesDouble([
  174. new CodeSample(
  175. "<?php echo 'BEFORE';".PHP_EOL,
  176. ),
  177. new CodeSample(
  178. "<?php echo 'BEFORE'.'-B';".PHP_EOL,
  179. ),
  180. ]),
  181. ];
  182. yield 'rule with code samples (one with matching PHP version, one NOT)' => [
  183. "Description of the `Foo/samples` rule.
  184. Summary of the rule.
  185. Description of the rule.
  186. Fixing examples:
  187. * Example #1.
  188. ---------- begin diff ----------
  189. --- Original
  190. +++ New
  191. @@ -1,1 +1,1 @@
  192. -<?php echo 'BEFORE';
  193. +<?php echo 'AFTER';
  194. ".'
  195. ----------- end diff -----------
  196. ',
  197. false,
  198. false,
  199. self::createFixerWithSamplesDouble([
  200. new CodeSample(
  201. "<?php echo 'BEFORE';".PHP_EOL,
  202. ),
  203. new VersionSpecificCodeSample(
  204. "<?php echo 'BEFORE'.'-B';".PHP_EOL,
  205. new VersionSpecification(20_00_00)
  206. ),
  207. ]),
  208. ];
  209. yield 'rule with code samples (all with NOT matching PHP version)' => [
  210. 'Description of the `Foo/samples` rule.
  211. Summary of the rule.
  212. Description of the rule.
  213. Fixing examples cannot be demonstrated on the current PHP version.
  214. ',
  215. false,
  216. false,
  217. self::createFixerWithSamplesDouble([
  218. new VersionSpecificCodeSample(
  219. "<?php echo 'BEFORE';".PHP_EOL,
  220. new VersionSpecification(20_00_00)
  221. ),
  222. new VersionSpecificCodeSample(
  223. "<?php echo 'BEFORE'.'-B';".PHP_EOL,
  224. new VersionSpecification(20_00_00)
  225. ),
  226. ]),
  227. ];
  228. yield 'rule that is part of ruleset' => [
  229. '/^Description of the `binary_operator_spaces` rule.
  230. .*
  231. ----------- end diff -----------
  232. '.preg_quote("Fixer is part of the following rule sets:
  233. * @PER with config: ['default' => 'at_least_single_space']
  234. * @PER-CS with config: ['default' => 'at_least_single_space']
  235. * @PER-CS1.0 with config: ['default' => 'at_least_single_space']
  236. * @PER-CS2.0 with config: ['default' => 'at_least_single_space']
  237. * @PSR12 with config: ['default' => 'at_least_single_space']
  238. * @PhpCsFixer with default config
  239. * @Symfony with default config").'
  240. $/s',
  241. true,
  242. false,
  243. new BinaryOperatorSpacesFixer(),
  244. ];
  245. }
  246. public function testExecuteStatusCode(): void
  247. {
  248. $this->expectDeprecation('Rule "Foo/bar" is deprecated. Use "Foo/baz" instead.');
  249. // @TODO 4.0 Remove these expectations:
  250. $this->expectDeprecation('Rule set "@PER" is deprecated. Use "@PER-CS" instead.');
  251. $this->expectDeprecation('Rule set "@PER:risky" is deprecated. Use "@PER-CS:risky" instead.');
  252. self::assertSame(0, $this->execute('Foo/bar', false)->getStatusCode());
  253. }
  254. public function testExecuteWithUnknownRuleName(): void
  255. {
  256. $application = new Application();
  257. $application->add(new DescribeCommand(new FixerFactory()));
  258. $command = $application->find('describe');
  259. $commandTester = new CommandTester($command);
  260. $this->expectException(\InvalidArgumentException::class);
  261. $this->expectExceptionMessageMatches('#^Rule "Foo/bar" not found\.$#');
  262. $commandTester->execute([
  263. 'command' => $command->getName(),
  264. 'name' => 'Foo/bar',
  265. ]);
  266. }
  267. public function testExecuteWithUnknownSetName(): void
  268. {
  269. $application = new Application();
  270. $application->add(new DescribeCommand(new FixerFactory()));
  271. $command = $application->find('describe');
  272. $commandTester = new CommandTester($command);
  273. $this->expectException(\InvalidArgumentException::class);
  274. $this->expectExceptionMessageMatches('#^Set "@NoSuchSet" not found\.$#');
  275. $commandTester->execute([
  276. 'command' => $command->getName(),
  277. 'name' => '@NoSuchSet',
  278. ]);
  279. }
  280. public function testExecuteWithoutName(): void
  281. {
  282. $application = new Application();
  283. $application->add(new DescribeCommand(new FixerFactory()));
  284. $command = $application->find('describe');
  285. $commandTester = new CommandTester($command);
  286. $this->expectException(\RuntimeException::class);
  287. $this->expectExceptionMessageMatches('/^Not enough arguments( \(missing: "name"\))?\.$/');
  288. $commandTester->execute([
  289. 'command' => $command->getName(),
  290. ]);
  291. }
  292. public function testGetAlternativeSuggestion(): void
  293. {
  294. $this->expectException(\InvalidArgumentException::class);
  295. $this->expectExceptionMessageMatches('#^Rule "Foo2/bar" not found\. Did you mean "Foo/bar"\?$#');
  296. $this->execute('Foo2/bar', false);
  297. }
  298. public function testFixerClassNameIsExposedWhenVerbose(): void
  299. {
  300. // @TODO 4.0 Remove these expectations:
  301. $this->expectDeprecation('Rule set "@PER" is deprecated. Use "@PER-CS" instead.');
  302. $this->expectDeprecation('Rule set "@PER:risky" is deprecated. Use "@PER-CS:risky" instead.');
  303. $fixer = new class implements FixerInterface {
  304. public function isCandidate(Tokens $tokens): bool
  305. {
  306. throw new \LogicException('Not implemented.');
  307. }
  308. public function isRisky(): bool
  309. {
  310. return true;
  311. }
  312. public function fix(\SplFileInfo $file, Tokens $tokens): void
  313. {
  314. throw new \LogicException('Not implemented.');
  315. }
  316. public function getDefinition(): FixerDefinition
  317. {
  318. return new FixerDefinition('Fixes stuff.', []);
  319. }
  320. public function getName(): string
  321. {
  322. return 'Foo/bar_baz';
  323. }
  324. public function getPriority(): int
  325. {
  326. return 0;
  327. }
  328. public function supports(\SplFileInfo $file): bool
  329. {
  330. throw new \LogicException('Not implemented.');
  331. }
  332. };
  333. $fixerFactory = new FixerFactory();
  334. $fixerFactory->registerFixer($fixer, true);
  335. $application = new Application();
  336. $application->add(new DescribeCommand($fixerFactory));
  337. $command = $application->find('describe');
  338. $commandTester = new CommandTester($command);
  339. $commandTester->execute(
  340. [
  341. 'command' => $command->getName(),
  342. 'name' => 'Foo/bar_baz',
  343. ],
  344. [
  345. 'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
  346. ]
  347. );
  348. self::assertStringContainsString(str_replace("\0", '\\', \get_class($fixer)), $commandTester->getDisplay(true));
  349. }
  350. public function testCommandDescribesCustomFixer(): void
  351. {
  352. // @TODO 4.0 Remove these expectations:
  353. $this->expectDeprecation('Rule set "@PER" is deprecated. Use "@PER-CS" instead.');
  354. $this->expectDeprecation('Rule set "@PER:risky" is deprecated. Use "@PER-CS:risky" instead.');
  355. $application = new Application();
  356. $application->add(new DescribeCommand());
  357. $command = $application->find('describe');
  358. $commandTester = new CommandTester($command);
  359. $commandTester->execute([
  360. 'command' => $command->getName(),
  361. 'name' => (new DescribeFixtureFixer())->getName(),
  362. '--config' => __DIR__.'/../../Fixtures/DescribeCommand/.php-cs-fixer.fixture.php',
  363. ]);
  364. $expected =
  365. "Description of the `Vendor/describe_fixture` rule.
  366. Fixture for describe command.
  367. Fixing examples:
  368. * Example #1.
  369. ---------- begin diff ----------
  370. --- Original
  371. +++ New
  372. @@ -1,2 +1,2 @@
  373. <?php
  374. -echo 'describe fixture';
  375. +echo 'fixture for describe';
  376. ".'
  377. ----------- end diff -----------
  378. ';
  379. self::assertSame($expected, $commandTester->getDisplay(true));
  380. self::assertSame(0, $commandTester->getStatusCode());
  381. }
  382. /**
  383. * @param list<CodeSampleInterface> $samples
  384. */
  385. private static function createFixerWithSamplesDouble(array $samples): FixerInterface
  386. {
  387. return new class($samples) extends AbstractFixer {
  388. /**
  389. * @var list<CodeSampleInterface>
  390. */
  391. private $samples;
  392. /**
  393. * @param list<CodeSampleInterface> $samples
  394. */
  395. public function __construct(
  396. array $samples
  397. ) {
  398. parent::__construct();
  399. $this->samples = $samples;
  400. }
  401. public function getName(): string
  402. {
  403. return 'Foo/samples';
  404. }
  405. public function getDefinition(): FixerDefinitionInterface
  406. {
  407. return new FixerDefinition(
  408. 'Summary of the rule.',
  409. $this->samples,
  410. 'Description of the rule.',
  411. null,
  412. );
  413. }
  414. public function isCandidate(Tokens $tokens): bool
  415. {
  416. return true;
  417. }
  418. public function applyFix(\SplFileInfo $file, Tokens $tokens): void
  419. {
  420. $tokens[3] = new Token([
  421. $tokens[3]->getId(),
  422. "'AFTER'",
  423. ]);
  424. }
  425. };
  426. }
  427. private static function createConfigurableDeprecatedFixerDouble(): FixerInterface
  428. {
  429. return new class implements ConfigurableFixerInterface, DeprecatedFixerInterface {
  430. /** @var array<string, mixed> */
  431. private array $configuration;
  432. public function configure(array $configuration): void
  433. {
  434. $this->configuration = $configuration;
  435. }
  436. public function getConfigurationDefinition(): FixerConfigurationResolver
  437. {
  438. $functionNames = ['foo', 'test'];
  439. return new FixerConfigurationResolver([
  440. (new AliasedFixerOptionBuilder(new FixerOptionBuilder('functions', 'List of `function` names to fix.'), 'funcs'))
  441. ->setAllowedTypes(['string[]'])
  442. ->setAllowedValues([new AllowedValueSubset($functionNames)])
  443. ->setDefault($functionNames)
  444. ->getOption(),
  445. (new FixerOptionBuilder('deprecated_option', 'A deprecated option.'))
  446. ->setAllowedTypes(['bool'])
  447. ->setDefault(false)
  448. ->setDeprecationMessage('Use option `functions` instead.')
  449. ->getOption(),
  450. ]);
  451. }
  452. public function getSuccessorsNames(): array
  453. {
  454. return ['Foo/baz'];
  455. }
  456. public function isCandidate(Tokens $tokens): bool
  457. {
  458. throw new \LogicException('Not implemented.');
  459. }
  460. public function isRisky(): bool
  461. {
  462. return true;
  463. }
  464. public function fix(\SplFileInfo $file, Tokens $tokens): void
  465. {
  466. $tokens[3] = new Token([
  467. $tokens[3]->getId(),
  468. [] !== $this->configuration ? '\'good stuff and good thing\'' : '\'good stuff and bad thing\'',
  469. ]);
  470. }
  471. public function getDefinition(): FixerDefinition
  472. {
  473. return new FixerDefinition(
  474. 'Fixes stuff.',
  475. [
  476. new CodeSample(
  477. "<?php echo 'bad stuff and bad thing';\n"
  478. ),
  479. new CodeSample(
  480. "<?php echo 'bad stuff and bad thing';\n",
  481. ['functions' => ['foo', 'bar']]
  482. ),
  483. ],
  484. 'Replaces bad stuff with good stuff.',
  485. 'Can break stuff.'
  486. );
  487. }
  488. public function getName(): string
  489. {
  490. return 'Foo/bar';
  491. }
  492. public function getPriority(): int
  493. {
  494. return 0;
  495. }
  496. public function supports(\SplFileInfo $file): bool
  497. {
  498. throw new \LogicException('Not implemented.');
  499. }
  500. };
  501. }
  502. private function execute(string $name, bool $decorated, ?FixerInterface $fixer = null): CommandTester
  503. {
  504. $fixer ??= self::createConfigurableDeprecatedFixerDouble();
  505. $fixerClassName = \get_class($fixer);
  506. $isBuiltIn = str_starts_with($fixerClassName, 'PhpCsFixer') && !str_contains($fixerClassName, '@anon');
  507. $fixerFactory = new FixerFactory();
  508. $fixerFactory->registerFixer($fixer, !$isBuiltIn);
  509. $application = new Application();
  510. $application->add(new DescribeCommand($fixerFactory));
  511. $command = $application->find('describe');
  512. $commandTester = new CommandTester($command);
  513. $commandTester->execute(
  514. [
  515. 'command' => $command->getName(),
  516. 'name' => $name,
  517. ],
  518. [
  519. 'decorated' => $decorated,
  520. ]
  521. );
  522. return $commandTester;
  523. }
  524. }