TypeExpressionTest.php 32 KB

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