FinalInternalClassFixerTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  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\Fixer\ClassNotation;
  13. use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
  14. use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
  15. /**
  16. * @internal
  17. *
  18. * @covers \PhpCsFixer\Fixer\ClassNotation\FinalInternalClassFixer
  19. *
  20. * @extends AbstractFixerTestCase<\PhpCsFixer\Fixer\ClassNotation\FinalInternalClassFixer>
  21. *
  22. * @phpstan-import-type _AutogeneratedInputConfiguration from \PhpCsFixer\Fixer\ClassNotation\FinalInternalClassFixer
  23. */
  24. final class FinalInternalClassFixerTest extends AbstractFixerTestCase
  25. {
  26. /**
  27. * @param _AutogeneratedInputConfiguration $configuration
  28. *
  29. * @dataProvider provideFixCases
  30. */
  31. public function testFix(string $expected, ?string $input = null, array $configuration = []): void
  32. {
  33. $this->fixer->configure($configuration);
  34. $this->doTest($expected, $input);
  35. }
  36. public static function provideFixCases(): iterable
  37. {
  38. $input = $expected = '<?php ';
  39. for ($i = 1; $i < 10; ++$i) {
  40. $input .= \sprintf("/** @internal */\nclass class%d\n{\n}\n", $i);
  41. $expected .= \sprintf("/** @internal */\nfinal class class%d\n{\n}\n", $i);
  42. }
  43. yield 'fix multiple classes' => [
  44. $expected,
  45. $input,
  46. ];
  47. yield [
  48. '<?php
  49. /** @internal */
  50. final class class1
  51. {
  52. }
  53. interface A {}
  54. trait B{}
  55. /** @internal */
  56. final class class2
  57. {
  58. }
  59. ',
  60. '<?php
  61. /** @internal */
  62. class class1
  63. {
  64. }
  65. interface A {}
  66. trait B{}
  67. /** @internal */
  68. class class2
  69. {
  70. }
  71. ',
  72. ];
  73. yield [
  74. '<?php
  75. /** @internal */
  76. final class class1
  77. {
  78. }
  79. /** @internal */
  80. final class class2
  81. {
  82. }
  83. /**
  84. * @internal
  85. * @final
  86. */
  87. class class3
  88. {
  89. }
  90. /**
  91. * @internal
  92. */
  93. abstract class class4 {}
  94. ',
  95. '<?php
  96. /** @internal */
  97. final class class1
  98. {
  99. }
  100. /** @internal */
  101. class class2
  102. {
  103. }
  104. /**
  105. * @internal
  106. * @final
  107. */
  108. class class3
  109. {
  110. }
  111. /**
  112. * @internal
  113. */
  114. abstract class class4 {}
  115. ',
  116. ];
  117. yield [
  118. '<?php
  119. /**
  120. * @ annotation_with_space_after_at_sign
  121. */
  122. class A {}
  123. ',
  124. ];
  125. yield 'indent before `class`' => [
  126. '<?php /** @internal */
  127. final class class1
  128. {
  129. }',
  130. '<?php /** @internal */
  131. class class1
  132. {
  133. }',
  134. ];
  135. yield 'multiple classes, first with internal annotation and second without internal annotation' => [
  136. '<?php
  137. /** @internal */
  138. final class Foo {}
  139. class Bar {}
  140. ',
  141. '<?php
  142. /** @internal */
  143. class Foo {}
  144. class Bar {}
  145. ',
  146. ];
  147. yield 'multiple classes, first without internal annotation and second with internal annotation' => [
  148. '<?php
  149. class Foo {}
  150. /** @internal */
  151. final class Bar {}
  152. ',
  153. '<?php
  154. class Foo {}
  155. /** @internal */
  156. class Bar {}
  157. ',
  158. ];
  159. yield [
  160. "<?php\n/** @CUSTOM */final class A{}",
  161. "<?php\n/** @CUSTOM */class A{}",
  162. [
  163. 'include' => ['@Custom'],
  164. ],
  165. ];
  166. yield [
  167. '<?php
  168. /**
  169. * @CUSTOM
  170. * @abc
  171. */
  172. final class A{}
  173. /**
  174. * @CUSTOM
  175. */
  176. final class B{}
  177. ',
  178. '<?php
  179. /**
  180. * @CUSTOM
  181. * @abc
  182. */
  183. class A{}
  184. /**
  185. * @CUSTOM
  186. */
  187. class B{}
  188. ',
  189. [
  190. 'include' => ['@Custom', '@abc'],
  191. ],
  192. ];
  193. yield [
  194. '<?php
  195. /**
  196. * @CUSTOM
  197. * @internal
  198. */
  199. final class A{}
  200. /**
  201. * @CUSTOM
  202. * @internal
  203. * @other
  204. */
  205. final class B{}
  206. /**
  207. * @CUSTOM
  208. * @internal
  209. * @not-fix
  210. */
  211. class C{}
  212. ',
  213. '<?php
  214. /**
  215. * @CUSTOM
  216. * @internal
  217. */
  218. class A{}
  219. /**
  220. * @CUSTOM
  221. * @internal
  222. * @other
  223. */
  224. class B{}
  225. /**
  226. * @CUSTOM
  227. * @internal
  228. * @not-fix
  229. */
  230. class C{}
  231. ',
  232. [
  233. 'include' => ['@Custom', '@internal'],
  234. 'exclude' => ['@not-fix'],
  235. ],
  236. ];
  237. yield [
  238. '<?php
  239. /**
  240. * @internal
  241. */
  242. final class A{}
  243. /**
  244. * @abc
  245. */
  246. class B{}
  247. ',
  248. '<?php
  249. /**
  250. * @internal
  251. */
  252. class A{}
  253. /**
  254. * @abc
  255. */
  256. class B{}
  257. ',
  258. [
  259. 'exclude' => ['abc'],
  260. ],
  261. ];
  262. yield [
  263. '<?php final class A{}',
  264. '<?php class A{}',
  265. ['consider_absent_docblock_as_internal_class' => true],
  266. ];
  267. yield 'class with annotation with matching include and partial matching exclude' => [
  268. '<?php
  269. /** @HelloWorld */
  270. final class Foo {}
  271. ',
  272. '<?php
  273. /** @HelloWorld */
  274. class Foo {}
  275. ',
  276. [
  277. 'include' => ['HelloWorld'],
  278. 'exclude' => ['Hello'],
  279. ],
  280. ];
  281. yield [
  282. '<?php
  283. /** @internal */
  284. $a = new class (){};',
  285. ];
  286. yield [
  287. '<?php
  288. /** @internal */
  289. $a = new class{};',
  290. ];
  291. yield [
  292. '<?php $object = new /**/ class(){};',
  293. ];
  294. }
  295. /**
  296. * @group legacy
  297. *
  298. * @param _AutogeneratedInputConfiguration $config
  299. *
  300. * @dataProvider provideInvalidConfigurationCases
  301. */
  302. public function testInvalidConfiguration(array $config, string $exceptionExpression, ?string $deprecationMessage = null): void
  303. {
  304. $this->expectException(InvalidFixerConfigurationException::class);
  305. $this->expectExceptionMessageMatches($exceptionExpression);
  306. if (null !== $deprecationMessage) {
  307. $this->expectDeprecation($deprecationMessage);
  308. }
  309. $this->fixer->configure($config);
  310. }
  311. /**
  312. * @return iterable<array{array<string, mixed>, string, 2?: string}>
  313. */
  314. public static function provideInvalidConfigurationCases(): iterable
  315. {
  316. yield 'same annotation in both lists' => [
  317. [
  318. 'include' => ['@internal123', 'a'],
  319. 'exclude' => ['@internal123', 'b'],
  320. ],
  321. \sprintf('#^%s$#', preg_quote('[final_internal_class] Annotation cannot be used in both "include" and "exclude" list, got duplicates: "internal123".', '#')),
  322. ];
  323. yield 'both new and old include set' => [
  324. [
  325. 'annotation_include' => ['@internal', 'a'],
  326. 'include' => ['@internal', 'b'],
  327. ],
  328. \sprintf('#^%s$#', preg_quote('[final_internal_class] Configuration cannot contain deprecated option "annotation_include" and new option "include".', '#')),
  329. 'Option "annotation_include" for rule "final_internal_class" is deprecated and will be removed in version 4.0. Use "include" to configure PHPDoc annotations tags and attributes.',
  330. ];
  331. yield 'both new and old exclude set' => [
  332. [
  333. 'annotation_exclude' => ['@internal', 'a'],
  334. 'exclude' => ['@internal', 'b'],
  335. ],
  336. \sprintf('#^%s$#', preg_quote('[final_internal_class] Configuration cannot contain deprecated option "annotation_exclude" and new option "exclude".', '#')),
  337. 'Option "annotation_exclude" for rule "final_internal_class" is deprecated and will be removed in version 4.0. Use "exclude" to configure PHPDoc annotations tags and attributes.',
  338. ];
  339. }
  340. /**
  341. * @param _AutogeneratedInputConfiguration $config
  342. *
  343. * @dataProvider provideFix80Cases
  344. *
  345. * @requires PHP 8.0
  346. */
  347. public function testFix80(string $expected, ?string $input, array $config): void
  348. {
  349. $this->fixer->configure($config);
  350. $this->doTest($expected, $input);
  351. }
  352. /**
  353. * @return iterable<int|string, array{0: string, 1: null|string, 2: array{consider_absent_docblock_as_internal_class? : bool, exclude?: list<string>, include?: list<string>}}>
  354. */
  355. public static function provideFix80Cases(): iterable
  356. {
  357. yield 'multiple attributes, all configured as not to fix' => [
  358. '<?php
  359. #[X]
  360. #[A]
  361. class Foo {}',
  362. null,
  363. ['exclude' => ['a', 'X']],
  364. ];
  365. yield 'multiple attributes, one configured as to fix, one as not to fix' => [
  366. '<?php
  367. #[Internal]
  368. #[A]
  369. class Foo {}',
  370. null,
  371. [
  372. 'include' => ['internal'],
  373. 'exclude' => ['A'],
  374. ],
  375. ];
  376. yield 'multiple attributes, one configured as to fix' => [
  377. '<?php
  378. #[Internal]
  379. #[A]
  380. final class Foo {}',
  381. '<?php
  382. #[Internal]
  383. #[A]
  384. class Foo {}',
  385. ['include' => ['internal']],
  386. ];
  387. yield 'single attribute configured as to fix' => [
  388. '<?php
  389. #[Internal]
  390. final class Foo {}',
  391. '<?php
  392. #[Internal]
  393. class Foo {}',
  394. ['include' => ['internal']],
  395. ];
  396. yield 'class that should be ignored as it has an attribute not included with absent docblock as true' => [
  397. '<?php
  398. #[StandWithUkraine]
  399. class Foo {}',
  400. null,
  401. ['consider_absent_docblock_as_internal_class' => true],
  402. ];
  403. yield 'mixed bag of cases' => [
  404. '<?php
  405. #[Entity(repositoryClass: PostRepository::class)]
  406. class User
  407. {}
  408. #[ORM\Entity]
  409. #[Index(name: "category_idx", columns: ["category"])]
  410. final class Article
  411. {}
  412. #[A]
  413. class ArticleB
  414. {}
  415. #[B]
  416. final class Foo {}
  417. #[C]
  418. class FooX {}
  419. $object1 = new #[ExampleAttribute] class(){};
  420. $object2 = new /* */ class(){};
  421. $object3 = new #[B] #[ExampleAttribute] class(){};
  422. /**
  423. * @B
  424. */
  425. final class PhpDocClass{}
  426. ',
  427. '<?php
  428. #[Entity(repositoryClass: PostRepository::class)]
  429. class User
  430. {}
  431. #[ORM\Entity]
  432. #[Index(name: "category_idx", columns: ["category"])]
  433. class Article
  434. {}
  435. #[A]
  436. class ArticleB
  437. {}
  438. #[B]
  439. class Foo {}
  440. #[C]
  441. class FooX {}
  442. $object1 = new #[ExampleAttribute] class(){};
  443. $object2 = new /* */ class(){};
  444. $object3 = new #[B] #[ExampleAttribute] class(){};
  445. /**
  446. * @B
  447. */
  448. class PhpDocClass{}
  449. ',
  450. [
  451. 'exclude' => ['Entity', 'A'],
  452. 'include' => ['orm\entity', 'B'],
  453. ],
  454. ];
  455. yield 'multiple classes, first configured with attribute, second without attribute' => [
  456. '<?php
  457. #[Internal]
  458. final class Foo {}
  459. class Bar {}',
  460. '<?php
  461. #[Internal]
  462. class Foo {}
  463. class Bar {}',
  464. ['include' => ['internal']],
  465. ];
  466. yield 'multiple classes, first configured without attribute, second with attribute' => [
  467. '<?php
  468. class Foo {}
  469. #[Internal]
  470. final class Bar {}',
  471. '<?php
  472. class Foo {}
  473. #[Internal]
  474. class Bar {}',
  475. ['include' => ['internal']],
  476. ];
  477. yield 'include by attribute, but exclude by doc' => [
  478. '<?php
  479. /** @final */
  480. #[A]
  481. class Foo {}',
  482. null,
  483. [
  484. 'exclude' => ['final'],
  485. 'include' => ['A'],
  486. ],
  487. ];
  488. yield 'include by phpDoc, but exclude by attribute' => [
  489. '<?php
  490. /** @a */
  491. #[Internal]
  492. class Foo {}',
  493. null,
  494. [
  495. 'exclude' => ['Internal'],
  496. 'include' => ['A'],
  497. ],
  498. ];
  499. yield 'comment between attributes' => [
  500. '<?php
  501. #[A]
  502. /**
  503. * @B
  504. */
  505. #[C]
  506. final class Foo {}',
  507. '<?php
  508. #[A]
  509. /**
  510. * @B
  511. */
  512. #[C]
  513. class Foo {}',
  514. [
  515. 'include' => ['A', 'C'],
  516. ],
  517. ];
  518. }
  519. /**
  520. * @param _AutogeneratedInputConfiguration $config
  521. *
  522. * @dataProvider provideFix82Cases
  523. *
  524. * @requires PHP 8.2
  525. */
  526. public function testFix82(string $expected, ?string $input, array $config): void
  527. {
  528. $this->fixer->configure($config);
  529. $this->doTest($expected, $input);
  530. }
  531. /**
  532. * @return iterable<int|string, array{0: string, 1: null|string, 2: array{consider_absent_docblock_as_internal_class? : bool, exclude?: list<string>, include?: list<string>}}>
  533. */
  534. public static function provideFix82Cases(): iterable
  535. {
  536. yield 'readonly with enabled `consider_absent_docblock_as_internal_class`' => [
  537. '<?php readonly final class A{}',
  538. '<?php readonly class A{}',
  539. ['consider_absent_docblock_as_internal_class' => true],
  540. ];
  541. yield 'readonly with `internal` attribute and comment in-between' => [
  542. '<?php #[Internal] readonly /* comment */ final class A{}',
  543. '<?php #[Internal] readonly /* comment */ class A{}',
  544. ['consider_absent_docblock_as_internal_class' => true],
  545. ];
  546. }
  547. }