Skip to content

Commit f9a2648

Browse files
Implement template default types
Co-authored-by: Richard van Velzen <[email protected]> Co-authored-by: Richard van Velzen <[email protected]>
1 parent c79b69a commit f9a2648

File tree

83 files changed

+861
-67
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+861
-67
lines changed

src/Analyser/MutatingScope.php

+6
Original file line numberDiff line numberDiff line change
@@ -2539,6 +2539,7 @@ private function createFirstClassCallable(
25392539
$templateTags[$templateType->getName()] = new TemplateTag(
25402540
$templateType->getName(),
25412541
$templateType->getBound(),
2542+
$templateType->getDefault(),
25422543
$templateType->getVariance(),
25432544
);
25442545
}
@@ -5606,6 +5607,11 @@ private function exactInstantiation(New_ $node, string $className): ?Type
56065607
$list[] = $templateType;
56075608
continue;
56085609
}
5610+
$default = $tag->getDefault();
5611+
if ($default !== null) {
5612+
$list[] = $default;
5613+
continue;
5614+
}
56095615
$bound = $tag->getBound();
56105616
if ($bound instanceof MixedType && $bound->isExplicitMixed()) {
56115617
$bound = new MixedType(false);

src/Dependency/DependencyResolver.php

+11
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,17 @@ private function addClassToDependencies(string $className, array &$dependenciesR
531531
}
532532
$dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass);
533533
}
534+
535+
$default = $templateTag->getDefault();
536+
if ($default === null) {
537+
continue;
538+
}
539+
foreach ($default->getReferencedClasses() as $referencedClass) {
540+
if (!$this->reflectionProvider->hasClass($referencedClass)) {
541+
continue;
542+
}
543+
$dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass);
544+
}
534545
}
535546

536547
foreach ($classReflection->getPropertyTags() as $propertyTag) {

src/PhpDoc/PhpDocNodeResolver.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope):
176176
$templateType->bound !== null
177177
? $this->typeNodeResolver->resolve($templateType->bound, $nameScope)
178178
: new MixedType(),
179+
$templateType->default !== null
180+
? $this->typeNodeResolver->resolve($templateType->default, $nameScope)
181+
: null,
179182
TemplateTypeVariance::createInvariant(),
180183
);
181184
}
@@ -327,9 +330,12 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope
327330
}
328331
}
329332

333+
$nameScopeWithoutCurrent = $nameScope->unsetTemplateType($valueNode->name);
334+
330335
$resolved[$valueNode->name] = new TemplateTag(
331336
$valueNode->name,
332-
$valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScope->unsetTemplateType($valueNode->name)) : new MixedType(true),
337+
$valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScopeWithoutCurrent) : new MixedType(true),
338+
$valueNode->default !== null ? $this->typeNodeResolver->resolve($valueNode->default, $nameScopeWithoutCurrent) : null,
333339
$variance,
334340
);
335341
$resolvedPrefix[$valueNode->name] = $prefix;

src/PhpDoc/Tag/TemplateTag.php

+6-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class TemplateTag
1515
/**
1616
* @param non-empty-string $name
1717
*/
18-
public function __construct(private string $name, private Type $bound, private TemplateTypeVariance $variance)
18+
public function __construct(private string $name, private Type $bound, private ?Type $default, private TemplateTypeVariance $variance)
1919
{
2020
}
2121

@@ -32,6 +32,11 @@ public function getBound(): Type
3232
return $this->bound;
3333
}
3434

35+
public function getDefault(): ?Type
36+
{
37+
return $this->default;
38+
}
39+
3540
public function getVariance(): TemplateTypeVariance
3641
{
3742
return $this->variance;

src/PhpDoc/TypeNodeResolver.php

+13
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
use Traversable;
107107
use function array_key_exists;
108108
use function array_map;
109+
use function array_values;
109110
use function count;
110111
use function explode;
111112
use function get_class;
@@ -792,6 +793,15 @@ static function (string $variance): TemplateTypeVariance {
792793

793794
$classReflection = $this->getReflectionProvider()->getClass($mainTypeClassName);
794795
if ($classReflection->isGeneric()) {
796+
$templateTypes = array_values($classReflection->getTemplateTypeMap()->getTypes());
797+
for ($i = count($genericTypes), $templateTypesCount = count($templateTypes); $i < $templateTypesCount; $i++) {
798+
$templateType = $templateTypes[$i];
799+
if (!$templateType instanceof TemplateType || $templateType->getDefault() === null) {
800+
continue;
801+
}
802+
$genericTypes[] = $templateType->getDefault();
803+
}
804+
795805
if (in_array($mainTypeClassName, [
796806
Traversable::class,
797807
IteratorAggregate::class,
@@ -910,6 +920,9 @@ private function resolveCallableTypeNode(CallableTypeNode $typeNode, NameScope $
910920
$templateType->bound !== null
911921
? $this->resolve($templateType->bound, $nameScope)
912922
: new MixedType(),
923+
$templateType->default !== null
924+
? $this->resolve($templateType->default, $nameScope)
925+
: null,
913926
TemplateTypeVariance::createInvariant(),
914927
);
915928
}

src/Reflection/ClassReflection.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -1442,7 +1442,7 @@ public function typeMapFromList(array $types): TemplateTypeMap
14421442
$map = [];
14431443
$i = 0;
14441444
foreach ($resolvedPhpDoc->getTemplateTags() as $tag) {
1445-
$map[$tag->getName()] = $types[$i] ?? $tag->getBound();
1445+
$map[$tag->getName()] = $types[$i] ?? $tag->getDefault() ?? $tag->getBound();
14461446
$i++;
14471447
}
14481448

@@ -1479,7 +1479,7 @@ public function typeMapToList(TemplateTypeMap $typeMap): array
14791479

14801480
$list = [];
14811481
foreach ($resolvedPhpDoc->getTemplateTags() as $tag) {
1482-
$list[] = $typeMap->getType($tag->getName()) ?? $tag->getBound();
1482+
$list[] = $typeMap->getType($tag->getName()) ?? $tag->getDefault() ?? $tag->getBound();
14831483
}
14841484

14851485
return $list;

src/Rules/Classes/LocalTypeAliasesCheck.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
use PHPStan\Type\VerbosityLevel;
2525
use function array_key_exists;
2626
use function array_merge;
27-
use function implode;
2827
use function in_array;
2928
use function sprintf;
3029

@@ -211,7 +210,7 @@ public function checkInTraitDefinitionContext(ClassReflection $reflection): arra
211210
$reflection->getDisplayName(),
212211
$aliasName,
213212
$name,
214-
implode(', ', $genericTypeNames),
213+
$genericTypeNames,
215214
))
216215
->identifier('missingType.generics')
217216
->build();

src/Rules/Classes/MethodTagCheck.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use PHPStan\Type\Type;
1717
use PHPStan\Type\VerbosityLevel;
1818
use function array_merge;
19-
use function implode;
2019
use function sprintf;
2120

2221
final class MethodTagCheck
@@ -174,7 +173,7 @@ private function checkMethodTypeInTraitDefinitionContext(ClassReflection $classR
174173
$methodName,
175174
$description,
176175
$innerName,
177-
implode(', ', $genericTypeNames),
176+
$genericTypeNames,
178177
))
179178
->identifier('missingType.generics')
180179
->build();

src/Rules/Classes/MixinCheck.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
use PHPStan\Rules\RuleErrorBuilder;
1515
use PHPStan\Type\VerbosityLevel;
1616
use function array_merge;
17-
use function implode;
1817
use function sprintf;
1918

2019
final class MixinCheck
@@ -90,7 +89,7 @@ public function checkInTraitDefinitionContext(ClassReflection $classReflection):
9089
$errors[] = RuleErrorBuilder::message(sprintf(
9190
'PHPDoc tag @mixin contains generic %s but does not specify its types: %s',
9291
$innerName,
93-
implode(', ', $genericTypeNames),
92+
$genericTypeNames,
9493
))
9594
->identifier('missingType.generics')
9695
->build();

src/Rules/Classes/PropertyTagCheck.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
use PHPStan\Type\Type;
1919
use PHPStan\Type\VerbosityLevel;
2020
use function array_merge;
21-
use function implode;
2221
use function sprintf;
2322

2423
final class PropertyTagCheck
@@ -155,7 +154,7 @@ private function checkPropertyTypeInTraitDefinitionContext(ClassReflection $clas
155154
$classReflection->getDisplayName(),
156155
$propertyName,
157156
$innerName,
158-
implode(', ', $genericTypeNames),
157+
$genericTypeNames,
159158
))
160159
->identifier('missingType.generics')
161160
->build();

src/Rules/Constants/MissingClassConstantTypehintRule.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use PHPStan\ShouldNotHappenException;
1313
use PHPStan\Type\VerbosityLevel;
1414
use function array_merge;
15-
use function implode;
1615
use function sprintf;
1716

1817
/**
@@ -76,7 +75,7 @@ private function processSingleConstant(ClassReflection $classReflection, string
7675
$constantReflection->getDeclaringClass()->getDisplayName(),
7776
$constantName,
7877
$name,
79-
implode(', ', $genericTypeNames),
78+
$genericTypeNames,
8079
))
8180
->identifier('missingType.generics')
8281
->build();

src/Rules/FunctionCallParametersCheck.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty
432432
$type = $type->resolve();
433433
}
434434

435-
if ($type instanceof TemplateType) {
435+
if ($type instanceof TemplateType && $type->getDefault() === null) {
436436
$returnTemplateTypes[$type->getName()] = true;
437437
return $type;
438438
}
@@ -444,7 +444,7 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty
444444
$parameterTemplateTypes = [];
445445
foreach ($originalParametersAcceptor->getParameters() as $parameter) {
446446
TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$parameterTemplateTypes): Type {
447-
if ($type instanceof TemplateType) {
447+
if ($type instanceof TemplateType && $type->getDefault() === null) {
448448
$parameterTemplateTypes[$type->getName()] = true;
449449
return $type;
450450
}

src/Rules/Functions/MissingFunctionParameterTypehintRule.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
use PHPStan\Type\MixedType;
1414
use PHPStan\Type\Type;
1515
use PHPStan\Type\VerbosityLevel;
16-
use function implode;
1716
use function sprintf;
1817

1918
/**
@@ -100,7 +99,7 @@ private function checkFunctionParameter(FunctionReflection $functionReflection,
10099
$functionReflection->getName(),
101100
$parameterMessage,
102101
$name,
103-
implode(', ', $genericTypeNames),
102+
$genericTypeNames,
104103
))
105104
->identifier('missingType.generics')
106105
->build();

src/Rules/Functions/MissingFunctionReturnTypehintRule.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use PHPStan\Rules\RuleErrorBuilder;
1111
use PHPStan\Type\MixedType;
1212
use PHPStan\Type\VerbosityLevel;
13-
use function implode;
1413
use function sprintf;
1514

1615
/**
@@ -58,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array
5857
'Function %s() return type with generic %s does not specify its types: %s',
5958
$functionReflection->getName(),
6059
$name,
61-
implode(', ', $genericTypeNames),
60+
$genericTypeNames,
6261
))
6362
->identifier('missingType.generics')
6463
->build();

src/Rules/Generics/ClassTemplateTypeRule.php

+3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ public function processNode(Node $node, Scope $scope): array
4949
sprintf('PHPDoc tag @template for %s cannot have existing type alias %%s as its name.', $displayName),
5050
sprintf('PHPDoc tag @template %%s for %s has invalid bound type %%s.', $displayName),
5151
sprintf('PHPDoc tag @template %%s for %s with bound type %%s is not supported.', $displayName),
52+
sprintf('PHPDoc tag @template %%s for %s has invalid default type %%s.', $displayName),
53+
sprintf('Default type %%s in PHPDoc tag @template %%s for %s is not subtype of bound type %%s.', $displayName),
54+
sprintf('PHPDoc tag @template %%s for %s does not have a default type but follows an optional @template %%s.', $displayName),
5255
);
5356
}
5457

src/Rules/Generics/FunctionTemplateTypeRule.php

+3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ public function processNode(Node $node, Scope $scope): array
6060
sprintf('PHPDoc tag @template for function %s() cannot have existing type alias %%s as its name.', $escapedFunctionName),
6161
sprintf('PHPDoc tag @template %%s for function %s() has invalid bound type %%s.', $escapedFunctionName),
6262
sprintf('PHPDoc tag @template %%s for function %s() with bound type %%s is not supported.', $escapedFunctionName),
63+
sprintf('PHPDoc tag @template %%s for function %s() has invalid default type %%s.', $escapedFunctionName),
64+
sprintf('Default type %%s in PHPDoc tag @template %%s for function %s() is not subtype of bound type %%s.', $escapedFunctionName),
65+
sprintf('PHPDoc tag @template %%s for function %s() does not have a default type but follows an optional @template %%s.', $escapedFunctionName),
6366
);
6467
}
6568

src/Rules/Generics/GenericAncestorsCheck.php

+15-1
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
1010
use PHPStan\Rules\RuleErrorBuilder;
1111
use PHPStan\Type\Generic\GenericObjectType;
12+
use PHPStan\Type\Generic\TemplateType;
1213
use PHPStan\Type\Generic\TemplateTypeVariance;
1314
use PHPStan\Type\Generic\TypeProjectionHelper;
1415
use PHPStan\Type\Type;
1516
use PHPStan\Type\VerbosityLevel;
1617
use function array_fill_keys;
18+
use function array_filter;
1719
use function array_keys;
1820
use function array_map;
1921
use function array_merge;
@@ -173,10 +175,22 @@ public function check(
173175
continue;
174176
}
175177

178+
$templateTypes = $unusedNameClassReflection->getTemplateTypeMap()->getTypes();
179+
$templateTypesCount = count($templateTypes);
180+
$requiredTemplateTypesCount = count(array_filter($templateTypes, static fn (Type $type) => $type instanceof TemplateType && $type->getDefault() === null));
181+
if ($requiredTemplateTypesCount === 0) {
182+
continue;
183+
}
184+
185+
$templateTypesList = implode(', ', array_keys($templateTypes));
186+
if ($requiredTemplateTypesCount !== $templateTypesCount) {
187+
$templateTypesList .= sprintf(' (%d-%d required)', $requiredTemplateTypesCount, $templateTypesCount);
188+
}
189+
176190
$messages[] = RuleErrorBuilder::message(sprintf(
177191
$genericClassInNonGenericObjectType,
178192
$unusedName,
179-
implode(', ', array_keys($unusedNameClassReflection->getTemplateTypeMap()->getTypes())),
193+
$templateTypesList,
180194
))
181195
->identifier('missingType.generics')
182196
->build();

src/Rules/Generics/GenericObjectTypeCheck.php

+15-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPStan\Type\Type;
1515
use PHPStan\Type\TypeTraverser;
1616
use PHPStan\Type\VerbosityLevel;
17+
use function array_filter;
1718
use function array_keys;
1819
use function array_values;
1920
use function count;
@@ -59,27 +60,37 @@ public function check(
5960
$genericTypeVariances = $genericType->getVariances();
6061
$templateTypesCount = count($templateTypes);
6162
$genericTypeTypesCount = count($genericTypeTypes);
62-
if ($templateTypesCount > $genericTypeTypesCount) {
63+
$requiredTemplateTypesCount = count(array_filter($templateTypes, static fn (Type $type) => $type instanceof TemplateType && $type->getDefault() === null));
64+
if ($requiredTemplateTypesCount > $genericTypeTypesCount) {
65+
$templateTypesList = implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes()));
66+
if ($requiredTemplateTypesCount !== $templateTypesCount) {
67+
$templateTypesList .= sprintf(' (%d-%d required).', $requiredTemplateTypesCount, $templateTypesCount);
68+
}
69+
6370
$messages[] = RuleErrorBuilder::message(sprintf(
6471
$notEnoughTypesMessage,
6572
$genericType->describe(VerbosityLevel::typeOnly()),
6673
$classLikeDescription,
6774
$classReflection->getDisplayName(false),
68-
implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())),
75+
$templateTypesList,
6976
))->identifier('generics.lessTypes')->build();
7077
} elseif ($templateTypesCount < $genericTypeTypesCount) {
78+
$templateTypesList = implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes()));
79+
if ($requiredTemplateTypesCount !== $templateTypesCount) {
80+
$templateTypesList .= sprintf(' (%d-%d required)', $requiredTemplateTypesCount, $templateTypesCount);
81+
}
82+
7183
$messages[] = RuleErrorBuilder::message(sprintf(
7284
$extraTypesMessage,
7385
$genericType->describe(VerbosityLevel::typeOnly()),
7486
$genericTypeTypesCount,
7587
$classLikeDescription,
7688
$classReflection->getDisplayName(false),
7789
$templateTypesCount,
78-
implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())),
90+
$templateTypesList,
7991
))->identifier('generics.moreTypes')->build();
8092
}
8193

82-
$templateTypesCount = count($templateTypes);
8394
for ($i = 0; $i < $templateTypesCount; $i++) {
8495
if (!isset($genericTypeTypes[$i])) {
8596
continue;

src/Rules/Generics/InterfaceTemplateTypeRule.php

+3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ public function processNode(Node $node, Scope $scope): array
4646
sprintf('PHPDoc tag @template for interface %s cannot have existing type alias %%s as its name.', $escapadInterfaceName),
4747
sprintf('PHPDoc tag @template %%s for interface %s has invalid bound type %%s.', $escapadInterfaceName),
4848
sprintf('PHPDoc tag @template %%s for interface %s with bound type %%s is not supported.', $escapadInterfaceName),
49+
sprintf('PHPDoc tag @template %%s for interface %s has invalid default type %%s.', $escapadInterfaceName),
50+
sprintf('Default type %%s in PHPDoc tag @template %%s for interface %s is not subtype of bound type %%s.', $escapadInterfaceName),
51+
sprintf('PHPDoc tag @template %%s for interface %s does not have a default type but follows an optional @template %%s.', $escapadInterfaceName),
4952
);
5053
}
5154

src/Rules/Generics/MethodTagTemplateTypeCheck.php

+3
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ public function check(
6161
sprintf('PHPDoc tag @method template for method %s::%s() cannot have existing type alias %%s as its name.', $escapedClassName, $escapedMethodName),
6262
sprintf('PHPDoc tag @method template %%s for method %s::%s() has invalid bound type %%s.', $escapedClassName, $escapedMethodName),
6363
sprintf('PHPDoc tag @method template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName),
64+
sprintf('PHPDoc tag @method template %%s for method %s::%s() has invalid default type %%s', $escapedClassName, $escapedMethodName),
65+
sprintf('Default type %%s in PHPDoc tag @method template %%s for method %s::%s() is not subtype of bound type %%s', $escapedClassName, $escapedMethodName),
66+
sprintf('PHPDoc tag @template %%s for method %s::%s() does not have a default type but follows an optional @template %%s.', $escapedClassName, $escapedMethodName),
6467
));
6568

6669
foreach (array_keys($methodTemplateTags) as $name) {

0 commit comments

Comments
 (0)