Skip to content

Commit 6fd6f91

Browse files
committed
Refactor TemplateManager variance checks for intersection types; simplify logic for covariant checks. Add ComplexUnionAndIntersectionSubtypesTest to validate union and intersection subtype assignability.
1 parent 0cfbf35 commit 6fd6f91

2 files changed

Lines changed: 166 additions & 21 deletions

File tree

src/Resolver/TemplateManager.php

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,16 +1061,6 @@ private static function checkExistingUnionVariance(UnionTypeNode $existing, Type
10611061

10621062
private static function checkExpectedIntersectionVariance(TypeNode $existing, IntersectionTypeNode $expected, string $variance): bool
10631063
{
1064-
if ($variance === GenericTypeNode::VARIANCE_COVARIANT) {
1065-
foreach ($expected->types as $intersectionMember) {
1066-
if (! self::checkVariance($existing, $intersectionMember, $variance)) {
1067-
return false;
1068-
}
1069-
}
1070-
1071-
return true;
1072-
}
1073-
10741064
if ($variance === GenericTypeNode::VARIANCE_CONTRAVARIANT) {
10751065
foreach ($expected->types as $intersectionMember) {
10761066
if (self::checkVariance($existing, $intersectionMember, $variance)) {
@@ -1081,21 +1071,17 @@ private static function checkExpectedIntersectionVariance(TypeNode $existing, In
10811071
return true;
10821072
}
10831073

1084-
return false;
1074+
foreach ($expected->types as $intersectionMember) {
1075+
if (! self::checkVariance($existing, $intersectionMember, GenericTypeNode::VARIANCE_COVARIANT)) {
1076+
return false;
1077+
}
1078+
}
1079+
1080+
return true;
10851081
}
10861082

10871083
private static function checkExistingIntersectionVariance(IntersectionTypeNode $existing, TypeNode $expected, string $variance): bool
10881084
{
1089-
if ($variance === GenericTypeNode::VARIANCE_COVARIANT) {
1090-
foreach ($existing->types as $existingMember) {
1091-
if (self::checkVariance($existingMember, $expected, $variance)) {
1092-
return true;
1093-
}
1094-
}
1095-
1096-
return true;
1097-
}
1098-
10991085
if ($variance === GenericTypeNode::VARIANCE_CONTRAVARIANT) {
11001086
foreach ($existing->types as $existingMember) {
11011087
if (! self::checkVariance($existingMember, $expected, $variance)) {
@@ -1106,6 +1092,12 @@ private static function checkExistingIntersectionVariance(IntersectionTypeNode $
11061092
return true;
11071093
}
11081094

1095+
foreach ($existing->types as $existingMember) {
1096+
if (self::checkVariance($existingMember, $expected, GenericTypeNode::VARIANCE_COVARIANT)) {
1097+
return true;
1098+
}
1099+
}
1100+
11091101
return false;
11101102
}
11111103

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use TypePHP\Exception\TypeError;
6+
use TypePHP\Tests\Fixtures\Domain\Animal;
7+
use TypePHP\Tests\Fixtures\Domain\Car;
8+
use TypePHP\Tests\Fixtures\Domain\Cat;
9+
use TypePHP\Tests\Fixtures\Domain\Dog;
10+
11+
class Bird extends Animal {}
12+
class Fish extends Animal {}
13+
14+
/**
15+
* Fixture representing an object that implements THREE interfaces
16+
*/
17+
class TripleInterfaceObject implements Countable, ArrayAccess, Iterator
18+
{
19+
private array $data = ['a' => 1];
20+
public function count(): int { return count($this->data); }
21+
public function offsetExists(mixed $offset): bool { return isset($this->data[$offset]); }
22+
public function offsetGet(mixed $offset): mixed { return $this->data[$offset] ?? null; }
23+
public function offsetSet(mixed $offset, mixed $value): void { $this->data[$offset] = $value; }
24+
public function offsetUnset(mixed $offset): void { unset($this->data[$offset]); }
25+
public function rewind(): void { reset($this->data); }
26+
public function current(): mixed { return current($this->data); }
27+
public function key(): mixed { return key($this->data); }
28+
public function next(): void { next($this->data); }
29+
public function valid(): bool { return key($this->data) !== null; }
30+
}
31+
32+
class DoubleInterfaceObject implements Countable, ArrayAccess
33+
{
34+
private array $data = ['a' => 1];
35+
public function count(): int { return count($this->data); }
36+
public function offsetExists(mixed $offset): bool { return isset($this->data[$offset]); }
37+
public function offsetGet(mixed $offset): mixed { return $this->data[$offset] ?? null; }
38+
public function offsetSet(mixed $offset, mixed $value): void { $this->data[$offset] = $value; }
39+
public function offsetUnset(mixed $offset): void { unset($this->data[$offset]); }
40+
}
41+
42+
/**
43+
* @template T
44+
*/
45+
class TypeSetHolder
46+
{
47+
/** @var array<int, T> */
48+
public array $items = [];
49+
}
50+
51+
describe('Complex Union, Intersection, and DNF Subtyping Guarantees', function () {
52+
describe('Union Subset Assignability', function () {
53+
test('allows assigning subset class union into superset class union (Dog|Cat into Dog|Cat|Bird)', function () {
54+
/** @var TypeSetHolder<Dog|Cat|Bird> $broadContainer */
55+
$broadContainer = new TypeSetHolder();
56+
57+
/** @var TypeSetHolder<Dog|Cat> $narrowContainer */
58+
$narrowContainer = new TypeSetHolder();
59+
60+
$broadContainer = $narrowContainer;
61+
expect($broadContainer)->toBe($narrowContainer);
62+
});
63+
64+
test('allows assigning subset string literal union into superset string literal union', function () {
65+
/** @var TypeSetHolder<'admin'|'editor'|'viewer'|'guest'> $broadRoles */
66+
$broadRoles = new TypeSetHolder();
67+
68+
/** @var TypeSetHolder<'admin'|'editor'> $narrowRoles */
69+
$narrowRoles = new TypeSetHolder();
70+
71+
$broadRoles = $narrowRoles;
72+
expect($broadRoles)->toBe($narrowRoles);
73+
});
74+
75+
test('allows assigning subset integer literal union into superset integer union', function () {
76+
/** @var TypeSetHolder<1|2|3|4|5> $broadNumbers */
77+
$broadNumbers = new TypeSetHolder();
78+
79+
/** @var TypeSetHolder<1|2> $narrowNumbers */
80+
$narrowNumbers = new TypeSetHolder();
81+
82+
$broadNumbers = $narrowNumbers;
83+
expect($broadNumbers)->toBe($narrowNumbers);
84+
});
85+
86+
test('strictly rejects assigning broader union into narrower subset union', function () {
87+
/** @var TypeSetHolder<Dog|Cat> $narrowContainer */
88+
$narrowContainer = new TypeSetHolder();
89+
90+
/** @var TypeSetHolder<Dog|Cat|Bird> $broadContainer */
91+
$broadContainer = new TypeSetHolder();
92+
93+
// Assigning broader (Dog|Cat|Bird) into narrower (Dog|Cat) must fail!
94+
expect(function () use (&$narrowContainer, $broadContainer) {
95+
$narrowContainer = $broadContainer;
96+
})->toThrow(TypeError::class);
97+
});
98+
});
99+
100+
describe('Intersection Subtyping (More Specific Intersection into Broader Intersection)', function () {
101+
test('allows triple-interface object into variable expecting double-interface intersection', function () {
102+
/** @var Countable&ArrayAccess $expected */
103+
$expected = new TripleInterfaceObject();
104+
105+
expect($expected)->toBeInstanceOf(TripleInterfaceObject::class);
106+
});
107+
108+
test('allows generic container of triple-interface objects into container expecting double-interface intersection', function () {
109+
/** @var TypeSetHolder<Countable&ArrayAccess> $container */
110+
$container = new TypeSetHolder();
111+
112+
/** @var TypeSetHolder<Countable&ArrayAccess&Iterator> $tripleContainer */
113+
$tripleContainer = new TypeSetHolder();
114+
115+
$container = $tripleContainer;
116+
expect($container)->toBe($tripleContainer);
117+
});
118+
119+
test('strictly rejects object missing one required interface of the intersection', function () {
120+
expect(function () {
121+
/** @var Countable&ArrayAccess&Iterator $strictContainer */
122+
$strictContainer = new DoubleInterfaceObject();
123+
})->toThrow(TypeError::class);
124+
});
125+
});
126+
127+
describe('Disjunctive Normal Form (DNF) Subtyping ((A&B) | (C&D))', function () {
128+
test('allows triple-interface object into DNF union of intersections', function () {
129+
/** @var (Countable&ArrayAccess)|(Iterator&Countable) $dnfTarget */
130+
$dnfTarget = new TripleInterfaceObject();
131+
132+
expect($dnfTarget)->toBeInstanceOf(TripleInterfaceObject::class);
133+
});
134+
135+
test('allows generic container of triple-interface objects into container expecting DNF union', function () {
136+
/** @var TypeSetHolder<(Countable&ArrayAccess)|(Iterator&Countable)> $dnfContainer */
137+
$dnfContainer = new TypeSetHolder();
138+
139+
/** @var TypeSetHolder<Countable&ArrayAccess&Iterator> $tripleContainer */
140+
$tripleContainer = new TypeSetHolder();
141+
142+
$dnfContainer = $tripleContainer;
143+
expect($dnfContainer)->toBe($tripleContainer);
144+
});
145+
146+
test('strictly rejects object failing all branches of the DNF union', function () {
147+
expect(function () {
148+
/** @var (Countable&ArrayAccess)|(Iterator&Countable) $dnfTarget */
149+
$dnfTarget = new Car();
150+
})->toThrow(TypeError::class);
151+
});
152+
});
153+
});

0 commit comments

Comments
 (0)