Skip to content

Commit 55b9163

Browse files
Collect types declared in @throws tags and consider them dependencies (1/2) (#507)
1 parent 26417fe commit 55b9163

File tree

4 files changed

+256
-92
lines changed

4 files changed

+256
-92
lines changed

src/Analyzer/Docblock.php

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Arkitect\Analyzer;
6+
7+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
8+
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
9+
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
10+
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
11+
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
12+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
13+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
14+
15+
class Docblock
16+
{
17+
private PhpDocNode $phpDocNode;
18+
19+
public function __construct(PhpDocNode $phpDocNode)
20+
{
21+
$this->phpDocNode = $phpDocNode;
22+
}
23+
24+
public function getParamTagTypesByName(string $name): ?string
25+
{
26+
foreach ($this->phpDocNode->getParamTagValues() as $paramTag) {
27+
if ($paramTag->parameterName === $name) {
28+
return $this->getType($paramTag->type);
29+
}
30+
}
31+
32+
return null;
33+
}
34+
35+
public function getReturnTagTypes(): array
36+
{
37+
$returnTypes = array_map(
38+
fn (ReturnTagValueNode $returnTag) => $this->getType($returnTag->type),
39+
$this->phpDocNode->getReturnTagValues()
40+
);
41+
42+
// remove null values
43+
return array_filter($returnTypes);
44+
}
45+
46+
public function getVarTagTypes(): array
47+
{
48+
$varTypes = array_map(
49+
fn (VarTagValueNode $varTag) => $this->getType($varTag->type),
50+
$this->phpDocNode->getVarTagValues()
51+
);
52+
53+
// remove null values
54+
return array_filter($varTypes);
55+
}
56+
57+
public function getDoctrineLikeAnnotationTypes(): array
58+
{
59+
$doctrineAnnotations = [];
60+
61+
foreach ($this->phpDocNode->getTags() as $tag) {
62+
if ('@' === $tag->name[0] && !str_contains($tag->name, '@var')) {
63+
$doctrineAnnotations[] = str_replace('@', '', $tag->name);
64+
}
65+
}
66+
67+
return $doctrineAnnotations;
68+
}
69+
70+
private function getType(TypeNode $typeNode): ?string
71+
{
72+
if ($typeNode instanceof IdentifierTypeNode) {
73+
// this handles ClassName
74+
return $typeNode->name;
75+
}
76+
77+
if ($typeNode instanceof GenericTypeNode) {
78+
// this handles list<ClassName>
79+
if (1 === \count($typeNode->genericTypes)) {
80+
return (string) $typeNode->genericTypes[0];
81+
}
82+
83+
// this handles array<int, ClassName>
84+
if (2 === \count($typeNode->genericTypes)) {
85+
return (string) $typeNode->genericTypes[1];
86+
}
87+
}
88+
89+
// this handles ClassName[]
90+
if ($typeNode instanceof ArrayTypeNode) {
91+
return (string) $typeNode->type;
92+
}
93+
94+
return null;
95+
}
96+
}

src/Analyzer/DocblockParser.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
namespace Arkitect\Analyzer;
55

6-
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
76
use PHPStan\PhpDocParser\Lexer\Lexer;
87
use PHPStan\PhpDocParser\Parser\PhpDocParser;
98
use PHPStan\PhpDocParser\Parser\TokenIterator;
@@ -20,11 +19,11 @@ public function __construct(PhpDocParser $innerParser, Lexer $innerLexer)
2019
$this->innerLexer = $innerLexer;
2120
}
2221

23-
public function parse(string $docblock): PhpDocNode
22+
public function parse(string $docblock): Docblock
2423
{
2524
$tokens = $this->innerLexer->tokenize($docblock);
2625
$tokenIterator = new TokenIterator($tokens);
2726

28-
return $this->innerParser->parse($tokenIterator);
27+
return new Docblock($this->innerParser->parse($tokenIterator));
2928
}
3029
}

src/Analyzer/DocblockTypesResolver.php

Lines changed: 40 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@
1414
use PhpParser\Node\Stmt;
1515
use PhpParser\NodeAbstract;
1616
use PhpParser\NodeVisitorAbstract;
17-
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
18-
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
19-
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
20-
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
2117

2218
/**
2319
* This class is used to collect type information from dockblocks, in particular
@@ -68,51 +64,39 @@ public function enterNode(Node $node): void
6864

6965
$this->resolveFunctionTypes($node);
7066

71-
$this->resolveParamTypes($node);
67+
$this->resolvePropertyTypes($node);
7268
}
7369

74-
private function resolveParamTypes(Node $node): void
70+
private function resolvePropertyTypes(Node $node): void
7571
{
7672
if (!($node instanceof Stmt\Property)) {
7773
return;
7874
}
7975

80-
$phpDocNode = $this->parseDocblock($node);
76+
$docblock = $this->parseDocblock($node);
8177

82-
if (null === $phpDocNode) {
78+
if (null === $docblock) {
8379
return;
8480
}
8581

86-
if ($this->isNodeOfTypeArray($node)) {
87-
$arrayItemType = null;
82+
$arrayItemType = $docblock->getVarTagTypes();
83+
$arrayItemType = array_pop($arrayItemType);
8884

89-
foreach ($phpDocNode->getVarTagValues() as $tagValue) {
90-
$arrayItemType = $this->getArrayItemType($tagValue->type);
91-
}
92-
93-
if (null !== $arrayItemType) {
94-
$node->type = $this->resolveName(new Name($arrayItemType), Stmt\Use_::TYPE_NORMAL);
95-
96-
return;
97-
}
98-
}
85+
if (null !== $arrayItemType) {
86+
$node->type = $this->resolveName(new Name($arrayItemType), Stmt\Use_::TYPE_NORMAL);
9987

100-
foreach ($phpDocNode->getVarTagValues() as $tagValue) {
101-
$type = $this->resolveName(new Name((string) $tagValue->type), Stmt\Use_::TYPE_NORMAL);
102-
$node->type = $type;
103-
break;
88+
return;
10489
}
10590

10691
if ($this->parseCustomAnnotations && !($node->type instanceof FullyQualified)) {
107-
foreach ($phpDocNode->getTags() as $tagValue) {
108-
if ('@' === $tagValue->name[0] && !str_contains($tagValue->name, '@var')) {
109-
$customTag = str_replace('@', '', $tagValue->name);
110-
$type = $this->resolveName(new Name($customTag), Stmt\Use_::TYPE_NORMAL);
111-
$node->type = $type;
112-
113-
break;
114-
}
92+
$doctrineAnnotations = $docblock->getDoctrineLikeAnnotationTypes();
93+
$doctrineAnnotations = array_shift($doctrineAnnotations);
94+
95+
if (null === $doctrineAnnotations) {
96+
return;
11597
}
98+
99+
$node->type = $this->resolveName(new Name($doctrineAnnotations), Stmt\Use_::TYPE_NORMAL);
116100
}
117101
}
118102

@@ -127,38 +111,41 @@ private function resolveFunctionTypes(Node $node): void
127111
return;
128112
}
129113

130-
$phpDocNode = $this->parseDocblock($node);
114+
$docblock = $this->parseDocblock($node);
131115

132-
if (null === $phpDocNode) { // no docblock, nothing to do
116+
if (null === $docblock) { // no docblock, nothing to do
133117
return;
134118
}
135119

120+
// extract param types from param tags
136121
foreach ($node->params as $param) {
137-
if (!$this->isNodeOfTypeArray($param)) { // not an array, nothing to do
122+
if (!$this->isTypeArray($param->type)) { // not an array, nothing to do
123+
continue;
124+
}
125+
126+
if (!($param->var instanceof Expr\Variable) || !\is_string($param->var->name)) {
138127
continue;
139128
}
140129

141-
foreach ($phpDocNode->getParamTagValues() as $phpDocParam) {
142-
if ($param->var instanceof Expr\Variable && \is_string($param->var->name) && $phpDocParam->parameterName === ('$'.$param->var->name)) {
143-
$arrayItemType = $this->getArrayItemType($phpDocParam->type);
130+
$type = $docblock->getParamTagTypesByName('$'.$param->var->name);
144131

145-
if (null !== $arrayItemType) {
146-
$param->type = $this->resolveName(new Name($arrayItemType), Stmt\Use_::TYPE_NORMAL);
147-
}
148-
}
132+
if (null === $type) {
133+
continue;
149134
}
135+
136+
$param->type = $this->resolveName(new Name($type), Stmt\Use_::TYPE_NORMAL);
150137
}
151138

152-
if ($node->returnType instanceof Node\Identifier && 'array' === $node->returnType->name) {
153-
$arrayItemType = null;
139+
// extract return type from return tag
140+
if ($this->isTypeArray($node->returnType)) {
141+
$type = $docblock->getReturnTagTypes();
142+
$type = array_pop($type);
154143

155-
foreach ($phpDocNode->getReturnTagValues() as $tagValue) {
156-
$arrayItemType = $this->getArrayItemType($tagValue->type);
144+
if (null === $type) {
145+
return;
157146
}
158147

159-
if (null !== $arrayItemType) {
160-
$node->returnType = $this->resolveName(new Name($arrayItemType), Stmt\Use_::TYPE_NORMAL);
161-
}
148+
$node->returnType = $this->resolveName(new Name($type), Stmt\Use_::TYPE_NORMAL);
162149
}
163150
}
164151

@@ -178,14 +165,6 @@ private function resolveName(Name $name, int $type): Name
178165
return $resolvedName;
179166
}
180167

181-
// unqualified names inside a namespace cannot be resolved at compile-time
182-
// add the namespaced version of the name as an attribute
183-
$name->setAttribute('namespacedName', FullyQualified::concat(
184-
$this->nameContext->getNamespace(),
185-
$name,
186-
$name->getAttributes()
187-
));
188-
189168
return $name;
190169
}
191170

@@ -218,7 +197,7 @@ private function addAlias(Node\UseItem $use, int $type, ?Name $prefix = null): v
218197
);
219198
}
220199

221-
private function parseDocblock(NodeAbstract $node): ?PhpDocNode
200+
private function parseDocblock(NodeAbstract $node): ?Docblock
222201
{
223202
if (null === $node->getDocComment()) {
224203
return null;
@@ -231,38 +210,10 @@ private function parseDocblock(NodeAbstract $node): ?PhpDocNode
231210
}
232211

233212
/**
234-
* @param Node\Param|Stmt\Property $node
213+
* @param Node\Identifier|Name|Node\ComplexType|null $type
235214
*/
236-
private function isNodeOfTypeArray($node): bool
237-
{
238-
return null !== $node->type && isset($node->type->name) && 'array' === $node->type->name;
239-
}
240-
241-
private function getArrayItemType(TypeNode $typeNode): ?string
215+
private function isTypeArray($type): bool
242216
{
243-
$arrayItemType = null;
244-
245-
if ($typeNode instanceof GenericTypeNode) {
246-
if (1 === \count($typeNode->genericTypes)) {
247-
// this handles list<ClassName>
248-
$arrayItemType = (string) $typeNode->genericTypes[0];
249-
} elseif (2 === \count($typeNode->genericTypes)) {
250-
// this handles array<int, ClassName>
251-
$arrayItemType = (string) $typeNode->genericTypes[1];
252-
}
253-
}
254-
255-
if ($typeNode instanceof ArrayTypeNode) {
256-
// this handles ClassName[]
257-
$arrayItemType = (string) $typeNode->type;
258-
}
259-
260-
$validFqcn = '/^[a-zA-Z_\x7f-\xff\\\\][a-zA-Z0-9_\x7f-\xff\\\\]*[a-zA-Z0-9_\x7f-\xff]$/';
261-
262-
if (null !== $arrayItemType && !(bool) preg_match($validFqcn, $arrayItemType)) {
263-
return null;
264-
}
265-
266-
return $arrayItemType;
217+
return null !== $type && isset($type->name) && 'array' === $type->name;
267218
}
268219
}

0 commit comments

Comments
 (0)