Skip to content

Commit 5668c05

Browse files
Recreate @var PHPDoc type from Type::toPhpDocNode() before reporting it as wrong
1 parent bca8902 commit 5668c05

File tree

5 files changed

+125
-22
lines changed

5 files changed

+125
-22
lines changed

src/Rules/PhpDoc/VarTagTypeRuleHelper.php

+71-17
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44

55
use PhpParser\Node;
66
use PhpParser\Node\Expr;
7+
use PHPStan\Analyser\NameScope;
78
use PHPStan\Analyser\Scope;
89
use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
10+
use PHPStan\PhpDoc\NameScopeAlreadyBeingCreatedException;
911
use PHPStan\PhpDoc\Tag\VarTag;
12+
use PHPStan\PhpDoc\TypeNodeResolver;
1013
use PHPStan\Rules\IdentifierRuleError;
1114
use PHPStan\Rules\RuleErrorBuilder;
1215
use PHPStan\Type\ArrayType;
16+
use PHPStan\Type\FileTypeMapper;
1317
use PHPStan\Type\Generic\GenericObjectType;
1418
use PHPStan\Type\MixedType;
1519
use PHPStan\Type\ObjectType;
@@ -24,7 +28,12 @@
2428
final class VarTagTypeRuleHelper
2529
{
2630

27-
public function __construct(private bool $checkTypeAgainstPhpDocType, private bool $strictWideningCheck)
31+
public function __construct(
32+
private TypeNodeResolver $typeNodeResolver,
33+
private FileTypeMapper $fileTypeMapper,
34+
private bool $checkTypeAgainstPhpDocType,
35+
private bool $strictWideningCheck,
36+
)
2837
{
2938
}
3039

@@ -76,7 +85,7 @@ public function checkExprType(Scope $scope, Node\Expr $expr, Type $varTagType):
7685
$errors = [];
7786
$exprNativeType = $scope->getNativeType($expr);
7887
$containsPhpStanType = $this->containsPhpStanType($varTagType);
79-
if ($this->shouldVarTagTypeBeReported($expr, $exprNativeType, $varTagType)) {
88+
if ($this->shouldVarTagTypeBeReported($scope, $expr, $exprNativeType, $varTagType)) {
8089
$verbosity = VerbosityLevel::getRecommendedLevelByType($exprNativeType, $varTagType);
8190
$errors[] = RuleErrorBuilder::message(sprintf(
8291
'PHPDoc tag @var with type %s is not subtype of native type %s.',
@@ -86,7 +95,7 @@ public function checkExprType(Scope $scope, Node\Expr $expr, Type $varTagType):
8695
} else {
8796
$exprType = $scope->getType($expr);
8897
if (
89-
$this->shouldVarTagTypeBeReported($expr, $exprType, $varTagType)
98+
$this->shouldVarTagTypeBeReported($scope, $expr, $exprType, $varTagType)
9099
&& ($this->checkTypeAgainstPhpDocType || $containsPhpStanType)
91100
) {
92101
$verbosity = VerbosityLevel::getRecommendedLevelByType($exprType, $varTagType);
@@ -127,22 +136,22 @@ private function containsPhpStanType(Type $type): bool
127136
return false;
128137
}
129138

130-
private function shouldVarTagTypeBeReported(Node\Expr $expr, Type $type, Type $varTagType): bool
139+
private function shouldVarTagTypeBeReported(Scope $scope, Node\Expr $expr, Type $type, Type $varTagType): bool
131140
{
132141
if ($expr instanceof Expr\Array_) {
133142
if ($expr->items === []) {
134143
$type = new ArrayType(new MixedType(), new MixedType());
135144
}
136145

137-
return $type->isSuperTypeOf($varTagType)->no();
146+
return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType);
138147
}
139148

140149
if ($expr instanceof Expr\ConstFetch) {
141-
return $type->isSuperTypeOf($varTagType)->no();
150+
return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType);
142151
}
143152

144153
if ($expr instanceof Node\Scalar) {
145-
return $type->isSuperTypeOf($varTagType)->no();
154+
return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType);
146155
}
147156

148157
if ($expr instanceof Expr\New_) {
@@ -151,42 +160,87 @@ private function shouldVarTagTypeBeReported(Node\Expr $expr, Type $type, Type $v
151160
}
152161
}
153162

154-
return $this->checkType($type, $varTagType);
163+
return $this->checkType($scope, $type, $varTagType);
155164
}
156165

157-
private function checkType(Type $type, Type $varTagType, int $depth = 0): bool
166+
private function checkType(Scope $scope, Type $type, Type $varTagType, int $depth = 0): bool
158167
{
159168
if ($this->strictWideningCheck) {
160-
return !$type->isSuperTypeOf($varTagType)->yes();
169+
return !$this->isSuperTypeOfVarType($scope, $type, $varTagType);
161170
}
162171

163172
if ($type->isConstantArray()->yes()) {
164173
if ($type->isIterableAtLeastOnce()->no()) {
165174
$type = new ArrayType(new MixedType(), new MixedType());
166-
return $type->isSuperTypeOf($varTagType)->no();
175+
return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType);
167176
}
168177
}
169178

170179
if ($type->isIterable()->yes() && $varTagType->isIterable()->yes()) {
171-
if ($type->isSuperTypeOf($varTagType)->no()) {
180+
if (!$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType)) {
172181
return true;
173182
}
174183

175184
$innerType = $type->getIterableValueType();
176185
$innerVarTagType = $varTagType->getIterableValueType();
177186

178187
if ($type->equals($innerType) || $varTagType->equals($innerVarTagType)) {
179-
return !$innerType->isSuperTypeOf($innerVarTagType)->yes();
188+
return !$this->isSuperTypeOfVarType($scope, $innerType, $innerVarTagType);
180189
}
181190

182-
return $this->checkType($innerType, $innerVarTagType, $depth + 1);
191+
return $this->checkType($scope, $innerType, $innerVarTagType, $depth + 1);
183192
}
184193

185-
if ($type->isConstantValue()->yes() && $depth === 0) {
186-
return $type->isSuperTypeOf($varTagType)->no();
194+
if ($depth === 0 && $type->isConstantValue()->yes()) {
195+
return !$this->isAtLeastMaybeSuperTypeOfVarType($scope, $type, $varTagType);
187196
}
188197

189-
return !$type->isSuperTypeOf($varTagType)->yes();
198+
return !$this->isSuperTypeOfVarType($scope, $type, $varTagType);
199+
}
200+
201+
private function isSuperTypeOfVarType(Scope $scope, Type $type, Type $varTagType): bool
202+
{
203+
if ($type->isSuperTypeOf($varTagType)->yes()) {
204+
return true;
205+
}
206+
207+
try {
208+
$type = $this->typeNodeResolver->resolve($type->toPhpDocNode(), $this->createNameScope($scope));
209+
} catch (NameScopeAlreadyBeingCreatedException) {
210+
return true;
211+
}
212+
213+
return $type->isSuperTypeOf($varTagType)->yes();
214+
}
215+
216+
private function isAtLeastMaybeSuperTypeOfVarType(Scope $scope, Type $type, Type $varTagType): bool
217+
{
218+
if (!$type->isSuperTypeOf($varTagType)->no()) {
219+
return true;
220+
}
221+
222+
try {
223+
$type = $this->typeNodeResolver->resolve($type->toPhpDocNode(), $this->createNameScope($scope));
224+
} catch (NameScopeAlreadyBeingCreatedException) {
225+
return true;
226+
}
227+
228+
return !$type->isSuperTypeOf($varTagType)->no();
229+
}
230+
231+
/**
232+
* @throws NameScopeAlreadyBeingCreatedException
233+
*/
234+
private function createNameScope(Scope $scope): NameScope
235+
{
236+
$function = $scope->getFunction();
237+
238+
return $this->fileTypeMapper->getNameScope(
239+
$scope->getFile(),
240+
$scope->isInClass() ? $scope->getClassReflection()->getName() : null,
241+
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
242+
$function !== null ? $function->getName() : null,
243+
);
190244
}
191245

192246
}

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

+1-3
Original file line numberDiff line numberDiff line change
@@ -1198,9 +1198,7 @@ public function testBug5091(): void
11981198
public function testBug9459(): void
11991199
{
12001200
$errors = $this->runAnalyse(__DIR__ . '/data/bug-9459.php');
1201-
$this->assertCount(1, $errors);
1202-
$this->assertSame('PHPDoc tag @var with type callable(): array is not subtype of native type Closure(): array{}.', $errors[0]->getMessage());
1203-
$this->assertSame(10, $errors[0]->getLine());
1201+
$this->assertCount(0, $errors);
12041202
}
12051203

12061204
public function testBug9573(): void

tests/PHPStan/Rules/PhpDoc/VarTagChangedExpressionTypeRuleTest.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
namespace PHPStan\Rules\PhpDoc;
44

5+
use PHPStan\PhpDoc\TypeNodeResolver;
56
use PHPStan\Rules\Rule;
67
use PHPStan\Testing\RuleTestCase;
8+
use PHPStan\Type\FileTypeMapper;
79

810
/**
911
* @extends RuleTestCase<VarTagChangedExpressionTypeRule>
@@ -13,7 +15,12 @@ class VarTagChangedExpressionTypeRuleTest extends RuleTestCase
1315

1416
protected function getRule(): Rule
1517
{
16-
return new VarTagChangedExpressionTypeRule(new VarTagTypeRuleHelper(true, true));
18+
return new VarTagChangedExpressionTypeRule(new VarTagTypeRuleHelper(
19+
self::getContainer()->getByType(TypeNodeResolver::class),
20+
self::getContainer()->getByType(FileTypeMapper::class),
21+
true,
22+
true,
23+
));
1724
}
1825

1926
public function testRule(): void

tests/PHPStan/Rules/PhpDoc/WrongVariableNameInVarTagRuleTest.php

+16-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Rules\PhpDoc;
44

5+
use PHPStan\PhpDoc\TypeNodeResolver;
56
use PHPStan\Rules\Rule;
67
use PHPStan\Testing\RuleTestCase;
78
use PHPStan\Type\FileTypeMapper;
@@ -23,7 +24,12 @@ protected function getRule(): Rule
2324
{
2425
return new WrongVariableNameInVarTagRule(
2526
self::getContainer()->getByType(FileTypeMapper::class),
26-
new VarTagTypeRuleHelper($this->checkTypeAgainstPhpDocType, $this->strictWideningCheck),
27+
new VarTagTypeRuleHelper(
28+
self::getContainer()->getByType(TypeNodeResolver::class),
29+
self::getContainer()->getByType(FileTypeMapper::class),
30+
$this->checkTypeAgainstPhpDocType,
31+
$this->strictWideningCheck,
32+
),
2733
$this->checkTypeAgainstNativeType,
2834
);
2935
}
@@ -182,6 +188,15 @@ public function testBug4505(): void
182188
$this->analyse([__DIR__ . '/data/bug-4505.php'], []);
183189
}
184190

191+
public function testBug12458(): void
192+
{
193+
$this->checkTypeAgainstNativeType = true;
194+
$this->checkTypeAgainstPhpDocType = true;
195+
$this->strictWideningCheck = true;
196+
197+
$this->analyse([__DIR__ . '/data/bug-12458.php'], []);
198+
}
199+
185200
public function testEnums(): void
186201
{
187202
if (PHP_VERSION_ID < 80100) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug12458;
4+
5+
class HelloWorld
6+
{
7+
/**
8+
* @param list<HelloWorld> $a
9+
*/
10+
public function test(array $a): void
11+
{
12+
/** @var \Closure(): list<HelloWorld> $c */
13+
$c = function () use ($a): array {
14+
return $a;
15+
};
16+
}
17+
18+
/**
19+
* @template T of HelloWorld
20+
* @param list<T> $a
21+
*/
22+
public function testGeneric(array $a): void
23+
{
24+
/** @var \Closure(): list<T> $c */
25+
$c = function () use ($a): array {
26+
return $a;
27+
};
28+
}
29+
}

0 commit comments

Comments
 (0)