TypeExpressionTest.php 28 KB

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