TypeExpressionTest.php 26 KB

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