Skip to content

Commit 2d0ee50

Browse files
committed
tests for invalud usage
1 parent fd300d5 commit 2d0ee50

File tree

7 files changed

+194
-20
lines changed

7 files changed

+194
-20
lines changed

src/PhpDoc/TypeNodeResolver.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
use PHPStan\Type\VoidType;
117117
use Traversable;
118118
use function array_key_exists;
119+
use function array_filter;
119120
use function array_map;
120121
use function array_values;
121122
use function count;
@@ -515,6 +516,15 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco
515516
}
516517

517518
if (!$nameScope->shouldBypassTypeAliases()) {
519+
// Handle a generic alias referenced without type arguments.
520+
// resolveWithDefaults() applies declared defaults so params with defaults are
521+
// substituted, while required params remain as TemplateType placeholders
522+
// (which getRawGenericTypeAliasesUsage() later detects and reports).
523+
$genericAlias = $this->findGenericTypeAlias($typeNode->name, $nameScope);
524+
if ($genericAlias !== null) {
525+
return $genericAlias->resolveWithDefaults($this);
526+
}
527+
518528
$typeAlias = $this->getTypeAliasResolver()->resolveTypeAlias($typeNode->name, $nameScope);
519529
if ($typeAlias !== null) {
520530
return $typeAlias;
@@ -868,6 +878,15 @@ static function (string $variance): TemplateTypeVariance {
868878
// falling through to class-based generic resolution.
869879
$genericTypeAlias = $this->findGenericTypeAlias($typeNode->type->name, $nameScope);
870880
if ($genericTypeAlias !== null) {
881+
$templateNodes = $genericTypeAlias->getTemplateTagValueNodes();
882+
$totalParams = count($templateNodes);
883+
$requiredParams = count(array_filter($templateNodes, static fn ($tvn) => $tvn->default === null));
884+
$providedArgs = count($genericTypes);
885+
886+
if ($providedArgs > $totalParams || $providedArgs < $requiredParams) {
887+
return new ErrorType();
888+
}
889+
871890
return $genericTypeAlias->resolveWithArgs($this, $genericTypes);
872891
}
873892

src/Type/TypeAlias.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
99
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
1010
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
11+
use PHPStan\Type\Generic\TemplateType;
1112
use PHPStan\Type\Generic\TemplateTypeFactory;
1213
use PHPStan\Type\Generic\TemplateTypeHelper;
1314
use PHPStan\Type\Generic\TemplateTypeMap;
1415
use PHPStan\Type\Generic\TemplateTypeScope;
1516
use PHPStan\Type\Generic\TemplateTypeVariance;
1617
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
18+
use PHPStan\Type\TypeTraverser;
1719
use function array_map;
1820
use function array_values;
1921
use function count;
@@ -112,6 +114,41 @@ public function resolveWithArgs(TypeNodeResolver $typeNodeResolver, array $args)
112114
);
113115
}
114116

117+
/**
118+
* Resolves the alias body applying default values for template params that declare one.
119+
* Template params without defaults remain as TemplateType placeholders so that callers
120+
* (e.g. MissingTypehintCheck) can detect bare generic alias usage.
121+
*/
122+
public function resolveWithDefaults(TypeNodeResolver $typeNodeResolver): Type
123+
{
124+
$baseType = $this->resolve($typeNodeResolver);
125+
126+
if (count($this->templateTagValueNodes) === 0) {
127+
return $baseType;
128+
}
129+
130+
// Collect default values for params that declare one.
131+
$defaultsMap = [];
132+
foreach ($this->templateTagValueNodes as $tvn) {
133+
if ($tvn->default !== null) {
134+
$defaultsMap[$tvn->name] = $typeNodeResolver->resolve($tvn->default, $this->nameScope);
135+
}
136+
}
137+
138+
if (count($defaultsMap) === 0) {
139+
return $baseType;
140+
}
141+
142+
// Replace only TemplateType instances scoped to THIS alias that have a declared default.
143+
$aliasName = $this->aliasName;
144+
return TypeTraverser::map($baseType, static function (Type $type, callable $traverse) use ($defaultsMap, $aliasName): Type {
145+
if ($type instanceof TemplateType && $type->getScope()->getTypeAliasName() === $aliasName && isset($defaultsMap[$type->getName()])) {
146+
return $defaultsMap[$type->getName()];
147+
}
148+
return $traverse($type);
149+
});
150+
}
151+
115152
/**
116153
* Builds a NameScope augmented with TemplateType placeholders for each declared template param,
117154
* so the alias body can reference them (e.g. `TFilter` resolves to a TemplateType).

test-generic-aliases.php

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// ---------------------------------------------------------------------------
44
// Generic @phpstan-type demo
55
// ---------------------------------------------------------------------------
6-
use function PHPStan\dumpType;
76

87
/**
98
* @template ProviderFilter of array<string, mixed>
@@ -33,7 +32,6 @@ final class SkuProvider extends Provider
3332
#[\Override]
3433
public function find(array $request): array
3534
{
36-
// dumpType($request);
3735
// PHPStan now knows $request is array{filters?: array{skuId?: int, condition?: string}, ...}
3836
$filters = $request['filters'] ?? [];
3937

@@ -58,8 +56,8 @@ final class PairHolder
5856
*/
5957
public function use(array $pair): void
6058
{
61-
echo $pair['first']; // string
62-
echo $pair['second']; // int
59+
echo $pair['first']; // string
60+
echo (string) $pair['second']; // int → cast to string for echo
6361
}
6462
}
6563

@@ -91,11 +89,10 @@ public function getUser(): array
9189
final class PagedRepo
9290
{
9391
/**
94-
* @return Page<\stdClass>
92+
* @return Page<\stdClass> // resolves to array{items: list<stdClass>, total: int, page: int}
9593
*/
9694
public function getPage(): array
9795
{
98-
dumpType($this->getPage()); // should show array{items: list<stdClass>, total: int, page: int}
9996
return ['items' => [], 'total' => 0, 'page' => 1];
10097
}
10198
}
@@ -117,8 +114,8 @@ final class Settings
117114

118115
public function check(): void
119116
{
120-
dumpType($this->timeout['value']); // int
121-
dumpType($this->name['value']); // string
117+
// $this->timeout['value'] int
118+
// $this->name['value'] string
122119
}
123120
}
124121

@@ -133,12 +130,11 @@ public function check(): void
133130
final class ItemRepo
134131
{
135132
/**
136-
* @param ItemList<string> $items
133+
* @param ItemList<string> $items // list<array{id: int, data: string}>
137134
*/
138135
public function process(array $items): void
139136
{
140-
dumpType($items); // list<array{id: int, data: string}>
141-
dumpType($items[0]['data']); // string
137+
// $items[0]['data'] — string
142138
}
143139
}
144140

@@ -156,8 +152,8 @@ final class PairConsumer
156152
*/
157153
public function check(array $p): void
158154
{
159-
dumpType($p['first']); // int
160-
dumpType($p['second']); // bool
155+
// $p['first'] int
156+
// $p['second'] bool
161157
}
162158
}
163159

@@ -172,12 +168,12 @@ final class DefaultConsumer
172168
{
173169
/**
174170
* @param WithDefault<int> $explicit no error: type arg provided
175-
* @param WithDefault $implicit no error: T has a default
171+
* @param WithDefault $implicit no error: T has a default (string)
176172
*/
177173
public function check(array $explicit, array $implicit): void
178174
{
179-
dumpType($explicit['value']); // int
180-
dumpType($implicit['value']); // BUG: shows raw TemplateType instead of string default not applied when alias used without args
175+
// $explicit['value'] int
176+
// $implicit['value']string (default applied ✓)
181177
}
182178
}
183179

@@ -191,12 +187,12 @@ public function check(array $explicit, array $implicit): void
191187
final class RangeHolder
192188
{
193189
/**
194-
* @param Range<int> $r
190+
* @param Range<int> $r
195191
* @return Range<float>
196192
*/
197193
public function convert(array $r): array
198194
{
199-
dumpType($r['min']); // int
195+
// $r['min'] int
200196
return ['min' => (float) $r['min'], 'max' => (float) $r['max']];
201197
}
202198
}
@@ -211,7 +207,8 @@ public function convert(array $r): array
211207
final class TooManyArgs
212208
{
213209
/**
214-
* @param Single<int, string> $x TODO: should error — Single takes 1 type arg, 2 given (not yet detected)
210+
* @param Single<int, string> $x ERROR: Single takes 1 type arg, 2 given
211+
* @phpstan-ignore parameter.unresolvableType, missingType.iterableValue
215212
*/
216213
public function check(array $x): void {}
217214
}
@@ -226,7 +223,8 @@ public function check(array $x): void {}
226223
final class TooFewArgs
227224
{
228225
/**
229-
* @param KeyValue<string> $x TODO: should error — KeyValue requires 2 type args (not yet detected)
226+
* @param KeyValue<string> $x ERROR: KeyValue requires 2 type args, 1 given
227+
* @phpstan-ignore parameter.unresolvableType, missingType.iterableValue
230228
*/
231229
public function check(array $x): void {}
232230
}

tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,20 @@ public function testGenericTypeAliasMissingTypehint(): void
166166
]);
167167
}
168168

169+
public function testGenericTypeAliasWrongArgCount(): void
170+
{
171+
$this->analyse([__DIR__ . '/../PhpDoc/data/generic-type-alias-wrong-arg-count.php'], [
172+
[
173+
'Method GenericTypeAliasWrongArgCount\TooManyArgs::badParam() has parameter $x with no value type specified in iterable type array.',
174+
17,
175+
MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP,
176+
],
177+
[
178+
'Method GenericTypeAliasWrongArgCount\TooFewArgs::badParam() has parameter $x with no value type specified in iterable type array.',
179+
47,
180+
MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP,
181+
],
182+
]);
183+
}
184+
169185
}

tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,20 @@ public function testGenericTypeAliasMissingTypehint(): void
138138
]);
139139
}
140140

141+
public function testGenericTypeAliasWrongArgCount(): void
142+
{
143+
$this->analyse([__DIR__ . '/../PhpDoc/data/generic-type-alias-wrong-arg-count.php'], [
144+
[
145+
'Method GenericTypeAliasWrongArgCount\TooManyArgs::badReturn() return type has no value type specified in iterable type array.',
146+
22,
147+
MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP,
148+
],
149+
[
150+
'Method GenericTypeAliasWrongArgCount\TooFewArgs::badReturn() return type has no value type specified in iterable type array.',
151+
52,
152+
MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP,
153+
],
154+
]);
155+
}
156+
141157
}

tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,4 +497,26 @@ public function testBug11463b(): void
497497
$this->analyse([__DIR__ . '/data/bug-11463b.php'], []);
498498
}
499499

500+
public function testGenericTypeAliasWrongArgCount(): void
501+
{
502+
$this->analyse([__DIR__ . '/data/generic-type-alias-wrong-arg-count.php'], [
503+
[
504+
'PHPDoc tag @param for parameter $x contains unresolvable type.',
505+
17,
506+
],
507+
[
508+
'PHPDoc tag @return contains unresolvable type.',
509+
22,
510+
],
511+
[
512+
'PHPDoc tag @param for parameter $x contains unresolvable type.',
513+
47,
514+
],
515+
[
516+
'PHPDoc tag @return contains unresolvable type.',
517+
52,
518+
],
519+
]);
520+
}
521+
500522
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace GenericTypeAliasWrongArgCount;
4+
5+
// ---------------------------------------------------------------------------
6+
// Too many type args
7+
// ---------------------------------------------------------------------------
8+
9+
/**
10+
* @phpstan-type Single<T> array{value: T}
11+
*/
12+
final class TooManyArgs
13+
{
14+
/**
15+
* @param Single<int, string> $x
16+
*/
17+
public function badParam(array $x): void {}
18+
19+
/**
20+
* @return Single<int, string>
21+
*/
22+
public function badReturn(): array { return ['value' => 1]; }
23+
24+
/**
25+
* @param Single<int> $ok
26+
*/
27+
public function goodParam(array $ok): void {}
28+
29+
/**
30+
* @return Single<int>
31+
*/
32+
public function goodReturn(): array { return ['value' => 1]; }
33+
}
34+
35+
// ---------------------------------------------------------------------------
36+
// Too few required type args (partial application of multi-param alias)
37+
// ---------------------------------------------------------------------------
38+
39+
/**
40+
* @phpstan-type KeyVal<TKey of array-key, TValue> array{key: TKey, value: TValue}
41+
*/
42+
final class TooFewArgs
43+
{
44+
/**
45+
* @param KeyVal<string> $x
46+
*/
47+
public function badParam(array $x): void {}
48+
49+
/**
50+
* @return KeyVal<string>
51+
*/
52+
public function badReturn(): array { return ['key' => 'k', 'value' => 'v']; }
53+
54+
/**
55+
* @param KeyVal<string, int> $ok
56+
*/
57+
public function goodParam(array $ok): void {}
58+
59+
/**
60+
* @return KeyVal<string, int>
61+
*/
62+
public function goodReturn(): array { return ['key' => 'k', 'value' => 1]; }
63+
}
64+
65+
66+

0 commit comments

Comments
 (0)