TypeExpressionTest.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907
  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'];
  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. * @param NamespaceUseAnalysis[] $namespaceUses
  270. *
  271. * @dataProvider provideGetCommonTypeCases
  272. */
  273. public function testGetCommonType(string $typesExpression, ?string $expectedCommonType, NamespaceAnalysis $namespace = null, array $namespaceUses = []): void
  274. {
  275. $expression = new TypeExpression($typesExpression, $namespace, $namespaceUses);
  276. self::assertSame($expectedCommonType, $expression->getCommonType());
  277. }
  278. public static function provideGetCommonTypeCases(): iterable
  279. {
  280. $globalNamespace = new NamespaceAnalysis('', '', 0, 999, 0, 999);
  281. $appNamespace = new NamespaceAnalysis('App', 'App', 0, 999, 0, 999);
  282. $useTraversable = new NamespaceUseAnalysis('Traversable', 'Traversable', false, 0, 0, NamespaceUseAnalysis::TYPE_CLASS);
  283. $useObjectAsTraversable = new NamespaceUseAnalysis('Foo', 'Traversable', false, 0, 0, NamespaceUseAnalysis::TYPE_CLASS);
  284. yield ['true', 'bool'];
  285. yield ['false', 'bool'];
  286. yield ['bool', 'bool'];
  287. yield ['int', 'int'];
  288. yield ['float', 'float'];
  289. yield ['string', 'string'];
  290. yield ['array', 'array'];
  291. yield ['object', 'object'];
  292. yield ['self', 'self'];
  293. yield ['static', 'static'];
  294. yield ['bool[]', 'array'];
  295. yield ['int[]', 'array'];
  296. yield ['float[]', 'array'];
  297. yield ['string[]', 'array'];
  298. yield ['array[]', 'array'];
  299. yield ['bool[][]', 'array'];
  300. yield ['int[][]', 'array'];
  301. yield ['float[][]', 'array'];
  302. yield ['string[][]', 'array'];
  303. yield ['array[][]', 'array'];
  304. yield ['bool [ ]', 'array'];
  305. yield ['bool [ ][ ]', 'array'];
  306. yield ['array|iterable', 'iterable'];
  307. yield ['iterable|array', 'iterable'];
  308. yield ['array|Traversable', 'iterable'];
  309. yield ['array|\Traversable', 'iterable'];
  310. yield ['array|Traversable', 'iterable', $globalNamespace];
  311. yield ['iterable|Traversable', 'iterable'];
  312. yield ['array<string>', 'array'];
  313. yield ['array<int, string>', 'array'];
  314. yield ['array < string >', 'array'];
  315. yield ['list<int>', 'array'];
  316. yield ['iterable<string>', 'iterable'];
  317. yield ['iterable<int, string>', 'iterable'];
  318. yield ['\Traversable<string>', '\Traversable'];
  319. yield ['Traversable<int, string>', 'Traversable'];
  320. yield ['Collection<string>', 'Collection'];
  321. yield ['Collection<int, string>', 'Collection'];
  322. yield ['array{string}', 'array'];
  323. yield ['array { 1: string, \Closure(): void }', 'array'];
  324. yield ['Closure(): void', 'Closure'];
  325. yield ['array<int, string>|iterable<int, string>', 'iterable'];
  326. yield ['int[]|string[]', 'array'];
  327. yield ['int|null', 'int'];
  328. yield ['null|int', 'int'];
  329. yield ['?int', 'int'];
  330. yield ['?array<Foo>', 'array'];
  331. yield ['?list<Foo>', 'array'];
  332. yield ['void', 'void'];
  333. yield ['never', 'never'];
  334. yield ['array|Traversable', 'iterable', null, [$useTraversable]];
  335. yield ['array|Traversable', 'iterable', $globalNamespace, [$useTraversable]];
  336. yield ['array|Traversable', 'iterable', $appNamespace, [$useTraversable]];
  337. yield ['self|static', 'self'];
  338. yield ['array|Traversable', null, null, [$useObjectAsTraversable]];
  339. yield ['array|Traversable', null, $globalNamespace, [$useObjectAsTraversable]];
  340. yield ['array|Traversable', null, $appNamespace, [$useObjectAsTraversable]];
  341. yield ['bool|int', null];
  342. yield ['string|bool', null];
  343. yield ['array<int, string>|Collection<int, string>', null];
  344. }
  345. /**
  346. * @dataProvider provideAllowsNullCases
  347. */
  348. public function testAllowsNull(string $typesExpression, bool $expectNullAllowed): void
  349. {
  350. $expression = new TypeExpression($typesExpression, null, []);
  351. self::assertSame($expectNullAllowed, $expression->allowsNull());
  352. }
  353. public static function provideAllowsNullCases(): iterable
  354. {
  355. yield ['null', true];
  356. yield ['mixed', true];
  357. yield ['null|mixed', true];
  358. yield ['int|bool|null', true];
  359. yield ['int|bool|mixed', true];
  360. yield ['int', false];
  361. yield ['bool', false];
  362. yield ['string', false];
  363. yield ['?int', true];
  364. yield ['?\Closure(): void', true];
  365. }
  366. /**
  367. * @dataProvider provideSortTypesCases
  368. */
  369. public function testSortTypes(string $typesExpression, string $expectResult): void
  370. {
  371. $sortCaseFx = static fn (TypeExpression $a, TypeExpression $b): int => strcasecmp($a->toString(), $b->toString());
  372. $sortCrc32Fx = static fn (TypeExpression $a, TypeExpression $b): int => crc32($a->toString()) <=> crc32($b->toString());
  373. $expression = $this->parseTypeExpression($typesExpression, null, []);
  374. $expression->sortTypes($sortCaseFx);
  375. self::assertSame($expectResult, $expression->toString());
  376. $expression->sortTypes($sortCrc32Fx);
  377. $expression->sortTypes($sortCaseFx);
  378. self::assertSame($expectResult, $expression->toString());
  379. }
  380. public static function provideSortTypesCases(): iterable
  381. {
  382. yield 'not a union type' => [
  383. 'int',
  384. 'int',
  385. ];
  386. yield 'simple' => [
  387. 'int|bool',
  388. 'bool|int',
  389. ];
  390. yield 'multiple union' => [
  391. 'C___|D____|B__|A',
  392. 'A|B__|C___|D____',
  393. ];
  394. yield 'multiple intersect' => [
  395. 'C___&D____&B__&A',
  396. 'A&B__&C___&D____',
  397. ];
  398. yield 'simple in generic' => [
  399. 'array<int|bool>',
  400. 'array<bool|int>',
  401. ];
  402. yield 'generic with multiple types' => [
  403. 'array<int|bool, string|float>',
  404. 'array<bool|int, float|string>',
  405. ];
  406. yield 'generic with trailing comma' => [
  407. 'array<int|bool,>',
  408. 'array<bool|int,>',
  409. ];
  410. yield 'simple in array shape with int key' => [
  411. 'array{0: int|bool}',
  412. 'array{0: bool|int}',
  413. ];
  414. yield 'simple in array shape with string key' => [
  415. 'array{"foo": int|bool}',
  416. 'array{"foo": bool|int}',
  417. ];
  418. yield 'simple in array shape with multiple keys' => [
  419. 'array{0: int|bool, "foo": int|bool}',
  420. 'array{0: bool|int, "foo": bool|int}',
  421. ];
  422. yield 'simple in array shape with implicit key' => [
  423. 'array{int|bool}',
  424. 'array{bool|int}',
  425. ];
  426. yield 'simple in array shape with trailing comma' => [
  427. 'array{int|bool,}',
  428. 'array{bool|int,}',
  429. ];
  430. yield 'simple in array shape with multiple types with trailing comma' => [
  431. 'array{int|bool, Foo|Bar, }',
  432. 'array{bool|int, Bar|Foo, }',
  433. ];
  434. yield 'simple in array shape' => [
  435. 'list{int, Foo|Bar}',
  436. 'list{int, Bar|Foo}',
  437. ];
  438. yield 'array shape with multiple colons - array shape' => [
  439. 'array{array{x:int|bool}, a:array{x:int|bool}}',
  440. 'array{array{x:bool|int}, a:array{x:bool|int}}',
  441. ];
  442. yield 'array shape with multiple colons - callable' => [
  443. 'array{array{x:int|bool}, int|bool, callable(): void}',
  444. 'array{array{x:bool|int}, bool|int, callable(): void}',
  445. ];
  446. yield 'simple in callable argument' => [
  447. 'callable(int|bool)',
  448. 'callable(bool|int)',
  449. ];
  450. yield 'callable with multiple arguments' => [
  451. 'callable(int|bool, null|array)',
  452. 'callable(bool|int, array|null)',
  453. ];
  454. yield 'simple in callable return type' => [
  455. 'callable(): (string|float)',
  456. 'callable(): (float|string)',
  457. ];
  458. yield 'callable with union return type and within union itself' => [
  459. 'callable(): (string|float)|bool',
  460. 'bool|callable(): (float|string)',
  461. ];
  462. yield 'callable with multiple named arguments' => [
  463. 'callable(int|bool $b, null|array $a)',
  464. 'callable(bool|int $b, array|null $a)',
  465. ];
  466. yield 'callable with complex arguments' => [
  467. 'callable(B|A&, D|Closure(): void..., array{}$foo=, $this $foo=): array{}',
  468. 'callable(A|B&, Closure(): void|D..., array{}$foo=, $this $foo=): array{}',
  469. ];
  470. yield 'callable with trailing comma' => [
  471. 'Closure( Y|X , ): B|A',
  472. 'A|Closure( X|Y , ): B',
  473. ];
  474. yield 'simple in Closure argument' => [
  475. 'Closure(int|bool)',
  476. 'Closure(bool|int)',
  477. ];
  478. yield 'Closure with multiple arguments' => [
  479. 'Closure(int|bool, null|array)',
  480. 'Closure(bool|int, array|null)',
  481. ];
  482. yield 'simple in Closure argument with trailing comma' => [
  483. 'Closure(int|bool,)',
  484. 'Closure(bool|int,)',
  485. ];
  486. yield 'simple in Closure argument multiple arguments with trailing comma' => [
  487. 'Closure(int|bool, null|array,)',
  488. 'Closure(bool|int, array|null,)',
  489. ];
  490. yield 'simple in Closure return type' => [
  491. 'Closure(): (string|float)',
  492. 'Closure(): (float|string)',
  493. ];
  494. yield 'Closure with union return type and within union itself' => [
  495. 'Closure(): (string|float)|bool',
  496. 'bool|Closure(): (float|string)',
  497. ];
  498. yield 'with multiple nesting levels' => [
  499. 'array{0: Foo<int|bool>|Bar<callable(string|float|array<int|bool>): (Foo|Bar)>}',
  500. 'array{0: Bar<callable(array<bool|int>|float|string): (Bar|Foo)>|Foo<bool|int>}',
  501. ];
  502. yield 'with multiple nesting levels and callable within union' => [
  503. 'array{0: Foo<int|bool>|Bar<callable(string|float|array<int|bool>): (Foo|Bar)|Baz>}',
  504. 'array{0: Bar<Baz|callable(array<bool|int>|float|string): (Bar|Foo)>|Foo<bool|int>}',
  505. ];
  506. yield 'complex type with Closure with $this' => [
  507. 'array<string, string|array{ string|\Closure(mixed, string, $this): (int|float) }>|false',
  508. 'array<string, array{ \Closure(mixed, string, $this): (float|int)|string }|string>|false',
  509. ];
  510. yield 'nullable generic' => [
  511. '?array<Foo|Bar>',
  512. '?array<Bar|Foo>',
  513. ];
  514. yield 'nullable callable' => [
  515. '?callable(Foo|Bar): (Foo|Bar)',
  516. '?callable(Bar|Foo): (Bar|Foo)',
  517. ];
  518. // This union type makes no sense in general (it should be `Bar|callable|null`)
  519. // but let's ensure nullable types are also sorted.
  520. yield 'nullable callable with union return type and within union itself' => [
  521. '?callable(Foo|Bar): (Foo|Bar)|?Bar',
  522. '?Bar|?callable(Bar|Foo): (Bar|Foo)',
  523. ];
  524. yield 'nullable array shape' => [
  525. '?array{0: Foo|Bar}',
  526. '?array{0: Bar|Foo}',
  527. ];
  528. yield 'simple types alternation' => [
  529. 'array<Foo&Bar>',
  530. 'array<Bar&Foo>',
  531. ];
  532. yield 'nesty stuff' => [
  533. 'array<Level11&array<Level2|array<Level31&Level32>>>',
  534. 'array<array<array<Level31&Level32>|Level2>&Level11>',
  535. ];
  536. yield 'parenthesized' => [
  537. '(Foo|Bar)',
  538. '(Bar|Foo)',
  539. ];
  540. yield 'parenthesized intersect' => [
  541. '(Foo&Bar)',
  542. '(Bar&Foo)',
  543. ];
  544. yield 'parenthesized in closure return type' => [
  545. 'Closure(Y|X): (string|float)',
  546. 'Closure(X|Y): (float|string)',
  547. ];
  548. yield 'conditional with variable' => [
  549. '($x is (CFoo|(CBaz&CBar)) ? (TFoo|(TBaz&TBar)) : (FFoo|(FBaz&FBar)))',
  550. '($x is ((CBar&CBaz)|CFoo) ? ((TBar&TBaz)|TFoo) : ((FBar&FBaz)|FFoo))',
  551. ];
  552. yield 'conditional with type' => [
  553. '((Foo|Bar) is x ? y : z)',
  554. '((Bar|Foo) is x ? y : z)',
  555. ];
  556. yield 'conditional in conditional' => [
  557. '((Foo|Bar) is x ? ($x is (CFoo|CBar) ? (TFoo|TBar) : (FFoo|FBar)) : z)',
  558. '((Bar|Foo) is x ? ($x is (CBar|CFoo) ? (TBar|TFoo) : (FBar|FFoo)) : z)',
  559. ];
  560. yield 'large numbers' => [
  561. '18_446_744_073_709_551_616|-8.2023437675747321e-18_446_744_073_709_551_616',
  562. '-8.2023437675747321e-18_446_744_073_709_551_616|18_446_744_073_709_551_616',
  563. ];
  564. }
  565. /**
  566. * @return list<array{int, string}|list>
  567. */
  568. private function checkInnerTypeExpressionsStartIndex(TypeExpression $typeExpression): array
  569. {
  570. $innerTypeExpressions = \Closure::bind(static fn () => $typeExpression->innerTypeExpressions, null, TypeExpression::class)();
  571. $res = [];
  572. foreach ($innerTypeExpressions as ['start_index' => $innerStartIndex, 'expression' => $innerExpression]) {
  573. $innerExpressionStr = $innerExpression->toString();
  574. self::assertSame(
  575. $innerExpressionStr,
  576. substr($typeExpression->toString(), $innerStartIndex, \strlen($innerExpressionStr))
  577. );
  578. $res[] = [$innerStartIndex, $innerExpressionStr];
  579. $res[] = $this->checkInnerTypeExpressionsStartIndex($innerExpression);
  580. }
  581. return $res;
  582. }
  583. /**
  584. * Should be removed once https://github.com/php/php-src/pull/11396 is merged.
  585. */
  586. private function clearPcreRegexCache(): void
  587. {
  588. // there is no explicit php function to clear PCRE regex cache, but based
  589. // on https://www.php.net/manual/en/intro.pcre.php there are 4096 cache slots
  590. // pruned in FIFO fashion, so to clear the cache, replace all existing
  591. // cache slots with dummy regexes
  592. for ($i = 0; $i < 4096; ++$i) {
  593. preg_match('/^'.$i.'/', '');
  594. }
  595. }
  596. /**
  597. * Parse type expression with and without PCRE JIT.
  598. *
  599. * @param NamespaceUseAnalysis[] $namespaceUses
  600. */
  601. private function parseTypeExpression(string $value, ?NamespaceAnalysis $namespace, array $namespaceUses): TypeExpression
  602. {
  603. $pcreJitBackup = \ini_get('pcre.jit');
  604. $expression = null;
  605. $innerExpressionsDataWithoutJit = null;
  606. try {
  607. foreach ([false, true] as $pcreJit) {
  608. ini_set('pcre.jit', $pcreJit ? '1' : '0');
  609. $this->clearPcreRegexCache();
  610. $expression = new TypeExpression($value, null, []);
  611. $innerExpressionsData = $this->checkInnerTypeExpressionsStartIndex($expression);
  612. if (false === $pcreJit) {
  613. $innerExpressionsDataWithoutJit = $innerExpressionsData;
  614. } else {
  615. self::assertSame($innerExpressionsDataWithoutJit, $innerExpressionsData);
  616. }
  617. }
  618. } finally {
  619. ini_set('pcre.jit', $pcreJitBackup);
  620. $this->clearPcreRegexCache();
  621. }
  622. return $expression;
  623. }
  624. }