AnnotationTest.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707
  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\DocBlock;
  13. use PhpCsFixer\DocBlock\Annotation;
  14. use PhpCsFixer\DocBlock\DocBlock;
  15. use PhpCsFixer\DocBlock\Line;
  16. use PhpCsFixer\Tests\TestCase;
  17. use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
  18. use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
  19. /**
  20. * @author Graham Campbell <hello@gjcampbell.co.uk>
  21. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  22. *
  23. * @internal
  24. *
  25. * @covers \PhpCsFixer\DocBlock\Annotation
  26. */
  27. final class AnnotationTest extends TestCase
  28. {
  29. /**
  30. * This represents the content an entire docblock.
  31. */
  32. private static string $sample = '/**
  33. * Test docblock.
  34. *
  35. * @param string $hello
  36. * @param bool $test Description
  37. * extends over many lines
  38. *
  39. * @param adkjbadjasbdand $asdnjkasd
  40. *
  41. * @throws \Exception asdnjkasd
  42. *
  43. * asdasdasdasdasdasdasdasd
  44. * kasdkasdkbasdasdasdjhbasdhbasjdbjasbdjhb
  45. *
  46. * @return void
  47. */';
  48. /**
  49. * This represents the content of each annotation.
  50. *
  51. * @var list<string>
  52. */
  53. private static array $content = [
  54. " * @param string \$hello\n",
  55. " * @param bool \$test Description\n * extends over many lines\n",
  56. " * @param adkjbadjasbdand \$asdnjkasd\n",
  57. " * @throws \\Exception asdnjkasd\n *\n * asdasdasdasdasdasdasdasd\n * kasdkasdkbasdasdasdjhbasdhbasjdbjasbdjhb\n",
  58. " * @return void\n",
  59. ];
  60. /**
  61. * This represents the start indexes of each annotation.
  62. *
  63. * @var list<int>
  64. */
  65. private static array $start = [3, 4, 7, 9, 14];
  66. /**
  67. * This represents the start indexes of each annotation.
  68. *
  69. * @var list<int>
  70. */
  71. private static array $end = [3, 5, 7, 12, 14];
  72. /**
  73. * This represents the tag type of each annotation.
  74. *
  75. * @var list<string>
  76. */
  77. private static array $tags = ['param', 'param', 'param', 'throws', 'return'];
  78. /**
  79. * @dataProvider provideGetContentCases
  80. */
  81. public function testGetContent(int $index, string $content): void
  82. {
  83. $doc = new DocBlock(self::$sample);
  84. $annotation = $doc->getAnnotation($index);
  85. self::assertSame($content, $annotation->getContent());
  86. self::assertSame($content, (string) $annotation);
  87. }
  88. /**
  89. * @return iterable<array{int, string}>
  90. */
  91. public static function provideGetContentCases(): iterable
  92. {
  93. foreach (self::$content as $index => $content) {
  94. yield [$index, $content];
  95. }
  96. }
  97. /**
  98. * @dataProvider provideStartCases
  99. */
  100. public function testStart(int $index, int $start): void
  101. {
  102. $doc = new DocBlock(self::$sample);
  103. $annotation = $doc->getAnnotation($index);
  104. self::assertSame($start, $annotation->getStart());
  105. }
  106. /**
  107. * @return iterable<array{int, int}>
  108. */
  109. public static function provideStartCases(): iterable
  110. {
  111. foreach (self::$start as $index => $start) {
  112. yield [$index, $start];
  113. }
  114. }
  115. /**
  116. * @dataProvider provideEndCases
  117. */
  118. public function testEnd(int $index, int $end): void
  119. {
  120. $doc = new DocBlock(self::$sample);
  121. $annotation = $doc->getAnnotation($index);
  122. self::assertSame($end, $annotation->getEnd());
  123. }
  124. /**
  125. * @return iterable<array{int, int}>
  126. */
  127. public static function provideEndCases(): iterable
  128. {
  129. foreach (self::$end as $index => $end) {
  130. yield [$index, $end];
  131. }
  132. }
  133. /**
  134. * @dataProvider provideGetTagCases
  135. */
  136. public function testGetTag(int $index, string $tag): void
  137. {
  138. $doc = new DocBlock(self::$sample);
  139. $annotation = $doc->getAnnotation($index);
  140. self::assertSame($tag, $annotation->getTag()->getName());
  141. }
  142. /**
  143. * @return iterable<array{int, string}>
  144. */
  145. public static function provideGetTagCases(): iterable
  146. {
  147. foreach (self::$tags as $index => $tag) {
  148. yield [$index, $tag];
  149. }
  150. }
  151. /**
  152. * @dataProvider provideRemoveCases
  153. */
  154. public function testRemove(int $index, int $start, int $end): void
  155. {
  156. $doc = new DocBlock(self::$sample);
  157. $annotation = $doc->getAnnotation($index);
  158. $annotation->remove();
  159. self::assertSame('', $annotation->getContent());
  160. self::assertSame('', $doc->getLine($start)->getContent());
  161. self::assertSame('', $doc->getLine($end)->getContent());
  162. }
  163. /**
  164. * @return iterable<array{int, int, int}>
  165. */
  166. public static function provideRemoveCases(): iterable
  167. {
  168. foreach (self::$start as $index => $start) {
  169. yield [$index, $start, self::$end[$index]];
  170. }
  171. }
  172. /**
  173. * @dataProvider provideRemoveEdgeCasesCases
  174. */
  175. public function testRemoveEdgeCases(string $expected, string $input): void
  176. {
  177. $doc = new DocBlock($input);
  178. $annotation = $doc->getAnnotation(0);
  179. $annotation->remove();
  180. self::assertSame($expected, $doc->getContent());
  181. }
  182. /**
  183. * @return iterable<array{string, string}>
  184. */
  185. public static function provideRemoveEdgeCasesCases(): iterable
  186. {
  187. // Single line
  188. yield ['', '/** @return null*/'];
  189. yield ['', '/** @return null */'];
  190. yield ['', '/** @return null */'];
  191. // Multi line, annotation on start line
  192. yield [
  193. '/**
  194. */',
  195. '/** @return null
  196. */',
  197. ];
  198. yield [
  199. '/**
  200. */',
  201. '/** @return null '.'
  202. */',
  203. ];
  204. // Multi line, annotation on end line
  205. yield [
  206. '/**
  207. */',
  208. '/**
  209. * @return null*/',
  210. ];
  211. yield [
  212. '/**
  213. */',
  214. '/**
  215. * @return null */',
  216. ];
  217. }
  218. /**
  219. * @param list<string> $expected
  220. *
  221. * @dataProvider provideTypeParsingCases
  222. */
  223. public function testTypeParsing(array $expected, string $input): void
  224. {
  225. $tag = new Annotation([new Line($input)]);
  226. self::assertSame($expected, $tag->getTypes());
  227. }
  228. public static function provideTypeParsingCases(): iterable
  229. {
  230. yield [
  231. ['int'],
  232. ' * @method int method()',
  233. ];
  234. yield [
  235. ['int[]'],
  236. " * @return int[]\r",
  237. ];
  238. yield [
  239. ['int[]'],
  240. " * @return int[]\r\n",
  241. ];
  242. yield [
  243. ['Foo[][]'],
  244. ' * @method Foo[][] method()',
  245. ];
  246. yield [
  247. ['int[]'],
  248. ' * @method int[] method()',
  249. ];
  250. yield [
  251. ['int[]', 'null'],
  252. ' * @method int[]|null method()',
  253. ];
  254. yield [
  255. ['int[]', 'null', '?int', 'array'],
  256. ' * @method int[]|null|?int|array method()',
  257. ];
  258. yield [
  259. ['null', 'Foo\Bar', '\Baz\Bax', 'int[]'],
  260. ' * @method null|Foo\Bar|\Baz\Bax|int[] method()',
  261. ];
  262. yield [
  263. ['gen<int>'],
  264. ' * @method gen<int> method()',
  265. ];
  266. yield [
  267. ['int', 'gen<int>'],
  268. ' * @method int|gen<int> method()',
  269. ];
  270. yield [
  271. ['\int', '\gen<\int, \bool>'],
  272. ' * @method \int|\gen<\int, \bool> method()',
  273. ];
  274. yield [
  275. ['gen<int, int>'],
  276. ' * @method gen<int, int> method()',
  277. ];
  278. yield [
  279. ['gen<int, bool|string>'],
  280. ' * @method gen<int, bool|string> method()',
  281. ];
  282. yield [
  283. ['gen<int, string[]>'],
  284. ' * @method gen<int, string[]> method() <> a',
  285. ];
  286. yield [
  287. ['gen<int, gener<string, bool>>'],
  288. ' * @method gen<int, gener<string, bool>> method() foo <a >',
  289. ];
  290. yield [
  291. ['gen<int, gener<string, null|bool>>'],
  292. ' * @method gen<int, gener<string, null|bool>> method()',
  293. ];
  294. yield [
  295. ['null', 'gen<int, gener<string, bool>>', 'int', 'string[]'],
  296. ' * @method null|gen<int, gener<string, bool>>|int|string[] method() foo <a >',
  297. ];
  298. yield [
  299. ['null', 'gen<int, gener<string, bool>>', 'int', 'array<int, string>', 'string[]'],
  300. ' * @method null|gen<int, gener<string, bool>>|int|array<int, string>|string[] method() foo <a >',
  301. ];
  302. yield [
  303. ['this'],
  304. '/** @return this */',
  305. ];
  306. yield [
  307. ['@this'],
  308. '/** @return @this */',
  309. ];
  310. yield [
  311. ['$SELF', 'int'],
  312. '/** @return $SELF|int */',
  313. ];
  314. yield [
  315. ['array<string|int, string>'],
  316. '/** @var array<string|int, string>',
  317. ];
  318. yield [
  319. ['int'],
  320. " * @return int\n",
  321. ];
  322. yield [
  323. ['int'],
  324. " * @return int\r\n",
  325. ];
  326. yield [
  327. ['Collection<Foo<Bar>, Foo<Baz>>'],
  328. '/** @var Collection<Foo<Bar>, Foo<Baz>>',
  329. ];
  330. yield [
  331. ['int', 'string'],
  332. '/** @var int | string',
  333. ];
  334. yield [
  335. ['Foo::*'],
  336. '/** @var Foo::*',
  337. ];
  338. yield [
  339. ['Foo::A'],
  340. '/** @var Foo::A',
  341. ];
  342. yield [
  343. ['Foo::A', 'Foo::B'],
  344. '/** @var Foo::A|Foo::B',
  345. ];
  346. yield [
  347. ['Foo::A*'],
  348. '/** @var Foo::A*',
  349. ];
  350. yield [
  351. ['array<Foo::A*>', 'null'],
  352. '/** @var array<Foo::A*>|null',
  353. ];
  354. yield [
  355. ['null', 'true', 'false', '1', '-1', '1.5', '-1.5', '.5', '1.', "'a'", '"b"'],
  356. '/** @var null|true|false|1|-1|1.5|-1.5|.5|1.|\'a\'|"b"',
  357. ];
  358. yield [
  359. ['int', '"a"', 'A<B<C, D>, E<F::*|G[]>>'],
  360. '/** @param int | "a" | A<B<C, D>, E<F::*|G[]>> $foo */',
  361. ];
  362. yield [
  363. ['class-string<Foo>'],
  364. '/** @var class-string<Foo> */',
  365. ];
  366. yield [
  367. ['A', 'B'],
  368. '/** @var A&B */',
  369. ];
  370. yield [
  371. ['A', 'B'],
  372. '/** @var A & B */',
  373. ];
  374. yield [
  375. ['array{1: bool, 2: bool}'],
  376. '/** @var array{1: bool, 2: bool} */',
  377. ];
  378. yield [
  379. ['array{a: int|string, b?: bool}'],
  380. '/** @var array{a: int|string, b?: bool} */',
  381. ];
  382. yield [
  383. ['array{\'a\': "a", "b"?: \'b\'}'],
  384. '/** @var array{\'a\': "a", "b"?: \'b\'} */',
  385. ];
  386. yield [
  387. ['array { a : int | string , b ? : A<B, C> }'],
  388. '/** @var array { a : int | string , b ? : A<B, C> } */',
  389. ];
  390. yield [
  391. ['callable(string)'],
  392. '/** @param callable(string) $function',
  393. ];
  394. yield [
  395. ['callable(string): bool'],
  396. '/** @param callable(string): bool $function',
  397. ];
  398. yield [
  399. ['callable(array<int, string>, array<int, Foo>): bool'],
  400. '/** @param callable(array<int, string>, array<int, Foo>): bool $function',
  401. ];
  402. yield [
  403. ['array<int, callable(string): bool>'],
  404. '/** @param array<int, callable(string): bool> $function',
  405. ];
  406. yield [
  407. ['callable(string): callable(int)'],
  408. '/** @param callable(string): callable(int) $function',
  409. ];
  410. yield [
  411. ['callable(string) : callable(int) : bool'],
  412. '/** @param callable(string) : callable(int) : bool $function',
  413. ];
  414. yield [
  415. ['TheCollection<callable(Foo, Bar,Baz): Foo[]>', 'string[]', 'null'],
  416. '* @param TheCollection<callable(Foo, Bar,Baz): Foo[]>|string[]|null $x',
  417. ];
  418. yield [
  419. ['Closure(string)'],
  420. '/** @param Closure(string) $function',
  421. ];
  422. yield [
  423. ['closure()'],
  424. '/** @param closure() $function',
  425. ];
  426. yield [
  427. ['array < int , callable ( string ) : bool >'],
  428. '/** @param array < int , callable ( string ) : bool > $function',
  429. ];
  430. }
  431. /**
  432. * @param list<string> $expected
  433. * @param list<string> $new
  434. *
  435. * @dataProvider provideTypesCases
  436. */
  437. public function testTypes(array $expected, array $new, string $input, string $output): void
  438. {
  439. $line = new Line($input);
  440. $tag = new Annotation([$line]);
  441. self::assertSame($expected, $tag->getTypes());
  442. $tag->setTypes($new);
  443. self::assertSame($new, $tag->getTypes());
  444. self::assertSame($output, $line->getContent());
  445. }
  446. public static function provideTypesCases(): iterable
  447. {
  448. yield [['Foo', 'null'], ['Bar[]'], ' * @param Foo|null $foo', ' * @param Bar[] $foo'];
  449. yield [['false'], ['bool'], '* @return false', '* @return bool'];
  450. yield [['RUNTIMEEEEeXCEPTION'], [\Throwable::class], "* \t@throws\t \t RUNTIMEEEEeXCEPTION\t\t\t\t\t\t\t\n\n\n", "* \t@throws\t \t Throwable\t\t\t\t\t\t\t\n\n\n"];
  451. yield [['RUNTIMEEEEeXCEPTION'], [\Throwable::class], "*\t@throws\t \t RUNTIMEEEEeXCEPTION\t\t\t\t\t\t\t\n\n\n", "*\t@throws\t \t Throwable\t\t\t\t\t\t\t\n\n\n"];
  452. yield [['RUNTIMEEEEeXCEPTION'], [\Throwable::class], "*@throws\t \t RUNTIMEEEEeXCEPTION\t\t\t\t\t\t\t\n\n\n", "*@throws\t \t Throwable\t\t\t\t\t\t\t\n\n\n"];
  453. yield [['string'], ['string', 'null'], ' * @method string getString()', ' * @method string|null getString()'];
  454. yield [['Foo', 'Bar'], ['Bar', 'Foo'], ' * @param Foo&Bar $x', ' * @param Bar&Foo $x'];
  455. }
  456. /**
  457. * @param list<string> $expected
  458. *
  459. * @dataProvider provideNormalizedTypesCases
  460. */
  461. public function testNormalizedTypes(array $expected, string $input): void
  462. {
  463. $line = new Line($input);
  464. $tag = new Annotation([$line]);
  465. self::assertSame($expected, $tag->getNormalizedTypes());
  466. }
  467. public static function provideNormalizedTypesCases(): iterable
  468. {
  469. yield [['null', 'string'], '* @param StRiNg|NuLl $foo'];
  470. yield [['void'], '* @return Void'];
  471. yield [['bar', 'baz', 'foo', 'null', 'qux'], '* @return Foo|Bar|Baz|Qux|null'];
  472. yield [['bool', 'int'], '* @param bool|int $foo'];
  473. yield [['bool', 'int'], '* @param bool&int $foo'];
  474. yield [['bool', 'int'], '* @param bool|int ...$foo'];
  475. yield [['bool', 'int'], '* @param bool|int &$foo'];
  476. yield [['bool', 'int'], '* @param bool|int &...$foo'];
  477. yield [['bool', 'int'], '* @param bool|int$foo'];
  478. yield [['bool', 'int'], '* @param bool|int&$foo'];
  479. yield [['bool', 'int'], '* @param bool|int&...$foo'];
  480. yield [['bar&baz', 'foo'], '* @param Foo|Bar&Baz&$param'];
  481. yield [['bar&baz', 'foo'], '* @param Baz&Bar|Foo&$param'];
  482. }
  483. public function testGetTypesOnBadTag(): void
  484. {
  485. $this->expectException(\RuntimeException::class);
  486. $this->expectExceptionMessage('This tag does not support types');
  487. $tag = new Annotation([new Line(' * @deprecated since Symfony 1.2')]);
  488. $tag->getTypes();
  489. }
  490. public function testSetTypesOnBadTag(): void
  491. {
  492. $this->expectException(\RuntimeException::class);
  493. $this->expectExceptionMessage('This tag does not support types');
  494. $tag = new Annotation([new Line(' * @author Chuck Norris')]);
  495. $tag->setTypes(['string']);
  496. }
  497. public function testGetTagsWithTypes(): void
  498. {
  499. $tags = Annotation::getTagsWithTypes();
  500. foreach ($tags as $tag) {
  501. self::assertIsString($tag);
  502. self::assertNotEmpty($tag);
  503. }
  504. }
  505. /**
  506. * @param list<NamespaceUseAnalysis> $namespaceUses
  507. *
  508. * @dataProvider provideGetTypeExpressionCases
  509. */
  510. public function testGetTypeExpression(string $line, ?NamespaceAnalysis $namespace, array $namespaceUses, ?string $expectedCommonType): void
  511. {
  512. $annotation = new Annotation([new Line($line)], $namespace, $namespaceUses);
  513. $result = $annotation->getTypeExpression();
  514. self::assertSame($expectedCommonType, $result->getCommonType());
  515. }
  516. public static function provideGetTypeExpressionCases(): iterable
  517. {
  518. $appNamespace = new NamespaceAnalysis('App', 'App', 0, 999, 0, 999);
  519. $useTraversable = new NamespaceUseAnalysis(NamespaceUseAnalysis::TYPE_CLASS, \Traversable::class, \Traversable::class, false, false, 0, 999);
  520. yield ['* @param array|Traversable $foo', null, [], 'iterable'];
  521. yield ['* @param array|Traversable $foo', $appNamespace, [], null];
  522. yield ['* @param array|Traversable $foo', $appNamespace, [$useTraversable], 'iterable'];
  523. }
  524. /**
  525. * @dataProvider provideGetVariableNameCases
  526. */
  527. public function testGetVariableName(string $line, ?string $expectedVariableName): void
  528. {
  529. $annotation = new Annotation([new Line($line)]);
  530. self::assertSame($expectedVariableName, $annotation->getVariableName());
  531. }
  532. /**
  533. * @return iterable<array{string, null|string}>
  534. */
  535. public static function provideGetVariableNameCases(): iterable
  536. {
  537. yield ['* @param int $foo', '$foo'];
  538. yield ['* @param int $foo some description', '$foo'];
  539. yield ['/** @param int $foo*/', '$foo'];
  540. yield ['* @param int', null];
  541. yield ['* @var int $foo', '$foo'];
  542. yield ['* @var int $foo some description', '$foo'];
  543. yield ['/** @var int $foo*/', '$foo'];
  544. yield ['* @var int', null];
  545. yield ['* @param $foo', '$foo'];
  546. yield ['* @param &$foo', '$foo'];
  547. yield ['* @param & $foo', '$foo'];
  548. yield ['* @param int &$foo', '$foo'];
  549. yield ['* @param int & $foo', '$foo'];
  550. yield ['* @param int ...$foo', '$foo'];
  551. yield ['* @param int ... $foo', '$foo'];
  552. yield ['* @param int&...$foo', '$foo'];
  553. yield ['* @param int &...$foo', '$foo'];
  554. yield ['* @param int & ...$foo', '$foo'];
  555. yield ['* @param int & ... $foo', '$foo'];
  556. yield ['* @param ?int $foo=null invalid description', '$foo'];
  557. yield ['* @param int $počet Special chars in variable name', '$počet'];
  558. }
  559. }