TypeExpressionTest.php 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192
  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\TypeExpression;
  14. use PhpCsFixer\Tests\TestCase;
  15. use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
  16. use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
  17. /**
  18. * @covers \PhpCsFixer\DocBlock\TypeExpression
  19. *
  20. * @internal
  21. */
  22. final class TypeExpressionTest extends TestCase
  23. {
  24. /**
  25. * @param null|list<string> $expectedTypes
  26. *
  27. * @dataProvider provideGetConstTypesCases
  28. * @dataProvider provideGetTypesCases
  29. */
  30. public function testGetTypes(string $typesExpression, ?array $expectedTypes = null): void
  31. {
  32. if (null === $expectedTypes) {
  33. $expectedTypes = [$typesExpression];
  34. }
  35. $expression = $this->parseTypeExpression($typesExpression, null, []);
  36. self::assertSame($expectedTypes, $expression->getTypes());
  37. $unionTestNs = '__UnionTest__';
  38. $unionExpression = $this->parseTypeExpression(
  39. $unionTestNs.'\A|'.$typesExpression.'|'.$unionTestNs.'\Z',
  40. null,
  41. []
  42. );
  43. if (!$expression->isCompositeType() || $expression->isUnionType()) {
  44. self::assertSame(
  45. [$unionTestNs.'\A', ...$expectedTypes, $unionTestNs.'\Z'],
  46. [...$unionExpression->getTypes()]
  47. );
  48. }
  49. }
  50. /**
  51. * @return iterable<int|string, array{0: string, 1?: null|list<string>}>
  52. */
  53. public static function provideGetTypesCases(): iterable
  54. {
  55. yield ['int'];
  56. yield ['Foo5'];
  57. yield ['🚀_kůň'];
  58. yield ['positive-int'];
  59. yield ['?int'];
  60. yield ['? int'];
  61. yield ['int[]'];
  62. yield ['Foo[][]'];
  63. yield ['Foo [ ] []'];
  64. yield ['int[]|null', ['int[]', 'null']];
  65. yield ['int[]|null|?int|array', ['int[]', 'null', '?int', 'array']];
  66. yield ['null|Foo\Bar|\Baz\Bax|int[]', ['null', 'Foo\Bar', '\Baz\Bax', 'int[]']];
  67. yield ['gen<int>'];
  68. yield ['int|gen<int>', ['int', 'gen<int>']];
  69. yield ['\int|\gen<\int, \bool>', ['\int', '\gen<\int, \bool>']];
  70. yield ['gen<int, int>'];
  71. yield ['gen<int, bool|string>'];
  72. yield ['gen<int, string[]>'];
  73. yield ['gen<int, gener<string, bool>>'];
  74. yield ['gen<int, gener<string, null|bool>>'];
  75. yield ['gen<int>[][]'];
  76. yield ['non-empty-array<int>'];
  77. yield ['null|gen<int, gener<string, bool>>|int|string[]', ['null', 'gen<int, gener<string, bool>>', 'int', 'string[]']];
  78. yield ['null|gen<int, gener<string, bool>>|int|array<int, string>|string[]', ['null', 'gen<int, gener<string, bool>>', 'int', 'array<int, string>', 'string[]']];
  79. yield ['this'];
  80. yield ['@this'];
  81. yield ['$SELF|int', ['$SELF', 'int']];
  82. yield ['array<string|int, string>'];
  83. yield ['Collection<Foo<Bar>, Foo<Baz>>'];
  84. yield ['int | string', ['int', 'string']];
  85. yield ['Foo::*'];
  86. yield ['Foo::A'];
  87. yield ['Foo::A|Foo::B', ['Foo::A', 'Foo::B']];
  88. yield ['Foo::A*'];
  89. yield ['Foo::*0*_Bar'];
  90. yield ['?Foo::*[]'];
  91. yield ['array<Foo::A*>|null', ['array<Foo::A*>', 'null']];
  92. yield ['null|true|false|1|-1|1.5|-1.5|.5|1.|\'a\'|"b"', ['null', 'true', 'false', '1', '-1', '1.5', '-1.5', '.5', '1.', "'a'", '"b"']];
  93. yield ['int | "a" | A<B<C, D>, E<F::*|G[]>>', ['int', '"a"', 'A<B<C, D>, E<F::*|G[]>>']];
  94. yield ['class-string<Foo>'];
  95. yield ['A&B', ['A', 'B']];
  96. yield ['A & B', ['A', 'B']];
  97. yield ['array{}'];
  98. yield ['object{ }'];
  99. yield ['array{1: bool, 2: bool}'];
  100. yield ['array{a: int|string, b?: bool}'];
  101. yield ['array{\'a\': "a", "b"?: \'b\'}'];
  102. yield ['array { a : int | string , b ? : A<B, C> }'];
  103. yield ['array{bool, int}'];
  104. yield ['array{bool,}'];
  105. yield ['list{int, bool}'];
  106. yield ['object{ bool, foo2: int }'];
  107. yield ['ArRAY{ 1 }'];
  108. yield ['lIst{ 1 }'];
  109. yield ['OBJECT { x: 1 }'];
  110. yield ['array{a: int, b: int, with-dash: int}'];
  111. yield ['callable'];
  112. yield ['callable(string)'];
  113. yield ['? callable(string): bool'];
  114. yield ['CAllable(string): bool'];
  115. yield ['callable(string,): bool'];
  116. yield ['callable(array<int, string>, array<int, Foo>): bool'];
  117. yield ['array<int, callable(string): bool>'];
  118. yield ['callable(string): callable(int)'];
  119. yield ['callable(string) : callable(int) : bool'];
  120. yield ['TheCollection<callable(Foo, Bar,Baz): Foo[]>|string[]|null', ['TheCollection<callable(Foo, Bar,Baz): Foo[]>', 'string[]', 'null']];
  121. yield ['Closure()'];
  122. yield ['Closure(string)'];
  123. yield ['\closure(string): void'];
  124. yield [\Closure::class];
  125. yield ['\Closure()'];
  126. yield ['\Closure(string)'];
  127. yield ['\Closure(string, bool)'];
  128. yield ['\Closure(string|int, bool)'];
  129. yield ['\Closure(string):bool'];
  130. yield ['\Closure(string): bool'];
  131. yield ['\Closure(string|int, bool): bool'];
  132. yield ['\Closure(float|int): (bool|int)'];
  133. yield ['Closure<T>(): T'];
  134. yield ['Closure<Tx, Ty>(): array{x: Tx, y: Ty}'];
  135. yield ['array < int , callable ( string ) : bool >'];
  136. yield ['Closure<T of Foo>(T): T'];
  137. yield ['Closure< T1 of Foo, T2 AS Foo >(T1): T2'];
  138. yield ['Closure<T = Foo>(T): T'];
  139. yield ['Closure<T1=int, T2 of Foo = Foo2>(T1): T2'];
  140. yield ['Closure<T of string = \'\'>(T): T'];
  141. yield ['Closure<Closure_can_be_regular_class>'];
  142. yield ['Closure(int $a)'];
  143. yield ['Closure(int $a): bool'];
  144. yield ['Closure(int $a, array<Closure(int ...$args): Item<X>>): bool'];
  145. yield ['Closure_can_be_aliased()'];
  146. yield ['Closure_can_be_aliased(): (u|v)'];
  147. yield ['(int)'];
  148. yield ['(int|\Exception)'];
  149. yield ['($foo is int ? false : true)'];
  150. yield ['($foo🚀3 is int ? false : true)'];
  151. yield ['\'a\\\'s"\\\\\n\r\t\'|"b\"s\'\\\\\n\r\t"', ['\'a\\\'s"\\\\\n\r\t\'', '"b\"s\'\\\\\n\r\t"']];
  152. yield ['string'.str_repeat('[]', 128)];
  153. yield [str_repeat('array<', 120).'string'.str_repeat('>', 120)];
  154. yield [self::makeLongArrayShapeType()];
  155. }
  156. /**
  157. * @return iterable<array{string}>
  158. */
  159. public static function provideGetConstTypesCases(): iterable
  160. {
  161. foreach ([
  162. 'null',
  163. 'true',
  164. 'FALSE',
  165. '123',
  166. '+123',
  167. '-123',
  168. '0b0110101',
  169. '0o777',
  170. '0x7Fb4',
  171. '-0O777',
  172. '-0X7Fb4',
  173. '123_456',
  174. '0b01_01_01',
  175. '-0X7_Fb_4',
  176. '18_446_744_073_709_551_616', // 64-bit unsigned long + 1, larger than PHP_INT_MAX
  177. '123.4',
  178. '.123',
  179. '123.',
  180. '123e4',
  181. '123E4',
  182. '12.3e4',
  183. '+123.5',
  184. '-123.',
  185. '-123.4',
  186. '-.123',
  187. '-123e-4',
  188. '-12.3e-4',
  189. '-1_2.3_4e5_6',
  190. '123E+80',
  191. '8.2023437675747321', // greater precision than 64-bit double
  192. '-0.0',
  193. '\'\'',
  194. '\'foo\'',
  195. '\'\\\\\'',
  196. '\'\\\'\'',
  197. ] as $type) {
  198. yield [$type];
  199. }
  200. }
  201. /**
  202. * @dataProvider provideParseInvalidExceptionCases
  203. */
  204. public function testParseInvalidException(string $value): void
  205. {
  206. $this->expectException(\Exception::class);
  207. $this->expectExceptionMessage('Unable to parse phpdoc type');
  208. new TypeExpression($value, null, []);
  209. }
  210. /**
  211. * @return iterable<int|string, array{string}>
  212. */
  213. public static function provideParseInvalidExceptionCases(): iterable
  214. {
  215. yield [''];
  216. yield ['0_class_cannot_start_with_number'];
  217. yield ['$0_variable_cannot_start_with_number'];
  218. yield ['class cannot contain space'];
  219. yield ['\\\class_with_double_backslash'];
  220. yield ['class\\\with_double_backslash'];
  221. yield ['class_with_end_backslash\\'];
  222. yield ['class/with_slash'];
  223. yield ['class--with_double_dash'];
  224. yield ['class.with_dot'];
  225. yield ['class,with_comma'];
  226. yield ['class@with_at_sign'];
  227. yield ['class:with_colon'];
  228. yield ['class#with_hash'];
  229. yield ['class//with_double_slash'];
  230. yield ['class$with_dollar'];
  231. yield ['class;with_semicolon'];
  232. yield ['class=with_equal_sign'];
  233. yield ['class+with_plus'];
  234. yield ['class?with_question_mark'];
  235. yield ['class*with_star'];
  236. yield ['class%with_percent'];
  237. yield ['(unclosed_parenthesis'];
  238. yield [')unclosed_parenthesis'];
  239. yield ['unclosed_parenthesis('];
  240. yield ['((unclosed_parenthesis)'];
  241. yield ['|vertical_bar_start'];
  242. yield ['&ampersand_start'];
  243. yield ['~tilde_start'];
  244. yield ['vertical_bar_end|'];
  245. yield ['ampersand_end&'];
  246. yield ['tilde_end~'];
  247. yield ['class||double_vertical_bar'];
  248. yield ['class&&double_ampersand'];
  249. yield ['class~~double_tilde'];
  250. yield ['array<'];
  251. yield ['array<<'];
  252. yield ['array>'];
  253. yield ['array<<>'];
  254. yield ['array<>>'];
  255. yield ['array{'];
  256. yield ['array{ $this: 5 }'];
  257. yield ['g<,>'];
  258. yield ['g<,no_leading_comma>'];
  259. yield ['10__000'];
  260. yield ['[ array_syntax_is_invalid ]'];
  261. yield ['\' unclosed string'];
  262. yield ['\' unclosed string \\\''];
  263. yield 'generic with no arguments' => ['f<>'];
  264. yield 'generic Closure with no arguments' => ['Closure<>(): void'];
  265. yield 'generic Closure with non-identifier template argument' => ['Closure<A|B>(): void'];
  266. yield [substr(self::makeLongArrayShapeType(), 0, -1)];
  267. }
  268. public function testHugeType(): void
  269. {
  270. $nFlat = 2_000;
  271. $types = [];
  272. for ($i = 0; $i < $nFlat; ++$i) {
  273. $types[] = '\X\Foo'.$i;
  274. }
  275. $str = implode('|', $types);
  276. $expression = new TypeExpression($str, null, []);
  277. self::assertSame($types, $expression->getTypes());
  278. for ($i = 0; $i < 100; ++$i) {
  279. $str = 'array'.(1 === $i % 2 ? '{' : '<').$str.(1 === $i % 2 ? '}' : '>');
  280. }
  281. $typeLeft = '\Closure(A|B): void';
  282. $typeRight = '\Closure('.$typeLeft.'): void';
  283. $expression = new TypeExpression($typeLeft.'|('.$str.')|'.$typeRight, null, []);
  284. self::assertSame([$typeLeft, '('.$str.')', $typeRight], $expression->getTypes());
  285. }
  286. /**
  287. * @dataProvider provideGetTypesGlueCases
  288. */
  289. public function testGetTypesGlue(?string $expectedTypesGlue, string $typesExpression): void
  290. {
  291. $expression = new TypeExpression($typesExpression, null, []);
  292. self::assertSame($expectedTypesGlue, $expression->getTypesGlue());
  293. }
  294. /**
  295. * @return iterable<array{0: null|'&'|'|', 1: string}>
  296. */
  297. public static function provideGetTypesGlueCases(): iterable
  298. {
  299. yield [null, 'string'];
  300. yield ['|', 'bool|string'];
  301. yield ['&', 'Foo&Bar'];
  302. }
  303. /**
  304. * @dataProvider provideIsCompositeTypeCases
  305. */
  306. public function testIsCompositeType(bool $expectedIsCompositeType, string $typeExpression): void
  307. {
  308. $expression = new TypeExpression($typeExpression, null, []);
  309. self::assertSame($expectedIsCompositeType, $expression->isCompositeType());
  310. }
  311. /**
  312. * @return iterable<array{0: bool, 1: string}>
  313. */
  314. public static function provideIsCompositeTypeCases(): iterable
  315. {
  316. yield [false, 'string'];
  317. yield [false, 'iterable<Foo>'];
  318. yield [true, 'iterable&stringable'];
  319. yield [true, 'bool|string'];
  320. yield [true, 'Foo|(Bar&Baz)'];
  321. }
  322. /**
  323. * @dataProvider provideIsUnionTypeCases
  324. */
  325. public function testIsUnionType(bool $expectedIsUnionType, string $typeExpression): void
  326. {
  327. $expression = new TypeExpression($typeExpression, null, []);
  328. self::assertSame($expectedIsUnionType, $expression->isUnionType());
  329. }
  330. /**
  331. * @return iterable<array{0: bool, 1: string}>
  332. */
  333. public static function provideIsUnionTypeCases(): iterable
  334. {
  335. yield [false, 'string'];
  336. yield [false, 'iterable&stringable'];
  337. yield [true, 'bool|string'];
  338. yield [true, 'int|string|null'];
  339. yield [true, 'int|?string'];
  340. yield [true, 'int|null'];
  341. yield [false, '?int'];
  342. yield [true, 'Foo|Bar'];
  343. }
  344. /**
  345. * @dataProvider provideIsIntersectionTypeCases
  346. */
  347. public function testIsIntersectionType(bool $expectedIsIntersectionType, string $typeExpression): void
  348. {
  349. $expression = new TypeExpression($typeExpression, null, []);
  350. self::assertSame($expectedIsIntersectionType, $expression->isIntersectionType());
  351. }
  352. /**
  353. * @return iterable<array{0: bool, 1: string}>
  354. */
  355. public static function provideIsIntersectionTypeCases(): iterable
  356. {
  357. yield [false, 'string'];
  358. yield [false, 'string|int'];
  359. yield [true, 'Foo&Bar'];
  360. yield [true, 'Foo&Bar&?Baz'];
  361. yield [true, '\iterable&\Stringable'];
  362. }
  363. /**
  364. * @param list<NamespaceUseAnalysis> $namespaceUses
  365. *
  366. * @dataProvider provideGetCommonTypeCases
  367. */
  368. public function testGetCommonType(string $typesExpression, ?string $expectedCommonType, ?NamespaceAnalysis $namespace = null, array $namespaceUses = []): void
  369. {
  370. $expression = new TypeExpression($typesExpression, $namespace, $namespaceUses);
  371. self::assertSame($expectedCommonType, $expression->getCommonType());
  372. }
  373. public static function provideGetCommonTypeCases(): iterable
  374. {
  375. $globalNamespace = new NamespaceAnalysis('', '', 0, 999, 0, 999);
  376. $appNamespace = new NamespaceAnalysis('App', 'App', 0, 999, 0, 999);
  377. $useTraversable = new NamespaceUseAnalysis(NamespaceUseAnalysis::TYPE_CLASS, \Traversable::class, \Traversable::class, false, false, 0, 0);
  378. $useObjectAsTraversable = new NamespaceUseAnalysis(NamespaceUseAnalysis::TYPE_CLASS, 'Foo', \Traversable::class, false, false, 0, 0);
  379. yield ['true', 'bool'];
  380. yield ['false', 'bool'];
  381. yield ['bool', 'bool'];
  382. yield ['int', 'int'];
  383. yield ['float', 'float'];
  384. yield ['string', 'string'];
  385. yield ['array', 'array'];
  386. yield ['object', 'object'];
  387. yield ['self', 'self'];
  388. yield ['static', 'static'];
  389. yield ['bool[]', 'array'];
  390. yield ['int[]', 'array'];
  391. yield ['float[]', 'array'];
  392. yield ['string[]', 'array'];
  393. yield ['array[]', 'array'];
  394. yield ['bool[][]', 'array'];
  395. yield ['int[][]', 'array'];
  396. yield ['float[][]', 'array'];
  397. yield ['string[][]', 'array'];
  398. yield ['array[][]', 'array'];
  399. yield ['bool [ ]', 'array'];
  400. yield ['bool [ ][ ]', 'array'];
  401. yield ['array|iterable', 'iterable'];
  402. yield ['iterable|array', 'iterable'];
  403. yield ['array|Traversable', 'iterable'];
  404. yield ['array|\Traversable', 'iterable'];
  405. yield ['array|Traversable', 'iterable', $globalNamespace];
  406. yield ['iterable|Traversable', 'iterable'];
  407. yield ['array<string>', 'array'];
  408. yield ['array<int, string>', 'array'];
  409. yield ['array < string >', 'array'];
  410. yield ['list<int>', 'array'];
  411. yield ['iterable<string>', 'iterable'];
  412. yield ['iterable<int, string>', 'iterable'];
  413. yield ['\Traversable<string>', '\Traversable'];
  414. yield ['Traversable<int, string>', 'Traversable'];
  415. yield ['Collection<string>', 'Collection'];
  416. yield ['Collection<int, string>', 'Collection'];
  417. yield ['array{string}', 'array'];
  418. yield ['array { 1: string, \Closure(): void }', 'array'];
  419. yield ['Closure(): void', \Closure::class];
  420. yield ['array<int, string>|iterable<int, string>', 'iterable'];
  421. yield ['int[]|string[]', 'array'];
  422. yield ['int|null', 'int'];
  423. yield ['null|int', 'int'];
  424. yield ['?int', 'int'];
  425. yield ['?array<Foo>', 'array'];
  426. yield ['?list<Foo>', 'array'];
  427. yield ['void', 'void'];
  428. yield ['never', 'never'];
  429. yield ['array|Traversable', 'iterable', null, [$useTraversable]];
  430. yield ['array|Traversable', 'iterable', $globalNamespace, [$useTraversable]];
  431. yield ['array|Traversable', 'iterable', $appNamespace, [$useTraversable]];
  432. yield ['self|static', 'self'];
  433. yield ['array|Traversable', null, null, [$useObjectAsTraversable]];
  434. yield ['array|Traversable', null, $globalNamespace, [$useObjectAsTraversable]];
  435. yield ['array|Traversable', null, $appNamespace, [$useObjectAsTraversable]];
  436. yield ['bool|int', null];
  437. yield ['string|bool', null];
  438. yield ['array<int, string>|Collection<int, string>', null];
  439. }
  440. /**
  441. * @dataProvider provideAllowsNullCases
  442. */
  443. public function testAllowsNull(string $typesExpression, bool $expectNullAllowed): void
  444. {
  445. $expression = new TypeExpression($typesExpression, null, []);
  446. self::assertSame($expectNullAllowed, $expression->allowsNull());
  447. }
  448. /**
  449. * @return iterable<array{string, bool}>
  450. */
  451. public static function provideAllowsNullCases(): iterable
  452. {
  453. yield ['null', true];
  454. yield ['mixed', true];
  455. yield ['null|mixed', true];
  456. yield ['int|bool|null', true];
  457. yield ['int|bool|mixed', true];
  458. yield ['int', false];
  459. yield ['bool', false];
  460. yield ['string', false];
  461. yield ['?int', true];
  462. yield ['?\Closure(): void', true];
  463. }
  464. public function testMapTypes(): void
  465. {
  466. $typeExpression = new TypeExpression('Foo|Bar|($v is \Closure(X, Y): Z ? U : (V&W))', null, []);
  467. $addLeadingSlash = static function (TypeExpression $type) {
  468. $value = $type->toString();
  469. if (!str_starts_with($value, '\\') && !str_starts_with($value, '(')) {
  470. return new TypeExpression('\\'.$value, null, []);
  471. }
  472. return $type;
  473. };
  474. $removeLeadingSlash = static function (TypeExpression $type) {
  475. $value = $type->toString();
  476. if (str_starts_with($value, '\\')) {
  477. return new TypeExpression(substr($value, 1), null, []);
  478. }
  479. return $type;
  480. };
  481. $callLog = [];
  482. $typeExpression->mapTypes(static function (TypeExpression $type) use (&$callLog) {
  483. $callLog[] = $type->toString();
  484. if ('Y' === $type->toString()) {
  485. return new TypeExpression('_y_', null, []);
  486. }
  487. return $type;
  488. });
  489. self::assertSame([
  490. 'Foo',
  491. 'Bar',
  492. '\Closure',
  493. 'X',
  494. 'Y',
  495. 'Z',
  496. '\Closure(X, _y_): Z',
  497. 'U',
  498. 'V',
  499. 'W',
  500. 'V&W',
  501. '(V&W)',
  502. '($v is \Closure(X, _y_): Z ? U : (V&W))',
  503. 'Foo|Bar|($v is \Closure(X, _y_): Z ? U : (V&W))',
  504. ], $callLog);
  505. $typeExpression = $typeExpression->mapTypes($addLeadingSlash);
  506. $this->checkInnerTypeExpressionsStartIndex($typeExpression);
  507. self::assertSame('\Foo|\Bar|($v is \Closure(\X, \Y): \Z ? \U : (\V&\W))', $typeExpression->toString());
  508. $typeExpression = $typeExpression->mapTypes($addLeadingSlash);
  509. $this->checkInnerTypeExpressionsStartIndex($typeExpression);
  510. self::assertSame('\Foo|\Bar|($v is \Closure(\X, \Y): \Z ? \U : (\V&\W))', $typeExpression->toString());
  511. $typeExpression = $typeExpression->mapTypes($removeLeadingSlash);
  512. $this->checkInnerTypeExpressionsStartIndex($typeExpression);
  513. self::assertSame('Foo|Bar|($v is Closure(X, Y): Z ? U : (V&W))', $typeExpression->toString());
  514. $typeExpression = $typeExpression->mapTypes($removeLeadingSlash);
  515. $this->checkInnerTypeExpressionsStartIndex($typeExpression);
  516. self::assertSame('Foo|Bar|($v is Closure(X, Y): Z ? U : (V&W))', $typeExpression->toString());
  517. $typeExpression = $typeExpression->mapTypes($addLeadingSlash);
  518. $this->checkInnerTypeExpressionsStartIndex($typeExpression);
  519. self::assertSame('\Foo|\Bar|($v is \Closure(\X, \Y): \Z ? \U : (\V&\W))', $typeExpression->toString());
  520. }
  521. public function testWalkTypes(): void
  522. {
  523. $typeExpression = new TypeExpression('Foo|Bar|($v is \Closure(X, Y): Z ? U : (V&W))', null, []);
  524. $callLog = [];
  525. $typeExpression->walkTypes(static function (TypeExpression $type) use (&$callLog): void {
  526. $callLog[] = $type->toString();
  527. });
  528. self::assertSame([
  529. 'Foo',
  530. 'Bar',
  531. '\Closure',
  532. 'X',
  533. 'Y',
  534. 'Z',
  535. '\Closure(X, Y): Z',
  536. 'U',
  537. 'V',
  538. 'W',
  539. 'V&W',
  540. '(V&W)',
  541. '($v is \Closure(X, Y): Z ? U : (V&W))',
  542. 'Foo|Bar|($v is \Closure(X, Y): Z ? U : (V&W))',
  543. ], $callLog);
  544. }
  545. /**
  546. * @dataProvider provideSortTypesCases
  547. */
  548. public function testSortTypes(string $typesExpression, string $expectResult): void
  549. {
  550. $sortCaseFx = static fn (TypeExpression $a, TypeExpression $b): int => strcasecmp($a->toString(), $b->toString());
  551. $sortCrc32Fx = static fn (TypeExpression $a, TypeExpression $b): int => crc32($a->toString()) <=> crc32($b->toString());
  552. $expression = $this->parseTypeExpression($typesExpression, null, []);
  553. $expression = $expression->sortTypes($sortCaseFx);
  554. $this->checkInnerTypeExpressionsStartIndex($expression);
  555. self::assertSame($expectResult, $expression->toString());
  556. $expression = $expression->sortTypes($sortCrc32Fx);
  557. $this->checkInnerTypeExpressionsStartIndex($expression);
  558. $expression = $expression->sortTypes($sortCaseFx);
  559. $this->checkInnerTypeExpressionsStartIndex($expression);
  560. self::assertSame($expectResult, $expression->toString());
  561. }
  562. /**
  563. * @return iterable<string, array{string, string}>
  564. */
  565. public static function provideSortTypesCases(): iterable
  566. {
  567. yield 'not a union type' => [
  568. 'int',
  569. 'int',
  570. ];
  571. yield 'simple' => [
  572. 'int|bool',
  573. 'bool|int',
  574. ];
  575. yield 'multiple union' => [
  576. 'C___|D____|B__|A',
  577. 'A|B__|C___|D____',
  578. ];
  579. yield 'multiple intersect' => [
  580. 'C___&D____&B__&A',
  581. 'A&B__&C___&D____',
  582. ];
  583. yield 'simple in generic' => [
  584. 'array<int|bool>',
  585. 'array<bool|int>',
  586. ];
  587. yield 'generic with multiple types' => [
  588. 'array<int|bool, string|float>',
  589. 'array<bool|int, float|string>',
  590. ];
  591. yield 'generic with trailing comma' => [
  592. 'array<int|bool,>',
  593. 'array<bool|int,>',
  594. ];
  595. yield 'simple in array shape with int key' => [
  596. 'array{0: int|bool}',
  597. 'array{0: bool|int}',
  598. ];
  599. yield 'simple in array shape with string key' => [
  600. 'array{"foo": int|bool}',
  601. 'array{"foo": bool|int}',
  602. ];
  603. yield 'simple in array shape with multiple keys' => [
  604. 'array{0: int|bool, "foo": int|bool}',
  605. 'array{0: bool|int, "foo": bool|int}',
  606. ];
  607. yield 'simple in array shape with implicit key' => [
  608. 'array{int|bool}',
  609. 'array{bool|int}',
  610. ];
  611. yield 'simple in array shape with trailing comma' => [
  612. 'array{int|bool,}',
  613. 'array{bool|int,}',
  614. ];
  615. yield 'simple in array shape with multiple types with trailing comma' => [
  616. 'array{int|bool, Foo|Bar, }',
  617. 'array{bool|int, Bar|Foo, }',
  618. ];
  619. yield 'simple in array shape' => [
  620. 'list{int, Foo|Bar}',
  621. 'list{int, Bar|Foo}',
  622. ];
  623. yield 'array shape with multiple colons - array shape' => [
  624. 'array{array{x:int|bool}, a:array{x:int|bool}}',
  625. 'array{array{x:bool|int}, a:array{x:bool|int}}',
  626. ];
  627. yield 'array shape with multiple colons - callable' => [
  628. 'array{array{x:int|bool}, int|bool, callable(): void}',
  629. 'array{array{x:bool|int}, bool|int, callable(): void}',
  630. ];
  631. yield 'simple in callable argument' => [
  632. 'callable(int|bool)',
  633. 'callable(bool|int)',
  634. ];
  635. yield 'callable with multiple arguments' => [
  636. 'callable(int|bool, null|array)',
  637. 'callable(bool|int, array|null)',
  638. ];
  639. yield 'simple in callable return type' => [
  640. 'callable(): (string|float)',
  641. 'callable(): (float|string)',
  642. ];
  643. yield 'callable with union return type and within union itself' => [
  644. 'callable(): (string|float)|bool',
  645. 'bool|callable(): (float|string)',
  646. ];
  647. yield 'callable with multiple named arguments' => [
  648. 'callable(int|bool $b, null|array $a)',
  649. 'callable(bool|int $b, array|null $a)',
  650. ];
  651. yield 'callable with complex arguments' => [
  652. 'callable(B|A&, D|Closure(): void..., array{}$foo=, $this $foo=): array{}',
  653. 'callable(A|B&, Closure(): void|D..., array{}$foo=, $this $foo=): array{}',
  654. ];
  655. yield 'callable with trailing comma' => [
  656. 'Closure( Y|X , ): B|A',
  657. 'A|Closure( X|Y , ): B',
  658. ];
  659. yield 'simple in Closure argument' => [
  660. 'Closure(int|bool)',
  661. 'Closure(bool|int)',
  662. ];
  663. yield 'Closure with multiple arguments' => [
  664. 'Closure(int|bool, null|array)',
  665. 'Closure(bool|int, array|null)',
  666. ];
  667. yield 'simple in Closure argument with trailing comma' => [
  668. 'Closure(int|bool,)',
  669. 'Closure(bool|int,)',
  670. ];
  671. yield 'simple in Closure argument multiple arguments with trailing comma' => [
  672. 'Closure(int|bool, null|array,)',
  673. 'Closure(bool|int, array|null,)',
  674. ];
  675. yield 'simple in Closure return type' => [
  676. 'Closure(): (string|float)',
  677. 'Closure(): (float|string)',
  678. ];
  679. yield 'Closure with union return type and within union itself' => [
  680. 'Closure(): (string|float)|bool',
  681. 'bool|Closure(): (float|string)',
  682. ];
  683. yield 'with multiple nesting levels' => [
  684. 'array{0: Foo<int|bool>|Bar<callable(string|float|array<int|bool>): (Foo|Bar)>}',
  685. 'array{0: Bar<callable(array<bool|int>|float|string): (Bar|Foo)>|Foo<bool|int>}',
  686. ];
  687. yield 'with multiple nesting levels and callable within union' => [
  688. 'array{0: Foo<int|bool>|Bar<callable(string|float|array<int|bool>): (Foo|Bar)|Baz>}',
  689. 'array{0: Bar<Baz|callable(array<bool|int>|float|string): (Bar|Foo)>|Foo<bool|int>}',
  690. ];
  691. yield 'complex type with Closure with $this' => [
  692. 'array<string, string|array{ string|\Closure(mixed, string, $this): (int|float) }>|false',
  693. 'array<string, array{ \Closure(mixed, string, $this): (float|int)|string }|string>|false',
  694. ];
  695. yield 'generic Closure' => [
  696. 'Closure<B, A>(y|x, U<p|o>|B|A): (Y|B|X)',
  697. 'Closure<B, A>(x|y, A|B|U<o|p>): (B|X|Y)',
  698. ];
  699. yield 'generic Closure with bound template' => [
  700. 'Closure<B of J|I, C, A of V|U, D of object>(B|A): array{B, A, B, C, D}',
  701. 'Closure<B of I|J, C, A of U|V, D of object>(A|B): array{B, A, B, C, D}',
  702. ];
  703. yield 'generic Closure with template with default' => [
  704. 'Closure<T = B&A>(T): void',
  705. 'Closure<T = A&B>(T): void',
  706. ];
  707. yield 'nullable generic' => [
  708. '?array<Foo|Bar>',
  709. '?array<Bar|Foo>',
  710. ];
  711. yield 'nullable callable' => [
  712. '?callable(Foo|Bar): (Foo|Bar)',
  713. '?callable(Bar|Foo): (Bar|Foo)',
  714. ];
  715. // This union type makes no sense in general (it should be `Bar|callable|null`)
  716. // but let's ensure nullable types are also sorted.
  717. yield 'nullable callable with union return type and within union itself' => [
  718. '?callable(Foo|Bar): (Foo|Bar)|?Bar',
  719. '?Bar|?callable(Bar|Foo): (Bar|Foo)',
  720. ];
  721. yield 'nullable array shape' => [
  722. '?array{0: Foo|Bar}',
  723. '?array{0: Bar|Foo}',
  724. ];
  725. yield 'simple types alternation' => [
  726. 'array<Foo&Bar>',
  727. 'array<Bar&Foo>',
  728. ];
  729. yield 'nesty stuff' => [
  730. 'array<Level11&array<Level2|array<Level31&Level32>>>',
  731. 'array<array<array<Level31&Level32>|Level2>&Level11>',
  732. ];
  733. yield 'parenthesized' => [
  734. '(Foo|Bar)',
  735. '(Bar|Foo)',
  736. ];
  737. yield 'parenthesized intersect' => [
  738. '(Foo&Bar)',
  739. '(Bar&Foo)',
  740. ];
  741. yield 'parenthesized in closure return type' => [
  742. 'Closure(Y|X): (string|float)',
  743. 'Closure(X|Y): (float|string)',
  744. ];
  745. yield 'conditional with variable' => [
  746. '($x is (CFoo|(CBaz&CBar)) ? (TFoo|(TBaz&TBar)) : (FFoo|(FBaz&FBar)))',
  747. '($x is ((CBar&CBaz)|CFoo) ? ((TBar&TBaz)|TFoo) : ((FBar&FBaz)|FFoo))',
  748. ];
  749. yield 'conditional with type' => [
  750. '((Foo|Bar) is x ? y : z)',
  751. '((Bar|Foo) is x ? y : z)',
  752. ];
  753. yield 'conditional in conditional' => [
  754. '((Foo|Bar) is x ? ($x is (CFoo|CBar) ? (TFoo|TBar) : (FFoo|FBar)) : z)',
  755. '((Bar|Foo) is x ? ($x is (CBar|CFoo) ? (TBar|TFoo) : (FBar|FFoo)) : z)',
  756. ];
  757. yield 'large numbers' => [
  758. '18_446_744_073_709_551_616|-8.2023437675747321e-18_446_744_073_709_551_616',
  759. '-8.2023437675747321e-18_446_744_073_709_551_616|18_446_744_073_709_551_616',
  760. ];
  761. yield 'mixed 2x | and & glue' => [
  762. 'Foo|Foo2|Baz&Bar',
  763. 'Bar&Baz|Foo|Foo2',
  764. ];
  765. yield 'mixed | and 2x & glue' => [
  766. 'Foo|Baz&Baz2&Bar',
  767. 'Bar&Baz&Baz2|Foo',
  768. ];
  769. }
  770. private static function makeLongArrayShapeType(): string
  771. {
  772. return 'array{'.implode(
  773. ', ',
  774. array_map(
  775. static fn (int $k): string => \sprintf('key%sno%d: int', 0 === $k % 2 ? '-' : '_', $k),
  776. range(1, 1_000),
  777. ),
  778. ).'}';
  779. }
  780. /**
  781. * Return type is recursive.
  782. *
  783. * @return list<array{int, string}|list<mixed>>
  784. */
  785. private function checkInnerTypeExpressionsStartIndex(TypeExpression $typeExpression): array
  786. {
  787. $innerTypeExpressions = \Closure::bind(static fn () => $typeExpression->innerTypeExpressions, null, TypeExpression::class)();
  788. $res = [];
  789. foreach ($innerTypeExpressions as ['start_index' => $innerStartIndex, 'expression' => $innerExpression]) {
  790. $innerExpressionStr = $innerExpression->toString();
  791. self::assertSame(
  792. $innerExpressionStr,
  793. substr($typeExpression->toString(), $innerStartIndex, \strlen($innerExpressionStr))
  794. );
  795. $res[] = [$innerStartIndex, $innerExpressionStr];
  796. $res[] = $this->checkInnerTypeExpressionsStartIndex($innerExpression);
  797. }
  798. return $res;
  799. }
  800. /**
  801. * Should be removed once https://github.com/php/php-src/pull/11396 is merged.
  802. */
  803. private function clearPcreRegexCache(): void
  804. {
  805. // there is no explicit php function to clear PCRE regex cache, but based
  806. // on https://www.php.net/manual/en/intro.pcre.php there are 4096 cache slots
  807. // pruned in FIFO fashion, so to clear the cache, replace all existing
  808. // cache slots with dummy regexes
  809. for ($i = 0; $i < 4_096; ++$i) {
  810. preg_match('/^'.$i.'/', '');
  811. }
  812. }
  813. /**
  814. * Parse type expression with and without PCRE JIT.
  815. *
  816. * @param list<NamespaceUseAnalysis> $namespaceUses
  817. */
  818. private function parseTypeExpression(string $value, ?NamespaceAnalysis $namespace, array $namespaceUses): TypeExpression
  819. {
  820. $pcreJitBackup = \ini_get('pcre.jit');
  821. $expression = null;
  822. $innerExpressionsDataWithoutJit = null;
  823. try {
  824. foreach ([false, true] as $pcreJit) {
  825. ini_set('pcre.jit', $pcreJit ? '1' : '0');
  826. $this->clearPcreRegexCache();
  827. $expression = new TypeExpression($value, null, []);
  828. $innerExpressionsData = $this->checkInnerTypeExpressionsStartIndex($expression);
  829. if (false === $pcreJit) {
  830. $innerExpressionsDataWithoutJit = $innerExpressionsData;
  831. } else {
  832. self::assertSame($innerExpressionsDataWithoutJit, $innerExpressionsData);
  833. }
  834. }
  835. } finally {
  836. ini_set('pcre.jit', $pcreJitBackup);
  837. $this->clearPcreRegexCache();
  838. }
  839. return $expression;
  840. }
  841. }