DescribeCommandTest.php 19 KB

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