AnnotationTest.php 19 KB

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