AnnotationTest.php 18 KB

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