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