CommentsAnalyzerTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  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\Tokenizer\Analyzer;
  13. use PhpCsFixer\Tests\TestCase;
  14. use PhpCsFixer\Tokenizer\Analyzer\CommentsAnalyzer;
  15. use PhpCsFixer\Tokenizer\Tokens;
  16. /**
  17. * @author Kuba Werłos <werlos@gmail.com>
  18. *
  19. * @internal
  20. *
  21. * @covers \PhpCsFixer\Tokenizer\Analyzer\CommentsAnalyzer
  22. */
  23. final class CommentsAnalyzerTest extends TestCase
  24. {
  25. public function testWhenNotPointingToComment(): void
  26. {
  27. $analyzer = new CommentsAnalyzer();
  28. $tokens = Tokens::fromCode('<?php $no; $comment; $here;');
  29. $this->expectException(\InvalidArgumentException::class);
  30. $this->expectExceptionMessage('Given index must point to a comment.');
  31. $analyzer->getCommentBlockIndices($tokens, 4);
  32. }
  33. /**
  34. * @param list<int> $borders
  35. *
  36. * @dataProvider provideCommentsCases
  37. */
  38. public function testComments(string $code, int $index, array $borders): void
  39. {
  40. $tokens = Tokens::fromCode($code);
  41. $analyzer = new CommentsAnalyzer();
  42. self::assertSame($borders, $analyzer->getCommentBlockIndices($tokens, $index));
  43. self::assertFalse($analyzer->isHeaderComment($tokens, $index));
  44. }
  45. public static function provideCommentsCases(): array
  46. {
  47. return [
  48. 'discover all 4 comments for the 1st comment with slash' => [
  49. '<?php
  50. $foo;
  51. // one
  52. // two
  53. // three
  54. // four
  55. $bar;',
  56. 4,
  57. [4, 6, 8, 10],
  58. ],
  59. 'discover all 4 comments for the 1st comment with hash' => [
  60. '<?php
  61. $foo;
  62. # one
  63. # two
  64. # three
  65. # four
  66. $bar;',
  67. 4,
  68. [4, 6, 8, 10],
  69. ],
  70. 'discover 3 comments out of 4 for the 2nd comment' => [
  71. '<?php
  72. $foo;
  73. // one
  74. // two
  75. // three
  76. // four
  77. $bar;',
  78. 6,
  79. [6, 8, 10],
  80. ],
  81. 'discover 3 comments when empty line separates 4th' => [
  82. '<?php
  83. $foo;
  84. // one
  85. // two
  86. // three
  87. // four
  88. $bar;',
  89. 4,
  90. [4, 6, 8],
  91. ],
  92. 'discover 3 comments when empty line of CR separates 4th' => [
  93. str_replace("\n", "\r", '<?php
  94. $foo;
  95. // one
  96. // two
  97. // three
  98. // four
  99. $bar;'),
  100. 4,
  101. [4, 6, 8],
  102. ],
  103. 'discover correctly when mix of slash and hash' => [
  104. '<?php
  105. $foo;
  106. // one
  107. // two
  108. # three
  109. // four
  110. $bar;',
  111. 4,
  112. [4, 6],
  113. ],
  114. 'do not group asterisk comments' => [
  115. '<?php
  116. $foo;
  117. /* one */
  118. /* two */
  119. /* three */
  120. $bar;',
  121. 4,
  122. [4],
  123. ],
  124. 'handle fancy indent' => [
  125. '<?php
  126. $foo;
  127. // one
  128. // two
  129. // three
  130. // four
  131. $bar;',
  132. 4,
  133. [4, 6, 8, 10],
  134. ],
  135. ];
  136. }
  137. public function testHeaderCommentAcceptsOnlyComments(): void
  138. {
  139. $tokens = Tokens::fromCode('<?php 1; 2; 3;');
  140. $analyzer = new CommentsAnalyzer();
  141. $this->expectException(\InvalidArgumentException::class);
  142. $analyzer->isHeaderComment($tokens, 2);
  143. }
  144. /**
  145. * @dataProvider provideHeaderCommentCases
  146. */
  147. public function testHeaderComment(string $code, int $index): void
  148. {
  149. $tokens = Tokens::fromCode($code);
  150. $analyzer = new CommentsAnalyzer();
  151. self::assertTrue($analyzer->isHeaderComment($tokens, $index));
  152. }
  153. public static function provideHeaderCommentCases(): array
  154. {
  155. return [
  156. ['<?php /* Comment */ namespace Foo;', 1],
  157. ['<?php /** Comment */ namespace Foo;', 1],
  158. ['<?php declare(strict_types=1); /* Comment */ namespace Foo;', 9],
  159. ['<?php /* We test this one */ /* Foo */ namespace Bar;', 1],
  160. ['<?php /** Comment */ namespace Foo; declare(strict_types=1); /* Comment */ namespace Foo;', 1],
  161. ];
  162. }
  163. /**
  164. * @dataProvider provideNotHeaderCommentCases
  165. */
  166. public function testNotHeaderComment(string $code, int $index): void
  167. {
  168. $tokens = Tokens::fromCode($code);
  169. $analyzer = new CommentsAnalyzer();
  170. self::assertFalse($analyzer->isHeaderComment($tokens, $index));
  171. }
  172. public static function provideNotHeaderCommentCases(): array
  173. {
  174. return [
  175. ['<?php $foo; /* Comment */ $bar;', 4],
  176. ['<?php foo(); /* Comment */ $bar;', 6],
  177. ['<?php namespace Foo; /* Comment */ class Bar {};', 6],
  178. ['<?php /* It is not header when no content after */', 1],
  179. ['<?php /* Foo */ /* We test this one */ namespace Bar;', 3],
  180. ['<?php /* Foo */ declare(strict_types=1); /* We test this one */ namespace Bar;', 11],
  181. ];
  182. }
  183. public function testPhpdocCandidateAcceptsOnlyComments(): void
  184. {
  185. $tokens = Tokens::fromCode('<?php 1; 2; 3;');
  186. $analyzer = new CommentsAnalyzer();
  187. $this->expectException(\InvalidArgumentException::class);
  188. $analyzer->isBeforeStructuralElement($tokens, 2);
  189. }
  190. /**
  191. * @dataProvider providePhpdocCandidateCases
  192. */
  193. public function testPhpdocCandidate(string $code): void
  194. {
  195. $tokens = Tokens::fromCode($code);
  196. $index = $tokens->getNextTokenOfKind(0, [[T_COMMENT], [T_DOC_COMMENT]]);
  197. $analyzer = new CommentsAnalyzer();
  198. self::assertTrue($analyzer->isBeforeStructuralElement($tokens, $index));
  199. }
  200. public static function providePhpdocCandidateCases(): array
  201. {
  202. return [
  203. ['<?php /* @var Foo */ $bar = "baz";'],
  204. ['<?php /* Before namespace */ namespace Foo;'],
  205. ['<?php /* Before class */ class Foo {}'],
  206. ['<?php /* Before class */ abstract class Foo {}'],
  207. ['<?php /* Before class */ final class Foo {}'],
  208. ['<?php /* Before trait */ trait Foo {}'],
  209. ['<?php /* Before interface */ interface Foo {}'],
  210. ['<?php /* Before anonymous function */ function () {};'],
  211. ['<?php class Foo { /* Before property */ private $bar; }'],
  212. ['<?php class Foo { /* Before property */ protected $bar; }'],
  213. ['<?php class Foo { /* Before property */ public $bar; }'],
  214. ['<?php class Foo { /* Before property */ var $bar; }'],
  215. ['<?php class Foo { /* Before function */ function bar() {} }'],
  216. ['<?php class Foo { /* Before use */ use Bar; }'],
  217. ['<?php class Foo { /* Before function */ final function bar() {} }'],
  218. ['<?php class Foo { /* Before function */ private function bar() {} }'],
  219. ['<?php class Foo { /* Before function */ protected function bar() {} }'],
  220. ['<?php class Foo { /* Before function */ public function bar() {} }'],
  221. ['<?php class Foo { /* Before function */ static function bar() {} }'],
  222. ['<?php class Foo { /* Before function */ abstract function bar(); }'],
  223. ['<?php class Foo { /* Before constant */ const FOO = 42; }'],
  224. ['<?php /* Before require */ require "foo/php";'],
  225. ['<?php /* Before require_once */ require_once "foo/php";'],
  226. ['<?php /* Before include */ include "foo/php";'],
  227. ['<?php /* Before include_once */ include_once "foo/php";'],
  228. ['<?php /* @var array $foo */ foreach ($foo as $bar) {};'],
  229. ['<?php /* @var int $foo */ if ($foo === -1) {};'],
  230. ['<?php /* @var SomeClass $foo */ switch ($foo) { default: exit; };'],
  231. ['<?php /* @var bool $foo */ while ($foo) { $foo--; };'],
  232. ['<?php /* @var int $i */ for ($i = 0; $i < 16; $i++) {};'],
  233. ['<?php /* @var int $i @var int $j */ list($i, $j) = getValues();'],
  234. ['<?php /* @var string $s */ print($s);'],
  235. ['<?php /* @var string $s */ echo($s);'],
  236. ['<?php /* @var User $bar */ ($baz = tmp())->doSomething();'],
  237. ['<?php /* @var User $bar */ list($bar) = a();'],
  238. ['<?php /* Before anonymous function */ $fn = fn($x) => $x + 1;'],
  239. ['<?php /* Before anonymous function */ fn($x) => $x + 1;'],
  240. ['<?php /* @var int $x */ [$x] = [2];'],
  241. ];
  242. }
  243. /**
  244. * @dataProvider provideNotPhpdocCandidateCases
  245. */
  246. public function testNotPhpdocCandidate(string $code): void
  247. {
  248. $tokens = Tokens::fromCode($code);
  249. $index = $tokens->getNextTokenOfKind(0, [[T_COMMENT], [T_DOC_COMMENT]]);
  250. $analyzer = new CommentsAnalyzer();
  251. self::assertFalse($analyzer->isBeforeStructuralElement($tokens, $index));
  252. }
  253. public static function provideNotPhpdocCandidateCases(): array
  254. {
  255. return [
  256. ['<?php class Foo {} /* At the end of file */'],
  257. ['<?php class Foo { public $baz; public function baz(); /* At the end of class */ }'],
  258. ['<?php /* Before increment */ $i++;'],
  259. ['<?php /* Comment, but not doc block */ if ($foo === -1) {};'],
  260. ['<?php
  261. $a = $b[1]; // @phpstan-ignore-line
  262. static::bar();',
  263. ],
  264. ['<?php /* @var int $a */ [$b] = [2];'],
  265. ];
  266. }
  267. /**
  268. * @dataProvider providePhpdocCandidatePhp80Cases
  269. *
  270. * @requires PHP 8.0
  271. */
  272. public function testPhpdocCandidatePhp80(string $code): void
  273. {
  274. $tokens = Tokens::fromCode($code);
  275. $index = $tokens->getNextTokenOfKind(0, [[T_COMMENT], [T_DOC_COMMENT]]);
  276. $analyzer = new CommentsAnalyzer();
  277. self::assertTrue($analyzer->isBeforeStructuralElement($tokens, $index));
  278. }
  279. public static function providePhpdocCandidatePhp80Cases(): iterable
  280. {
  281. yield 'attribute between class and phpDoc' => [
  282. '<?php
  283. /**
  284. * @Annotation
  285. */
  286. #[CustomAnnotationA]
  287. Class MyAnnotation3 {}',
  288. ];
  289. }
  290. /**
  291. * @dataProvider providePhpdocCandidatePhp81Cases
  292. *
  293. * @requires PHP 8.1
  294. */
  295. public function testPhpdocCandidatePhp81(string $code): void
  296. {
  297. $tokens = Tokens::fromCode($code);
  298. $index = $tokens->getNextTokenOfKind(0, [[T_COMMENT], [T_DOC_COMMENT]]);
  299. $analyzer = new CommentsAnalyzer();
  300. self::assertTrue($analyzer->isBeforeStructuralElement($tokens, $index));
  301. }
  302. public static function providePhpdocCandidatePhp81Cases(): iterable
  303. {
  304. yield 'public readonly' => [
  305. '<?php class Foo { /* */ public readonly int $a1; }',
  306. ];
  307. yield 'readonly public' => [
  308. '<?php class Foo { /* */ readonly public int $a1; }',
  309. ];
  310. yield 'readonly union' => [
  311. '<?php class Foo { /* */ readonly A|B $a1; }',
  312. ];
  313. yield 'public final const' => [
  314. '<?php final class Foo2 extends B implements A
  315. {
  316. /* */
  317. public final const Y = "i";
  318. }',
  319. ];
  320. yield 'final public const' => [
  321. '<?php final class Foo2 extends B implements A
  322. {
  323. /* */
  324. final public const Y = "i";
  325. }',
  326. ];
  327. yield 'enum' => [
  328. '<?php /* Before enum */ enum Foo {}',
  329. ];
  330. yield 'enum with deprecated case' => [
  331. '<?php
  332. enum Foo: int {
  333. /**
  334. * @deprecated Lorem ipsum
  335. */
  336. case BAR = 1;
  337. }',
  338. ];
  339. }
  340. /**
  341. * @dataProvider provideNotPhpdocCandidatePhp811Cases
  342. *
  343. * @requires PHP 8.1
  344. */
  345. public function testNotPhpdocCandidatePhp81(string $code): void
  346. {
  347. $tokens = Tokens::fromCode($code);
  348. $index = $tokens->getNextTokenOfKind(0, [[T_COMMENT], [T_DOC_COMMENT]]);
  349. $analyzer = new CommentsAnalyzer();
  350. self::assertFalse($analyzer->isBeforeStructuralElement($tokens, $index));
  351. }
  352. public static function provideNotPhpdocCandidatePhp811Cases(): iterable
  353. {
  354. yield 'enum and switch' => [
  355. '<?php
  356. enum E {}
  357. switch ($x) {
  358. /* */
  359. case 1: return 2;
  360. }
  361. ',
  362. ];
  363. yield 'switch and enum' => [
  364. '<?php
  365. switch ($x) {
  366. /* */
  367. case 1: return 2;
  368. }
  369. enum E {}
  370. ',
  371. ];
  372. }
  373. }