Skip to content

Commit 6447bf1

Browse files
.
1 parent 3372a76 commit 6447bf1

File tree

4 files changed

+112
-96
lines changed

4 files changed

+112
-96
lines changed

src/Analyzer/Docblock.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66

77
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
88
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
9+
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
910
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
1011
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
12+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
1113
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
1214

1315
class Docblock
@@ -41,8 +43,37 @@ public function getReturnTagTypes(): array
4143
return array_filter($returnTypes);
4244
}
4345

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+
4470
private function getType(TypeNode $typeNode): ?string
4571
{
72+
if ($typeNode instanceof IdentifierTypeNode) {
73+
// this handles ClassName
74+
return $typeNode->name;
75+
}
76+
4677
if ($typeNode instanceof GenericTypeNode) {
4778
// this handles list<ClassName>
4879
if (1 === \count($typeNode->genericTypes)) {

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: 19 additions & 81 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,14 +111,12 @@ 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

136-
$docblock = new Docblock($phpDocNode);
137-
138120
// extract param types from param tags
139121
foreach ($node->params as $param) {
140122
if (!$this->isTypeArray($param->type)) { // not an array, nothing to do
@@ -183,14 +165,6 @@ private function resolveName(Name $name, int $type): Name
183165
return $resolvedName;
184166
}
185167

186-
// unqualified names inside a namespace cannot be resolved at compile-time
187-
// add the namespaced version of the name as an attribute
188-
$name->setAttribute('namespacedName', FullyQualified::concat(
189-
$this->nameContext->getNamespace(),
190-
$name,
191-
$name->getAttributes()
192-
));
193-
194168
return $name;
195169
}
196170

@@ -223,7 +197,7 @@ private function addAlias(Node\UseItem $use, int $type, ?Name $prefix = null): v
223197
);
224198
}
225199

226-
private function parseDocblock(NodeAbstract $node): ?PhpDocNode
200+
private function parseDocblock(NodeAbstract $node): ?Docblock
227201
{
228202
if (null === $node->getDocComment()) {
229203
return null;
@@ -235,47 +209,11 @@ private function parseDocblock(NodeAbstract $node): ?PhpDocNode
235209
return $this->docblockParser->parse($docComment->getText());
236210
}
237211

238-
/**
239-
* @param Node\Param|Stmt\Property $node
240-
*/
241-
private function isNodeOfTypeArray($node): bool
242-
{
243-
return null !== $node->type && isset($node->type->name) && 'array' === $node->type->name;
244-
}
245-
246212
/**
247213
* @param Node\Identifier|Name|Node\ComplexType|null $type
248214
*/
249215
private function isTypeArray($type): bool
250216
{
251217
return null !== $type && isset($type->name) && 'array' === $type->name;
252218
}
253-
254-
private function getArrayItemType(TypeNode $typeNode): ?string
255-
{
256-
$arrayItemType = null;
257-
258-
if ($typeNode instanceof GenericTypeNode) {
259-
if (1 === \count($typeNode->genericTypes)) {
260-
// this handles list<ClassName>
261-
$arrayItemType = (string) $typeNode->genericTypes[0];
262-
} elseif (2 === \count($typeNode->genericTypes)) {
263-
// this handles array<int, ClassName>
264-
$arrayItemType = (string) $typeNode->genericTypes[1];
265-
}
266-
}
267-
268-
if ($typeNode instanceof ArrayTypeNode) {
269-
// this handles ClassName[]
270-
$arrayItemType = (string) $typeNode->type;
271-
}
272-
273-
$validFqcn = '/^[a-zA-Z_\x7f-\xff\\\\][a-zA-Z0-9_\x7f-\xff\\\\]*[a-zA-Z0-9_\x7f-\xff]$/';
274-
275-
if (null !== $arrayItemType && !(bool) preg_match($validFqcn, $arrayItemType)) {
276-
return null;
277-
}
278-
279-
return $arrayItemType;
280-
}
281219
}

tests/Unit/Analyzer/DocblockParserTest.php

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace Arkitect\Tests\Unit\Analyzer;
66

7-
use Arkitect\Analyzer\Docblock;
87
use Arkitect\Analyzer\DocblockParserFactory;
98
use PHPUnit\Framework\TestCase;
109

@@ -24,20 +23,17 @@ public function test_it_should_exctract_types_from_param_tag(): void
2423
* @param int $aValue
2524
* @param MyPlainDto $plainDto
2625
*/
27-
}
2826
PHP;
2927

30-
$phpdoc = $parser->parse($code);
31-
32-
$db = new Docblock($phpdoc);
28+
$db = $parser->parse($code);
3329

3430
self::assertEquals('MyDto', $db->getParamTagTypesByName('$dtoList'));
3531
self::assertEquals('MyOtherDto', $db->getParamTagTypesByName('$dtoList2'));
3632
self::assertEquals('ValueObject', $db->getParamTagTypesByName('$voList'));
3733
self::assertEquals('User', $db->getParamTagTypesByName('$user'));
3834

39-
self::assertNull($db->getParamTagTypesByName('$aValue'));
40-
self::assertNull($db->getParamTagTypesByName('$plainDto'));
35+
self::assertEquals('int', $db->getParamTagTypesByName('$aValue'));
36+
self::assertEquals('MyPlainDto', $db->getParamTagTypesByName('$plainDto'));
4137
}
4238

4339
public function test_it_should_extract_return_type_from_return_tag(): void
@@ -53,18 +49,70 @@ public function test_it_should_extract_return_type_from_return_tag(): void
5349
* @return int
5450
* @return MyPlainDto
5551
*/
56-
}
5752
PHP;
5853

59-
$phpdoc = $parser->parse($code);
60-
61-
$db = new Docblock($phpdoc);
54+
$db = $parser->parse($code);
6255

6356
$returnTypes = $db->getReturnTagTypes();
64-
self::assertCount(4, $returnTypes);
57+
self::assertCount(6, $returnTypes);
6558
self::assertEquals('MyDto', $returnTypes[0]);
6659
self::assertEquals('MyOtherDto', $returnTypes[1]);
6760
self::assertEquals('ValueObject', $returnTypes[2]);
6861
self::assertEquals('User', $returnTypes[3]);
62+
self::assertEquals('int', $returnTypes[4]);
63+
self::assertEquals('MyPlainDto', $returnTypes[5]);
64+
}
65+
66+
public function test_it_should_extract_types_from_var_tag(): void
67+
{
68+
$parser = DocblockParserFactory::create();
69+
70+
$code = <<< 'PHP'
71+
/**
72+
* @var MyDto[] $dtoList
73+
* @var list<MyOtherDto> $dtoList2
74+
* @var array<int, ValueObject> $voList
75+
* @var array<User> $user
76+
* @var int $aValue
77+
* @var MyPlainDto $plainDto
78+
*/
79+
PHP;
80+
81+
$db = $parser->parse($code);
82+
83+
$varTags = $db->getVarTagTypes();
84+
self::assertCount(6, $varTags);
85+
self::assertEquals('MyDto', $varTags[0]);
86+
self::assertEquals('MyOtherDto', $varTags[1]);
87+
self::assertEquals('ValueObject', $varTags[2]);
88+
self::assertEquals('User', $varTags[3]);
89+
self::assertEquals('int', $varTags[4]);
90+
self::assertEquals('MyPlainDto', $varTags[5]);
91+
}
92+
93+
public function test_it_should_extract_doctrine_like_annotations(): void
94+
{
95+
$parser = DocblockParserFactory::create();
96+
97+
$code = <<< 'PHP'
98+
/**
99+
* @ORM\Id
100+
* @ORM\Column(type="integer")
101+
* @ORM\GeneratedValue
102+
* @Assert\NotBlank
103+
* @Assert\Length(min=5)
104+
*/
105+
PHP;
106+
107+
$db = $parser->parse($code);
108+
109+
$doctrineAnnotations = $db->getDoctrineLikeAnnotationTypes();
110+
111+
self::assertCount(5, $doctrineAnnotations);
112+
self::assertEquals('ORM\Id', $doctrineAnnotations[0]);
113+
self::assertEquals('ORM\Column', $doctrineAnnotations[1]);
114+
self::assertEquals('ORM\GeneratedValue', $doctrineAnnotations[2]);
115+
self::assertEquals('Assert\NotBlank', $doctrineAnnotations[3]);
116+
self::assertEquals('Assert\Length', $doctrineAnnotations[4]);
69117
}
70118
}

0 commit comments

Comments
 (0)