Skip to content

Commit 50ff7bc

Browse files
committed
Extract IncompatiblePhpDocTypeCheck from IncompatiblePhpDocTypeRule
1 parent bc044b7 commit 50ff7bc

File tree

5 files changed

+265
-216
lines changed

5 files changed

+265
-216
lines changed

conf/config.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,9 @@ services:
10211021
-
10221022
class: PHPStan\Rules\PhpDoc\GenericCallableRuleHelper
10231023

1024+
-
1025+
class: PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeCheck
1026+
10241027
-
10251028
class: PHPStan\Rules\PhpDoc\VarTagTypeRuleHelper
10261029
arguments:

src/PhpDoc/StubValidator.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
use PHPStan\Rules\PhpDoc\GenericCallableRuleHelper;
8181
use PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule;
8282
use PHPStan\Rules\PhpDoc\IncompatibleParamImmediatelyInvokedCallableRule;
83+
use PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeCheck;
8384
use PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule;
8485
use PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule;
8586
use PHPStan\Rules\PhpDoc\IncompatibleSelfOutTypeRule;
@@ -225,7 +226,7 @@ private function getRuleRegistry(Container $container): RuleRegistry
225226
new MethodTagTemplateTypeRule($methodTagTemplateTypeCheck),
226227
new MethodSignatureVarianceRule($varianceCheck),
227228
new TraitTemplateTypeRule($fileTypeMapper, $templateTypeCheck),
228-
new IncompatiblePhpDocTypeRule($fileTypeMapper, $genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper),
229+
new IncompatiblePhpDocTypeRule($fileTypeMapper, new IncompatiblePhpDocTypeCheck($genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper)),
229230
new IncompatiblePropertyPhpDocTypeRule($genericObjectTypeCheck, $unresolvableTypeHelper, $genericCallableRuleHelper),
230231
new InvalidPhpDocTagValueRule(
231232
$container->getByType(Lexer::class),
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
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\Internal\SprintfHelper;
8+
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
9+
use PHPStan\PhpDoc\Tag\ParamOutTag;
10+
use PHPStan\PhpDoc\Tag\ParamTag;
11+
use PHPStan\Rules\Generics\GenericObjectTypeCheck;
12+
use PHPStan\Rules\IdentifierRuleError;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use PHPStan\Type\ClosureType;
15+
use PHPStan\Type\Generic\TemplateType;
16+
use PHPStan\Type\Type;
17+
use PHPStan\Type\VerbosityLevel;
18+
use function array_merge;
19+
use function in_array;
20+
use function sprintf;
21+
22+
final class IncompatiblePhpDocTypeCheck
23+
{
24+
25+
public function __construct(
26+
private GenericObjectTypeCheck $genericObjectTypeCheck,
27+
private UnresolvableTypeHelper $unresolvableTypeHelper,
28+
private GenericCallableRuleHelper $genericCallableRuleHelper,
29+
)
30+
{
31+
}
32+
33+
/**
34+
* @param array<string, Type> $nativeParameterTypes
35+
* @param array<string, bool> $byRefParameters
36+
* @return list<IdentifierRuleError>
37+
*/
38+
public function check(
39+
Scope $scope,
40+
Node $node,
41+
ResolvedPhpDocBlock $resolvedPhpDoc,
42+
string $functionName,
43+
array $nativeParameterTypes,
44+
array $byRefParameters,
45+
Type $nativeReturnType,
46+
): array
47+
{
48+
$errors = [];
49+
50+
foreach (['@param' => $resolvedPhpDoc->getParamTags(), '@param-out' => $resolvedPhpDoc->getParamOutTags(), '@param-closure-this' => $resolvedPhpDoc->getParamClosureThisTags()] as $tagName => $parameters) {
51+
foreach ($parameters as $parameterName => $phpDocParamTag) {
52+
$phpDocParamType = $phpDocParamTag->getType();
53+
54+
if (!isset($nativeParameterTypes[$parameterName])) {
55+
$errors[] = RuleErrorBuilder::message(sprintf(
56+
'PHPDoc tag %s references unknown parameter: $%s',
57+
$tagName,
58+
$parameterName,
59+
))->identifier('parameter.notFound')->build();
60+
61+
} elseif (
62+
$this->unresolvableTypeHelper->containsUnresolvableType($phpDocParamType)
63+
) {
64+
$errors[] = RuleErrorBuilder::message(sprintf(
65+
'PHPDoc tag %s for parameter $%s contains unresolvable type.',
66+
$tagName,
67+
$parameterName,
68+
))->identifier('parameter.unresolvableType')->build();
69+
70+
} else {
71+
$nativeParamType = $nativeParameterTypes[$parameterName];
72+
if (
73+
$phpDocParamTag instanceof ParamTag
74+
&& $phpDocParamTag->isVariadic()
75+
&& $phpDocParamType->isArray()->yes()
76+
&& $nativeParamType->isArray()->no()
77+
) {
78+
$phpDocParamType = $phpDocParamType->getIterableValueType();
79+
}
80+
81+
$escapedParameterName = SprintfHelper::escapeFormatString($parameterName);
82+
$escapedTagName = SprintfHelper::escapeFormatString($tagName);
83+
84+
$errors = array_merge($errors, $this->genericObjectTypeCheck->check(
85+
$phpDocParamType,
86+
sprintf(
87+
'PHPDoc tag %s for parameter $%s contains generic type %%s but %%s %%s is not generic.',
88+
$escapedTagName,
89+
$escapedParameterName,
90+
),
91+
sprintf(
92+
'Generic type %%s in PHPDoc tag %s for parameter $%s does not specify all template types of %%s %%s: %%s',
93+
$escapedTagName,
94+
$escapedParameterName,
95+
),
96+
sprintf(
97+
'Generic type %%s in PHPDoc tag %s for parameter $%s specifies %%d template types, but %%s %%s supports only %%d: %%s',
98+
$escapedTagName,
99+
$escapedParameterName,
100+
),
101+
sprintf(
102+
'Type %%s in generic type %%s in PHPDoc tag %s for parameter $%s is not subtype of template type %%s of %%s %%s.',
103+
$escapedTagName,
104+
$escapedParameterName,
105+
),
106+
sprintf(
107+
'Call-site variance of %%s in generic type %%s in PHPDoc tag %s for parameter $%s is in conflict with %%s template type %%s of %%s %%s.',
108+
$escapedTagName,
109+
$escapedParameterName,
110+
),
111+
sprintf(
112+
'Call-site variance of %%s in generic type %%s in PHPDoc tag %s for parameter $%s is redundant, template type %%s of %%s %%s has the same variance.',
113+
$escapedTagName,
114+
$escapedParameterName,
115+
),
116+
));
117+
118+
$errors = array_merge($errors, $this->genericCallableRuleHelper->check(
119+
$node,
120+
$scope,
121+
sprintf('%s for parameter $%s', $escapedTagName, $escapedParameterName),
122+
$phpDocParamType,
123+
$functionName,
124+
$resolvedPhpDoc->getTemplateTags(),
125+
$scope->isInClass() ? $scope->getClassReflection() : null,
126+
));
127+
128+
if ($phpDocParamTag instanceof ParamOutTag) {
129+
if (!$byRefParameters[$parameterName]) {
130+
$errors[] = RuleErrorBuilder::message(sprintf(
131+
'Parameter $%s for PHPDoc tag %s is not passed by reference.',
132+
$parameterName,
133+
$tagName,
134+
))->identifier('parameter.notByRef')->build();
135+
136+
}
137+
continue;
138+
}
139+
140+
if (in_array($tagName, ['@param', '@param-out'], true)) {
141+
$isParamSuperType = $nativeParamType->isSuperTypeOf($phpDocParamType);
142+
if ($isParamSuperType->no()) {
143+
$errors[] = RuleErrorBuilder::message(sprintf(
144+
'PHPDoc tag %s for parameter $%s with type %s is incompatible with native type %s.',
145+
$tagName,
146+
$parameterName,
147+
$phpDocParamType->describe(VerbosityLevel::typeOnly()),
148+
$nativeParamType->describe(VerbosityLevel::typeOnly()),
149+
))->identifier('parameter.phpDocType')->build();
150+
151+
} elseif ($isParamSuperType->maybe()) {
152+
$errorBuilder = RuleErrorBuilder::message(sprintf(
153+
'PHPDoc tag %s for parameter $%s with type %s is not subtype of native type %s.',
154+
$tagName,
155+
$parameterName,
156+
$phpDocParamType->describe(VerbosityLevel::typeOnly()),
157+
$nativeParamType->describe(VerbosityLevel::typeOnly()),
158+
))->identifier('parameter.phpDocType');
159+
if ($phpDocParamType instanceof TemplateType) {
160+
$errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocParamType->getName(), $nativeParamType->describe(VerbosityLevel::typeOnly())));
161+
}
162+
163+
$errors[] = $errorBuilder->build();
164+
}
165+
}
166+
167+
if ($tagName === '@param-closure-this') {
168+
$isNonClosure = (new ClosureType())->isSuperTypeOf($nativeParamType)->no();
169+
if ($isNonClosure) {
170+
$errors[] = RuleErrorBuilder::message(sprintf(
171+
'PHPDoc tag %s is for parameter $%s with non-Closure type %s.',
172+
$tagName,
173+
$parameterName,
174+
$nativeParamType->describe(VerbosityLevel::typeOnly()),
175+
))->identifier('paramClosureThis.nonClosure')->build();
176+
}
177+
}
178+
}
179+
}
180+
}
181+
182+
if ($resolvedPhpDoc->getReturnTag() !== null) {
183+
$phpDocReturnType = $resolvedPhpDoc->getReturnTag()->getType();
184+
185+
if (
186+
$this->unresolvableTypeHelper->containsUnresolvableType($phpDocReturnType)
187+
) {
188+
$errors[] = RuleErrorBuilder::message('PHPDoc tag @return contains unresolvable type.')->identifier('return.unresolvableType')->build();
189+
190+
} else {
191+
$isReturnSuperType = $nativeReturnType->isSuperTypeOf($phpDocReturnType);
192+
$errors = array_merge($errors, $this->genericObjectTypeCheck->check(
193+
$phpDocReturnType,
194+
'PHPDoc tag @return contains generic type %s but %s %s is not generic.',
195+
'Generic type %s in PHPDoc tag @return does not specify all template types of %s %s: %s',
196+
'Generic type %s in PHPDoc tag @return specifies %d template types, but %s %s supports only %d: %s',
197+
'Type %s in generic type %s in PHPDoc tag @return is not subtype of template type %s of %s %s.',
198+
'Call-site variance of %s in generic type %s in PHPDoc tag @return is in conflict with %s template type %s of %s %s.',
199+
'Call-site variance of %s in generic type %s in PHPDoc tag @return is redundant, template type %s of %s %s has the same variance.',
200+
));
201+
if ($isReturnSuperType->no()) {
202+
$errors[] = RuleErrorBuilder::message(sprintf(
203+
'PHPDoc tag @return with type %s is incompatible with native type %s.',
204+
$phpDocReturnType->describe(VerbosityLevel::typeOnly()),
205+
$nativeReturnType->describe(VerbosityLevel::typeOnly()),
206+
))->identifier('return.phpDocType')->build();
207+
208+
} elseif ($isReturnSuperType->maybe()) {
209+
$errorBuilder = RuleErrorBuilder::message(sprintf(
210+
'PHPDoc tag @return with type %s is not subtype of native type %s.',
211+
$phpDocReturnType->describe(VerbosityLevel::typeOnly()),
212+
$nativeReturnType->describe(VerbosityLevel::typeOnly()),
213+
))->identifier('return.phpDocType');
214+
if ($phpDocReturnType instanceof TemplateType) {
215+
$errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocReturnType->getName(), $nativeReturnType->describe(VerbosityLevel::typeOnly())));
216+
}
217+
218+
$errors[] = $errorBuilder->build();
219+
}
220+
221+
$errors = array_merge($errors, $this->genericCallableRuleHelper->check(
222+
$node,
223+
$scope,
224+
'@return',
225+
$phpDocReturnType,
226+
$functionName,
227+
$resolvedPhpDoc->getTemplateTags(),
228+
$scope->isInClass() ? $scope->getClassReflection() : null,
229+
));
230+
}
231+
}
232+
233+
return $errors;
234+
}
235+
236+
}

0 commit comments

Comments
 (0)