Skip to content

Commit b216d06

Browse files
committed
Fix PHPDocs with generics in property hooks
1 parent 7cea81a commit b216d06

File tree

7 files changed

+266
-10
lines changed

7 files changed

+266
-10
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,11 @@ services:
317317
tags:
318318
- phpstan.parser.richParserNodeVisitor
319319

320+
-
321+
class: PHPStan\Parser\PropertyHookNameVisitor
322+
tags:
323+
- phpstan.parser.richParserNodeVisitor
324+
320325
-
321326
class: PHPStan\Node\Printer\ExprPrinter
322327

src/Analyser/NodeScopeResolver.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
use PHPStan\Parser\ClosureArgVisitor;
124124
use PHPStan\Parser\ImmediatelyInvokedClosureVisitor;
125125
use PHPStan\Parser\Parser;
126+
use PHPStan\Parser\PropertyHookNameVisitor;
126127
use PHPStan\Php\PhpVersion;
127128
use PHPStan\PhpDoc\PhpDocInheritanceResolver;
128129
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
@@ -6243,6 +6244,11 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n
62436244
}
62446245
} elseif ($node instanceof Node\Stmt\Function_) {
62456246
$functionName = trim($scope->getNamespace() . '\\' . $node->name->name, '\\');
6247+
} elseif ($node instanceof Node\PropertyHook) {
6248+
$propertyName = $node->getAttribute(PropertyHookNameVisitor::ATTRIBUTE_NAME);
6249+
if ($propertyName !== null) {
6250+
$functionName = sprintf('$%s::%s', $propertyName, $node->name->toString());
6251+
}
62466252
}
62476253

62486254
if ($docComment !== null && $resolvedPhpDoc === null) {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Parser;
4+
5+
use PhpParser\Node;
6+
use PhpParser\NodeVisitorAbstract;
7+
use function count;
8+
use function is_string;
9+
10+
final class PropertyHookNameVisitor extends NodeVisitorAbstract
11+
{
12+
13+
public const ATTRIBUTE_NAME = 'propertyName';
14+
15+
public function enterNode(Node $node): ?Node
16+
{
17+
if ($node instanceof Node\Stmt\Property) {
18+
if (count($node->hooks) === 0) {
19+
return null;
20+
}
21+
22+
$propertyName = null;
23+
foreach ($node->props as $prop) {
24+
$propertyName = $prop->name->toString();
25+
break;
26+
}
27+
28+
if (!isset($propertyName)) {
29+
return null;
30+
}
31+
32+
foreach ($node->hooks as $hook) {
33+
$hook->setAttribute(self::ATTRIBUTE_NAME, $propertyName);
34+
}
35+
36+
return $node;
37+
}
38+
39+
if ($node instanceof Node\Param) {
40+
if (count($node->hooks) === 0) {
41+
return null;
42+
}
43+
if (!$node->var instanceof Node\Expr\Variable) {
44+
return null;
45+
}
46+
if (!is_string($node->var->name)) {
47+
return null;
48+
}
49+
50+
foreach ($node->hooks as $hook) {
51+
$hook->setAttribute(self::ATTRIBUTE_NAME, $node->var->name);
52+
}
53+
54+
return $node;
55+
}
56+
57+
return null;
58+
}
59+
60+
}

src/Parser/SimpleParser.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public function __construct(
1717
private NameResolver $nameResolver,
1818
private VariadicMethodsVisitor $variadicMethodsVisitor,
1919
private VariadicFunctionsVisitor $variadicFunctionsVisitor,
20+
private PropertyHookNameVisitor $propertyHookNameVisitor,
2021
)
2122
{
2223
}
@@ -52,6 +53,7 @@ public function parseString(string $sourceCode): array
5253
$nodeTraverser->addVisitor($this->nameResolver);
5354
$nodeTraverser->addVisitor($this->variadicMethodsVisitor);
5455
$nodeTraverser->addVisitor($this->variadicFunctionsVisitor);
56+
$nodeTraverser->addVisitor($this->propertyHookNameVisitor);
5557

5658
/** @var array<Node\Stmt> */
5759
return $nodeTraverser->traverse($nodes);

src/Type/FileTypeMapper.php

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPStan\Broker\AnonymousClassNameHelper;
1010
use PHPStan\File\FileHelper;
1111
use PHPStan\Parser\Parser;
12+
use PHPStan\Parser\PropertyHookNameVisitor;
1213
use PHPStan\PhpDoc\PhpDocNodeResolver;
1314
use PHPStan\PhpDoc\PhpDocStringResolver;
1415
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
@@ -279,6 +280,11 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA
279280
}
280281
} elseif ($node instanceof Node\Stmt\Function_) {
281282
$functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
283+
} elseif ($node instanceof Node\PropertyHook) {
284+
$propertyName = $node->getAttribute(PropertyHookNameVisitor::ATTRIBUTE_NAME);
285+
if ($propertyName !== null) {
286+
$functionStack[] = sprintf('$%s::%s', $propertyName, $node->name->toString());
287+
}
282288
}
283289

284290
$className = $classStack[count($classStack) - 1] ?? null;
@@ -291,6 +297,17 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA
291297
$phpDocNodeMap[$nameScopeKey] = $this->phpDocStringResolver->resolve($docComment);
292298
}
293299

300+
return null;
301+
} elseif ($node instanceof Node\PropertyHook) {
302+
$propertyName = $node->getAttribute(PropertyHookNameVisitor::ATTRIBUTE_NAME);
303+
if ($propertyName !== null) {
304+
$docComment = GetLastDocComment::forNode($node);
305+
if ($docComment !== null) {
306+
$nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName);
307+
$phpDocNodeMap[$nameScopeKey] = $this->phpDocStringResolver->resolve($docComment);
308+
}
309+
}
310+
294311
return null;
295312
}
296313

@@ -376,6 +393,15 @@ static function (Node $node) use (&$namespace, &$functionStack, &$classStack): v
376393
}
377394

378395
array_pop($functionStack);
396+
} elseif ($node instanceof Node\PropertyHook) {
397+
$propertyName = $node->getAttribute(PropertyHookNameVisitor::ATTRIBUTE_NAME);
398+
if ($propertyName !== null) {
399+
if (count($functionStack) === 0) {
400+
throw new ShouldNotHappenException();
401+
}
402+
403+
array_pop($functionStack);
404+
}
379405
}
380406
},
381407
);
@@ -476,13 +502,19 @@ function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFoun
476502
}
477503
} elseif ($node instanceof Node\Stmt\Function_) {
478504
$functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
505+
} elseif ($node instanceof Node\PropertyHook) {
506+
$propertyName = $node->getAttribute(PropertyHookNameVisitor::ATTRIBUTE_NAME);
507+
if ($propertyName !== null) {
508+
$functionStack[] = sprintf('$%s::%s', $propertyName, $node->name->toString());
509+
}
479510
}
480511

481512
$className = $classStack[count($classStack) - 1] ?? null;
482513
$functionName = $functionStack[count($functionStack) - 1] ?? null;
483514
$nameScopeKey = $this->getNameScopeKey($originalClassFileName, $className, $lookForTrait, $functionName);
484515

485516
if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) {
517+
// property hook skipped on purpose, it does not support @template
486518
if (array_key_exists($nameScopeKey, $phpDocNodeMap)) {
487519
$phpDocNode = $phpDocNodeMap[$nameScopeKey];
488520
$typeMapStack[] = function () use ($namespace, $uses, $className, $lookForTrait, $functionName, $phpDocNode, $typeMapStack, $typeAliasStack, $constUses): TemplateTypeMap {
@@ -512,16 +544,20 @@ function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFoun
512544
$typeAliasesMap = $typeAliasStack[count($typeAliasStack) - 1] ?? [];
513545

514546
if (
515-
$node instanceof Node\Stmt
516-
&& !$node instanceof Node\Stmt\Namespace_
517-
&& !$node instanceof Node\Stmt\Declare_
518-
&& !$node instanceof Node\Stmt\Use_
519-
&& !$node instanceof Node\Stmt\GroupUse
520-
&& !$node instanceof Node\Stmt\TraitUse
521-
&& !$node instanceof Node\Stmt\TraitUseAdaptation
522-
&& !$node instanceof Node\Stmt\InlineHTML
523-
&& !($node instanceof Node\Stmt\Expression && $node->expr instanceof Node\Expr\Include_)
524-
&& !array_key_exists($nameScopeKey, $nameScopeMap)
547+
(
548+
$node instanceof Node\PropertyHook
549+
|| (
550+
$node instanceof Node\Stmt
551+
&& !$node instanceof Node\Stmt\Namespace_
552+
&& !$node instanceof Node\Stmt\Declare_
553+
&& !$node instanceof Node\Stmt\Use_
554+
&& !$node instanceof Node\Stmt\GroupUse
555+
&& !$node instanceof Node\Stmt\TraitUse
556+
&& !$node instanceof Node\Stmt\TraitUseAdaptation
557+
&& !$node instanceof Node\Stmt\InlineHTML
558+
&& !($node instanceof Node\Stmt\Expression && $node->expr instanceof Node\Expr\Include_)
559+
)
560+
) && !array_key_exists($nameScopeKey, $nameScopeMap)
525561
) {
526562
$nameScopeMap[$nameScopeKey] = static fn (): NameScope => new NameScope(
527563
$namespace,
@@ -537,6 +573,7 @@ function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFoun
537573
}
538574

539575
if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_) {
576+
// property hook skipped on purpose, it does not support @template
540577
if (array_key_exists($nameScopeKey, $phpDocNodeMap)) {
541578
return self::POP_TYPE_MAP_STACK;
542579
}
@@ -704,6 +741,15 @@ static function (Node $node, $callbackResult) use (&$namespace, &$functionStack,
704741
}
705742

706743
array_pop($functionStack);
744+
} elseif ($node instanceof Node\PropertyHook) {
745+
$propertyName = $node->getAttribute(PropertyHookNameVisitor::ATTRIBUTE_NAME);
746+
if ($propertyName !== null) {
747+
if (count($functionStack) === 0) {
748+
throw new ShouldNotHappenException();
749+
}
750+
751+
array_pop($functionStack);
752+
}
707753
}
708754
if ($callbackResult !== self::POP_TYPE_MAP_STACK) {
709755
return;

tests/PHPStan/Analyser/nsrt/property-hooks.php

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,139 @@ public function __construct(
148148
}
149149

150150
}
151+
152+
/**
153+
* @template T of \stdClass
154+
*/
155+
class FooGenerics
156+
{
157+
158+
/** @var array<T> */
159+
public array $m {
160+
set (array $val) {
161+
assertType('array<T of stdClass (class PropertyHooksTypes\FooGenerics, argument)>', $val);
162+
}
163+
}
164+
165+
public int $n {
166+
/** @param int|array<T> $val */
167+
set (int|array $val) {
168+
assertType('array<T of stdClass (class PropertyHooksTypes\FooGenerics, argument)>|int', $val);
169+
}
170+
}
171+
172+
}
173+
174+
/**
175+
* @template T of \stdClass
176+
*/
177+
class FooGenericsConstructor
178+
{
179+
180+
public function __construct(
181+
/** @var array<T> */
182+
public array $l {
183+
set {
184+
assertType('array<T of stdClass (class PropertyHooksTypes\FooGenericsConstructor, argument)>', $value);
185+
}
186+
},
187+
/** @var array<T> */
188+
public array $m {
189+
set (array $val) {
190+
assertType('array<T of stdClass (class PropertyHooksTypes\FooGenericsConstructor, argument)>', $val);
191+
}
192+
},
193+
public int $n {
194+
/** @param int|array<T> $val */
195+
set (int|array $val) {
196+
assertType('array<T of stdClass (class PropertyHooksTypes\FooGenericsConstructor, argument)>|int', $val);
197+
}
198+
},
199+
) {
200+
201+
}
202+
203+
}
204+
205+
/**
206+
* @template T of \stdClass
207+
*/
208+
class FooGenericsConstructor2
209+
{
210+
211+
/**
212+
* @param array<T> $l
213+
* @param array<T> $m
214+
*/
215+
public function __construct(
216+
public array $l {
217+
set {
218+
assertType('array<T of stdClass (class PropertyHooksTypes\FooGenericsConstructor2, argument)>', $value);
219+
}
220+
},
221+
public array $m {
222+
set (array $val) {
223+
assertType('array<T of stdClass (class PropertyHooksTypes\FooGenericsConstructor2, argument)>', $val);
224+
}
225+
},
226+
public int $n {
227+
/** @param int|array<T> $val */
228+
set (int|array $val) {
229+
assertType('array<T of stdClass (class PropertyHooksTypes\FooGenericsConstructor2, argument)>|int', $val);
230+
}
231+
},
232+
) {
233+
234+
}
235+
236+
}
237+
238+
class FooGenericsConstructorWithT
239+
{
240+
241+
/**
242+
* @template T of \stdClass
243+
*/
244+
public function __construct(
245+
/** @var array<T> */
246+
public array $l {
247+
set {
248+
assertType('array<T of stdClass (method PropertyHooksTypes\FooGenericsConstructorWithT::__construct(), argument)>', $value);
249+
}
250+
},
251+
/** @var array<T> */
252+
public array $m {
253+
set (array $val) {
254+
assertType('array<T of stdClass (method PropertyHooksTypes\FooGenericsConstructorWithT::__construct(), argument)>', $val);
255+
}
256+
},
257+
) {
258+
259+
}
260+
261+
}
262+
263+
class FooGenericsConstructorWithT2
264+
{
265+
266+
/**
267+
* @template T of \stdClass
268+
* @param array<T> $l
269+
* @param array<T> $m
270+
*/
271+
public function __construct(
272+
public array $l {
273+
set {
274+
assertType('array<T of stdClass (method PropertyHooksTypes\FooGenericsConstructorWithT2::__construct(), argument)>', $value);
275+
}
276+
},
277+
public array $m {
278+
set (array $val) {
279+
assertType('array<T of stdClass (method PropertyHooksTypes\FooGenericsConstructorWithT2::__construct(), argument)>', $val);
280+
}
281+
},
282+
) {
283+
284+
}
285+
286+
}

tests/PHPStan/Parser/CleaningParserTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public function testParse(
7070
new NameResolver(),
7171
new VariadicMethodsVisitor(),
7272
new VariadicFunctionsVisitor(),
73+
new PropertyHookNameVisitor(),
7374
),
7475
new PhpVersion($phpVersionId),
7576
);

0 commit comments

Comments
 (0)