Skip to content

Commit 6cf6612

Browse files
authored
Merge pull request #48 from smeghead/bug-doc-comment-parse
fix Type Description like `array<int, string>` don't berecoganized.
2 parents a618bfe + e179715 commit 6cf6612

File tree

10 files changed

+152
-11
lines changed

10 files changed

+152
-11
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# CHANGELOG
22

3+
### Features
4+
5+
* introduced PHPStan in Develop environment.
6+
7+
### Bug fix
8+
9+
* fix Type Description like `array<int, string>` don't berecoganized.
10+
311
## v1.1.0 (2023-04-24)
412

513

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"require": {
1010
"php" : ">=8.0",
1111
"symfony/finder": "^5.3",
12-
"nikic/php-parser": "^4.13"
12+
"nikic/php-parser": "^4.13",
13+
"phpstan/phpdoc-parser": "^1.23"
1314
},
1415
"require-dev": {
1516
"phpunit/phpunit": "^9.5",

dogfood-package.png

23.3 KB
Loading

dogfood.png

92 Bytes
Loading

phpstan.neon

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,3 @@ parameters:
22
level: 6
33
paths:
44
- src
5-
- tests

src/DiagramElement/PackageRelations.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99

1010
class PackageRelations
1111
{
12-
/** @var array<string, \Smeghead\PhpClassDiagram\Php\PhpType[]> */
12+
/** @var array<string, array<int, PhpType>> */
1313
private array $uses;
1414
private Package $rootPackage;
1515

1616
/**
17-
* @param array<string, \Smeghead\PhpClassDiagram\Php\PhpType[]> $uses
17+
* @param array<string, array<int, PhpType>> $uses
1818
* @param Package $rootPackage
1919
*/
2020
public function __construct(array $uses, Package $rootPackage)

src/Php/Doc/PhpDocComment.php

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
namespace Smeghead\PhpClassDiagram\Php\Doc;
66

77
use PhpParser\Node\Stmt;
8+
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
9+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
10+
use PHPStan\PhpDocParser\Lexer\Lexer;
11+
use PHPStan\PhpDocParser\Parser\ConstExprParser;
12+
use PHPStan\PhpDocParser\Parser\PhpDocParser;
13+
use PHPStan\PhpDocParser\Parser\TokenIterator;
14+
use PHPStan\PhpDocParser\Parser\TypeParser;
815

916
class PhpDocComment
1017
{
@@ -36,23 +43,56 @@ public function getDescription(): string
3643
}
3744

3845
public function getVarTypeName(): string {
39-
if (preg_match('/\@var\s+(\S+)\s.*/', $this->text . ' ', $matches)) {
40-
return $matches[1];
46+
$phpDocNode = $this->getParseResult();
47+
$vars = $phpDocNode->getVarTagValues();
48+
if (count($vars) > 0) {
49+
if ( ! empty($vars[0]->type)) {
50+
return $this->convertUnionExpression($vars[0]->type->__toString());
51+
}
4152
}
4253
return '';
4354
}
4455

4556
public function getParamTypeName(string $paramName): string {
46-
if (preg_match(sprintf('/\@param\s+(\S+)\s+\$%s.*/', $paramName), $this->text . ' ', $matches)) {
47-
return $matches[1];
57+
$phpDocNode = $this->getParseResult();
58+
$paramTags = array_filter($phpDocNode->getParamTagValues(), function(ParamTagValueNode $node) use ($paramName) {
59+
return $node->parameterName === sprintf('$%s', $paramName);
60+
}); // ParamTagValueNode[]
61+
if (count($paramTags) > 0) {
62+
if ( ! empty($paramTags[0]->type)) {
63+
return $this->convertUnionExpression($paramTags[0]->type->__toString());
64+
}
4865
}
4966
return '';
5067
}
5168

5269
public function getReturnTypeName(): string {
53-
if (preg_match('/\@return\s+(\S+)\s+/', $this->text . ' ', $matches)) {
54-
return $matches[1];
70+
$phpDocNode = $this->getParseResult();
71+
$returns = $phpDocNode->getReturnTagValues();
72+
if (count($returns) > 0) {
73+
if ( ! empty($returns[0]->type)) {
74+
return $this->convertUnionExpression($returns[0]->type->__toString());
75+
}
5576
}
5677
return '';
5778
}
79+
80+
private function getParseResult(): PhpDocNode
81+
{
82+
$lexer = new Lexer();
83+
$constExprParser = new ConstExprParser();
84+
$typeParser = new TypeParser($constExprParser);
85+
$phpDocParser = new PhpDocParser($typeParser, $constExprParser);
86+
$tokens = new TokenIterator($lexer->tokenize('/** ' . $this->text . ' */'));
87+
return $phpDocParser->parse($tokens); // PhpDocNode
88+
}
89+
90+
private function convertUnionExpression(string $difinition): string
91+
{
92+
$difinition = preg_replace('/^\((.*)\)$/', '$1', $difinition);
93+
$typeStrings = array_map(function(string $x){
94+
return trim($x);
95+
}, explode('|', $difinition));
96+
return implode('|', $typeStrings);
97+
}
5898
}

src/Php/PhpTypeExpression.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ private function __construct(NodeAbstract $stmt, string $targetType, array $curr
4646

4747
$type = $stmt->{$targetType === self::RETURN_TYPE ? 'returnType' : 'type'};
4848
if (!empty($docString)) {
49-
foreach (explode('|', $docString) as $typeString) {
49+
$docString = preg_replace('/^\((.*)\)$/', '$1', $docString);
50+
$typeStrings = array_map(function(string $x){
51+
return trim($x);
52+
}, explode('|', $docString));
53+
foreach ($typeStrings as $typeString) {
5054
$this->types[] = $this->parseType($type, $currentNamespace, $typeString);
5155
}
5256
} else if ($type instanceof UnionType) {

test/PhpTypeExpressionTest.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,4 +456,84 @@ public function testMethodReturnObjectArray(): void
456456
$this->assertSame('Tag[]', $types[0]->getName(), 'name');
457457
$this->assertSame(false, $types[0]->getNullable(), 'nullable');
458458
}
459+
public function testVarArrayAlternative(): void
460+
{
461+
// /** @var array<int, Tag> 付与されたタグ一覧 */
462+
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
463+
$filename = sprintf('%s/phpdoc/product/Product.php', $this->fixtureDir);
464+
try {
465+
$ast = $parser->parse(file_get_contents($filename));
466+
} catch (Error $error) {
467+
throw new \Exception("Parse error: {$error->getMessage()} file: {$filename}\n");
468+
}
469+
$finder = new NodeFinder();
470+
$target = $finder->findFirst($ast, function(Node $node){
471+
return $node instanceof Property && $node->props[0]->name->toString() === 'alternativeTags';
472+
});
473+
$expression = PhpTypeExpression::buildByVar($target, ['hoge', 'fuga', 'product'], []);
474+
$types = $expression->getTypes();
475+
476+
$this->assertSame(['hoge', 'fuga', 'product'], $types[0]->getNamespace(), 'namespace');
477+
$this->assertSame('array<int, Tag>', $types[0]->getName(), 'name');
478+
$this->assertSame(false, $types[0]->getNullable(), 'nullable');
479+
480+
}
481+
public function testMethodParameterAltaernativeTag(): void
482+
{
483+
// /**
484+
// * @param array<int, Tag> $tags tags
485+
// * @return array<int, Tag> tags
486+
// */
487+
// public function arrayTags(array $tags): array
488+
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
489+
$filename = sprintf('%s/phpdoc/product/Product.php', $this->fixtureDir);
490+
try {
491+
$ast = $parser->parse(file_get_contents($filename));
492+
} catch (Error $error) {
493+
throw new \Exception("Parse error: {$error->getMessage()} file: {$filename}\n");
494+
}
495+
$finder = new NodeFinder();
496+
$method = $finder->findFirst($ast, function(Node $node){
497+
return $node instanceof ClassMethod && $node->name->toString() === 'arrayTags';
498+
});
499+
$param = $finder->findFirst($method, function (Node $node) {
500+
return $node instanceof Param && $node->var->name === 'tags';
501+
});
502+
$uses = [new PhpType(['hoge', 'fuga', 'product'], '', 'Tag')];
503+
$expression = PhpTypeExpression::buildByMethodParam($param, ['hoge', 'fuga', 'product'], $method, 'tags', $uses);
504+
$types = $expression->getTypes();
505+
506+
$this->assertSame(['hoge', 'fuga', 'product'], $types[0]->getNamespace(), 'namespace');
507+
$this->assertSame('array<int, Tag>', $types[0]->getName(), 'name');
508+
$this->assertSame(false, $types[0]->getNullable(), 'nullable');
509+
}
510+
public function testMethodReturnAltaernativeTag(): void
511+
{
512+
// /**
513+
// * @param array<int, Tag> $tags tags
514+
// * @return array<int, Tag> tags
515+
// */
516+
// public function arrayTags(array $tags): array
517+
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
518+
$filename = sprintf('%s/phpdoc/product/Product.php', $this->fixtureDir);
519+
try {
520+
$ast = $parser->parse(file_get_contents($filename));
521+
} catch (Error $error) {
522+
throw new \Exception("Parse error: {$error->getMessage()} file: {$filename}\n");
523+
}
524+
$finder = new NodeFinder();
525+
$method = $finder->findFirst($ast, function(Node $node){
526+
return $node instanceof ClassMethod && $node->name->toString() === 'arrayTags';
527+
});
528+
$param = $finder->findFirst($method, function (Node $node) {
529+
return $node instanceof Param && $node->var->name === 'tags';
530+
});
531+
$uses = [new PhpType(['hoge', 'fuga', 'product'], '', 'Tag')];
532+
$expression = PhpTypeExpression::buildByMethodReturn($method, ['hoge', 'fuga', 'product'], $uses);
533+
$types = $expression->getTypes();
534+
535+
$this->assertSame(['hoge', 'fuga', 'product'], $types[0]->getNamespace(), 'namespace');
536+
$this->assertSame('array<int, Tag>', $types[0]->getName(), 'name');
537+
$this->assertSame(false, $types[0]->getNullable(), 'nullable');
538+
}
459539
}

test/fixtures/phpdoc/product/Product.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,13 @@ class Product {
2222

2323
/** @var array<int, Tag> 付与されたタグ一覧 */
2424
private array $alternativeTags;
25+
26+
/**
27+
* @param array<int, Tag> $tags tags
28+
* @return array<int, Tag> tags
29+
*/
30+
public function arrayTags(array $tags): array
31+
{
32+
return $tags;
33+
}
2534
}

0 commit comments

Comments
 (0)