Skip to content

Commit 95c0a58

Browse files
committed
Check invalid @param-closure-this
1 parent c79d03c commit 95c0a58

File tree

4 files changed

+155
-24
lines changed

4 files changed

+155
-24
lines changed

src/PhpDoc/PhpDocNodeResolver.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
use PHPStan\Type\Generic\TemplateTypeScope;
3737
use PHPStan\Type\Generic\TemplateTypeVariance;
3838
use PHPStan\Type\MixedType;
39+
use PHPStan\Type\ObjectWithoutClassType;
3940
use PHPStan\Type\Type;
4041
use PHPStan\Type\TypeCombinator;
4142
use function array_key_exists;
@@ -421,7 +422,12 @@ public function resolveParamClosureThisTags(PhpDocNode $phpDocNode, NameScope $n
421422
foreach (['@param-closure-this', '@phpstan-param-closure-this'] as $tagName) {
422423
foreach ($phpDocNode->getParamClosureThisTagValues($tagName) as $tagValue) {
423424
$parameterName = substr($tagValue->parameterName, 1);
424-
$closureThisTypes[$parameterName] = new ParamClosureThisTag($this->typeNodeResolver->resolve($tagValue->type, $nameScope));
425+
$closureThisTypes[$parameterName] = new ParamClosureThisTag(
426+
TypeCombinator::intersect(
427+
$this->typeNodeResolver->resolve($tagValue->type, $nameScope),
428+
new ObjectWithoutClassType(),
429+
),
430+
);
425431
}
426432
}
427433

src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php

+38-23
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
use PHPStan\Rules\Rule;
1313
use PHPStan\Rules\RuleErrorBuilder;
1414
use PHPStan\ShouldNotHappenException;
15+
use PHPStan\Type\ClosureType;
1516
use PHPStan\Type\FileTypeMapper;
1617
use PHPStan\Type\Generic\TemplateType;
1718
use PHPStan\Type\Type;
1819
use PHPStan\Type\VerbosityLevel;
1920
use function array_merge;
21+
use function in_array;
2022
use function is_string;
2123
use function sprintf;
2224
use function trim;
@@ -68,10 +70,9 @@ public function processNode(Node $node, Scope $scope): array
6870

6971
$errors = [];
7072

71-
foreach ([$resolvedPhpDoc->getParamTags(), $resolvedPhpDoc->getParamOutTags()] as $parameters) {
73+
foreach (['@param' => $resolvedPhpDoc->getParamTags(), '@param-out' => $resolvedPhpDoc->getParamOutTags(), '@param-closure-this' => $resolvedPhpDoc->getParamClosureThisTags()] as $tagName => $parameters) {
7274
foreach ($parameters as $parameterName => $phpDocParamTag) {
7375
$phpDocParamType = $phpDocParamTag->getType();
74-
$tagName = $phpDocParamTag instanceof ParamTag ? '@param' : '@param-out';
7576

7677
if (!isset($nativeParameterTypes[$parameterName])) {
7778
$errors[] = RuleErrorBuilder::message(sprintf(
@@ -99,7 +100,6 @@ public function processNode(Node $node, Scope $scope): array
99100
) {
100101
$phpDocParamType = $phpDocParamType->getIterableValueType();
101102
}
102-
$isParamSuperType = $nativeParamType->isSuperTypeOf($phpDocParamType);
103103

104104
$escapedParameterName = SprintfHelper::escapeFormatString($parameterName);
105105
$escapedTagName = SprintfHelper::escapeFormatString($tagName);
@@ -160,28 +160,43 @@ public function processNode(Node $node, Scope $scope): array
160160
continue;
161161
}
162162

163-
if ($isParamSuperType->no()) {
164-
$errors[] = RuleErrorBuilder::message(sprintf(
165-
'PHPDoc tag %s for parameter $%s with type %s is incompatible with native type %s.',
166-
$tagName,
167-
$parameterName,
168-
$phpDocParamType->describe(VerbosityLevel::typeOnly()),
169-
$nativeParamType->describe(VerbosityLevel::typeOnly()),
170-
))->identifier('parameter.phpDocType')->build();
171-
172-
} elseif ($isParamSuperType->maybe()) {
173-
$errorBuilder = RuleErrorBuilder::message(sprintf(
174-
'PHPDoc tag %s for parameter $%s with type %s is not subtype of native type %s.',
175-
$tagName,
176-
$parameterName,
177-
$phpDocParamType->describe(VerbosityLevel::typeOnly()),
178-
$nativeParamType->describe(VerbosityLevel::typeOnly()),
179-
))->identifier('parameter.phpDocType');
180-
if ($phpDocParamType instanceof TemplateType) {
181-
$errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocParamType->getName(), $nativeParamType->describe(VerbosityLevel::typeOnly())));
163+
if (in_array($tagName, ['@param', '@param-out'], true)) {
164+
$isParamSuperType = $nativeParamType->isSuperTypeOf($phpDocParamType);
165+
if ($isParamSuperType->no()) {
166+
$errors[] = RuleErrorBuilder::message(sprintf(
167+
'PHPDoc tag %s for parameter $%s with type %s is incompatible with native type %s.',
168+
$tagName,
169+
$parameterName,
170+
$phpDocParamType->describe(VerbosityLevel::typeOnly()),
171+
$nativeParamType->describe(VerbosityLevel::typeOnly()),
172+
))->identifier('parameter.phpDocType')->build();
173+
174+
} elseif ($isParamSuperType->maybe()) {
175+
$errorBuilder = RuleErrorBuilder::message(sprintf(
176+
'PHPDoc tag %s for parameter $%s with type %s is not subtype of native type %s.',
177+
$tagName,
178+
$parameterName,
179+
$phpDocParamType->describe(VerbosityLevel::typeOnly()),
180+
$nativeParamType->describe(VerbosityLevel::typeOnly()),
181+
))->identifier('parameter.phpDocType');
182+
if ($phpDocParamType instanceof TemplateType) {
183+
$errorBuilder->tip(sprintf('Write @template %s of %s to fix this.', $phpDocParamType->getName(), $nativeParamType->describe(VerbosityLevel::typeOnly())));
184+
}
185+
186+
$errors[] = $errorBuilder->build();
182187
}
188+
}
183189

184-
$errors[] = $errorBuilder->build();
190+
if ($tagName === '@param-closure-this') {
191+
$isNonClosure = (new ClosureType())->isSuperTypeOf($nativeParamType)->no();
192+
if ($isNonClosure) {
193+
$errors[] = RuleErrorBuilder::message(sprintf(
194+
'PHPDoc tag %s is for parameter $%s with non-Closure type %s.',
195+
$tagName,
196+
$parameterName,
197+
$nativeParamType->describe(VerbosityLevel::typeOnly()),
198+
))->identifier('paramClosureThis.nonClosure')->build();
199+
}
185200
}
186201
}
187202
}

tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php

+38
Original file line numberDiff line numberDiff line change
@@ -442,4 +442,42 @@ public function testBug10622B(): void
442442
$this->analyse([__DIR__ . '/data/bug-10622b.php'], []);
443443
}
444444

445+
public function testParamClosureThis(): void
446+
{
447+
$this->analyse([__DIR__ . '/data/param-closure-this.php'], [
448+
[
449+
'PHPDoc tag @param-closure-this references unknown parameter: $b',
450+
20,
451+
],
452+
[
453+
'PHPDoc tag @param-closure-this for parameter $i contains unresolvable type.',
454+
27,
455+
],
456+
[
457+
'PHPDoc tag @param-closure-this for parameter $i contains unresolvable type.',
458+
34,
459+
],
460+
[
461+
'PHPDoc tag @param-closure-this is for parameter $i with non-Closure type string.',
462+
41,
463+
],
464+
[
465+
'PHPDoc tag @param-closure-this for parameter $i contains generic type Exception<int, float> but class Exception is not generic.',
466+
48,
467+
],
468+
[
469+
'Generic type ParamClosureThisPhpDocRule\FooBar<mixed> in PHPDoc tag @param-closure-this for parameter $i does not specify all template types of class ParamClosureThisPhpDocRule\FooBar: T, TT',
470+
55,
471+
],
472+
[
473+
'Type mixed in generic type ParamClosureThisPhpDocRule\FooBar<mixed> in PHPDoc tag @param-closure-this for parameter $i is not subtype of template type T of int of class ParamClosureThisPhpDocRule\FooBar.',
474+
55,
475+
],
476+
[
477+
'Generic type ParamClosureThisPhpDocRule\FooBar<int> in PHPDoc tag @param-closure-this for parameter $i does not specify all template types of class ParamClosureThisPhpDocRule\FooBar: T, TT',
478+
62,
479+
],
480+
]);
481+
}
482+
445483
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace ParamClosureThisPhpDocRule;
4+
5+
class Foo
6+
{
7+
8+
}
9+
10+
/**
11+
* @param-closure-this Foo $i
12+
*/
13+
function validParamClosureThis(callable $i) {
14+
15+
}
16+
17+
/**
18+
* @param-closure-this Foo $b
19+
*/
20+
function invalidParamClosureThisParamName($a) {
21+
22+
}
23+
24+
/**
25+
* @param-closure-this string $i
26+
*/
27+
function nonObjectParamClosureThis(callable $i) {
28+
29+
}
30+
31+
/**
32+
* @param-closure-this \stdClass&\Exception $i
33+
*/
34+
function unresolvableParamClosureThis(callable $i) {
35+
36+
}
37+
38+
/**
39+
* @param-closure-this Foo $i
40+
*/
41+
function paramClosureThisAboveNonClosure(string $i) {
42+
43+
}
44+
45+
/**
46+
* @param-closure-this \Exception<int, float> $i
47+
*/
48+
function invalidParamClosureThisGeneric(callable $i) {
49+
50+
}
51+
52+
/**
53+
* @param-closure-this FooBar<mixed> $i
54+
*/
55+
function invalidParamClosureThisWrongGenericParams(callable $i) {
56+
57+
}
58+
59+
/**
60+
* @param-closure-this FooBar<int> $i
61+
*/
62+
function invalidParamClosureThisNotAllGenericParams(callable $i) {
63+
64+
}
65+
66+
/**
67+
* @template T of int
68+
* @template TT of string
69+
*/
70+
class FooBar {
71+
72+
}

0 commit comments

Comments
 (0)