DescribeCommandTest.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  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 = "Description of the `Vendor/describe_fixture` rule.
  365. Fixture for describe command.
  366. Fixing examples:
  367. * Example #1.
  368. ---------- begin diff ----------
  369. --- Original
  370. +++ New
  371. @@ -1,2 +1,2 @@
  372. <?php
  373. -echo 'describe fixture';
  374. +echo 'fixture for describe';
  375. ".'
  376. ----------- end diff -----------
  377. ';
  378. self::assertSame($expected, $commandTester->getDisplay(true));
  379. self::assertSame(0, $commandTester->getStatusCode());
  380. }
  381. /**
  382. * @param list<CodeSampleInterface> $samples
  383. */
  384. private static function createFixerWithSamplesDouble(array $samples): FixerInterface
  385. {
  386. return new class($samples) extends AbstractFixer {
  387. /**
  388. * @var list<CodeSampleInterface>
  389. */
  390. private array $samples;
  391. /**
  392. * @param list<CodeSampleInterface> $samples
  393. */
  394. public function __construct(
  395. array $samples
  396. ) {
  397. parent::__construct();
  398. $this->samples = $samples;
  399. }
  400. public function getName(): string
  401. {
  402. return 'Foo/samples';
  403. }
  404. public function getDefinition(): FixerDefinitionInterface
  405. {
  406. return new FixerDefinition(
  407. 'Summary of the rule.',
  408. $this->samples,
  409. 'Description of the rule.',
  410. null,
  411. );
  412. }
  413. public function isCandidate(Tokens $tokens): bool
  414. {
  415. return true;
  416. }
  417. public function applyFix(\SplFileInfo $file, Tokens $tokens): void
  418. {
  419. $tokens[3] = new Token([
  420. $tokens[3]->getId(),
  421. "'AFTER'",
  422. ]);
  423. }
  424. };
  425. }
  426. private static function createConfigurableDeprecatedFixerDouble(): FixerInterface
  427. {
  428. return new class implements ConfigurableFixerInterface, DeprecatedFixerInterface {
  429. /** @var array<string, mixed> */
  430. private array $configuration;
  431. public function configure(array $configuration): void
  432. {
  433. $this->configuration = $configuration;
  434. }
  435. public function getConfigurationDefinition(): FixerConfigurationResolver
  436. {
  437. $functionNames = ['foo', 'test'];
  438. return new FixerConfigurationResolver([
  439. (new AliasedFixerOptionBuilder(new FixerOptionBuilder('functions', 'List of `function` names to fix.'), 'funcs'))
  440. ->setAllowedTypes(['string[]'])
  441. ->setAllowedValues([new AllowedValueSubset($functionNames)])
  442. ->setDefault($functionNames)
  443. ->getOption(),
  444. (new FixerOptionBuilder('deprecated_option', 'A deprecated option.'))
  445. ->setAllowedTypes(['bool'])
  446. ->setDefault(false)
  447. ->setDeprecationMessage('Use option `functions` instead.')
  448. ->getOption(),
  449. ]);
  450. }
  451. public function getSuccessorsNames(): array
  452. {
  453. return ['Foo/baz'];
  454. }
  455. public function isCandidate(Tokens $tokens): bool
  456. {
  457. throw new \LogicException('Not implemented.');
  458. }
  459. public function isRisky(): bool
  460. {
  461. return true;
  462. }
  463. public function fix(\SplFileInfo $file, Tokens $tokens): void
  464. {
  465. $tokens[3] = new Token([
  466. $tokens[3]->getId(),
  467. [] !== $this->configuration ? '\'good stuff and good thing\'' : '\'good stuff and bad thing\'',
  468. ]);
  469. }
  470. public function getDefinition(): FixerDefinition
  471. {
  472. return new FixerDefinition(
  473. 'Fixes stuff.',
  474. [
  475. new CodeSample(
  476. "<?php echo 'bad stuff and bad thing';\n"
  477. ),
  478. new CodeSample(
  479. "<?php echo 'bad stuff and bad thing';\n",
  480. ['functions' => ['foo', 'bar']]
  481. ),
  482. ],
  483. 'Replaces bad stuff with good stuff.',
  484. 'Can break stuff.'
  485. );
  486. }
  487. public function getName(): string
  488. {
  489. return 'Foo/bar';
  490. }
  491. public function getPriority(): int
  492. {
  493. return 0;
  494. }
  495. public function supports(\SplFileInfo $file): bool
  496. {
  497. throw new \LogicException('Not implemented.');
  498. }
  499. };
  500. }
  501. private function execute(string $name, bool $decorated, ?FixerInterface $fixer = null): CommandTester
  502. {
  503. $fixer ??= self::createConfigurableDeprecatedFixerDouble();
  504. $fixerClassName = \get_class($fixer);
  505. $isBuiltIn = str_starts_with($fixerClassName, 'PhpCsFixer') && !str_contains($fixerClassName, '@anon');
  506. $fixerFactory = new FixerFactory();
  507. $fixerFactory->registerFixer($fixer, !$isBuiltIn);
  508. $application = new Application();
  509. $application->add(new DescribeCommand($fixerFactory));
  510. $command = $application->find('describe');
  511. $commandTester = new CommandTester($command);
  512. $commandTester->execute(
  513. [
  514. 'command' => $command->getName(),
  515. 'name' => $name,
  516. ],
  517. [
  518. 'decorated' => $decorated,
  519. ]
  520. );
  521. return $commandTester;
  522. }
  523. }