Skip to content

Commit 6313bbb

Browse files
committed
Read promoted property type from PHPDocs
1 parent 9b19a6f commit 6313bbb

17 files changed

+365
-20
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"nette/utils": "^3.1.3",
2424
"nikic/php-parser": "4.10.2",
2525
"ondram/ci-detector": "^3.4.0",
26-
"ondrejmirtes/better-reflection": "4.3.41",
26+
"ondrejmirtes/better-reflection": "4.3.42",
2727
"phpdocumentor/reflection-docblock": "4.3.4",
2828
"phpstan/php-8-stubs": "^0.1.6",
2929
"phpstan/phpdoc-parser": "^0.4.9",

composer.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/PhpDoc/PhpDocInheritanceResolver.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ public function resolvePhpDocForProperty(
2222
ClassReflection $classReflection,
2323
string $classReflectionFileName,
2424
?string $declaringTraitName,
25-
string $propertyName
26-
): ?ResolvedPhpDocBlock
25+
string $propertyName,
26+
?string $constructorName
27+
): ResolvedPhpDocBlock
2728
{
2829
$phpDocBlock = PhpDocBlock::resolvePhpDocBlockForProperty(
2930
$docComment,
@@ -36,7 +37,7 @@ public function resolvePhpDocForProperty(
3637
[]
3738
);
3839

39-
return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, $declaringTraitName, null);
40+
return $this->docBlockTreeToResolvedDocBlock($phpDocBlock, $declaringTraitName, $constructorName);
4041
}
4142

4243
/**

src/Reflection/Php/PhpClassReflectionExtension.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,12 +221,19 @@ private function createProperty(
221221
if ($resolvedPhpDoc === null) {
222222
if ($declaringClassReflection->getFileName() !== false) {
223223
$declaringTraitName = $this->findPropertyTrait($propertyReflection);
224+
$constructorName = null;
225+
if (method_exists($propertyReflection, 'isPromoted') && $propertyReflection->isPromoted()) {
226+
if ($declaringClassReflection->hasConstructor()) {
227+
$constructorName = $declaringClassReflection->getConstructor()->getName();
228+
}
229+
}
224230
$resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForProperty(
225231
$docComment,
226232
$declaringClassReflection,
227233
$declaringClassReflection->getFileName(),
228234
$declaringTraitName,
229-
$propertyName
235+
$propertyName,
236+
$constructorName
230237
);
231238
$phpDocBlockClassReflection = $declaringClassReflection;
232239
}
@@ -241,6 +248,20 @@ private function createProperty(
241248
$phpDocType = $varTags[0]->getType();
242249
} elseif (isset($varTags[$propertyName])) {
243250
$phpDocType = $varTags[$propertyName]->getType();
251+
} elseif (isset($constructorName) && $declaringClassReflection->getFileName() !== false) {
252+
$constructorDocComment = $declaringClassReflection->getConstructor()->getDocComment();
253+
$resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod(
254+
$constructorDocComment,
255+
$declaringClassReflection->getFileName(),
256+
$declaringClassReflection,
257+
$declaringTraitName,
258+
$constructorName,
259+
[]
260+
);
261+
$paramTags = $resolvedPhpDoc->getParamTags();
262+
if (isset($paramTags[$propertyReflection->getName()])) {
263+
$phpDocType = $paramTags[$propertyReflection->getName()]->getType();
264+
}
244265
}
245266
if (!isset($phpDocBlockClassReflection)) {
246267
throw new \PHPStan\ShouldNotHappenException();

src/Reflection/Php/PhpPropertyReflection.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,15 @@ public function canChangeTypeAfterAssignment(): bool
119119
return true;
120120
}
121121

122+
public function isPromoted(): bool
123+
{
124+
if (!method_exists($this->reflection, 'isPromoted')) {
125+
return false;
126+
}
127+
128+
return $this->reflection->isPromoted();
129+
}
130+
122131
public function hasPhpDoc(): bool
123132
{
124133
return $this->phpDocType !== null;

src/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRule.php

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,19 @@ public function processNode(Node $node, Scope $scope): array
4444
}
4545

4646
$phpDocType = $propertyReflection->getPhpDocType();
47+
$description = 'PHPDoc tag @var';
48+
if ($propertyReflection->isPromoted()) {
49+
$description = 'PHPDoc type';
50+
}
4751

4852
$messages = [];
4953
if (
5054
$phpDocType instanceof ErrorType
5155
|| ($phpDocType instanceof NeverType && !$phpDocType->isExplicit())
5256
) {
5357
$messages[] = RuleErrorBuilder::message(sprintf(
54-
'PHPDoc tag @var for property %s::$%s contains unresolvable type.',
58+
'%s for property %s::$%s contains unresolvable type.',
59+
$description,
5560
$propertyReflection->getDeclaringClass()->getName(),
5661
$propertyName
5762
))->build();
@@ -61,7 +66,8 @@ public function processNode(Node $node, Scope $scope): array
6166
$isSuperType = $nativeType->isSuperTypeOf($phpDocType);
6267
if ($isSuperType->no()) {
6368
$messages[] = RuleErrorBuilder::message(sprintf(
64-
'PHPDoc tag @var for property %s::$%s with type %s is incompatible with native type %s.',
69+
'%s for property %s::$%s with type %s is incompatible with native type %s.',
70+
$description,
6571
$propertyReflection->getDeclaringClass()->getDisplayName(),
6672
$propertyName,
6773
$phpDocType->describe(VerbosityLevel::typeOnly()),
@@ -70,7 +76,8 @@ public function processNode(Node $node, Scope $scope): array
7076

7177
} elseif ($isSuperType->maybe()) {
7278
$messages[] = RuleErrorBuilder::message(sprintf(
73-
'PHPDoc tag @var for property %s::$%s with type %s is not subtype of native type %s.',
79+
'%s for property %s::$%s with type %s is not subtype of native type %s.',
80+
$description,
7481
$propertyReflection->getDeclaringClass()->getDisplayName(),
7582
$propertyName,
7683
$phpDocType->describe(VerbosityLevel::typeOnly()),
@@ -81,22 +88,26 @@ public function processNode(Node $node, Scope $scope): array
8188
$messages = array_merge($messages, $this->genericObjectTypeCheck->check(
8289
$phpDocType,
8390
sprintf(
84-
'PHPDoc tag @var for property %s::$%s contains generic type %%s but class %%s is not generic.',
91+
'%s for property %s::$%s contains generic type %%s but class %%s is not generic.',
92+
$description,
8593
$propertyReflection->getDeclaringClass()->getDisplayName(),
8694
$propertyName
8795
),
8896
sprintf(
89-
'Generic type %%s in PHPDoc tag @var for property %s::$%s does not specify all template types of class %%s: %%s',
97+
'Generic type %%s in %s for property %s::$%s does not specify all template types of class %%s: %%s',
98+
$description,
9099
$propertyReflection->getDeclaringClass()->getDisplayName(),
91100
$propertyName
92101
),
93102
sprintf(
94-
'Generic type %%s in PHPDoc tag @var for property %s::$%s specifies %%d template types, but class %%s supports only %%d: %%s',
103+
'Generic type %%s in %s for property %s::$%s specifies %%d template types, but class %%s supports only %%d: %%s',
104+
$description,
95105
$propertyReflection->getDeclaringClass()->getDisplayName(),
96106
$propertyName
97107
),
98108
sprintf(
99-
'Type %%s in generic type %%s in PHPDoc tag @var for property %s::$%s is not subtype of template type %%s of class %%s.',
109+
'Type %%s in generic type %%s in %s for property %s::$%s is not subtype of template type %%s of class %%s.',
110+
$description,
100111
$propertyReflection->getDeclaringClass()->getDisplayName(),
101112
$propertyName
102113
)

src/Type/FileTypeMapper.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ private function shouldPhpDocNodeBeCachedToDisk(PhpDocNode $phpDocNode): bool
202202
private function getResolvedPhpDocMap(string $fileName): array
203203
{
204204
if (!isset($this->memoryCache[$fileName])) {
205-
$cacheKey = sprintf('%s-phpdocstring', $fileName);
205+
$cacheKey = sprintf('%s-phpdocstring-v2', $fileName);
206206
$variableCacheKey = implode(',', array_map(static function (array $file): string {
207207
return sprintf('%s-%d', $file['filename'], $file['modifiedTime']);
208208
}, $this->getCachedDependentFilesWithTimestamps($fileName)));
@@ -375,6 +375,11 @@ function (\PhpParser\Node $node) use ($fileName, $lookForTrait, $traitMethodAlia
375375
$functionName = $traitMethodAliases[$functionName];
376376
}
377377
$resolvableTemplateTypes = true;
378+
} elseif (
379+
$node instanceof Node\Param
380+
&& $node->flags !== 0
381+
) {
382+
$resolvableTemplateTypes = true;
378383
} elseif ($node instanceof Node\Stmt\Function_) {
379384
$functionName = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
380385
$resolvableTemplateTypes = true;

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10259,6 +10259,15 @@ public function dataBug4016(): array
1025910259
return $this->gatherAssertTypes(__DIR__ . '/data/bug-4016.php');
1026010260
}
1026110261

10262+
public function dataPromotedProperties(): array
10263+
{
10264+
if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) {
10265+
return [];
10266+
}
10267+
10268+
return $this->gatherAssertTypes(__DIR__ . '/data/promoted-properties-types.php');
10269+
}
10270+
1026210271
/**
1026310272
* @dataProvider dataBug2574
1026410273
* @dataProvider dataBug2577
@@ -10353,6 +10362,7 @@ public function dataBug4016(): array
1035310362
* @dataProvider dataBug3993
1035410363
* @dataProvider dataBug3997
1035510364
* @dataProvider dataBug4016
10365+
* @dataProvider dataPromotedProperties
1035610366
* @param string $assertType
1035710367
* @param string $file
1035810368
* @param mixed ...$args
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php // lint >= 8.0
2+
3+
namespace PromotedPropertiesTypes;
4+
5+
use function PHPStan\Analyser\assertType;
6+
7+
/**
8+
* @template T
9+
*/
10+
class Foo
11+
{
12+
13+
/**
14+
* @param array<int, string> $anotherPhpDocArray
15+
* @param T $anotherTemplateProperty
16+
* @param string $bothProperty
17+
*/
18+
public function __construct(
19+
public $noType,
20+
public int $nativeIntType,
21+
/** @var array<int, string> */ public $phpDocArray,
22+
public $anotherPhpDocArray,
23+
/** @var array<int, string> */ public array $yetAnotherPhpDocArray,
24+
/** @var T */ public $templateProperty,
25+
public $anotherTemplateProperty,
26+
/** @var int */ public $bothProperty
27+
) { }
28+
29+
}
30+
31+
function (Foo $foo): void {
32+
assertType('mixed', $foo->noType);
33+
assertType('int', $foo->nativeIntType);
34+
assertType('array<int, string>', $foo->phpDocArray);
35+
assertType('array<int, string>', $foo->anotherPhpDocArray);
36+
assertType('array<int, string>', $foo->yetAnotherPhpDocArray);
37+
assertType('int', $foo->bothProperty);
38+
};
39+
40+
/**
41+
* @extends Foo<\stdClass>
42+
*/
43+
class Bar extends Foo
44+
{
45+
46+
}
47+
48+
function (Bar $bar): void {
49+
assertType('stdClass', $bar->templateProperty);
50+
assertType('stdClass', $bar->anotherTemplateProperty);
51+
};
52+
53+
/**
54+
* @template T
55+
*/
56+
class Lorem
57+
{
58+
59+
/**
60+
* @param T $foo
61+
*/
62+
public function __construct(
63+
public $foo
64+
) { }
65+
66+
}
67+
68+
function (): void {
69+
$lorem = new Lorem(new \stdClass);
70+
assertType('stdClass', $lorem->foo);
71+
};

tests/PHPStan/Rules/PhpDoc/IncompatiblePropertyPhpDocTypeRuleTest.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,50 @@ public function testNativeTypes(): void
7979
]);
8080
}
8181

82+
public function testPromotedProperties(): void
83+
{
84+
if (PHP_VERSION_ID < 80000 && !self::$useStaticReflectionProvider) {
85+
$this->markTestSkipped('Test requires PHP 8.0.');
86+
}
87+
88+
$this->analyse([__DIR__ . '/data/incompatible-property-promoted.php'], [
89+
[
90+
'PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$bar contains unresolvable type.',
91+
16,
92+
],
93+
[
94+
'PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$classStringInt contains unresolvable type.',
95+
22,
96+
],
97+
[
98+
'PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$fooGeneric contains generic type InvalidPhpDocDefinitions\Foo<stdClass> but class InvalidPhpDocDefinitions\Foo is not generic.',
99+
28,
100+
],
101+
[
102+
'Generic type InvalidPhpDocDefinitions\FooGeneric<int> in PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$notEnoughTypesGenericfoo does not specify all template types of class InvalidPhpDocDefinitions\FooGeneric: T, U',
103+
34,
104+
],
105+
[
106+
'Generic type InvalidPhpDocDefinitions\FooGeneric<int, InvalidArgumentException, string> in PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$tooManyTypesGenericfoo specifies 3 template types, but class InvalidPhpDocDefinitions\FooGeneric supports only 2: T, U',
107+
37,
108+
],
109+
[
110+
'Type Throwable in generic type InvalidPhpDocDefinitions\FooGeneric<int, Throwable> in PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$invalidTypeGenericfoo is not subtype of template type U of Exception of class InvalidPhpDocDefinitions\FooGeneric.',
111+
40,
112+
],
113+
[
114+
'Type stdClass in generic type InvalidPhpDocDefinitions\FooGeneric<int, stdClass> in PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$anotherInvalidTypeGenericfoo is not subtype of template type U of Exception of class InvalidPhpDocDefinitions\FooGeneric.',
115+
43,
116+
],
117+
[
118+
'PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$unknownClassConstant contains unresolvable type.',
119+
46,
120+
],
121+
[
122+
'PHPDoc type for property InvalidPhpDocPromotedProperties\FooWithProperty::$unknownClassConstant2 contains unresolvable type.',
123+
49,
124+
],
125+
]);
126+
}
127+
82128
}

0 commit comments

Comments
 (0)