Skip to content

Commit fc68c0f

Browse files
mad-brillerondrejmirtes
authored andcommitted
Add rule to check for existing classes, type aliases and shadowing of CallableType and ClosureType templates on methods and functions.
1 parent d8e208e commit fc68c0f

File tree

10 files changed

+497
-16
lines changed

10 files changed

+497
-16
lines changed

conf/config.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,9 @@ services:
991991
-
992992
class: PHPStan\Rules\PhpDoc\UnresolvableTypeHelper
993993

994+
-
995+
class: PHPStan\Rules\PhpDoc\GenericCallableRuleHelper
996+
994997
-
995998
class: PHPStan\Rules\PhpDoc\VarTagTypeRuleHelper
996999
arguments:

src/PhpDoc/StubValidator.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
use PHPStan\Rules\Methods\MissingMethodReturnTypehintRule;
5353
use PHPStan\Rules\Methods\OverridingMethodRule;
5454
use PHPStan\Rules\MissingTypehintCheck;
55+
use PHPStan\Rules\PhpDoc\GenericCallableRuleHelper;
5556
use PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule;
5657
use PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule;
5758
use PHPStan\Rules\PhpDoc\InvalidPhpDocTagValueRule;
@@ -185,6 +186,7 @@ private function getRuleRegistry(Container $container): RuleRegistry
185186
$fileTypeMapper,
186187
$genericObjectTypeCheck,
187188
$unresolvableTypeHelper,
189+
$container->getByType(GenericCallableRuleHelper::class),
188190
),
189191
new IncompatiblePropertyPhpDocTypeRule($genericObjectTypeCheck, $unresolvableTypeHelper),
190192
new InvalidPhpDocTagValueRule(

src/PhpDoc/TypeNodeResolver.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -927,13 +927,13 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi
927927
$returnType = $this->resolve($typeNode->returnType, $nameScope);
928928

929929
if ($mainType instanceof CallableType) {
930-
return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap);
930+
return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap, null, $templateTags);
931931

932932
} elseif (
933933
$mainType instanceof ObjectType
934934
&& $mainType->getClassName() === Closure::class
935935
) {
936-
return new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap);
936+
return new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, null, null, $templateTags);
937937
}
938938

939939
return new ErrorType();

src/Rules/Generics/TemplateTypeCheck.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ public function check(
9898
$classNameNodePairs = array_map(static fn (string $referencedClass): ClassNameNodePair => new ClassNameNodePair($referencedClass, $node), $boundType->getReferencedClasses());
9999
$messages = array_merge($messages, $this->classCheck->checkClassNames($classNameNodePairs, $this->checkClassCaseSensitivity));
100100

101-
$boundType = $templateTag->getBound();
102101
$boundTypeClass = get_class($boundType);
103102
if (
104103
$boundTypeClass !== MixedType::class
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PhpDoc;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\PhpDoc\Tag\TemplateTag;
8+
use PHPStan\Reflection\ClassReflection;
9+
use PHPStan\Rules\Generics\TemplateTypeCheck;
10+
use PHPStan\Rules\RuleError;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
use PHPStan\Type\CallableType;
13+
use PHPStan\Type\ClosureType;
14+
use PHPStan\Type\Generic\TemplateTypeScope;
15+
use PHPStan\Type\Type;
16+
use PHPStan\Type\TypeTraverser;
17+
use PHPStan\Type\VerbosityLevel;
18+
use function array_keys;
19+
use function sprintf;
20+
21+
class GenericCallableRuleHelper
22+
{
23+
24+
public function __construct(
25+
private TemplateTypeCheck $templateTypeCheck,
26+
)
27+
{
28+
}
29+
30+
/**
31+
* @param array<string, TemplateTag> $functionTemplateTags
32+
*
33+
* @return array<RuleError>
34+
*/
35+
public function check(
36+
Node $node,
37+
Scope $scope,
38+
string $location,
39+
Type $callableType,
40+
string $functionName,
41+
array $functionTemplateTags,
42+
?ClassReflection $classReflection,
43+
): array
44+
{
45+
$errors = [];
46+
47+
TypeTraverser::map($callableType, function (Type $type, callable $traverse) use (&$errors, $node, $scope, $location, $functionName, $functionTemplateTags, $classReflection) {
48+
if (!($type instanceof CallableType || $type instanceof ClosureType)) {
49+
return $traverse($type);
50+
}
51+
52+
$typeDescription = $type->describe(VerbosityLevel::precise());
53+
54+
$errors = $this->templateTypeCheck->check(
55+
$scope,
56+
$node,
57+
TemplateTypeScope::createWithAnonymousFunction(),
58+
$type->getTemplateTags(),
59+
sprintf('PHPDoc tag %s template of %s cannot have existing class %%s as its name.', $location, $typeDescription),
60+
sprintf('PHPDoc tag %s template of %s cannot have existing type alias %%s as its name.', $location, $typeDescription),
61+
sprintf('PHPDoc tag %s template %%s of %s has invalid bound type %%s.', $location, $typeDescription),
62+
sprintf('PHPDoc tag %s template %%s of %s with bound type %%s is not supported.', $location, $typeDescription),
63+
);
64+
65+
$templateTags = $type->getTemplateTags();
66+
67+
$functionDescription = sprintf('function %s', $functionName);
68+
$classDescription = null;
69+
if ($classReflection !== null) {
70+
$classDescription = $classReflection->getDisplayName();
71+
$functionDescription = sprintf('method %s::%s', $classDescription, $functionName);
72+
}
73+
74+
foreach (array_keys($functionTemplateTags) as $name) {
75+
if (!isset($templateTags[$name])) {
76+
continue;
77+
}
78+
79+
$errors[] = RuleErrorBuilder::message(sprintf(
80+
'PHPDoc tag %s template %s of %s shadows @template %s for %s.',
81+
$location,
82+
$name,
83+
$typeDescription,
84+
$name,
85+
$functionDescription,
86+
))->build();
87+
}
88+
89+
if ($classReflection !== null) {
90+
foreach (array_keys($classReflection->getTemplateTags()) as $name) {
91+
if (!isset($templateTags[$name])) {
92+
continue;
93+
}
94+
95+
$errors[] = RuleErrorBuilder::message(sprintf(
96+
'PHPDoc tag %s template %s of %s shadows @template %s for class %s.',
97+
$location,
98+
$name,
99+
$typeDescription,
100+
$name,
101+
$classDescription,
102+
))->build();
103+
}
104+
}
105+
106+
return $traverse($type);
107+
});
108+
109+
return $errors;
110+
}
111+
112+
}

src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public function __construct(
3131
private FileTypeMapper $fileTypeMapper,
3232
private GenericObjectTypeCheck $genericObjectTypeCheck,
3333
private UnresolvableTypeHelper $unresolvableTypeHelper,
34+
private GenericCallableRuleHelper $genericCallableRuleHelper,
3435
)
3536
{
3637
}
@@ -137,6 +138,16 @@ public function processNode(Node $node, Scope $scope): array
137138
),
138139
));
139140

141+
$errors = array_merge($errors, $this->genericCallableRuleHelper->check(
142+
$node,
143+
$scope,
144+
sprintf('%s for parameter $%s', $escapedTagName, $escapedParameterName),
145+
$phpDocParamType,
146+
$functionName,
147+
$resolvedPhpDoc->getTemplateTags(),
148+
$scope->isInClass() ? $scope->getClassReflection() : null,
149+
));
150+
140151
if ($phpDocParamTag instanceof ParamOutTag) {
141152
if (!$byRefParameters[$parameterName]) {
142153
$errors[] = RuleErrorBuilder::message(sprintf(
@@ -215,6 +226,16 @@ public function processNode(Node $node, Scope $scope): array
215226

216227
$errors[] = $errorBuilder->build();
217228
}
229+
230+
$errors = array_merge($errors, $this->genericCallableRuleHelper->check(
231+
$node,
232+
$scope,
233+
'@return',
234+
$phpDocReturnType,
235+
$functionName,
236+
$resolvedPhpDoc->getTemplateTags(),
237+
$scope->isInClass() ? $scope->getClassReflection() : null,
238+
));
218239
}
219240
}
220241

src/Type/CallableType.php

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPStan\Analyser\OutOfClassScope;
66
use PHPStan\Php\PhpVersion;
7+
use PHPStan\PhpDoc\Tag\TemplateTag;
78
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
89
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
910
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
@@ -62,13 +63,15 @@ class CallableType implements CompoundType, ParametersAcceptor
6263
/**
6364
* @api
6465
* @param array<int, ParameterReflection>|null $parameters
66+
* @param array<string, TemplateTag> $templateTags
6567
*/
6668
public function __construct(
6769
?array $parameters = null,
6870
?Type $returnType = null,
6971
private bool $variadic = true,
7072
?TemplateTypeMap $templateTypeMap = null,
7173
?TemplateTypeMap $resolvedTemplateTypeMap = null,
74+
private array $templateTags = [],
7275
)
7376
{
7477
$this->parameters = $parameters ?? [];
@@ -78,6 +81,14 @@ public function __construct(
7881
$this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty();
7982
}
8083

84+
/**
85+
* @return array<string, TemplateTag>
86+
*/
87+
public function getTemplateTags(): array
88+
{
89+
return $this->templateTags;
90+
}
91+
8192
/**
8293
* @return string[]
8394
*/
@@ -202,6 +213,7 @@ function (): string {
202213
$this->variadic,
203214
$this->templateTypeMap,
204215
$this->resolvedTemplateTypeMap,
216+
$this->templateTags,
205217
);
206218

207219
return $printer->print($selfWithoutParameterNames->toPhpDocNode());
@@ -369,6 +381,7 @@ public function traverse(callable $cb): Type
369381
$this->isVariadic(),
370382
$this->templateTypeMap,
371383
$this->resolvedTemplateTypeMap,
384+
$this->templateTags,
372385
);
373386
}
374387

@@ -417,6 +430,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type
417430
$this->isVariadic(),
418431
$this->templateTypeMap,
419432
$this->resolvedTemplateTypeMap,
433+
$this->templateTags,
420434
);
421435
}
422436

@@ -568,14 +582,10 @@ public function toPhpDocNode(): TypeNode
568582
}
569583

570584
$templateTags = [];
571-
foreach ($this->templateTypeMap->getTypes() as $templateName => $templateType) {
572-
if (!$templateType instanceof TemplateType) {
573-
throw new ShouldNotHappenException();
574-
}
575-
585+
foreach ($this->templateTags as $templateName => $templateTag) {
576586
$templateTags[] = new TemplateTagValueNode(
577587
$templateName,
578-
$templateType->getBound()->toPhpDocNode(),
588+
$templateTag->getBound()->toPhpDocNode(),
579589
'',
580590
);
581591
}

src/Type/ClosureType.php

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Closure;
66
use PHPStan\Analyser\OutOfClassScope;
77
use PHPStan\Php\PhpVersion;
8+
use PHPStan\PhpDoc\Tag\TemplateTag;
89
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
910
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
1011
use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode;
@@ -24,7 +25,6 @@
2425
use PHPStan\Reflection\PropertyReflection;
2526
use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
2627
use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
27-
use PHPStan\ShouldNotHappenException;
2828
use PHPStan\TrinaryLogic;
2929
use PHPStan\Type\Constant\ConstantArrayType;
3030
use PHPStan\Type\Constant\ConstantBooleanType;
@@ -67,6 +67,7 @@ class ClosureType implements TypeWithClassName, ParametersAcceptor
6767
/**
6868
* @api
6969
* @param array<int, ParameterReflection> $parameters
70+
* @param array<string, TemplateTag> $templateTags
7071
*/
7172
public function __construct(
7273
private array $parameters,
@@ -75,6 +76,7 @@ public function __construct(
7576
?TemplateTypeMap $templateTypeMap = null,
7677
?TemplateTypeMap $resolvedTemplateTypeMap = null,
7778
?TemplateTypeVarianceMap $callSiteVarianceMap = null,
79+
private array $templateTags = [],
7880
)
7981
{
8082
$this->objectType = new ObjectType(Closure::class);
@@ -83,6 +85,14 @@ public function __construct(
8385
$this->callSiteVarianceMap = $callSiteVarianceMap ?? TemplateTypeVarianceMap::createEmpty();
8486
}
8587

88+
/**
89+
* @return array<string, TemplateTag>
90+
*/
91+
public function getTemplateTags(): array
92+
{
93+
return $this->templateTags;
94+
}
95+
8696
public function getClassName(): string
8797
{
8898
return $this->objectType->getClassName();
@@ -194,6 +204,7 @@ function (): string {
194204
$this->templateTypeMap,
195205
$this->resolvedTemplateTypeMap,
196206
$this->callSiteVarianceMap,
207+
$this->templateTags,
197208
);
198209

199210
return $printer->print($selfWithoutParameterNames->toPhpDocNode());
@@ -452,6 +463,7 @@ public function traverse(callable $cb): Type
452463
$this->templateTypeMap,
453464
$this->resolvedTemplateTypeMap,
454465
$this->callSiteVarianceMap,
466+
$this->templateTags,
455467
);
456468
}
457469

@@ -489,6 +501,10 @@ public function traverseSimultaneously(Type $right, callable $cb): Type
489501
$parameters,
490502
$cb($this->getReturnType(), $right->getReturnType()),
491503
$this->isVariadic(),
504+
$this->templateTypeMap,
505+
$this->resolvedTemplateTypeMap,
506+
$this->callSiteVarianceMap,
507+
$this->templateTags,
492508
);
493509
}
494510

@@ -621,14 +637,10 @@ public function toPhpDocNode(): TypeNode
621637
}
622638

623639
$templateTags = [];
624-
foreach ($this->templateTypeMap->getTypes() as $templateName => $templateType) {
625-
if (!$templateType instanceof TemplateType) {
626-
throw new ShouldNotHappenException();
627-
}
628-
640+
foreach ($this->templateTags as $templateName => $templateTag) {
629641
$templateTags[] = new TemplateTagValueNode(
630642
$templateName,
631-
$templateType->getBound()->toPhpDocNode(),
643+
$templateTag->getBound()->toPhpDocNode(),
632644
'',
633645
);
634646
}

0 commit comments

Comments
 (0)