Skip to content

Commit e88ea3f

Browse files
committed
Add NullableTypeInterface and improve types comparison
1 parent 031dde7 commit e88ea3f

22 files changed

+219
-176
lines changed

src/Symfony/Component/TypeInfo/Tests/Type/BackedEnumTypeTest.php

+2-9
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,16 @@ public function testToString()
2424
$this->assertSame(DummyBackedEnum::class, (string) new BackedEnumType(DummyBackedEnum::class, Type::int()));
2525
}
2626

27-
public function testIsNullable()
27+
public function testGetTypeIdentifier()
2828
{
29-
$this->assertFalse((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isNullable());
29+
$this->assertSame(TypeIdentifier::OBJECT, (new BackedEnumType(DummyBackedEnum::class, Type::int()))->getTypeIdentifier());
3030
}
3131

3232
public function testGetBaseType()
3333
{
3434
$this->assertEquals(new BackedEnumType(DummyBackedEnum::class, Type::int()), (new BackedEnumType(DummyBackedEnum::class, Type::int()))->getBaseType());
3535
}
3636

37-
public function testAsNonNullable()
38-
{
39-
$type = new BackedEnumType(DummyBackedEnum::class, Type::int());
40-
41-
$this->assertSame($type, $type->asNonNullable());
42-
}
43-
4437
public function testIsA()
4538
{
4639
$this->assertFalse((new BackedEnumType(DummyBackedEnum::class, Type::int()))->isA(TypeIdentifier::ARRAY));

src/Symfony/Component/TypeInfo/Tests/Type/CollectionTypeTest.php

-14
Original file line numberDiff line numberDiff line change
@@ -79,20 +79,6 @@ public function testGetBaseType()
7979
$this->assertEquals(Type::int(), Type::collection(Type::generic(Type::int(), Type::string()))->getBaseType());
8080
}
8181

82-
public function testIsNullable()
83-
{
84-
$this->assertFalse((new CollectionType(Type::generic(Type::builtin(TypeIdentifier::ARRAY), Type::int())))->isNullable());
85-
$this->assertTrue((new CollectionType(Type::generic(Type::null(), Type::int())))->isNullable());
86-
$this->assertTrue((new CollectionType(Type::generic(Type::mixed(), Type::int())))->isNullable());
87-
}
88-
89-
public function testAsNonNullable()
90-
{
91-
$type = new CollectionType(Type::builtin(TypeIdentifier::ITERABLE));
92-
93-
$this->assertSame($type, $type->asNonNullable());
94-
}
95-
9682
public function testIsA()
9783
{
9884
$type = new CollectionType(new GenericType(Type::builtin(TypeIdentifier::ARRAY), Type::string(), Type::bool()));

src/Symfony/Component/TypeInfo/Tests/Type/EnumTypeTest.php

+4-11
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,14 @@ public function testToString()
2323
$this->assertSame(DummyEnum::class, (string) new EnumType(DummyEnum::class));
2424
}
2525

26-
public function testGetBaseType()
27-
{
28-
$this->assertEquals(new EnumType(DummyEnum::class), (new EnumType(DummyEnum::class))->getBaseType());
29-
}
30-
31-
public function testIsNullable()
26+
public function testGetTypeIdentifier(): void
3227
{
33-
$this->assertFalse((new EnumType(DummyEnum::class))->isNullable());
28+
$this->assertSame(TypeIdentifier::OBJECT, (new EnumType(DummyEnum::class))->getTypeIdentifier());
3429
}
3530

36-
public function testAsNonNullable()
31+
public function testGetBaseType()
3732
{
38-
$type = new EnumType(DummyEnum::class);
39-
40-
$this->assertSame($type, $type->asNonNullable());
33+
$this->assertEquals(new EnumType(DummyEnum::class), (new EnumType(DummyEnum::class))->getBaseType());
4134
}
4235

4336
public function testIsA()

src/Symfony/Component/TypeInfo/Tests/Type/GenericTypeTest.php

+3-7
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,16 @@ public function testGetBaseType()
3535
$this->assertEquals(Type::object(), Type::generic(Type::object(), Type::int())->getBaseType());
3636
}
3737

38+
/**
39+
* This should fail! GenericType::isNullable() does not exist. We need to remove __call and use interfaces.
40+
*/
3841
public function testIsNullable()
3942
{
4043
$this->assertFalse((new GenericType(Type::builtin(TypeIdentifier::ARRAY), Type::int()))->isNullable());
4144
$this->assertTrue((new GenericType(Type::null(), Type::int()))->isNullable());
4245
$this->assertTrue((new GenericType(Type::mixed(), Type::int()))->isNullable());
4346
}
4447

45-
public function testAsNonNullable()
46-
{
47-
$type = new GenericType(Type::builtin(TypeIdentifier::ARRAY), Type::int());
48-
49-
$this->assertSame($type, $type->asNonNullable());
50-
}
51-
5248
public function testIsA()
5349
{
5450
$type = new GenericType(Type::builtin(TypeIdentifier::ARRAY), Type::string(), Type::bool());

src/Symfony/Component/TypeInfo/Tests/Type/IntersectionTypeTest.php

+44-44
Original file line numberDiff line numberDiff line change
@@ -17,89 +17,89 @@
1717
use Symfony\Component\TypeInfo\Type;
1818
use Symfony\Component\TypeInfo\Type\BuiltinType;
1919
use Symfony\Component\TypeInfo\Type\IntersectionType;
20+
use Symfony\Component\TypeInfo\Type\ObjectType;
2021
use Symfony\Component\TypeInfo\TypeIdentifier;
2122

2223
class IntersectionTypeTest extends TestCase
2324
{
24-
public function testCannotCreateWithOnlyOneType()
25+
public function testCannotCreateWithOnlyOneType(): void
2526
{
2627
$this->expectException(InvalidArgumentException::class);
27-
new IntersectionType(Type::int());
28+
new IntersectionType(Type::object('Foo'));
2829
}
2930

30-
public function testCannotCreateWithIntersectionTypeParts()
31+
public static function getInvalidParts(): iterable
32+
{
33+
$foo = Type::object('Foo');
34+
$bar = Type::object('Bar');
35+
36+
yield 'intersection' => [Type::intersection($foo, $bar), Type::intersection($foo, $bar)];
37+
yield 'union' => [Type::intersection($foo, $bar), Type::intersection($foo, $bar)];
38+
foreach (TypeIdentifier::cases() as $case) {
39+
yield $case->value => [Type::builtin($case), Type::builtin($case)];
40+
}
41+
yield 'generic<builtin>' => [Type::object('Foo'), Type::generic(Type::builtin('array'), Type::string())];
42+
yield 'collection<builtin>' => [Type::object('Foo'), Type::collection(Type::generic(Type::builtin('array')))];
43+
}
44+
45+
/**
46+
* @dataProvider getInvalidParts
47+
*/
48+
public function testCannotCreateWithNonObjectParts(Type ...$parts): void
3149
{
3250
$this->expectException(InvalidArgumentException::class);
33-
new IntersectionType(Type::int(), new IntersectionType());
51+
52+
new IntersectionType(...$parts);
3453
}
3554

36-
public function testSortTypesOnCreation()
55+
public function testCreateWithObjectParts(): void
3756
{
38-
$type = new IntersectionType(Type::int(), Type::string(), Type::bool());
39-
$this->assertEquals([Type::bool(), Type::int(), Type::string()], $type->getTypes());
57+
$foo = Type::object('Foo');
58+
$bar = Type::generic(Type::object('Bar'), Type::string());
59+
$baz = Type::collection(Type::generic(Type::object('Baz'), Type::string()));
60+
61+
$type = new IntersectionType($foo, $bar, $baz);
62+
$this->assertEquals([$bar, $baz, $foo], $type->getTypes());
4063
}
4164

4265
public function testAtLeastOneTypeIs()
4366
{
44-
$type = new IntersectionType(Type::int(), Type::string(), Type::bool());
67+
$type = new IntersectionType(Type::object('Foo'), Type::object('Bar'), Type::object('Baz'));
4568

46-
$this->assertTrue($type->atLeastOneTypeIs(fn (Type $t) => 'int' === (string) $t));
47-
$this->assertFalse($type->atLeastOneTypeIs(fn (Type $t) => 'float' === (string) $t));
69+
$this->assertTrue($type->atLeastOneTypeIs(fn (Type $t) => 'Bar' === (string) $t));
70+
$this->assertFalse($type->atLeastOneTypeIs(fn (Type $t) => 'Blip' === (string) $t));
4871
}
4972

5073
public function testEveryTypeIs()
5174
{
52-
$type = new IntersectionType(Type::int(), Type::string(), Type::bool());
53-
$this->assertTrue($type->everyTypeIs(fn (Type $t) => $t instanceof BuiltinType));
75+
$type = new IntersectionType(Type::object('Foo'), Type::object('Bar'), Type::object('Baz'));
76+
$this->assertTrue($type->everyTypeIs(fn (Type $t) => $t instanceof ObjectType));
5477

55-
$type = new IntersectionType(Type::int(), Type::string(), Type::template('T'));
56-
$this->assertFalse($type->everyTypeIs(fn (Type $t) => $t instanceof BuiltinType));
78+
$type = new IntersectionType(Type::object('Foo'), Type::object('Bar'), Type::generic(Type::object('Baz')));
79+
$this->assertFalse($type->everyTypeIs(fn (Type $t) => $t instanceof ObjectType));
5780
}
5881

5982
public function testGetBaseType()
6083
{
6184
$this->expectException(LogicException::class);
62-
(new IntersectionType(Type::string(), Type::int()))->getBaseType();
85+
(new IntersectionType(Type::object('Bar'), Type::object('Foo')))->getBaseType();
6386
}
6487

6588
public function testToString()
6689
{
67-
$type = new IntersectionType(Type::int(), Type::string(), Type::float());
68-
$this->assertSame('float&int&string', (string) $type);
69-
70-
$type = new IntersectionType(Type::int(), Type::string(), Type::union(Type::float(), Type::bool()));
71-
$this->assertSame('(bool|float)&int&string', (string) $type);
72-
}
73-
74-
public function testIsNullable()
75-
{
76-
$this->assertFalse((new IntersectionType(Type::int(), Type::string(), Type::float()))->isNullable());
77-
$this->assertTrue((new IntersectionType(Type::null(), Type::union(Type::int(), Type::mixed())))->isNullable());
78-
}
79-
80-
public function testAsNonNullable()
81-
{
82-
$type = new IntersectionType(Type::int(), Type::string(), Type::float());
83-
84-
$this->assertSame($type, $type->asNonNullable());
85-
}
86-
87-
public function testCannotTurnNullIntersectionAsNonNullable()
88-
{
89-
$this->expectException(LogicException::class);
90-
91-
$type = (new IntersectionType(Type::null(), Type::mixed()))->asNonNullable();
90+
$type = new IntersectionType(Type::object('Foo'), Type::object('Bar'), Type::generic(Type::object('Baz'), Type::string()));
91+
$this->assertSame('Bar&Baz<string>&Foo', (string) $type);
9292
}
9393

9494
public function testIsA()
9595
{
96-
$type = new IntersectionType(Type::int(), Type::string(), Type::float());
96+
$type = new IntersectionType(Type::object('Foo'), Type::object('Bar'));
9797
$this->assertFalse($type->isA(TypeIdentifier::ARRAY));
9898

99-
$type = new IntersectionType(Type::int(), Type::string(), Type::union(Type::float(), Type::bool()));
99+
$type = new IntersectionType(Type::object('Foo'), Type::object('Bar'));
100100
$this->assertFalse($type->isA(TypeIdentifier::INT));
101101

102-
$type = new IntersectionType(Type::int(), Type::union(Type::int(), Type::int()));
103-
$this->assertTrue($type->isA(TypeIdentifier::INT));
102+
$type = new IntersectionType(Type::object('Foo'), Type::object('Bar'));
103+
$this->assertTrue($type->isA(TypeIdentifier::OBJECT));
104104
}
105105
}

src/Symfony/Component/TypeInfo/Tests/Type/ObjectTypeTest.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ public function testToString()
2222
$this->assertSame(self::class, (string) new ObjectType(self::class));
2323
}
2424

25-
public function testIsNullable()
25+
public function testGetTypeIdentifier(): void
2626
{
27-
$this->assertFalse((new ObjectType(self::class))->isNullable());
27+
$this->assertSame(TypeIdentifier::OBJECT, (new ObjectType(self::class))->getTypeIdentifier());
2828
}
2929

3030
public function testGetBaseType()

src/Symfony/Component/TypeInfo/Tests/Type/UnionTypeTest.php

+5-8
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ public function testAsNonNullable()
5656
$type = new UnionType(Type::int(), Type::object(\stdClass::class), Type::mixed());
5757
$this->assertInstanceOf(UnionType::class, $type->asNonNullable());
5858
$this->assertEquals([
59+
Type::object(\stdClass::class),
5960
Type::builtin(TypeIdentifier::ARRAY),
6061
Type::bool(),
6162
Type::float(),
6263
Type::int(),
6364
Type::object(),
6465
Type::resource(),
65-
Type::object(\stdClass::class),
6666
Type::string(),
6767
], $type->asNonNullable()->getTypes());
6868
}
@@ -97,13 +97,13 @@ public function testToString()
9797
$type = new UnionType(Type::int(), Type::string(), Type::float());
9898
$this->assertSame('float|int|string', (string) $type);
9999

100-
$type = new UnionType(Type::int(), Type::string(), Type::intersection(Type::float(), Type::bool()));
101-
$this->assertSame('(bool&float)|int|string', (string) $type);
100+
$type = new UnionType(Type::int(), Type::string(), Type::intersection(Type::object('Foo'), Type::object('Bar')));
101+
$this->assertSame('(Bar&Foo)|int|string', (string) $type);
102102
}
103103

104104
public function testIsNullable()
105105
{
106-
$this->assertFalse((new UnionType(Type::int(), Type::intersection(Type::float(), Type::int())))->isNullable());
106+
$this->assertFalse((new UnionType(Type::int(), Type::intersection(Type::object('Foo'), Type::object('Bar'))))->isNullable());
107107
$this->assertTrue((new UnionType(Type::int(), Type::null()))->isNullable());
108108
$this->assertTrue((new UnionType(Type::int(), Type::mixed()))->isNullable());
109109
}
@@ -114,14 +114,11 @@ public function testIsA()
114114
$this->assertFalse($type->isNullable());
115115
$this->assertFalse($type->isA(TypeIdentifier::ARRAY));
116116

117-
$type = new UnionType(Type::int(), Type::string(), Type::intersection(Type::float(), Type::bool()));
117+
$type = new UnionType(Type::int(), Type::string(), Type::intersection(Type::object('Foo'), Type::object('Bar')));
118118
$this->assertTrue($type->isA(TypeIdentifier::INT));
119119
$this->assertTrue($type->isA(TypeIdentifier::STRING));
120120
$this->assertFalse($type->isA(TypeIdentifier::FLOAT));
121121
$this->assertFalse($type->isA(TypeIdentifier::BOOL));
122-
123-
$type = new UnionType(Type::string(), Type::intersection(Type::int(), Type::int()));
124-
$this->assertTrue($type->isA(TypeIdentifier::INT));
125122
}
126123

127124
public function testProxiesMethodsToNonNullableType()

src/Symfony/Component/TypeInfo/Tests/TypeFactoryTest.php

+8-3
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,14 @@ public function testCreateUnion()
185185

186186
public function testCreateIntersection()
187187
{
188-
$this->assertEquals(new IntersectionType(new BuiltinType(TypeIdentifier::INT), new ObjectType(self::class)), Type::intersection(Type::int(), Type::object(self::class)));
189-
$this->assertEquals(new IntersectionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::STRING)), Type::intersection(Type::int(), Type::string(), Type::int()));
190-
$this->assertEquals(new IntersectionType(new BuiltinType(TypeIdentifier::INT), new BuiltinType(TypeIdentifier::STRING)), Type::intersection(Type::int(), Type::intersection(Type::int(), Type::string())));
188+
$this->assertEquals(
189+
new IntersectionType(new ObjectType('Foo'), new ObjectType('Bar')),
190+
Type::intersection(Type::object('Foo'), Type::object('Bar'))
191+
);
192+
$this->assertEquals(
193+
new IntersectionType(new ObjectType('Foo'), new ObjectType('Bar'), new ObjectType('Baz')),
194+
Type::intersection(Type::object('Baz'), Type::intersection(Type::object('Foo'), Type::object('Bar')))
195+
);
191196
}
192197

193198
public function testCreateNullable()

src/Symfony/Component/TypeInfo/Tests/TypeResolver/ReflectionTypeResolverTest.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public function testResolve(Type $expectedType, \ReflectionType $reflection, ?Ty
4444
/**
4545
* @return iterable<array{0: Type, 1: \ReflectionType, 2?: TypeContext}>
4646
*/
47-
public function resolveDataProvider(): iterable
47+
public static function resolveDataProvider(): iterable
4848
{
4949
$typeContext = (new TypeContextFactory())->createFromClassName(ReflectionExtractableDummy::class);
5050
$reflection = new \ReflectionClass(ReflectionExtractableDummy::class);
@@ -89,7 +89,7 @@ public function testCannotResolveClassKeywordsWithoutTypeContext(\ReflectionType
8989
/**
9090
* @return iterable<array{0: \ReflectionType}>
9191
*/
92-
public function classKeywordsTypesDataProvider(): iterable
92+
public static function classKeywordsTypesDataProvider(): iterable
9393
{
9494
$reflection = new \ReflectionClass(ReflectionExtractableDummy::class);
9595

src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public function __toString(): string
6060
/**
6161
* @return iterable<array{0: Type, 1: string, 2?: TypeContext}>
6262
*/
63-
public function resolveDataProvider(): iterable
63+
public static function resolveDataProvider(): iterable
6464
{
6565
$typeContextFactory = new TypeContextFactory(new StringTypeResolver());
6666

@@ -153,10 +153,10 @@ public function resolveDataProvider(): iterable
153153
yield [Type::union(Type::int(), Type::string()), 'int|string'];
154154

155155
// intersection
156-
yield [Type::intersection(Type::int(), Type::string()), 'int&string'];
156+
yield [Type::intersection(Type::object('DateTimeInterface'), Type::object('DateTimeImmutable')), 'DateTimeImmutable&DateTimeInterface'];
157157

158158
// DNF
159-
yield [Type::union(Type::int(), Type::intersection(Type::string(), Type::bool())), 'int|(string&bool)'];
159+
yield [Type::union(Type::intersection(Type::object('DateTimeInterface'), Type::object('DateTimeImmutable')), Type::int()), 'int|(DateTimeImmutable&DateTimeInterface)'];
160160

161161
// collection objects
162162
yield [Type::collection(Type::object(\Traversable::class)), \Traversable::class];

src/Symfony/Component/TypeInfo/Type.php

+2-7
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ abstract class Type implements \Stringable
2727

2828
abstract public function getBaseType(): BuiltinType|ObjectType;
2929

30+
abstract public function getTypeIdentifier(): TypeIdentifier;
31+
3032
/**
3133
* @param TypeIdentifier|class-string $subject
3234
*/
3335
abstract public function isA(TypeIdentifier|string $subject): bool;
3436

35-
abstract public function asNonNullable(): self;
36-
3737
/**
3838
* @param callable(Type): bool $callable
3939
*/
@@ -42,11 +42,6 @@ public function is(callable $callable): bool
4242
return $callable($this);
4343
}
4444

45-
public function isNullable(): bool
46-
{
47-
return $this->is(fn (Type $t): bool => $t->isA(TypeIdentifier::NULL) || $t->isA(TypeIdentifier::MIXED));
48-
}
49-
5045
/**
5146
* Graceful fallback for unexisting methods.
5247
*

0 commit comments

Comments
 (0)