1+ <?php
2+
3+ declare (strict_types=1 );
4+
5+ /**
6+ * @template T of positive-int
7+ * @param T $value
8+ * @return T
9+ */
10+ function testPositiveIntBound (mixed $ value ): mixed
11+ {
12+ return $ value ;
13+ }
14+
15+ /**
16+ * @template T of non-empty-string
17+ * @param T $text
18+ * @return T
19+ */
20+ function testNonEmptyStringBound (mixed $ text ): mixed
21+ {
22+ return $ text ;
23+ }
24+
25+ /**
26+ * @template T of array{id: positive-int, role: 'admin'|'user'}
27+ * @param T $data
28+ * @return T
29+ */
30+ function testArrayShapeBound (mixed $ data ): mixed
31+ {
32+ return $ data ;
33+ }
34+
35+ /**
36+ * @template T of list<positive-int>
37+ * @param T $items
38+ * @return T
39+ */
40+ function testListBound (mixed $ items ): mixed
41+ {
42+ return $ items ;
43+ }
44+
45+ /**
46+ * @template T of int<1, 100>
47+ * @param T $percentage
48+ * @return T
49+ */
50+ function testIntRangeBound (mixed $ percentage ): mixed
51+ {
52+ return $ percentage ;
53+ }
54+
55+ /**
56+ * @template T of 'active'|'pending'
57+ * @param T $status
58+ * @return T
59+ */
60+ function testLiteralUnionBound (mixed $ status ): mixed
61+ {
62+ return $ status ;
63+ }
64+
65+ /**
66+ * @template T of Countable
67+ * @param class-string<T> $class
68+ */
69+ function testClassStringInterfaceBound (string $ class ): bool
70+ {
71+ return true ;
72+ }
73+
74+ /**
75+ * Default template with object upper bound (@template T of object = stdClass)
76+ *
77+ * @template T of object = stdClass
78+ *
79+ * @param mixed $value
80+ * @return T
81+ */
82+ function testDefaultObjectBound (mixed $ value ): mixed
83+ {
84+ return $ value ;
85+ }
86+
87+ /**
88+ * Default template with int-range upper bound (@template T of int<1, 100> = 50)
89+ *
90+ * @template T of int<1, 100> = 50
91+ *
92+ * @param mixed $value
93+ * @return T
94+ */
95+ function testDefaultIntRangeBound (mixed $ value ): mixed
96+ {
97+ return $ value ;
98+ }
99+
100+ /**
101+ * Template where T is inferred from $input, overriding default stdClass
102+ *
103+ * @template T of object = stdClass
104+ *
105+ * @param T $input
106+ * @param mixed $valueToReturn
107+ * @return T
108+ */
109+ function testInferredOverridesDefault (mixed $ input , mixed $ valueToReturn ): mixed
110+ {
111+ return $ valueToReturn ;
112+ }
113+
114+ describe ('Generic Template Bounds Stress Test ' , function () {
115+ test ('validates positive-int scalar bound ' , function () {
116+ expect (testPositiveIntBound (42 ))->toBe (42 );
117+
118+ expect (fn () => testPositiveIntBound (-10 ))
119+ ->toThrow (\TypeError::class, 'positive-int ' );
120+ expect (fn () => testPositiveIntBound (0 ))
121+ ->toThrow (\TypeError::class, 'positive-int ' );
122+ });
123+
124+ test ('validates non-empty-string scalar bound ' , function () {
125+ expect (testNonEmptyStringBound ('hello ' ))->toBe ('hello ' );
126+
127+ expect (fn () => testNonEmptyStringBound ('' ))
128+ ->toThrow (\TypeError::class, 'non-empty-string ' );
129+ });
130+
131+ test ('validates array shape template bound ' , function () {
132+ $ valid = ['id ' => 10 , 'role ' => 'admin ' ];
133+ expect (testArrayShapeBound ($ valid ))->toBe ($ valid );
134+ expect (fn () => testArrayShapeBound (['id ' => -5 , 'role ' => 'admin ' ]))
135+ ->toThrow (\TypeError::class, "['id'] " );
136+
137+ expect (fn () => testArrayShapeBound (['id ' => 10 , 'role ' => 'superadmin ' ]))
138+ ->toThrow (\TypeError::class, "['role'] " );
139+
140+ expect (fn () => testArrayShapeBound (['id ' => 10 ]))
141+ ->toThrow (\TypeError::class, "missing required key 'role' " );
142+ });
143+
144+ test ('validates list template bound ' , function () {
145+ expect (testListBound ([10 , 20 , 30 ]))->toBe ([10 , 20 , 30 ]);
146+
147+ expect (fn () => testListBound ([10 , -5 , 30 ]))
148+ ->toThrow (\TypeError::class, '[1] ' );
149+
150+ expect (fn () => testListBound (['key ' => 10 ]))
151+ ->toThrow (\TypeError::class, 'must be a list ' );
152+ });
153+
154+ test ('validates int range template bound ' , function () {
155+ expect (testIntRangeBound (50 ))->toBe (50 );
156+
157+ expect (fn () => testIntRangeBound (150 ))
158+ ->toThrow (\TypeError::class, '<= 100 ' );
159+ expect (fn () => testIntRangeBound (0 ))
160+ ->toThrow (\TypeError::class, '>= 1 ' );
161+ });
162+
163+ test ('validates literal union enum template bound ' , function () {
164+ expect (testLiteralUnionBound ('active ' ))->toBe ('active ' );
165+ expect (testLiteralUnionBound ('pending ' ))->toBe ('pending ' );
166+
167+ expect (fn () => testLiteralUnionBound ('archived ' ))
168+ ->toThrow (\TypeError::class, "('active' | 'pending') " );
169+ });
170+
171+ test ('validates class-string<T of Countable> interface bound ' , function () {
172+ expect (testClassStringInterfaceBound (ArrayObject::class))->toBeTrue ();
173+
174+ expect (fn () => testClassStringInterfaceBound (stdClass::class))
175+ ->toThrow (\TypeError::class, 'must be a class-string of Countable ' );
176+ });
177+
178+ test ('uses default template type when template T is unbound ' , function () {
179+ $ std = new stdClass ();
180+ expect (testDefaultObjectBound ($ std ))->toBe ($ std );
181+
182+ expect (fn () => testDefaultObjectBound (new DateTime ()))
183+ ->toThrow (\TypeError::class, 'Return value ' );
184+ });
185+
186+ test ('uses default scalar literal type when template T is unbound ' , function () {
187+ expect (testDefaultIntRangeBound (50 ))->toBe (50 );
188+
189+
190+ expect (fn () => testDefaultIntRangeBound (99 ))
191+ ->toThrow (\TypeError::class, 'Return value ' );
192+ });
193+
194+ test ('inferred template parameter from argument overrides default template type ' , function () {
195+ $ dt = new DateTime ();
196+
197+ expect (testInferredOverridesDefault ($ dt , $ dt ))->toBe ($ dt );
198+
199+ expect (fn () => testInferredOverridesDefault ($ dt , new stdClass ()))
200+ ->toThrow (\TypeError::class, 'Return value ' );
201+ });
202+ });
0 commit comments