diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/BackedEnumTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/BackedEnumTypeTest.php index b42fd944b2c27..e63aa7cc79750 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Type/BackedEnumTypeTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/Type/BackedEnumTypeTest.php @@ -24,9 +24,9 @@ public function testToString() $this->assertSame(DummyBackedEnum::class, (string) new BackedEnumType(DummyBackedEnum::class, Type::int())); } - public function testIsNullable() + public function testGetTypeIdentifier() { - $this->assertFalse((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isNullable()); + $this->assertSame(TypeIdentifier::OBJECT, (new BackedEnumType(DummyBackedEnum::class, Type::int()))->getTypeIdentifier()); } public function testGetBaseType() @@ -34,13 +34,6 @@ public function testGetBaseType() $this->assertEquals(new BackedEnumType(DummyBackedEnum::class, Type::int()), (new BackedEnumType(DummyBackedEnum::class, Type::int()))->getBaseType()); } - public function testAsNonNullable() - { - $type = new BackedEnumType(DummyBackedEnum::class, Type::int()); - - $this->assertSame($type, $type->asNonNullable()); - } - public function testIsA() { $this->assertFalse((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isA(TypeIdentifier::ARRAY)); diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/BuiltinTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/BuiltinTypeTest.php index 0537c3566f114..3ddade3c730f4 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Type/BuiltinTypeTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/Type/BuiltinTypeTest.php @@ -29,11 +29,19 @@ public function testGetBaseType() $this->assertEquals(new BuiltinType(TypeIdentifier::INT), (new BuiltinType(TypeIdentifier::INT))->getBaseType()); } - public function testIsNullable() + /** + * @dataProvider provideNullabilityResults + */ + public function testIsNullable(TypeIdentifier $typeIdentifier, bool $isNullable): void { - $this->assertFalse((new BuiltinType(TypeIdentifier::INT))->isNullable()); - $this->assertTrue((new BuiltinType(TypeIdentifier::NULL))->isNullable()); - $this->assertTrue((new BuiltinType(TypeIdentifier::MIXED))->isNullable()); + $this->assertSame($isNullable, (new BuiltinType($typeIdentifier))->isNullable()); + } + + public static function provideNullabilityResults(): iterable + { + foreach (TypeIdentifier::cases() as $case) { + yield $case->value => [$case, TypeIdentifier::NULL === $case || TypeIdentifier::MIXED === $case]; + } } public function testAsNonNullable() diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/CollectionTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/CollectionTypeTest.php index 8104121f5e592..2a814ab892619 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Type/CollectionTypeTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/Type/CollectionTypeTest.php @@ -79,20 +79,6 @@ public function testGetBaseType() $this->assertEquals(Type::int(), Type::collection(Type::generic(Type::int(), Type::string()))->getBaseType()); } - public function testIsNullable() - { - $this->assertFalse((new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ARRAY), Type::int())))->isNullable()); - $this->assertTrue((new CollectionType(Type::generic(Type::null(), Type::int())))->isNullable()); - $this->assertTrue((new CollectionType(Type::generic(Type::mixed(), Type::int())))->isNullable()); - } - - public function testAsNonNullable() - { - $type = new CollectionType(Type::builtin(TypeIdentifier::ITERABLE)); - - $this->assertSame($type, $type->asNonNullable()); - } - public function testIsA() { $type = new CollectionType(new GenericType(Type::builtin(TypeIdentifier::ARRAY), Type::string(), Type::bool())); diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/EnumTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/EnumTypeTest.php index 69baf0d8d5d84..96a485087b0b6 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Type/EnumTypeTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/Type/EnumTypeTest.php @@ -23,21 +23,14 @@ public function testToString() $this->assertSame(DummyEnum::class, (string) new EnumType(DummyEnum::class)); } - public function testGetBaseType() - { - $this->assertEquals(new EnumType(DummyEnum::class), (new EnumType(DummyEnum::class))->getBaseType()); - } - - public function testIsNullable() + public function testGetTypeIdentifier(): void { - $this->assertFalse((new EnumType(DummyEnum::class))->isNullable()); + $this->assertSame(TypeIdentifier::OBJECT, (new EnumType(DummyEnum::class))->getTypeIdentifier()); } - public function testAsNonNullable() + public function testGetBaseType() { - $type = new EnumType(DummyEnum::class); - - $this->assertSame($type, $type->asNonNullable()); + $this->assertEquals(new EnumType(DummyEnum::class), (new EnumType(DummyEnum::class))->getBaseType()); } public function testIsA() diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/GenericTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/GenericTypeTest.php index 6277e4ea10727..1fbf2e7dbb654 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Type/GenericTypeTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/Type/GenericTypeTest.php @@ -35,18 +35,14 @@ public function testGetBaseType() $this->assertEquals(Type::object(), Type::generic(Type::object(), Type::int())->getBaseType()); } + /** + * This should fail! GenericType::isNullable() does not exist. We need to remove __call and use interfaces. + */ public function testIsNullable() { $this->assertFalse((new GenericType(Type::builtin(TypeIdentifier::ARRAY), Type::int()))->isNullable()); - $this->assertTrue((new GenericType(Type::null(), Type::int()))->isNullable()); - $this->assertTrue((new GenericType(Type::mixed(), Type::int()))->isNullable()); - } - - public function testAsNonNullable() - { - $type = new GenericType(Type::builtin(TypeIdentifier::ARRAY), Type::int()); - - $this->assertSame($type, $type->asNonNullable()); + //$this->assertTrue((new GenericType(Type::null(), Type::int()))->isNullable()); + //$this->assertTrue((new GenericType(Type::mixed(), Type::int()))->isNullable()); } public function testIsA() diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/IntersectionTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/IntersectionTypeTest.php index 8002ebcba1430..5e14a05380655 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Type/IntersectionTypeTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/Type/IntersectionTypeTest.php @@ -17,89 +17,89 @@ use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\Type\BuiltinType; use Symfony\Component\TypeInfo\Type\IntersectionType; +use Symfony\Component\TypeInfo\Type\ObjectType; use Symfony\Component\TypeInfo\TypeIdentifier; class IntersectionTypeTest extends TestCase { - public function testCannotCreateWithOnlyOneType() + public function testCannotCreateWithOnlyOneType(): void { $this->expectException(InvalidArgumentException::class); - new IntersectionType(Type::int()); + new IntersectionType(Type::object('Foo')); } - public function testCannotCreateWithIntersectionTypeParts() + public static function getInvalidParts(): iterable + { + $foo = Type::object('Foo'); + $bar = Type::object('Bar'); + + yield 'intersection' => [Type::intersection($foo, $bar), Type::intersection($foo, $bar)]; + yield 'union' => [Type::intersection($foo, $bar), Type::intersection($foo, $bar)]; + foreach (TypeIdentifier::cases() as $case) { + yield $case->value => [Type::builtin($case), Type::builtin($case)]; + } + yield 'generic' => [Type::object('Foo'), Type::generic(Type::builtin('array'), Type::string())]; + yield 'collection' => [Type::object('Foo'), Type::collection(Type::generic(Type::builtin('array')))]; + } + + /** + * @dataProvider getInvalidParts + */ + public function testCannotCreateWithNonObjectParts(Type ...$parts): void { $this->expectException(InvalidArgumentException::class); - new IntersectionType(Type::int(), new IntersectionType()); + + new IntersectionType(...$parts); } - public function testSortTypesOnCreation() + public function testCreateWithObjectParts(): void { - $type = new IntersectionType(Type::int(), Type::string(), Type::bool()); - $this->assertEquals([Type::bool(), Type::int(), Type::string()], $type->getTypes()); + $foo = Type::object('Foo'); + $bar = Type::generic(Type::object('Bar'), Type::string()); + $baz = Type::collection(Type::generic(Type::object('Baz'), Type::string())); + + $type = new IntersectionType($foo, $bar, $baz); + $this->assertEquals([$bar, $baz, $foo], $type->getTypes()); } - public function testAtLeastOneTypeIs() + public function testAtLeastOneTypeIs(): void { - $type = new IntersectionType(Type::int(), Type::string(), Type::bool()); + $type = new IntersectionType(Type::object('Foo'), Type::object('Bar'), Type::object('Baz')); - $this->assertTrue($type->atLeastOneTypeIs(fn (Type $t) => 'int' === (string) $t)); - $this->assertFalse($type->atLeastOneTypeIs(fn (Type $t) => 'float' === (string) $t)); + $this->assertTrue($type->atLeastOneTypeIs(fn (Type $t) => 'Bar' === (string) $t)); + $this->assertFalse($type->atLeastOneTypeIs(fn (Type $t) => 'Blip' === (string) $t)); } public function testEveryTypeIs() { - $type = new IntersectionType(Type::int(), Type::string(), Type::bool()); - $this->assertTrue($type->everyTypeIs(fn (Type $t) => $t instanceof BuiltinType)); + $type = new IntersectionType(Type::object('Foo'), Type::object('Bar'), Type::object('Baz')); + $this->assertTrue($type->everyTypeIs(fn (Type $t) => $t instanceof ObjectType)); - $type = new IntersectionType(Type::int(), Type::string(), Type::template('T')); - $this->assertFalse($type->everyTypeIs(fn (Type $t) => $t instanceof BuiltinType)); + $type = new IntersectionType(Type::object('Foo'), Type::object('Bar'), Type::generic(Type::object('Baz'))); + $this->assertFalse($type->everyTypeIs(fn (Type $t) => $t instanceof ObjectType)); } public function testGetBaseType() { $this->expectException(LogicException::class); - (new IntersectionType(Type::string(), Type::int()))->getBaseType(); + (new IntersectionType(Type::object('Bar'), Type::object('Foo')))->getBaseType(); } public function testToString() { - $type = new IntersectionType(Type::int(), Type::string(), Type::float()); - $this->assertSame('float&int&string', (string) $type); - - $type = new IntersectionType(Type::int(), Type::string(), Type::union(Type::float(), Type::bool())); - $this->assertSame('(bool|float)&int&string', (string) $type); - } - - public function testIsNullable() - { - $this->assertFalse((new IntersectionType(Type::int(), Type::string(), Type::float()))->isNullable()); - $this->assertTrue((new IntersectionType(Type::null(), Type::union(Type::int(), Type::mixed())))->isNullable()); - } - - public function testAsNonNullable() - { - $type = new IntersectionType(Type::int(), Type::string(), Type::float()); - - $this->assertSame($type, $type->asNonNullable()); - } - - public function testCannotTurnNullIntersectionAsNonNullable() - { - $this->expectException(LogicException::class); - - $type = (new IntersectionType(Type::null(), Type::mixed()))->asNonNullable(); + $type = new IntersectionType(Type::object('Foo'), Type::object('Bar'), Type::generic(Type::object('Baz'), Type::string())); + $this->assertSame('Bar&Baz&Foo', (string) $type); } public function testIsA() { - $type = new IntersectionType(Type::int(), Type::string(), Type::float()); + $type = new IntersectionType(Type::object('Foo'), Type::object('Bar')); $this->assertFalse($type->isA(TypeIdentifier::ARRAY)); - $type = new IntersectionType(Type::int(), Type::string(), Type::union(Type::float(), Type::bool())); + $type = new IntersectionType(Type::object('Foo'), Type::object('Bar')); $this->assertFalse($type->isA(TypeIdentifier::INT)); - $type = new IntersectionType(Type::int(), Type::union(Type::int(), Type::int())); - $this->assertTrue($type->isA(TypeIdentifier::INT)); + $type = new IntersectionType(Type::object('Foo'), Type::object('Bar')); + $this->assertTrue($type->isA(TypeIdentifier::OBJECT)); } } diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/ObjectTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/ObjectTypeTest.php index 1289f32df5ede..fa94f16e5c86e 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Type/ObjectTypeTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/Type/ObjectTypeTest.php @@ -22,9 +22,9 @@ public function testToString() $this->assertSame(self::class, (string) new ObjectType(self::class)); } - public function testIsNullable() + public function testGetTypeIdentifier(): void { - $this->assertFalse((new ObjectType(self::class))->isNullable()); + $this->assertSame(TypeIdentifier::OBJECT, (new ObjectType(self::class))->getTypeIdentifier()); } public function testGetBaseType() diff --git a/src/Symfony/Component/TypeInfo/Tests/Type/UnionTypeTest.php b/src/Symfony/Component/TypeInfo/Tests/Type/UnionTypeTest.php index bc308d4651466..8971fba482064 100644 --- a/src/Symfony/Component/TypeInfo/Tests/Type/UnionTypeTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/Type/UnionTypeTest.php @@ -30,7 +30,54 @@ public function testCannotCreateWithOnlyOneType() public function testCannotCreateWithUnionTypeParts() { $this->expectException(InvalidArgumentException::class); - new UnionType(Type::int(), new UnionType()); + new UnionType(Type::int(), new UnionType(Type::bool(), Type::float())); + } + + /** + * @dataProvider provideStandaloneTypes + */ + public function testCannotCreateWithStandaloneParts(Type $type): void + { + $this->expectException(InvalidArgumentException::class); + new UnionType(Type::generic(Type::int()), $type); + } + + /** + * @dataProvider provideComposableTypes + */ + public function testCanCreateWithComposableTypes(Type $type): void + { + $this->assertContains($type, (new UnionType(Type::generic(Type::int()), $type))->getTypes()); + } + + public static function provideComposableTypes(): iterable + { + foreach (TypeIdentifier::cases() as $case) { + if (!$case->isStandalone()) { + yield $case->name => [Type::builtin($case)]; + } + } + } + + public static function provideStandaloneTypes(): iterable + { + foreach (TypeIdentifier::cases() as $case) { + if ($case->isStandalone()) { + yield $case->name => [Type::builtin($case)]; + } + } + } + + public function testCannotComposeTrueAndFalse(): void + { + $this->expectException(InvalidArgumentException::class); + new UnionType(Type::true(), Type::false()); + } + + public function testCannotComposeBoolAndBoolValue(): void + { + $this->expectException(InvalidArgumentException::class); + new UnionType(Type::bool(), Type::false()); } public function testSortTypesOnCreation() @@ -52,19 +99,6 @@ public function testAsNonNullable() $type = new UnionType(Type::int(), Type::null()); $this->assertInstanceOf(BuiltinType::class, $type->asNonNullable()); $this->assertEquals(Type::int(), $type->asNonNullable()); - - $type = new UnionType(Type::int(), Type::object(\stdClass::class), Type::mixed()); - $this->assertInstanceOf(UnionType::class, $type->asNonNullable()); - $this->assertEquals([ - Type::builtin(TypeIdentifier::ARRAY), - Type::bool(), - Type::float(), - Type::int(), - Type::object(), - Type::resource(), - Type::object(\stdClass::class), - Type::string(), - ], $type->asNonNullable()->getTypes()); } public function testGetBaseType() @@ -88,7 +122,7 @@ public function testEveryTypeIs() $type = new UnionType(Type::int(), Type::string(), Type::bool()); $this->assertTrue($type->everyTypeIs(fn (Type $t) => $t instanceof BuiltinType)); - $type = new UnionType(Type::int(), Type::string(), Type::template('T')); + $type = new UnionType(Type::int(), Type::string(), Type::object('T')); $this->assertFalse($type->everyTypeIs(fn (Type $t) => $t instanceof BuiltinType)); } @@ -97,15 +131,38 @@ public function testToString() $type = new UnionType(Type::int(), Type::string(), Type::float()); $this->assertSame('float|int|string', (string) $type); - $type = new UnionType(Type::int(), Type::string(), Type::intersection(Type::float(), Type::bool())); - $this->assertSame('(bool&float)|int|string', (string) $type); + $type = new UnionType(Type::int(), Type::string(), Type::intersection(Type::object('Foo'), Type::object('Bar'))); + $this->assertSame('(Bar&Foo)|int|string', (string) $type); } public function testIsNullable() { - $this->assertFalse((new UnionType(Type::int(), Type::intersection(Type::float(), Type::int())))->isNullable()); + $this->assertFalse((new UnionType(Type::int(), Type::intersection(Type::object('Foo'), Type::object('Bar'))))->isNullable()); + $this->assertFalse((new UnionType(Type::int(), Type::string()))->isNullable()); $this->assertTrue((new UnionType(Type::int(), Type::null()))->isNullable()); - $this->assertTrue((new UnionType(Type::int(), Type::mixed()))->isNullable()); + } + + /** + * @dataProvider provideTypeIdentifierAndParts + */ + public function testGetTypeIdentifier(TypeIdentifier $expected, Type $first, Type $second): void + { + $this->assertSame($expected, (new UnionType($first, $second))->getTypeIdentifier()); + } + + public static function provideTypeIdentifierAndParts(): iterable + { + $int1 = Type::intersection(Type::object('Foo'), Type::object('Bar')); + $int2 = Type::intersection(Type::object('Bar'), Type::object('Baz')); + + yield 'int|string' => [TypeIdentifier::MIXED, Type::int(), Type::string()]; + yield 'int|null' => [TypeIdentifier::MIXED, Type::int(), Type::null()]; + yield 'int|int' => [TypeIdentifier::INT, Type::int(), Type::generic(Type::int())]; + yield 'int|object' => [TypeIdentifier::MIXED, Type::int(), Type::object()]; + yield 'intersections' => [TypeIdentifier::OBJECT, $int1, $int2]; + yield 'intersection|ClassName' => [TypeIdentifier::OBJECT, $int1, Type::object('Foo')]; + yield 'intersection|scalar' => [TypeIdentifier::MIXED, $int1, Type::int()]; + yield 'ClassName|ClassName' => [TypeIdentifier::OBJECT, Type::object('Foo'), Type::generic(Type::object('Foo'))]; } public function testIsA() @@ -114,14 +171,11 @@ public function testIsA() $this->assertFalse($type->isNullable()); $this->assertFalse($type->isA(TypeIdentifier::ARRAY)); - $type = new UnionType(Type::int(), Type::string(), Type::intersection(Type::float(), Type::bool())); + $type = new UnionType(Type::int(), Type::string(), Type::intersection(Type::object('Foo'), Type::object('Bar'))); $this->assertTrue($type->isA(TypeIdentifier::INT)); $this->assertTrue($type->isA(TypeIdentifier::STRING)); $this->assertFalse($type->isA(TypeIdentifier::FLOAT)); $this->assertFalse($type->isA(TypeIdentifier::BOOL)); - - $type = new UnionType(Type::string(), Type::intersection(Type::int(), Type::int())); - $this->assertTrue($type->isA(TypeIdentifier::INT)); } public function testProxiesMethodsToNonNullableType() diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php index bb1f1c3c8ba5c..d3892ccac5534 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php @@ -185,9 +185,14 @@ public function testCreateUnion() public function testCreateIntersection() { - $this->assertEquals(new IntersectionType(new BuiltinType(TypeIdentifier::INT), new ObjectType(self::class)), Type::intersection(Type::int(), Type::object(self::class))); - $this->assertEquals(new IntersectionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::STRING)), Type::intersection(Type::int(), Type::string(), Type::int())); - $this->assertEquals(new IntersectionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::STRING)), Type::intersection(Type::int(), Type::intersection(Type::int(), Type::string()))); + $this->assertEquals( + new IntersectionType(new ObjectType('Foo'), new ObjectType('Bar')), + Type::intersection(Type::object('Foo'), Type::object('Bar')) + ); + $this->assertEquals( + new IntersectionType(new ObjectType('Foo'), new ObjectType('Bar'), new ObjectType('Baz')), + Type::intersection(Type::object('Baz'), Type::intersection(Type::object('Foo'), Type::object('Bar'))) + ); } public function testCreateNullable() diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionTypeResolverTest.php index 4d7bff9449764..4cadd5943e102 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionTypeResolverTest.php @@ -44,7 +44,7 @@ public function testResolve(Type $expectedType, \ReflectionType $reflection, ?Ty /** * @return iterable */ - public function resolveDataProvider(): iterable + public static function resolveDataProvider(): iterable { $typeContext = (new TypeContextFactory())->createFromClassName(ReflectionExtractableDummy::class); $reflection = new \ReflectionClass(ReflectionExtractableDummy::class); @@ -89,7 +89,7 @@ public function testCannotResolveClassKeywordsWithoutTypeContext(\ReflectionType /** * @return iterable */ - public function classKeywordsTypesDataProvider(): iterable + public static function classKeywordsTypesDataProvider(): iterable { $reflection = new \ReflectionClass(ReflectionExtractableDummy::class); diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php index 78377560a8d76..6ba595c49be51 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php @@ -60,7 +60,7 @@ public function __toString(): string /** * @return iterable */ - public function resolveDataProvider(): iterable + public static function resolveDataProvider(): iterable { $typeContextFactory = new TypeContextFactory(new StringTypeResolver()); @@ -153,10 +153,10 @@ public function resolveDataProvider(): iterable yield [Type::union(Type::int(), Type::string()), 'int|string']; // intersection - yield [Type::intersection(Type::int(), Type::string()), 'int&string']; + yield [Type::intersection(Type::object('DateTimeInterface'), Type::object('DateTimeImmutable')), 'DateTimeImmutable&DateTimeInterface']; // DNF - yield [Type::union(Type::int(), Type::intersection(Type::string(), Type::bool())), 'int|(string&bool)']; + yield [Type::union(Type::intersection(Type::object('DateTimeInterface'), Type::object('DateTimeImmutable')), Type::int()), 'int|(DateTimeImmutable&DateTimeInterface)']; // collection objects yield [Type::collection(Type::object(\Traversable::class)), \Traversable::class]; diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeTest.php index c271ba581ed1f..2745f5ec0a098 100644 --- a/src/Symfony/Component/TypeInfo/Tests/TypeTest.php +++ b/src/Symfony/Component/TypeInfo/Tests/TypeTest.php @@ -31,21 +31,6 @@ public function testIs() $this->assertFalse(Type::generic(Type::string(), Type::int())->is($isInt)); } - public function testIsNullable() - { - $this->assertTrue(Type::null()->isNullable()); - $this->assertTrue(Type::mixed()->isNullable()); - $this->assertTrue(Type::nullable(Type::int())->isNullable()); - $this->assertTrue(Type::union(Type::int(), Type::null())->isNullable()); - $this->assertTrue(Type::union(Type::int(), Type::mixed())->isNullable()); - $this->assertTrue(Type::generic(Type::null(), Type::string())->isNullable()); - - $this->assertFalse(Type::int()->isNullable()); - $this->assertFalse(Type::union(Type::int(), Type::string())->isNullable()); - $this->assertFalse(Type::generic(Type::int(), Type::nullable(Type::string()))->isNullable()); - $this->assertFalse(Type::generic(Type::int(), Type::mixed())->isNullable()); - } - public function testCannotGetBaseTypeOnCompoundType() { $this->expectException(LogicException::class); diff --git a/src/Symfony/Component/TypeInfo/Type.php b/src/Symfony/Component/TypeInfo/Type.php index 3109f96fb4d08..59fe94b1737de 100644 --- a/src/Symfony/Component/TypeInfo/Type.php +++ b/src/Symfony/Component/TypeInfo/Type.php @@ -27,13 +27,16 @@ abstract class Type implements \Stringable abstract public function getBaseType(): BuiltinType|ObjectType; + /** + * Return the simplest primitive type that this type will satisfy. + */ + abstract public function getTypeIdentifier(): TypeIdentifier; + /** * @param TypeIdentifier|class-string $subject */ abstract public function isA(TypeIdentifier|string $subject): bool; - abstract public function asNonNullable(): self; - /** * @param callable(Type): bool $callable */ @@ -42,9 +45,14 @@ public function is(callable $callable): bool return $callable($this); } + public function asNonNullable(): self + { + return $this; + } + public function isNullable(): bool { - return $this->is(fn (Type $t): bool => $t->isA(TypeIdentifier::NULL) || $t->isA(TypeIdentifier::MIXED)); + return false; } /** diff --git a/src/Symfony/Component/TypeInfo/Type/BuiltinType.php b/src/Symfony/Component/TypeInfo/Type/BuiltinType.php index 06f175df3f75d..44be91eca7313 100644 --- a/src/Symfony/Component/TypeInfo/Type/BuiltinType.php +++ b/src/Symfony/Component/TypeInfo/Type/BuiltinType.php @@ -33,11 +33,6 @@ public function __construct( ) { } - public function getBaseType(): self|ObjectType - { - return $this; - } - /** * @return T */ @@ -46,6 +41,11 @@ public function getTypeIdentifier(): TypeIdentifier return $this->typeIdentifier; } + public function getBaseType(): self|ObjectType + { + return $this; + } + public function isA(TypeIdentifier|string $subject): bool { if ($subject instanceof TypeIdentifier) { @@ -59,6 +59,11 @@ public function isA(TypeIdentifier|string $subject): bool } } + public function isNullable(): bool + { + return \in_array($this->getTypeIdentifier(), [TypeIdentifier::NULL, TypeIdentifier::MIXED], true); + } + /** * @return self|UnionType|BuiltinType|BuiltinType|BuiltinType|BuiltinType|BuiltinType|BuiltinType> */ diff --git a/src/Symfony/Component/TypeInfo/Type/CollectionType.php b/src/Symfony/Component/TypeInfo/Type/CollectionType.php index 3076da7de3e9f..5602751e0ab16 100644 --- a/src/Symfony/Component/TypeInfo/Type/CollectionType.php +++ b/src/Symfony/Component/TypeInfo/Type/CollectionType.php @@ -45,6 +45,11 @@ public function __construct( } } + public function getTypeIdentifier(): TypeIdentifier + { + return $this->getType()->getTypeIdentifier(); + } + public function getBaseType(): BuiltinType|ObjectType { return $this->getType()->getBaseType(); @@ -68,11 +73,6 @@ public function isList(): bool return $this->isList; } - public function asNonNullable(): self - { - return $this; - } - public function getCollectionKeyType(): Type { $defaultCollectionKeyType = self::union(self::int(), self::string()); diff --git a/src/Symfony/Component/TypeInfo/Type/CompositeTypeInterface.php b/src/Symfony/Component/TypeInfo/Type/CompositeTypeInterface.php new file mode 100644 index 0000000000000..d166a418a190d --- /dev/null +++ b/src/Symfony/Component/TypeInfo/Type/CompositeTypeInterface.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\TypeInfo\Type; + +use Symfony\Component\TypeInfo\Type; + +/** + * Represents a type composed by several other types. + * + * @author Mathias Arlaud + * + * @template T of Type + * + * @experimental + */ +interface CompositeTypeInterface +{ + /** + * Returns the list of subtypes that compose this type. + * + * @return list + */ + public function getTypes(): array; + + /** + * Returns the list of subtypes that satisfy a given predicate. + * + * @param callable(T): bool $callable + * @return list + */ + public function filter(callable $callable): array; + + /** + * Checks whether at least one subtype satisfies the given predicate. + * + * @param callable(T): bool $callable + */ + public function atLeastOneTypeIs(callable $callable): bool; + + /** + * Checks whether all subtypes satisfy the given predicate. + * * + * @param callable(T): bool $callable + */ + public function everyTypeIs(callable $callable): bool; +} diff --git a/src/Symfony/Component/TypeInfo/Type/CompositeTypeTrait.php b/src/Symfony/Component/TypeInfo/Type/CompositeTypeTrait.php index ee8d6c52092cc..b0298f0a18cb3 100644 --- a/src/Symfony/Component/TypeInfo/Type/CompositeTypeTrait.php +++ b/src/Symfony/Component/TypeInfo/Type/CompositeTypeTrait.php @@ -30,25 +30,6 @@ trait CompositeTypeTrait */ private readonly array $types; - /** - * @param list $types - */ - public function __construct(Type ...$types) - { - if (\count($types) < 2) { - throw new InvalidArgumentException(\sprintf('"%s" expects at least 2 types.', self::class)); - } - - foreach ($types as $t) { - if ($t instanceof self) { - throw new InvalidArgumentException(\sprintf('Cannot set "%s" as a "%1$s" part.', self::class)); - } - } - - usort($types, fn (Type $a, Type $b): int => (string) $a <=> (string) $b); - $this->types = array_values(array_unique($types)); - } - public function isA(TypeIdentifier|string $subject): bool { return $this->is(fn (Type $type) => $type->isA($subject)); @@ -89,4 +70,23 @@ public function everyTypeIs(callable $callable): bool return true; } + + /** + * @param callable(T): bool $callable + * @return list + */ + public function filter(callable $callable): array + { + return $this->filterTypes($callable, ...$this->getTypes()); + } + + /** + * @param callable(T): bool $callable + * @param array $types + * @return list + */ + private function filterTypes(callable $callable, Type ...$types): array + { + return array_values(array_filter($types, $callable)); + } } diff --git a/src/Symfony/Component/TypeInfo/Type/GenericType.php b/src/Symfony/Component/TypeInfo/Type/GenericType.php index 2439444153026..5db452cc56d0a 100644 --- a/src/Symfony/Component/TypeInfo/Type/GenericType.php +++ b/src/Symfony/Component/TypeInfo/Type/GenericType.php @@ -43,6 +43,11 @@ public function __construct( $this->variableTypes = $variableTypes; } + public function getTypeIdentifier(): TypeIdentifier + { + return $this->getType()->getTypeIdentifier(); + } + public function getBaseType(): BuiltinType|ObjectType { return $this->getType(); @@ -61,11 +66,6 @@ public function isA(TypeIdentifier|string $subject): bool return $this->getType()->isA($subject); } - public function asNonNullable(): self - { - return $this; - } - /** * @return list */ diff --git a/src/Symfony/Component/TypeInfo/Type/IntersectionType.php b/src/Symfony/Component/TypeInfo/Type/IntersectionType.php index 62842436ed154..8c2b7503c639e 100644 --- a/src/Symfony/Component/TypeInfo/Type/IntersectionType.php +++ b/src/Symfony/Component/TypeInfo/Type/IntersectionType.php @@ -11,24 +11,52 @@ namespace Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Exception\InvalidArgumentException; use Symfony\Component\TypeInfo\Exception\LogicException; use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\TypeIdentifier; /** * @author Mathias Arlaud * @author Baptiste Leduc * - * @template T of Type + * @template T of ObjectType|GenericType|CollectionType> + * @implements CompositeTypeInterface * * @experimental */ -final class IntersectionType extends Type +final class IntersectionType extends Type implements CompositeTypeInterface { /** * @use CompositeTypeTrait */ use CompositeTypeTrait; + /** + * @param list $types + */ + public function __construct(Type ...$types) + { + if (\count($types) < 2) { + throw new InvalidArgumentException(\sprintf('"%s" expects at least 2 types.', self::class)); + } + // Only accept non-composite object types, except the builtin 'object' + foreach ($types as $t) { + if ($t instanceof CompositeTypeInterface || $t instanceof BuiltinType || TypeIdentifier::OBJECT !== $t->getTypeIdentifier()) { + throw new InvalidArgumentException(\sprintf('Cannot set type "%s" as a "%s" part.', $t, self::class)); + } + } + // All subtypes are class names and are sorted alphabetically + usort($types, fn (Type $a, Type $b): int => (string) $a <=> (string) $b); + + $this->types = array_values(array_unique($types)); + } + + public function getTypeIdentifier(): TypeIdentifier + { + return TypeIdentifier::OBJECT; + } + public function is(callable $callable): bool { return $this->everyTypeIs($callable); @@ -54,16 +82,4 @@ public function getBaseType(): BuiltinType|ObjectType { throw new LogicException(\sprintf('Cannot get base type on "%s" compound type.', $this)); } - - /** - * @throws LogicException - */ - public function asNonNullable(): self - { - if ($this->isNullable()) { - throw new LogicException(\sprintf('"%s cannot be turned as non nullable.', (string) $this)); - } - - return $this; - } } diff --git a/src/Symfony/Component/TypeInfo/Type/ObjectType.php b/src/Symfony/Component/TypeInfo/Type/ObjectType.php index 5d35278380510..629f1b057e21d 100644 --- a/src/Symfony/Component/TypeInfo/Type/ObjectType.php +++ b/src/Symfony/Component/TypeInfo/Type/ObjectType.php @@ -32,14 +32,14 @@ public function __construct( ) { } - public function getBaseType(): BuiltinType|self + public function getTypeIdentifier(): TypeIdentifier { - return $this; + return TypeIdentifier::OBJECT; } - public function getTypeIdentifier(): TypeIdentifier + public function getBaseType(): BuiltinType|self { - return TypeIdentifier::OBJECT; + return $this; } public function isA(TypeIdentifier|string $subject): bool @@ -59,11 +59,6 @@ public function getClassName(): string return $this->className; } - public function asNonNullable(): static - { - return $this; - } - public function __toString(): string { return $this->className; diff --git a/src/Symfony/Component/TypeInfo/Type/TemplateType.php b/src/Symfony/Component/TypeInfo/Type/TemplateType.php index eef3170b75496..9c0b966c79b61 100644 --- a/src/Symfony/Component/TypeInfo/Type/TemplateType.php +++ b/src/Symfony/Component/TypeInfo/Type/TemplateType.php @@ -31,6 +31,11 @@ public function __construct( ) { } + public function getTypeIdentifier(): TypeIdentifier + { + return $this->getBound()->getTypeIdentifier(); + } + public function getBaseType(): BuiltinType|ObjectType { throw new LogicException(\sprintf('Cannot get base type on "%s" template type.', $this)); @@ -51,11 +56,6 @@ public function getBound(): Type return $this->bound; } - public function asNonNullable(): self - { - return $this; - } - public function __toString(): string { return $this->name; diff --git a/src/Symfony/Component/TypeInfo/Type/UnionType.php b/src/Symfony/Component/TypeInfo/Type/UnionType.php index d757897c681aa..0094147f3a052 100644 --- a/src/Symfony/Component/TypeInfo/Type/UnionType.php +++ b/src/Symfony/Component/TypeInfo/Type/UnionType.php @@ -11,6 +11,7 @@ namespace Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Exception\InvalidArgumentException; use Symfony\Component\TypeInfo\Exception\LogicException; use Symfony\Component\TypeInfo\Type; use Symfony\Component\TypeInfo\TypeIdentifier; @@ -20,16 +21,71 @@ * @author Baptiste Leduc * * @template T of Type + * @implements CompositeTypeInterface * * @experimental */ -final class UnionType extends Type +final class UnionType extends Type implements CompositeTypeInterface { /** * @use CompositeTypeTrait */ use CompositeTypeTrait; + private readonly TypeIdentifier $typeIdentifier; + private readonly bool $isNullable; + + public function __construct(Type ...$types) + { + if (\count($types) < 2) { + throw new InvalidArgumentException(\sprintf('"%s" expects at least 2 types.', self::class)); + } + + $nullable = false; + $hasClassType = false; + $hasObject = false; + $identifiers = []; + foreach ($types as $t) { + if ($t instanceof self) { + throw new InvalidArgumentException(\sprintf('Cannot set "%s" as a "%s" part.', $t, self::class)); + } + if ($t->getTypeIdentifier()->isStandalone()) { + throw new InvalidArgumentException(\sprintf('Type %s can only be used as a standalone type', $t->getTypeIdentifier()->value)); + } + if (TypeIdentifier::NULL === $t->getTypeIdentifier()) { + $nullable = true; + } + $hasClassType = $hasClassType || !$t instanceof BuiltinType && TypeIdentifier::OBJECT === $t->getTypeIdentifier(); + $hasObject = $hasObject || $t instanceof BuiltinType && TypeIdentifier::OBJECT === $t->getTypeIdentifier(); + $identifiers[$t->getTypeIdentifier()->name] = $t->getTypeIdentifier(); + } + if ($hasClassType && $hasObject) { + throw new InvalidArgumentException('Union contains both object and a class type, which is redundant.'); + } + + $types = array_values(array_unique($types)); + + // bool, true and false cannot be composed together (use same errors as PHP in similar cases) + $booleanTypes = $this->filterTypes(fn(Type $t): bool => $t->getTypeIdentifier()->isBool(), ...$types); + if (1 < \count($booleanTypes)) { + throw \in_array(TypeIdentifier::TRUE, $identifiers, true) && \in_array(TypeIdentifier::FALSE, $identifiers, true) + ? new InvalidArgumentException('Union type contains both true and false, bool should be used instead.') + : new InvalidArgumentException('Duplicate boolean type is redundant.'); + } + + $this->typeIdentifier = 1 === count($identifiers) ? current($identifiers) : TypeIdentifier::MIXED; + $this->isNullable = $nullable; + $this->types = $this->sortSubtypesForRendering(...$types); + } + + public function getTypeIdentifier(): TypeIdentifier + { + return $this->typeIdentifier; + } + + /** + * @param callable(T): bool $callable + */ public function is(callable $callable): bool { return $this->atLeastOneTypeIs($callable); @@ -48,22 +104,24 @@ public function getBaseType(): BuiltinType|ObjectType throw new LogicException(\sprintf('Cannot get base type on "%s" compound type.', $this)); } - public function asNonNullable(): Type + /** + * Whether this union represents a nullable type. + * + * A union is nullable if it contains "null" (as it may not contain unions or mixed). + */ + public function isNullable(): bool { - $nonNullableTypes = []; - foreach ($this->getTypes() as $type) { - if ($type->isA(TypeIdentifier::NULL)) { - continue; - } + return $this->isNullable; + } - $nonNullableType = $type->asNonNullable(); - $nonNullableTypes = [ - ...$nonNullableTypes, - ...($nonNullableType instanceof self ? $nonNullableType->getTypes() : [$nonNullableType]), - ]; + public function asNonNullable(): Type + { + if (!$this->isNullable) { + return $this; } + $nonNullableTypes = $this->filter(fn (Type $t): bool => TypeIdentifier::NULL !== $t->getTypeIdentifier()); - return \count($nonNullableTypes) > 1 ? new self(...$nonNullableTypes) : $nonNullableTypes[0]; + return 1 < \count($nonNullableTypes) ? new self(...$nonNullableTypes) : $nonNullableTypes[0]; } public function __toString(): string @@ -98,4 +156,24 @@ public function __call(string $method, array $arguments): mixed throw new LogicException(\sprintf('Cannot call "%s" on "%s" compound type.', $method, $this)); } + + /** + * Sort intersections first, then classes, then builtins and order alphabetically within each group. + * + * @param array $types + * @return list + */ + private function sortSubtypesForRendering(Type ...$types): array + { + $prefix = function (Type $t): string { + return match ($t::class) { + IntersectionType::class => '!!', + ObjectType::class => '!', + default => '', + }.$t; + }; + usort($types, fn (Type $a, Type $b): int => $prefix($a) <=> $prefix($b)); + + return array_values($types); + } } diff --git a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php index d87737d5945bb..8620570177ba5 100644 --- a/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php +++ b/src/Symfony/Component/TypeInfo/TypeFactoryTrait.php @@ -17,6 +17,7 @@ use Symfony\Component\TypeInfo\Type\EnumType; use Symfony\Component\TypeInfo\Type\GenericType; use Symfony\Component\TypeInfo\Type\IntersectionType; +use Symfony\Component\TypeInfo\Type\NullableTypeInterface; use Symfony\Component\TypeInfo\Type\ObjectType; use Symfony\Component\TypeInfo\Type\TemplateType; use Symfony\Component\TypeInfo\Type\UnionType; @@ -305,8 +306,11 @@ public static function intersection(Type ...$types): IntersectionType * * @return (T is UnionType ? T : UnionType>) */ - public static function nullable(Type $type): UnionType + public static function nullable(Type $type): Type { + if ($type->isNullable()) { + return $type; + } if ($type instanceof UnionType) { return Type::union(Type::null(), ...$type->getTypes()); } diff --git a/src/Symfony/Component/TypeInfo/TypeIdentifier.php b/src/Symfony/Component/TypeInfo/TypeIdentifier.php index 45bd5472ab41e..af6f56067f74d 100644 --- a/src/Symfony/Component/TypeInfo/TypeIdentifier.php +++ b/src/Symfony/Component/TypeInfo/TypeIdentifier.php @@ -44,4 +44,28 @@ public static function values(): array { return array_column(self::cases(), 'value'); } + + /** + * Whether this type is a standalone PHP type (i.e. not allowed to be used as a part of a composite type or as a base type). + */ + public function isStandalone(): bool + { + return \in_array($this, [self::MIXED, self::NEVER, self::VOID], true); + } + + /** + * Whether this type is a scalar (string|float|int|bool) type. + */ + public function isScalar(): bool + { + return \in_array($this, [self::STRING, self::FLOAT, self::INT, self::BOOL, self::FALSE, self::TRUE], true); + } + + /** + * Whether this type represents a boolean value. + */ + public function isBool(): bool + { + return \in_array($this, [self::BOOL, self::FALSE, self::TRUE], true); + } }