TypeExpressionTest.php 34 KB

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