Skip to content

Commit 5b7e474

Browse files
committed
Bleeding edge - check types in @method tags
1 parent 3e51899 commit 5b7e474

12 files changed

+526
-6
lines changed

conf/config.level2.neon

+10
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ rules:
4949
- PHPStan\Rules\PhpDoc\RequireExtendsDefinitionTraitRule
5050

5151
conditionalTags:
52+
PHPStan\Rules\Classes\MethodTagRule:
53+
phpstan.rules.rule: %featureToggles.absentTypeChecks%
54+
PHPStan\Rules\Classes\MethodTagTraitRule:
55+
phpstan.rules.rule: %featureToggles.absentTypeChecks%
5256
PHPStan\Rules\Classes\PropertyTagRule:
5357
phpstan.rules.rule: %featureToggles.absentTypeChecks%
5458
PHPStan\Rules\Classes\PropertyTagTraitRule:
@@ -79,6 +83,12 @@ services:
7983
tags:
8084
- phpstan.rules.rule
8185

86+
-
87+
class: PHPStan\Rules\Classes\MethodTagRule
88+
89+
-
90+
class: PHPStan\Rules\Classes\MethodTagTraitRule
91+
8292
-
8393
class: PHPStan\Rules\Classes\PropertyTagRule
8494

conf/config.neon

+5
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,11 @@ services:
917917
checkClassCaseSensitivity: %checkClassCaseSensitivity%
918918
absentTypeChecks: %featureToggles.absentTypeChecks%
919919

920+
-
921+
class: PHPStan\Rules\Classes\MethodTagCheck
922+
arguments:
923+
checkClassCaseSensitivity: %checkClassCaseSensitivity%
924+
920925
-
921926
class: PHPStan\Rules\Classes\PropertyTagCheck
922927
arguments:

src/PhpDoc/StubValidator.php

+7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
use PHPStan\Rules\Classes\LocalTypeAliasesCheck;
2727
use PHPStan\Rules\Classes\LocalTypeAliasesRule;
2828
use PHPStan\Rules\Classes\LocalTypeTraitAliasesRule;
29+
use PHPStan\Rules\Classes\MethodTagCheck;
30+
use PHPStan\Rules\Classes\MethodTagRule;
31+
use PHPStan\Rules\Classes\MethodTagTraitRule;
2932
use PHPStan\Rules\Classes\MixinRule;
3033
use PHPStan\Rules\Classes\PropertyTagCheck;
3134
use PHPStan\Rules\Classes\PropertyTagRule;
@@ -236,6 +239,10 @@ private function getRuleRegistry(Container $container): RuleRegistry
236239
if ((bool) $container->getParameter('featureToggles')['absentTypeChecks']) {
237240
$rules[] = new MissingMethodSelfOutTypeRule($missingTypehintCheck);
238241

242+
$methodTagCheck = new MethodTagCheck($reflectionProvider, $classNameCheck, $genericObjectTypeCheck, $missingTypehintCheck, $unresolvableTypeHelper, true);
243+
$rules[] = new MethodTagRule($methodTagCheck);
244+
$rules[] = new MethodTagTraitRule($methodTagCheck, $reflectionProvider);
245+
239246
$propertyTagCheck = new PropertyTagCheck($reflectionProvider, $classNameCheck, $genericObjectTypeCheck, $missingTypehintCheck, $unresolvableTypeHelper, true);
240247
$rules[] = new PropertyTagRule($propertyTagCheck);
241248
$rules[] = new PropertyTagTraitRule($propertyTagCheck, $reflectionProvider);

src/Reflection/ClassReflection.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -1745,7 +1745,7 @@ public function getPropertyTags(): array
17451745
}
17461746

17471747
/**
1748-
* @return array<MethodTag>
1748+
* @return array<string, MethodTag>
17491749
*/
17501750
public function getMethodTags(): array
17511751
{

src/Rules/Classes/MethodTagCheck.php

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Classes;
4+
5+
use PhpParser\Node\Stmt\ClassLike;
6+
use PHPStan\Internal\SprintfHelper;
7+
use PHPStan\Reflection\ClassReflection;
8+
use PHPStan\Reflection\ReflectionProvider;
9+
use PHPStan\Rules\ClassNameCheck;
10+
use PHPStan\Rules\ClassNameNodePair;
11+
use PHPStan\Rules\Generics\GenericObjectTypeCheck;
12+
use PHPStan\Rules\IdentifierRuleError;
13+
use PHPStan\Rules\MissingTypehintCheck;
14+
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
15+
use PHPStan\Rules\RuleErrorBuilder;
16+
use PHPStan\Type\Type;
17+
use PHPStan\Type\VerbosityLevel;
18+
use function array_merge;
19+
use function implode;
20+
use function sprintf;
21+
22+
final class MethodTagCheck
23+
{
24+
25+
public function __construct(
26+
private ReflectionProvider $reflectionProvider,
27+
private ClassNameCheck $classCheck,
28+
private GenericObjectTypeCheck $genericObjectTypeCheck,
29+
private MissingTypehintCheck $missingTypehintCheck,
30+
private UnresolvableTypeHelper $unresolvableTypeHelper,
31+
private bool $checkClassCaseSensitivity,
32+
)
33+
{
34+
}
35+
36+
/**
37+
* @return list<IdentifierRuleError>
38+
*/
39+
public function check(
40+
ClassReflection $classReflection,
41+
ClassLike $node,
42+
): array
43+
{
44+
$errors = [];
45+
foreach ($classReflection->getMethodTags() as $methodName => $methodTag) {
46+
$i = 0;
47+
foreach ($methodTag->getParameters() as $parameterName => $parameterTag) {
48+
$i++;
49+
$parameterDescription = sprintf('parameter #%d $%s', $i, $parameterName);
50+
foreach ($this->checkMethodType($classReflection, $methodName, $parameterDescription, $parameterTag->getType(), $node) as $error) {
51+
$errors[] = $error;
52+
}
53+
54+
if ($parameterTag->getDefaultValue() === null) {
55+
continue;
56+
}
57+
58+
foreach ($this->checkMethodType($classReflection, $methodName, sprintf('%s default value', $parameterDescription), $parameterTag->getDefaultValue(), $node) as $error) {
59+
$errors[] = $error;
60+
}
61+
}
62+
63+
foreach ($this->checkMethodType($classReflection, $methodName, 'return type', $methodTag->getReturnType(), $node) as $error) {
64+
$errors[] = $error;
65+
}
66+
}
67+
68+
return $errors;
69+
}
70+
71+
/**
72+
* @return list<IdentifierRuleError>
73+
*/
74+
private function checkMethodType(ClassReflection $classReflection, string $methodName, string $description, Type $type, ClassLike $node): array
75+
{
76+
if ($this->unresolvableTypeHelper->containsUnresolvableType($type)) {
77+
return [
78+
RuleErrorBuilder::message(sprintf(
79+
'PHPDoc tag @method for method %s::%s() %s contains unresolvable type.',
80+
$classReflection->getDisplayName(),
81+
$methodName,
82+
$description,
83+
))->identifier('methodTag.unresolvableType')
84+
->build(),
85+
];
86+
}
87+
88+
$escapedClassName = SprintfHelper::escapeFormatString($classReflection->getDisplayName());
89+
$escapedMethodName = SprintfHelper::escapeFormatString($methodName);
90+
$escapedDescription = SprintfHelper::escapeFormatString($description);
91+
92+
$errors = $this->genericObjectTypeCheck->check(
93+
$type,
94+
sprintf('PHPDoc tag @method for method %s::%s() %s contains generic type %%s but %%s %%s is not generic.', $escapedClassName, $escapedMethodName, $escapedDescription),
95+
sprintf('Generic type %%s in PHPDoc tag @method for method %s::%s() %s does not specify all template types of %%s %%s: %%s', $escapedClassName, $escapedMethodName, $escapedDescription),
96+
sprintf('Generic type %%s in PHPDoc tag @method for method %s::%s() %s specifies %%d template types, but %%s %%s supports only %%d: %%s', $escapedClassName, $escapedMethodName, $escapedDescription),
97+
sprintf('Type %%s in generic type %%s in PHPDoc tag @method for method %s::%s() %s is not subtype of template type %%s of %%s %%s.', $escapedClassName, $escapedMethodName, $escapedDescription),
98+
sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag @method for method %s::%s() %s is in conflict with %%s template type %%s of %%s %%s.', $escapedClassName, $escapedMethodName, $escapedDescription),
99+
sprintf('Call-site variance of %%s in generic type %%s in PHPDoc tag @method for method %s::%s() %s is redundant, template type %%s of %%s %%s has the same variance.', $escapedClassName, $escapedMethodName, $escapedDescription),
100+
);
101+
102+
foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($type) as [$innerName, $genericTypeNames]) {
103+
$errors[] = RuleErrorBuilder::message(sprintf(
104+
'PHPDoc tag @method for method %s::%s() %s contains generic %s but does not specify its types: %s',
105+
$classReflection->getDisplayName(),
106+
$methodName,
107+
$description,
108+
$innerName,
109+
implode(', ', $genericTypeNames),
110+
))
111+
->identifier('missingType.generics')
112+
->build();
113+
}
114+
115+
foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($type) as $iterableType) {
116+
$iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly());
117+
$errors[] = RuleErrorBuilder::message(sprintf(
118+
'%s %s has PHPDoc tag @method for method %s() %s with no value type specified in iterable type %s.',
119+
$classReflection->getClassTypeDescription(),
120+
$classReflection->getDisplayName(),
121+
$methodName,
122+
$description,
123+
$iterableTypeDescription,
124+
))
125+
->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP)
126+
->identifier('missingType.iterableValue')
127+
->build();
128+
}
129+
130+
foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($type) as $callableType) {
131+
$errors[] = RuleErrorBuilder::message(sprintf(
132+
'%s %s has PHPDoc tag @method for method %s() %s with no signature specified for %s.',
133+
$classReflection->getClassTypeDescription(),
134+
$classReflection->getDisplayName(),
135+
$methodName,
136+
$description,
137+
$callableType->describe(VerbosityLevel::typeOnly()),
138+
))->identifier('missingType.callable')->build();
139+
}
140+
141+
foreach ($type->getReferencedClasses() as $class) {
142+
if (!$this->reflectionProvider->hasClass($class)) {
143+
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @method for method %s::%s() %s contains unknown class %s.', $classReflection->getDisplayName(), $methodName, $description, $class))
144+
->identifier('class.notFound')
145+
->discoveringSymbolsTip()
146+
->build();
147+
} elseif ($this->reflectionProvider->getClass($class)->isTrait()) {
148+
$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @method for method %s::%s() %s contains invalid type %s.', $classReflection->getDisplayName(), $methodName, $description, $class))
149+
->identifier('methodTag.trait')
150+
->build();
151+
} else {
152+
$errors = array_merge(
153+
$errors,
154+
$this->classCheck->checkClassNames([
155+
new ClassNameNodePair($class, $node),
156+
], $this->checkClassCaseSensitivity),
157+
);
158+
}
159+
}
160+
161+
return $errors;
162+
}
163+
164+
}

src/Rules/Classes/MethodTagRule.php

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Classes;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\InClassNode;
8+
use PHPStan\Rules\Rule;
9+
10+
/**
11+
* @implements Rule<InClassNode>
12+
*/
13+
final class MethodTagRule implements Rule
14+
{
15+
16+
public function __construct(private MethodTagCheck $check)
17+
{
18+
}
19+
20+
public function getNodeType(): string
21+
{
22+
return InClassNode::class;
23+
}
24+
25+
public function processNode(Node $node, Scope $scope): array
26+
{
27+
return $this->check->check($node->getClassReflection(), $node->getOriginalNode());
28+
}
29+
30+
}
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Classes;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\ReflectionProvider;
8+
use PHPStan\Rules\Rule;
9+
10+
/**
11+
* @implements Rule<Node\Stmt\Trait_>
12+
*/
13+
final class MethodTagTraitRule implements Rule
14+
{
15+
16+
public function __construct(private MethodTagCheck $check, private ReflectionProvider $reflectionProvider)
17+
{
18+
}
19+
20+
public function getNodeType(): string
21+
{
22+
return Node\Stmt\Trait_::class;
23+
}
24+
25+
public function processNode(Node $node, Scope $scope): array
26+
{
27+
$traitName = $node->namespacedName;
28+
if ($traitName === null) {
29+
return [];
30+
}
31+
32+
if (!$this->reflectionProvider->hasClass($traitName->toString())) {
33+
return [];
34+
}
35+
36+
return $this->check->check($this->reflectionProvider->getClass($traitName->toString()), $node);
37+
}
38+
39+
}

0 commit comments

Comments
 (0)