Skip to content

Commit fd300d5

Browse files
committed
more tests
1 parent 56e8cfb commit fd300d5

File tree

4 files changed

+257
-0
lines changed

4 files changed

+257
-0
lines changed

test-generic-aliases.php

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,153 @@ public function getUser(): array
8080
return ['data' => ['id' => 1, 'name' => 'Alice'], 'status' => 200];
8181
}
8282
}
83+
84+
// ---------------------------------------------------------------------------
85+
// @return of generic alias
86+
// ---------------------------------------------------------------------------
87+
88+
/**
89+
* @phpstan-type Page<TItem of object> array{items: list<TItem>, total: int, page: int}
90+
*/
91+
final class PagedRepo
92+
{
93+
/**
94+
* @return Page<\stdClass>
95+
*/
96+
public function getPage(): array
97+
{
98+
dumpType($this->getPage()); // should show array{items: list<stdClass>, total: int, page: int}
99+
return ['items' => [], 'total' => 0, 'page' => 1];
100+
}
101+
}
102+
103+
// ---------------------------------------------------------------------------
104+
// @var property annotation
105+
// ---------------------------------------------------------------------------
106+
107+
/**
108+
* @phpstan-type Config<TValue> array{key: string, value: TValue}
109+
*/
110+
final class Settings
111+
{
112+
/** @var Config<int> */
113+
public array $timeout = ['key' => 'timeout', 'value' => 30];
114+
115+
/** @var Config<string> */
116+
public array $name = ['key' => 'name', 'value' => 'default'];
117+
118+
public function check(): void
119+
{
120+
dumpType($this->timeout['value']); // int
121+
dumpType($this->name['value']); // string
122+
}
123+
}
124+
125+
// ---------------------------------------------------------------------------
126+
// Nested generic alias (alias referencing another generic alias with type args)
127+
// ---------------------------------------------------------------------------
128+
129+
/**
130+
* @phpstan-type Item<T> array{id: int, data: T}
131+
* @phpstan-type ItemList<T> list<Item<T>>
132+
*/
133+
final class ItemRepo
134+
{
135+
/**
136+
* @param ItemList<string> $items
137+
*/
138+
public function process(array $items): void
139+
{
140+
dumpType($items); // list<array{id: int, data: string}>
141+
dumpType($items[0]['data']); // string
142+
}
143+
}
144+
145+
// ---------------------------------------------------------------------------
146+
// @phpstan-import-type of a generic alias, then used with type args
147+
// ---------------------------------------------------------------------------
148+
149+
/**
150+
* @phpstan-import-type Pair from PairHolder
151+
*/
152+
final class PairConsumer
153+
{
154+
/**
155+
* @param Pair<int, bool> $p
156+
*/
157+
public function check(array $p): void
158+
{
159+
dumpType($p['first']); // int
160+
dumpType($p['second']); // bool
161+
}
162+
}
163+
164+
// ---------------------------------------------------------------------------
165+
// Default type arg — using alias WITHOUT args should be OK (default kicks in)
166+
// ---------------------------------------------------------------------------
167+
168+
/**
169+
* @phpstan-type WithDefault<T = string> array{value: T}
170+
*/
171+
final class DefaultConsumer
172+
{
173+
/**
174+
* @param WithDefault<int> $explicit no error: type arg provided
175+
* @param WithDefault $implicit no error: T has a default
176+
*/
177+
public function check(array $explicit, array $implicit): void
178+
{
179+
dumpType($explicit['value']); // int
180+
dumpType($implicit['value']); // BUG: shows raw TemplateType instead of string — default not applied when alias used without args
181+
}
182+
}
183+
184+
// ---------------------------------------------------------------------------
185+
// Generic alias in a standalone function (not a class method)
186+
// ---------------------------------------------------------------------------
187+
188+
/**
189+
* @phpstan-type Range<T of int|float> array{min: T, max: T}
190+
*/
191+
final class RangeHolder
192+
{
193+
/**
194+
* @param Range<int> $r
195+
* @return Range<float>
196+
*/
197+
public function convert(array $r): array
198+
{
199+
dumpType($r['min']); // int
200+
return ['min' => (float) $r['min'], 'max' => (float) $r['max']];
201+
}
202+
}
203+
204+
// ---------------------------------------------------------------------------
205+
// Too many type args — should error
206+
// ---------------------------------------------------------------------------
207+
208+
/**
209+
* @phpstan-type Single<T> array{value: T}
210+
*/
211+
final class TooManyArgs
212+
{
213+
/**
214+
* @param Single<int, string> $x TODO: should error — Single takes 1 type arg, 2 given (not yet detected)
215+
*/
216+
public function check(array $x): void {}
217+
}
218+
219+
// ---------------------------------------------------------------------------
220+
// Too few required type args (partial application of multi-param alias) — should error
221+
// ---------------------------------------------------------------------------
222+
223+
/**
224+
* @phpstan-type KeyValue<TKey of array-key, TValue> array{key: TKey, value: TValue}
225+
*/
226+
final class TooFewArgs
227+
{
228+
/**
229+
* @param KeyValue<string> $x TODO: should error — KeyValue requires 2 type args (not yet detected)
230+
*/
231+
public function check(array $x): void {}
232+
}

tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,22 @@ public function testBug7662(): void
148148
]);
149149
}
150150

151+
public function testGenericTypeAliasMissingTypehint(): void
152+
{
153+
$this->analyse([__DIR__ . '/data/generic-type-alias-missing-typehint.php'], [
154+
[
155+
'Method GenericTypeAliasMissingTypehint\RawUsage::check() has parameter $b with generic type alias Filter but does not specify its types: TItem',
156+
18,
157+
],
158+
[
159+
'Method GenericTypeAliasMissingTypehint\PartialDefault::check() has parameter $noArgs with generic type alias Pair but does not specify its types: TFirst',
160+
61,
161+
],
162+
[
163+
'Method GenericTypeAliasMissingTypehint\ImportedRawUsage::check() has parameter $bad with generic type alias Filter but does not specify its types: TItem',
164+
77,
165+
],
166+
]);
167+
}
168+
151169
}

tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,14 @@ public function testInheritPhpDocReturnTypeWithNarrowerNativeReturnType(): void
128128
$this->analyse([__DIR__ . '/data/inherit-phpdoc-return-type-with-narrower-native-return-type.php'], []);
129129
}
130130

131+
public function testGenericTypeAliasMissingTypehint(): void
132+
{
133+
$this->analyse([__DIR__ . '/data/generic-type-alias-missing-typehint.php'], [
134+
[
135+
'Method GenericTypeAliasMissingTypehint\RawUsage::getRaw() return type with generic type alias Filter does not specify its types: TItem',
136+
28,
137+
],
138+
]);
139+
}
140+
131141
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace GenericTypeAliasMissingTypehint;
4+
5+
// ---------------------------------------------------------------------------
6+
// Raw usage of generic alias (no type args, no default) → should error
7+
// ---------------------------------------------------------------------------
8+
9+
/**
10+
* @phpstan-type Filter<TItem> array{items: list<TItem>}
11+
*/
12+
class RawUsage
13+
{
14+
/**
15+
* @param Filter<string> $a OK: type arg provided
16+
* @param Filter $b ERROR: Filter requires 1 type arg
17+
*/
18+
public function check(array $a, array $b): void {}
19+
20+
/**
21+
* @return Filter<int> OK
22+
*/
23+
public function getFiltered(): array { return ['items' => []]; }
24+
25+
/**
26+
* @return Filter ERROR: Filter requires 1 type arg
27+
*/
28+
public function getRaw(): array { return ['items' => []]; }
29+
}
30+
31+
// ---------------------------------------------------------------------------
32+
// Alias with a default — bare usage should NOT error
33+
// ---------------------------------------------------------------------------
34+
35+
/**
36+
* @phpstan-type WithDefault<T = string> array{value: T}
37+
*/
38+
class DefaultedUsage
39+
{
40+
/**
41+
* @param WithDefault $implicit OK: T has a default
42+
* @param WithDefault<int> $explicit OK: T provided
43+
*/
44+
public function check(array $implicit, array $explicit): void {}
45+
}
46+
47+
// ---------------------------------------------------------------------------
48+
// Two-param alias, one required, one defaulted
49+
// ---------------------------------------------------------------------------
50+
51+
/**
52+
* @phpstan-type Pair<TFirst, TSecond = bool> array{first: TFirst, second: TSecond}
53+
*/
54+
class PartialDefault
55+
{
56+
/**
57+
* @param Pair<string> $oneArg OK: TFirst provided, TSecond defaults to bool
58+
* @param Pair<string, int> $twoArgs OK: both provided
59+
* @param Pair $noArgs ERROR: TFirst has no default
60+
*/
61+
public function check(array $oneArg, array $twoArgs, array $noArgs): void {}
62+
}
63+
64+
// ---------------------------------------------------------------------------
65+
// Imported generic alias — raw usage should also error
66+
// ---------------------------------------------------------------------------
67+
68+
/**
69+
* @phpstan-import-type Filter from RawUsage
70+
*/
71+
class ImportedRawUsage
72+
{
73+
/**
74+
* @param Filter<bool> $ok OK
75+
* @param Filter $bad ERROR: Filter requires 1 type arg
76+
*/
77+
public function check(array $ok, array $bad): void {}
78+
}
79+

0 commit comments

Comments
 (0)