12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136 |
- <?php
- declare(strict_types=1);
- /*
- * This file is part of PHP CS Fixer.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- * Dariusz Rumiński <dariusz.ruminski@gmail.com>
- *
- * This source file is subject to the MIT license that is bundled
- * with this source code in the file LICENSE.
- */
- namespace PhpCsFixer\Tests\DocBlock;
- use PhpCsFixer\DocBlock\TypeExpression;
- use PhpCsFixer\Tests\TestCase;
- use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
- use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
- /**
- * @covers \PhpCsFixer\DocBlock\TypeExpression
- *
- * @internal
- */
- final class TypeExpressionTest extends TestCase
- {
- /**
- * @param null|list<string> $expectedTypes
- *
- * @dataProvider provideGetConstTypesCases
- * @dataProvider provideGetTypesCases
- */
- public function testGetTypes(string $typesExpression, ?array $expectedTypes = null): void
- {
- if (null === $expectedTypes) {
- $expectedTypes = [$typesExpression];
- }
- $expression = $this->parseTypeExpression($typesExpression, null, []);
- self::assertSame($expectedTypes, $expression->getTypes());
- $unionTestNs = '__UnionTest__';
- $unionExpression = $this->parseTypeExpression(
- $unionTestNs.'\A|'.$typesExpression.'|'.$unionTestNs.'\Z',
- null,
- []
- );
- if (!$expression->isUnionType() || '|' === $expression->getTypesGlue()) {
- self::assertSame(
- [$unionTestNs.'\A', ...$expectedTypes, $unionTestNs.'\Z'],
- [...$unionExpression->getTypes()]
- );
- }
- }
- public static function provideGetTypesCases(): iterable
- {
- yield ['int'];
- yield ['Foo5'];
- yield ['🚀_kůň'];
- yield ['positive-int'];
- yield ['?int'];
- yield ['? int'];
- yield ['int[]'];
- yield ['Foo[][]'];
- yield ['Foo [ ] []'];
- yield ['int[]|null', ['int[]', 'null']];
- yield ['int[]|null|?int|array', ['int[]', 'null', '?int', 'array']];
- yield ['null|Foo\Bar|\Baz\Bax|int[]', ['null', 'Foo\Bar', '\Baz\Bax', 'int[]']];
- yield ['gen<int>'];
- yield ['int|gen<int>', ['int', 'gen<int>']];
- yield ['\int|\gen<\int, \bool>', ['\int', '\gen<\int, \bool>']];
- yield ['gen<int, int>'];
- yield ['gen<int, bool|string>'];
- yield ['gen<int, string[]>'];
- yield ['gen<int, gener<string, bool>>'];
- yield ['gen<int, gener<string, null|bool>>'];
- yield ['gen<int>[][]'];
- yield ['non-empty-array<int>'];
- yield ['null|gen<int, gener<string, bool>>|int|string[]', ['null', 'gen<int, gener<string, bool>>', 'int', 'string[]']];
- yield ['null|gen<int, gener<string, bool>>|int|array<int, string>|string[]', ['null', 'gen<int, gener<string, bool>>', 'int', 'array<int, string>', 'string[]']];
- yield ['this'];
- yield ['@this'];
- yield ['$SELF|int', ['$SELF', 'int']];
- yield ['array<string|int, string>'];
- yield ['Collection<Foo<Bar>, Foo<Baz>>'];
- yield ['int | string', ['int', 'string']];
- yield ['Foo::*'];
- yield ['Foo::A'];
- yield ['Foo::A|Foo::B', ['Foo::A', 'Foo::B']];
- yield ['Foo::A*'];
- yield ['Foo::*0*_Bar'];
- yield ['?Foo::*[]'];
- yield ['array<Foo::A*>|null', ['array<Foo::A*>', 'null']];
- 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"']];
- yield ['int | "a" | A<B<C, D>, E<F::*|G[]>>', ['int', '"a"', 'A<B<C, D>, E<F::*|G[]>>']];
- yield ['class-string<Foo>'];
- yield ['A&B', ['A', 'B']];
- yield ['A & B', ['A', 'B']];
- yield ['array{}'];
- yield ['object{ }'];
- yield ['array{1: bool, 2: bool}'];
- yield ['array{a: int|string, b?: bool}'];
- yield ['array{\'a\': "a", "b"?: \'b\'}'];
- yield ['array { a : int | string , b ? : A<B, C> }'];
- yield ['array{bool, int}'];
- yield ['array{bool,}'];
- yield ['list{int, bool}'];
- yield ['object{ bool, foo2: int }'];
- yield ['ArRAY{ 1 }'];
- yield ['lIst{ 1 }'];
- yield ['OBJECT { x: 1 }'];
- yield ['array{a: int, b: int, with-dash: int}'];
- yield ['callable'];
- yield ['callable(string)'];
- yield ['? callable(string): bool'];
- yield ['CAllable(string): bool'];
- yield ['callable(string,): bool'];
- yield ['callable(array<int, string>, array<int, Foo>): bool'];
- yield ['array<int, callable(string): bool>'];
- yield ['callable(string): callable(int)'];
- yield ['callable(string) : callable(int) : bool'];
- yield ['TheCollection<callable(Foo, Bar,Baz): Foo[]>|string[]|null', ['TheCollection<callable(Foo, Bar,Baz): Foo[]>', 'string[]', 'null']];
- yield ['Closure()'];
- yield ['Closure(string)'];
- yield ['\closure(string): void'];
- yield [\Closure::class];
- yield ['\Closure()'];
- yield ['\Closure(string)'];
- yield ['\Closure(string, bool)'];
- yield ['\Closure(string|int, bool)'];
- yield ['\Closure(string):bool'];
- yield ['\Closure(string): bool'];
- yield ['\Closure(string|int, bool): bool'];
- yield ['\Closure(float|int): (bool|int)'];
- yield ['Closure<T>(): T'];
- yield ['Closure<Tx, Ty>(): array{x: Tx, y: Ty}'];
- yield ['array < int , callable ( string ) : bool >'];
- yield ['Closure<T of Foo>(T): T'];
- yield ['Closure< T1 of Foo, T2 AS Foo >(T1): T2'];
- yield ['Closure<T = Foo>(T): T'];
- yield ['Closure<T1=int, T2 of Foo = Foo2>(T1): T2'];
- yield ['Closure<T of string = \'\'>(T): T'];
- yield ['Closure<Closure_can_be_regular_class>'];
- yield ['Closure(int $a)'];
- yield ['Closure(int $a): bool'];
- yield ['Closure(int $a, array<Closure(int ...$args): Item<X>>): bool'];
- yield ['Closure_can_be_aliased()'];
- yield ['Closure_can_be_aliased(): (u|v)'];
- yield ['(int)'];
- yield ['(int|\Exception)'];
- yield ['($foo is int ? false : true)'];
- yield ['($foo🚀3 is int ? false : true)'];
- yield ['\'a\\\'s"\\\\\n\r\t\'|"b\"s\'\\\\\n\r\t"', ['\'a\\\'s"\\\\\n\r\t\'', '"b\"s\'\\\\\n\r\t"']];
- yield ['string'.str_repeat('[]', 128)];
- yield [str_repeat('array<', 128).'string'.str_repeat('>', 128)];
- yield [self::makeLongArrayShapeType()];
- }
- /**
- * @return iterable<array{string}>
- */
- public static function provideGetConstTypesCases(): iterable
- {
- foreach ([
- 'null',
- 'true',
- 'FALSE',
- '123',
- '+123',
- '-123',
- '0b0110101',
- '0o777',
- '0x7Fb4',
- '-0O777',
- '-0X7Fb4',
- '123_456',
- '0b01_01_01',
- '-0X7_Fb_4',
- '18_446_744_073_709_551_616', // 64-bit unsigned long + 1, larger than PHP_INT_MAX
- '123.4',
- '.123',
- '123.',
- '123e4',
- '123E4',
- '12.3e4',
- '+123.5',
- '-123.',
- '-123.4',
- '-.123',
- '-123e-4',
- '-12.3e-4',
- '-1_2.3_4e5_6',
- '123E+80',
- '8.2023437675747321', // greater precision than 64-bit double
- '-0.0',
- '\'\'',
- '\'foo\'',
- '\'\\\\\'',
- '\'\\\'\'',
- ] as $type) {
- yield [$type];
- }
- }
- /**
- * @dataProvider provideParseInvalidExceptionCases
- */
- public function testParseInvalidException(string $value): void
- {
- $this->expectException(\Exception::class);
- $this->expectExceptionMessage('Unable to parse phpdoc type');
- new TypeExpression($value, null, []);
- }
- /**
- * @return iterable<int|string, array{string}>
- */
- public static function provideParseInvalidExceptionCases(): iterable
- {
- yield [''];
- yield ['0_class_cannot_start_with_number'];
- yield ['$0_variable_cannot_start_with_number'];
- yield ['class cannot contain space'];
- yield ['\\\class_with_double_backslash'];
- yield ['class\\\with_double_backslash'];
- yield ['class_with_end_backslash\\'];
- yield ['class/with_slash'];
- yield ['class--with_double_dash'];
- yield ['class.with_dot'];
- yield ['class,with_comma'];
- yield ['class@with_at_sign'];
- yield ['class:with_colon'];
- yield ['class#with_hash'];
- yield ['class//with_double_slash'];
- yield ['class$with_dollar'];
- yield ['class;with_semicolon'];
- yield ['class=with_equal_sign'];
- yield ['class+with_plus'];
- yield ['class?with_question_mark'];
- yield ['class*with_star'];
- yield ['class%with_percent'];
- yield ['(unclosed_parenthesis'];
- yield [')unclosed_parenthesis'];
- yield ['unclosed_parenthesis('];
- yield ['((unclosed_parenthesis)'];
- yield ['|vertical_bar_start'];
- yield ['&ersand_start'];
- yield ['~tilde_start'];
- yield ['vertical_bar_end|'];
- yield ['ampersand_end&'];
- yield ['tilde_end~'];
- yield ['class||double_vertical_bar'];
- yield ['class&&double_ampersand'];
- yield ['class~~double_tilde'];
- yield ['array<'];
- yield ['array<<'];
- yield ['array>'];
- yield ['array<<>'];
- yield ['array<>>'];
- yield ['array{'];
- yield ['array{ $this: 5 }'];
- yield ['g<,>'];
- yield ['g<,no_leading_comma>'];
- yield ['10__000'];
- yield ['[ array_syntax_is_invalid ]'];
- yield ['\' unclosed string'];
- yield ['\' unclosed string \\\''];
- yield 'generic with no arguments' => ['f<>'];
- yield 'generic Closure with no arguments' => ['Closure<>(): void'];
- yield 'generic Closure with non-identifier template argument' => ['Closure<A|B>(): void'];
- yield [substr(self::makeLongArrayShapeType(), 0, -1)];
- }
- public function testHugeType(): void
- {
- $nFlat = 2_000;
- $types = [];
- for ($i = 0; $i < $nFlat; ++$i) {
- $types[] = '\X\Foo'.$i;
- }
- $str = implode('|', $types);
- $expression = new TypeExpression($str, null, []);
- self::assertSame($types, $expression->getTypes());
- for ($i = 0; $i < 100; ++$i) {
- $str = 'array'.(1 === $i % 2 ? '{' : '<').$str.(1 === $i % 2 ? '}' : '>');
- }
- $typeLeft = '\Closure(A|B): void';
- $typeRight = '\Closure('.$typeLeft.'): void';
- $expression = new TypeExpression($typeLeft.'|('.$str.')|'.$typeRight, null, []);
- self::assertSame([$typeLeft, '('.$str.')', $typeRight], $expression->getTypes());
- }
- /**
- * @dataProvider provideGetTypesGlueCases
- */
- public function testGetTypesGlue(string $expectedTypesGlue, string $typesExpression): void
- {
- $expression = new TypeExpression($typesExpression, null, []);
- self::assertSame($expectedTypesGlue, $expression->getTypesGlue());
- }
- /**
- * @return iterable<array{0: '&'|'|', 1: string}>
- */
- public static function provideGetTypesGlueCases(): iterable
- {
- yield ['|', 'string']; // for backward behaviour
- yield ['|', 'bool|string'];
- yield ['&', 'Foo&Bar'];
- }
- /**
- * @dataProvider provideIsUnionTypeCases
- */
- public function testIsUnionType(bool $expectedIsUnionType, string $typesExpression): void
- {
- $expression = new TypeExpression($typesExpression, null, []);
- self::assertSame($expectedIsUnionType, $expression->isUnionType());
- }
- /**
- * @return iterable<array{0: bool, 1: string}>
- */
- public static function provideIsUnionTypeCases(): iterable
- {
- yield [false, 'string'];
- yield [true, 'bool|string'];
- yield [true, 'int|string|null'];
- yield [true, 'int|?string'];
- yield [true, 'int|null'];
- yield [false, '?int'];
- yield [true, 'Foo|Bar'];
- yield [true, 'Foo&Bar'];
- yield [true, 'Foo&Bar&?Baz'];
- }
- /**
- * @param list<NamespaceUseAnalysis> $namespaceUses
- *
- * @dataProvider provideGetCommonTypeCases
- */
- public function testGetCommonType(string $typesExpression, ?string $expectedCommonType, ?NamespaceAnalysis $namespace = null, array $namespaceUses = []): void
- {
- $expression = new TypeExpression($typesExpression, $namespace, $namespaceUses);
- self::assertSame($expectedCommonType, $expression->getCommonType());
- }
- public static function provideGetCommonTypeCases(): iterable
- {
- $globalNamespace = new NamespaceAnalysis('', '', 0, 999, 0, 999);
- $appNamespace = new NamespaceAnalysis('App', 'App', 0, 999, 0, 999);
- $useTraversable = new NamespaceUseAnalysis(NamespaceUseAnalysis::TYPE_CLASS, \Traversable::class, \Traversable::class, false, false, 0, 0);
- $useObjectAsTraversable = new NamespaceUseAnalysis(NamespaceUseAnalysis::TYPE_CLASS, 'Foo', \Traversable::class, false, false, 0, 0);
- yield ['true', 'bool'];
- yield ['false', 'bool'];
- yield ['bool', 'bool'];
- yield ['int', 'int'];
- yield ['float', 'float'];
- yield ['string', 'string'];
- yield ['array', 'array'];
- yield ['object', 'object'];
- yield ['self', 'self'];
- yield ['static', 'static'];
- yield ['bool[]', 'array'];
- yield ['int[]', 'array'];
- yield ['float[]', 'array'];
- yield ['string[]', 'array'];
- yield ['array[]', 'array'];
- yield ['bool[][]', 'array'];
- yield ['int[][]', 'array'];
- yield ['float[][]', 'array'];
- yield ['string[][]', 'array'];
- yield ['array[][]', 'array'];
- yield ['bool [ ]', 'array'];
- yield ['bool [ ][ ]', 'array'];
- yield ['array|iterable', 'iterable'];
- yield ['iterable|array', 'iterable'];
- yield ['array|Traversable', 'iterable'];
- yield ['array|\Traversable', 'iterable'];
- yield ['array|Traversable', 'iterable', $globalNamespace];
- yield ['iterable|Traversable', 'iterable'];
- yield ['array<string>', 'array'];
- yield ['array<int, string>', 'array'];
- yield ['array < string >', 'array'];
- yield ['list<int>', 'array'];
- yield ['iterable<string>', 'iterable'];
- yield ['iterable<int, string>', 'iterable'];
- yield ['\Traversable<string>', '\Traversable'];
- yield ['Traversable<int, string>', 'Traversable'];
- yield ['Collection<string>', 'Collection'];
- yield ['Collection<int, string>', 'Collection'];
- yield ['array{string}', 'array'];
- yield ['array { 1: string, \Closure(): void }', 'array'];
- yield ['Closure(): void', \Closure::class];
- yield ['array<int, string>|iterable<int, string>', 'iterable'];
- yield ['int[]|string[]', 'array'];
- yield ['int|null', 'int'];
- yield ['null|int', 'int'];
- yield ['?int', 'int'];
- yield ['?array<Foo>', 'array'];
- yield ['?list<Foo>', 'array'];
- yield ['void', 'void'];
- yield ['never', 'never'];
- yield ['array|Traversable', 'iterable', null, [$useTraversable]];
- yield ['array|Traversable', 'iterable', $globalNamespace, [$useTraversable]];
- yield ['array|Traversable', 'iterable', $appNamespace, [$useTraversable]];
- yield ['self|static', 'self'];
- yield ['array|Traversable', null, null, [$useObjectAsTraversable]];
- yield ['array|Traversable', null, $globalNamespace, [$useObjectAsTraversable]];
- yield ['array|Traversable', null, $appNamespace, [$useObjectAsTraversable]];
- yield ['bool|int', null];
- yield ['string|bool', null];
- yield ['array<int, string>|Collection<int, string>', null];
- }
- /**
- * @dataProvider provideAllowsNullCases
- */
- public function testAllowsNull(string $typesExpression, bool $expectNullAllowed): void
- {
- $expression = new TypeExpression($typesExpression, null, []);
- self::assertSame($expectNullAllowed, $expression->allowsNull());
- }
- /**
- * @return iterable<array{string, bool}>
- */
- public static function provideAllowsNullCases(): iterable
- {
- yield ['null', true];
- yield ['mixed', true];
- yield ['null|mixed', true];
- yield ['int|bool|null', true];
- yield ['int|bool|mixed', true];
- yield ['int', false];
- yield ['bool', false];
- yield ['string', false];
- yield ['?int', true];
- yield ['?\Closure(): void', true];
- }
- public function testMapTypes(): void
- {
- $typeExpression = new TypeExpression('Foo|Bar|($v is \Closure(X, Y): Z ? U : (V&W))', null, []);
- $addLeadingSlash = static function (TypeExpression $type) {
- $value = $type->toString();
- if (!str_starts_with($value, '\\') && !str_starts_with($value, '(')) {
- return new TypeExpression('\\'.$value, null, []);
- }
- return $type;
- };
- $removeLeadingSlash = static function (TypeExpression $type) {
- $value = $type->toString();
- if (str_starts_with($value, '\\')) {
- return new TypeExpression(substr($value, 1), null, []);
- }
- return $type;
- };
- $callLog = [];
- $typeExpression->mapTypes(static function (TypeExpression $type) use (&$callLog) {
- $callLog[] = $type->toString();
- if ('Y' === $type->toString()) {
- return new TypeExpression('_y_', null, []);
- }
- return $type;
- });
- self::assertSame([
- 'Foo',
- 'Bar',
- 'X',
- 'Y',
- 'Z',
- '\Closure(X, _y_): Z',
- 'U',
- 'V',
- 'W',
- 'V&W',
- '(V&W)',
- '($v is \Closure(X, _y_): Z ? U : (V&W))',
- 'Foo|Bar|($v is \Closure(X, _y_): Z ? U : (V&W))',
- ], $callLog);
- $typeExpression = $typeExpression->mapTypes($addLeadingSlash);
- $this->checkInnerTypeExpressionsStartIndex($typeExpression);
- self::assertSame('\Foo|\Bar|($v is \Closure(\X, \Y): \Z ? \U : (\V&\W))', $typeExpression->toString());
- $typeExpression = $typeExpression->mapTypes($addLeadingSlash);
- $this->checkInnerTypeExpressionsStartIndex($typeExpression);
- self::assertSame('\Foo|\Bar|($v is \Closure(\X, \Y): \Z ? \U : (\V&\W))', $typeExpression->toString());
- $typeExpression = $typeExpression->mapTypes($removeLeadingSlash);
- $this->checkInnerTypeExpressionsStartIndex($typeExpression);
- self::assertSame('Foo|Bar|($v is Closure(X, Y): Z ? U : (V&W))', $typeExpression->toString());
- $typeExpression = $typeExpression->mapTypes($removeLeadingSlash);
- $this->checkInnerTypeExpressionsStartIndex($typeExpression);
- self::assertSame('Foo|Bar|($v is Closure(X, Y): Z ? U : (V&W))', $typeExpression->toString());
- $typeExpression = $typeExpression->mapTypes($addLeadingSlash);
- $this->checkInnerTypeExpressionsStartIndex($typeExpression);
- self::assertSame('\Foo|\Bar|($v is \Closure(\X, \Y): \Z ? \U : (\V&\W))', $typeExpression->toString());
- }
- public function testWalkTypes(): void
- {
- $typeExpression = new TypeExpression('Foo|Bar|($v is \Closure(X, Y): Z ? U : (V&W))', null, []);
- $callLog = [];
- $typeExpression->walkTypes(static function (TypeExpression $type) use (&$callLog): void {
- $callLog[] = $type->toString();
- });
- self::assertSame([
- 'Foo',
- 'Bar',
- 'X',
- 'Y',
- 'Z',
- '\Closure(X, Y): Z',
- 'U',
- 'V',
- 'W',
- 'V&W',
- '(V&W)',
- '($v is \Closure(X, Y): Z ? U : (V&W))',
- 'Foo|Bar|($v is \Closure(X, Y): Z ? U : (V&W))',
- ], $callLog);
- }
- /**
- * @dataProvider provideSortTypesCases
- */
- public function testSortTypes(string $typesExpression, string $expectResult): void
- {
- $sortCaseFx = static fn (TypeExpression $a, TypeExpression $b): int => strcasecmp($a->toString(), $b->toString());
- $sortCrc32Fx = static fn (TypeExpression $a, TypeExpression $b): int => crc32($a->toString()) <=> crc32($b->toString());
- $expression = $this->parseTypeExpression($typesExpression, null, []);
- $expression = $expression->sortTypes($sortCaseFx);
- $this->checkInnerTypeExpressionsStartIndex($expression);
- self::assertSame($expectResult, $expression->toString());
- $expression = $expression->sortTypes($sortCrc32Fx);
- $this->checkInnerTypeExpressionsStartIndex($expression);
- $expression = $expression->sortTypes($sortCaseFx);
- $this->checkInnerTypeExpressionsStartIndex($expression);
- self::assertSame($expectResult, $expression->toString());
- }
- /**
- * @return iterable<string, array{string, string}>
- */
- public static function provideSortTypesCases(): iterable
- {
- yield 'not a union type' => [
- 'int',
- 'int',
- ];
- yield 'simple' => [
- 'int|bool',
- 'bool|int',
- ];
- yield 'multiple union' => [
- 'C___|D____|B__|A',
- 'A|B__|C___|D____',
- ];
- yield 'multiple intersect' => [
- 'C___&D____&B__&A',
- 'A&B__&C___&D____',
- ];
- yield 'simple in generic' => [
- 'array<int|bool>',
- 'array<bool|int>',
- ];
- yield 'generic with multiple types' => [
- 'array<int|bool, string|float>',
- 'array<bool|int, float|string>',
- ];
- yield 'generic with trailing comma' => [
- 'array<int|bool,>',
- 'array<bool|int,>',
- ];
- yield 'simple in array shape with int key' => [
- 'array{0: int|bool}',
- 'array{0: bool|int}',
- ];
- yield 'simple in array shape with string key' => [
- 'array{"foo": int|bool}',
- 'array{"foo": bool|int}',
- ];
- yield 'simple in array shape with multiple keys' => [
- 'array{0: int|bool, "foo": int|bool}',
- 'array{0: bool|int, "foo": bool|int}',
- ];
- yield 'simple in array shape with implicit key' => [
- 'array{int|bool}',
- 'array{bool|int}',
- ];
- yield 'simple in array shape with trailing comma' => [
- 'array{int|bool,}',
- 'array{bool|int,}',
- ];
- yield 'simple in array shape with multiple types with trailing comma' => [
- 'array{int|bool, Foo|Bar, }',
- 'array{bool|int, Bar|Foo, }',
- ];
- yield 'simple in array shape' => [
- 'list{int, Foo|Bar}',
- 'list{int, Bar|Foo}',
- ];
- yield 'array shape with multiple colons - array shape' => [
- 'array{array{x:int|bool}, a:array{x:int|bool}}',
- 'array{array{x:bool|int}, a:array{x:bool|int}}',
- ];
- yield 'array shape with multiple colons - callable' => [
- 'array{array{x:int|bool}, int|bool, callable(): void}',
- 'array{array{x:bool|int}, bool|int, callable(): void}',
- ];
- yield 'simple in callable argument' => [
- 'callable(int|bool)',
- 'callable(bool|int)',
- ];
- yield 'callable with multiple arguments' => [
- 'callable(int|bool, null|array)',
- 'callable(bool|int, array|null)',
- ];
- yield 'simple in callable return type' => [
- 'callable(): (string|float)',
- 'callable(): (float|string)',
- ];
- yield 'callable with union return type and within union itself' => [
- 'callable(): (string|float)|bool',
- 'bool|callable(): (float|string)',
- ];
- yield 'callable with multiple named arguments' => [
- 'callable(int|bool $b, null|array $a)',
- 'callable(bool|int $b, array|null $a)',
- ];
- yield 'callable with complex arguments' => [
- 'callable(B|A&, D|Closure(): void..., array{}$foo=, $this $foo=): array{}',
- 'callable(A|B&, Closure(): void|D..., array{}$foo=, $this $foo=): array{}',
- ];
- yield 'callable with trailing comma' => [
- 'Closure( Y|X , ): B|A',
- 'A|Closure( X|Y , ): B',
- ];
- yield 'simple in Closure argument' => [
- 'Closure(int|bool)',
- 'Closure(bool|int)',
- ];
- yield 'Closure with multiple arguments' => [
- 'Closure(int|bool, null|array)',
- 'Closure(bool|int, array|null)',
- ];
- yield 'simple in Closure argument with trailing comma' => [
- 'Closure(int|bool,)',
- 'Closure(bool|int,)',
- ];
- yield 'simple in Closure argument multiple arguments with trailing comma' => [
- 'Closure(int|bool, null|array,)',
- 'Closure(bool|int, array|null,)',
- ];
- yield 'simple in Closure return type' => [
- 'Closure(): (string|float)',
- 'Closure(): (float|string)',
- ];
- yield 'Closure with union return type and within union itself' => [
- 'Closure(): (string|float)|bool',
- 'bool|Closure(): (float|string)',
- ];
- yield 'with multiple nesting levels' => [
- 'array{0: Foo<int|bool>|Bar<callable(string|float|array<int|bool>): (Foo|Bar)>}',
- 'array{0: Bar<callable(array<bool|int>|float|string): (Bar|Foo)>|Foo<bool|int>}',
- ];
- yield 'with multiple nesting levels and callable within union' => [
- 'array{0: Foo<int|bool>|Bar<callable(string|float|array<int|bool>): (Foo|Bar)|Baz>}',
- 'array{0: Bar<Baz|callable(array<bool|int>|float|string): (Bar|Foo)>|Foo<bool|int>}',
- ];
- yield 'complex type with Closure with $this' => [
- 'array<string, string|array{ string|\Closure(mixed, string, $this): (int|float) }>|false',
- 'array<string, array{ \Closure(mixed, string, $this): (float|int)|string }|string>|false',
- ];
- yield 'generic Closure' => [
- 'Closure<B, A>(y|x, U<p|o>|B|A): (Y|B|X)',
- 'Closure<B, A>(x|y, A|B|U<o|p>): (B|X|Y)',
- ];
- yield 'generic Closure with bound template' => [
- 'Closure<B of J|I, C, A of V|U, D of object>(B|A): array{B, A, B, C, D}',
- 'Closure<B of I|J, C, A of U|V, D of object>(A|B): array{B, A, B, C, D}',
- ];
- yield 'generic Closure with template with default' => [
- 'Closure<T = B&A>(T): void',
- 'Closure<T = A&B>(T): void',
- ];
- yield 'nullable generic' => [
- '?array<Foo|Bar>',
- '?array<Bar|Foo>',
- ];
- yield 'nullable callable' => [
- '?callable(Foo|Bar): (Foo|Bar)',
- '?callable(Bar|Foo): (Bar|Foo)',
- ];
- // This union type makes no sense in general (it should be `Bar|callable|null`)
- // but let's ensure nullable types are also sorted.
- yield 'nullable callable with union return type and within union itself' => [
- '?callable(Foo|Bar): (Foo|Bar)|?Bar',
- '?Bar|?callable(Bar|Foo): (Bar|Foo)',
- ];
- yield 'nullable array shape' => [
- '?array{0: Foo|Bar}',
- '?array{0: Bar|Foo}',
- ];
- yield 'simple types alternation' => [
- 'array<Foo&Bar>',
- 'array<Bar&Foo>',
- ];
- yield 'nesty stuff' => [
- 'array<Level11&array<Level2|array<Level31&Level32>>>',
- 'array<array<array<Level31&Level32>|Level2>&Level11>',
- ];
- yield 'parenthesized' => [
- '(Foo|Bar)',
- '(Bar|Foo)',
- ];
- yield 'parenthesized intersect' => [
- '(Foo&Bar)',
- '(Bar&Foo)',
- ];
- yield 'parenthesized in closure return type' => [
- 'Closure(Y|X): (string|float)',
- 'Closure(X|Y): (float|string)',
- ];
- yield 'conditional with variable' => [
- '($x is (CFoo|(CBaz&CBar)) ? (TFoo|(TBaz&TBar)) : (FFoo|(FBaz&FBar)))',
- '($x is ((CBar&CBaz)|CFoo) ? ((TBar&TBaz)|TFoo) : ((FBar&FBaz)|FFoo))',
- ];
- yield 'conditional with type' => [
- '((Foo|Bar) is x ? y : z)',
- '((Bar|Foo) is x ? y : z)',
- ];
- yield 'conditional in conditional' => [
- '((Foo|Bar) is x ? ($x is (CFoo|CBar) ? (TFoo|TBar) : (FFoo|FBar)) : z)',
- '((Bar|Foo) is x ? ($x is (CBar|CFoo) ? (TBar|TFoo) : (FBar|FFoo)) : z)',
- ];
- yield 'large numbers' => [
- '18_446_744_073_709_551_616|-8.2023437675747321e-18_446_744_073_709_551_616',
- '-8.2023437675747321e-18_446_744_073_709_551_616|18_446_744_073_709_551_616',
- ];
- yield 'mixed 2x | and & glue' => [
- 'Foo|Foo2|Baz&Bar',
- 'Bar&Baz|Foo|Foo2',
- ];
- yield 'mixed | and 2x & glue' => [
- 'Foo|Baz&Baz2&Bar',
- 'Bar&Baz&Baz2|Foo',
- ];
- }
- private static function makeLongArrayShapeType(): string
- {
- return 'array{'.implode(
- ', ',
- array_map(
- static fn (int $k): string => \sprintf('key%sno%d: int', 0 === $k % 2 ? '-' : '_', $k),
- range(1, 1_000),
- ),
- ).'}';
- }
- /**
- * Return type is recursive.
- *
- * @return list<array{int, string}|list<mixed>>
- */
- private function checkInnerTypeExpressionsStartIndex(TypeExpression $typeExpression): array
- {
- $innerTypeExpressions = \Closure::bind(static fn () => $typeExpression->innerTypeExpressions, null, TypeExpression::class)();
- $res = [];
- foreach ($innerTypeExpressions as ['start_index' => $innerStartIndex, 'expression' => $innerExpression]) {
- $innerExpressionStr = $innerExpression->toString();
- self::assertSame(
- $innerExpressionStr,
- substr($typeExpression->toString(), $innerStartIndex, \strlen($innerExpressionStr))
- );
- $res[] = [$innerStartIndex, $innerExpressionStr];
- $res[] = $this->checkInnerTypeExpressionsStartIndex($innerExpression);
- }
- return $res;
- }
- /**
- * Should be removed once https://github.com/php/php-src/pull/11396 is merged.
- */
- private function clearPcreRegexCache(): void
- {
- // there is no explicit php function to clear PCRE regex cache, but based
- // on https://www.php.net/manual/en/intro.pcre.php there are 4096 cache slots
- // pruned in FIFO fashion, so to clear the cache, replace all existing
- // cache slots with dummy regexes
- for ($i = 0; $i < 4_096; ++$i) {
- preg_match('/^'.$i.'/', '');
- }
- }
- /**
- * Parse type expression with and without PCRE JIT.
- *
- * @param list<NamespaceUseAnalysis> $namespaceUses
- */
- private function parseTypeExpression(string $value, ?NamespaceAnalysis $namespace, array $namespaceUses): TypeExpression
- {
- $pcreJitBackup = \ini_get('pcre.jit');
- $expression = null;
- $innerExpressionsDataWithoutJit = null;
- try {
- foreach ([false, true] as $pcreJit) {
- ini_set('pcre.jit', $pcreJit ? '1' : '0');
- $this->clearPcreRegexCache();
- $expression = new TypeExpression($value, null, []);
- $innerExpressionsData = $this->checkInnerTypeExpressionsStartIndex($expression);
- if (false === $pcreJit) {
- $innerExpressionsDataWithoutJit = $innerExpressionsData;
- } else {
- self::assertSame($innerExpressionsDataWithoutJit, $innerExpressionsData);
- }
- }
- } finally {
- ini_set('pcre.jit', $pcreJitBackup);
- $this->clearPcreRegexCache();
- }
- return $expression;
- }
- }
|