CommentsAnalyzerTest.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  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(): iterable
  46. {
  47. yield 'discover all 4 comments for the 1st comment with slash' => [
  48. '<?php
  49. $foo;
  50. // one
  51. // two
  52. // three
  53. // four
  54. $bar;',
  55. 4,
  56. [4, 6, 8, 10],
  57. ];
  58. yield 'discover all 4 comments for the 1st comment with hash' => [
  59. '<?php
  60. $foo;
  61. # one
  62. # two
  63. # three
  64. # four
  65. $bar;',
  66. 4,
  67. [4, 6, 8, 10],
  68. ];
  69. yield 'discover 3 comments out of 4 for the 2nd comment' => [
  70. '<?php
  71. $foo;
  72. // one
  73. // two
  74. // three
  75. // four
  76. $bar;',
  77. 6,
  78. [6, 8, 10],
  79. ];
  80. yield 'discover 3 comments when empty line separates 4th' => [
  81. '<?php
  82. $foo;
  83. // one
  84. // two
  85. // three
  86. // four
  87. $bar;',
  88. 4,
  89. [4, 6, 8],
  90. ];
  91. yield 'discover 3 comments when empty line of CR separates 4th' => [
  92. str_replace("\n", "\r", '<?php
  93. $foo;
  94. // one
  95. // two
  96. // three
  97. // four
  98. $bar;'),
  99. 4,
  100. [4, 6, 8],
  101. ];
  102. yield 'discover correctly when mix of slash and hash' => [
  103. '<?php
  104. $foo;
  105. // one
  106. // two
  107. # three
  108. // four
  109. $bar;',
  110. 4,
  111. [4, 6],
  112. ];
  113. yield 'do not group asterisk comments' => [
  114. '<?php
  115. $foo;
  116. /* one */
  117. /* two */
  118. /* three */
  119. $bar;',
  120. 4,
  121. [4],
  122. ];
  123. yield 'handle fancy indent' => [
  124. '<?php
  125. $foo;
  126. // one
  127. // two
  128. // three
  129. // four
  130. $bar;',
  131. 4,
  132. [4, 6, 8, 10],
  133. ];
  134. }
  135. public function testHeaderCommentAcceptsOnlyComments(): void
  136. {
  137. $tokens = Tokens::fromCode('<?php 1; 2; 3;');
  138. $analyzer = new CommentsAnalyzer();
  139. $this->expectException(\InvalidArgumentException::class);
  140. $analyzer->isHeaderComment($tokens, 2);
  141. }
  142. /**
  143. * @dataProvider provideHeaderCommentCases
  144. */
  145. public function testHeaderComment(string $code, int $index): void
  146. {
  147. $tokens = Tokens::fromCode($code);
  148. $analyzer = new CommentsAnalyzer();
  149. self::assertTrue($analyzer->isHeaderComment($tokens, $index));
  150. }
  151. /**
  152. * @return iterable<array{string, int}>
  153. */
  154. public static function provideHeaderCommentCases(): iterable
  155. {
  156. yield ['<?php /* Comment */ namespace Foo;', 1];
  157. yield ['<?php /** Comment */ namespace Foo;', 1];
  158. yield ['<?php declare(strict_types=1); /* Comment */ namespace Foo;', 9];
  159. yield ['<?php /* We test this one */ /* Foo */ namespace Bar;', 1];
  160. yield ['<?php /** Comment */ namespace Foo; declare(strict_types=1); /* Comment */ namespace Foo;', 1];
  161. }
  162. /**
  163. * @dataProvider provideNotHeaderCommentCases
  164. */
  165. public function testNotHeaderComment(string $code, int $index): void
  166. {
  167. $tokens = Tokens::fromCode($code);
  168. $analyzer = new CommentsAnalyzer();
  169. self::assertFalse($analyzer->isHeaderComment($tokens, $index));
  170. }
  171. /**
  172. * @return iterable<array{string, int}>
  173. */
  174. public static function provideNotHeaderCommentCases(): iterable
  175. {
  176. yield ['<?php $foo; /* Comment */ $bar;', 4];
  177. yield ['<?php foo(); /* Comment */ $bar;', 6];
  178. yield ['<?php namespace Foo; /* Comment */ class Bar {};', 6];
  179. yield ['<?php /* It is not header when no content after */', 1];
  180. yield ['<?php /* Foo */ /* We test this one */ namespace Bar;', 3];
  181. yield ['<?php /* Foo */ declare(strict_types=1); /* We test this one */ namespace Bar;', 11];
  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. /**
  201. * @return iterable<array{string}>
  202. */
  203. public static function providePhpdocCandidateCases(): iterable
  204. {
  205. yield ['<?php /* @var Foo */ $bar = "baz";'];
  206. yield ['<?php /* Before namespace */ namespace Foo;'];
  207. yield ['<?php /* Before class */ class Foo {}'];
  208. yield ['<?php /* Before class */ abstract class Foo {}'];
  209. yield ['<?php /* Before class */ final class Foo {}'];
  210. yield ['<?php /* Before trait */ trait Foo {}'];
  211. yield ['<?php /* Before interface */ interface Foo {}'];
  212. yield ['<?php /* Before anonymous function */ function () {};'];
  213. yield ['<?php class Foo { /* Before property */ private $bar; }'];
  214. yield ['<?php class Foo { /* Before property */ protected $bar; }'];
  215. yield ['<?php class Foo { /* Before property */ public $bar; }'];
  216. yield ['<?php class Foo { /* Before property */ var $bar; }'];
  217. yield ['<?php class Foo { /* Before function */ function bar() {} }'];
  218. yield ['<?php class Foo { /* Before use */ use Bar; }'];
  219. yield ['<?php class Foo { /* Before function */ final function bar() {} }'];
  220. yield ['<?php class Foo { /* Before function */ private function bar() {} }'];
  221. yield ['<?php class Foo { /* Before function */ protected function bar() {} }'];
  222. yield ['<?php class Foo { /* Before function */ public function bar() {} }'];
  223. yield ['<?php class Foo { /* Before function */ static function bar() {} }'];
  224. yield ['<?php class Foo { /* Before function */ abstract function bar(); }'];
  225. yield ['<?php class Foo { /* Before constant */ const FOO = 42; }'];
  226. yield ['<?php /* Before require */ require "foo/php";'];
  227. yield ['<?php /* Before require_once */ require_once "foo/php";'];
  228. yield ['<?php /* Before include */ include "foo/php";'];
  229. yield ['<?php /* Before include_once */ include_once "foo/php";'];
  230. yield ['<?php /* @var array $foo */ foreach ($foo as $bar) {};'];
  231. yield ['<?php /* @var int $foo */ if ($foo === -1) {};'];
  232. yield ['<?php /* @var SomeClass $foo */ switch ($foo) { default: exit; };'];
  233. yield ['<?php /* @var bool $foo */ while ($foo) { $foo--; };'];
  234. yield ['<?php /* @var int $i */ for ($i = 0; $i < 16; $i++) {};'];
  235. yield ['<?php /* @var int $i @var int $j */ list($i, $j) = getValues();'];
  236. yield ['<?php /* @var string $s */ print($s);'];
  237. yield ['<?php /* @var string $s */ echo($s);'];
  238. yield ['<?php /* @var User $bar */ ($baz = tmp())->doSomething();'];
  239. yield ['<?php /* @var User $bar */ list($bar) = a();'];
  240. yield ['<?php /* Before anonymous function */ $fn = fn($x) => $x + 1;'];
  241. yield ['<?php /* Before anonymous function */ fn($x) => $x + 1;'];
  242. yield ['<?php /* @var int $x */ [$x] = [2];'];
  243. yield ['<?php /* @var string $x */ $x ??= $y;'];
  244. yield ['<?php /* @var string $x */ $x .= $y;'];
  245. yield ['<?php /* @var int $x */ $x &= 1;'];
  246. yield ['<?php /* @var int $x */ $x |= 1;'];
  247. yield ['<?php /* @var int $x */ $x ^= 1;'];
  248. yield ['<?php /* @var int $x */ $x >>= 1;'];
  249. yield ['<?php /* @var int $x */ $x <<= 1;'];
  250. yield ['<?php /* @var float $x */ $x += 10;'];
  251. yield ['<?php /* @var float $x */ $x -= 10;'];
  252. yield ['<?php /* @var float $x */ $x *= 10;'];
  253. yield ['<?php /* @var float $x */ $x /= 10;'];
  254. yield ['<?php /* @var float $x */ $x %= 10;'];
  255. yield ['<?php /* @var float $x */ $x **= 10;'];
  256. }
  257. /**
  258. * @dataProvider provideNotPhpdocCandidateCases
  259. */
  260. public function testNotPhpdocCandidate(string $code): void
  261. {
  262. $tokens = Tokens::fromCode($code);
  263. $index = $tokens->getNextTokenOfKind(0, [[T_COMMENT], [T_DOC_COMMENT]]);
  264. $analyzer = new CommentsAnalyzer();
  265. self::assertFalse($analyzer->isBeforeStructuralElement($tokens, $index));
  266. }
  267. /**
  268. * @return iterable<array{string}>
  269. */
  270. public static function provideNotPhpdocCandidateCases(): iterable
  271. {
  272. yield ['<?php class Foo {} /* At the end of file */'];
  273. yield ['<?php class Foo { public $baz; public function baz(); /* At the end of class */ }'];
  274. yield ['<?php /* Before increment */ $i++;'];
  275. yield ['<?php /* Comment, but not doc block */ if ($foo === -1) {};'];
  276. yield ['<?php
  277. $a = $b[1]; // @phpstan-ignore-line
  278. static::bar();',
  279. ];
  280. yield ['<?php /* @var int $a */ [$b] = [2];'];
  281. }
  282. /**
  283. * @dataProvider providePhpdocCandidatePhp80Cases
  284. *
  285. * @requires PHP 8.0
  286. */
  287. public function testPhpdocCandidatePhp80(string $code): void
  288. {
  289. $this->testPhpdocCandidate($code);
  290. }
  291. /**
  292. * @return iterable<string, array{string}>
  293. */
  294. public static function providePhpdocCandidatePhp80Cases(): iterable
  295. {
  296. yield 'attribute between class and phpDoc' => [
  297. '<?php
  298. /**
  299. * @Annotation
  300. */
  301. #[CustomAnnotationA]
  302. Class MyAnnotation3 {}',
  303. ];
  304. }
  305. /**
  306. * @dataProvider providePhpdocCandidatePhp81Cases
  307. *
  308. * @requires PHP 8.1
  309. */
  310. public function testPhpdocCandidatePhp81(string $code): void
  311. {
  312. $this->testPhpdocCandidate($code);
  313. }
  314. /**
  315. * @return iterable<string, array{string}>
  316. */
  317. public static function providePhpdocCandidatePhp81Cases(): iterable
  318. {
  319. yield 'public readonly' => [
  320. '<?php class Foo { /* */ public readonly int $a1; }',
  321. ];
  322. yield 'readonly public' => [
  323. '<?php class Foo { /* */ readonly public int $a1; }',
  324. ];
  325. yield 'readonly union' => [
  326. '<?php class Foo { /* */ readonly A|B $a1; }',
  327. ];
  328. yield 'public final const' => [
  329. '<?php final class Foo2 extends B implements A
  330. {
  331. /* */
  332. public final const Y = "i";
  333. }',
  334. ];
  335. yield 'final public const' => [
  336. '<?php final class Foo2 extends B implements A
  337. {
  338. /* */
  339. final public const Y = "i";
  340. }',
  341. ];
  342. yield 'enum' => [
  343. '<?php /* Before enum */ enum Foo {}',
  344. ];
  345. yield 'enum with deprecated case' => [
  346. '<?php
  347. enum Foo: int {
  348. /**
  349. * @deprecated Lorem ipsum
  350. */
  351. case BAR = 1;
  352. }',
  353. ];
  354. }
  355. /**
  356. * @dataProvider provideNotPhpdocCandidatePhp81Cases
  357. *
  358. * @requires PHP 8.1
  359. */
  360. public function testNotPhpdocCandidatePhp81(string $code): void
  361. {
  362. $this->testNotPhpdocCandidate($code);
  363. }
  364. /**
  365. * @return iterable<string, array{string}>
  366. */
  367. public static function provideNotPhpdocCandidatePhp81Cases(): iterable
  368. {
  369. yield 'enum and switch' => [
  370. '<?php
  371. enum E {}
  372. switch ($x) {
  373. /* */
  374. case 1: return 2;
  375. }
  376. ',
  377. ];
  378. yield 'switch and enum' => [
  379. '<?php
  380. switch ($x) {
  381. /* */
  382. case 1: return 2;
  383. }
  384. enum E {}
  385. ',
  386. ];
  387. }
  388. /**
  389. * @dataProvider provideReturnStatementCases
  390. */
  391. public function testReturnStatement(string $code, bool $expected): void
  392. {
  393. $tokens = Tokens::fromCode($code);
  394. $index = $tokens->getNextTokenOfKind(0, [[T_COMMENT], [T_DOC_COMMENT]]);
  395. $analyzer = new CommentsAnalyzer();
  396. self::assertSame($expected, $analyzer->isBeforeReturn($tokens, $index));
  397. }
  398. /**
  399. * @return iterable<string, array{string, bool}>
  400. */
  401. public static function provideReturnStatementCases(): iterable
  402. {
  403. yield 'docblock before var' => [
  404. '<?php
  405. function returnClassName()
  406. {
  407. /** @todo something */
  408. $var = 123;
  409. return;
  410. }
  411. ',
  412. false,
  413. ];
  414. yield 'comment before var' => [
  415. '<?php
  416. function returnClassName()
  417. {
  418. // @todo something
  419. $var = 123;
  420. return;
  421. }
  422. ',
  423. false,
  424. ];
  425. yield 'docblock return' => [
  426. '<?php
  427. function returnClassName()
  428. {
  429. /** @todo something */
  430. return;
  431. }
  432. ',
  433. true,
  434. ];
  435. yield 'comment return' => [
  436. '<?php
  437. function returnClassName()
  438. {
  439. // @todo something
  440. return;
  441. }
  442. ',
  443. true,
  444. ];
  445. }
  446. }