Skip to content

Commit

Permalink
Handle typed arrays (#355)
Browse files Browse the repository at this point in the history
* Refactor PhpDocNode computation

* Handle typed arrays in class properties (#344)

* Handle typed arrays in method params (#344)

* Handle typed arrays in return types (#344)

* Refactor PHPDoc library usage (#344)

* Bug fix for invalid types (#344)

* Add comments to method getArrayItemType (#344)

* Add test for typed array in methods with multiple params (#344)
  • Loading branch information
LuigiCardamone authored Feb 27, 2023
1 parent 259f654 commit 8531eef
Show file tree
Hide file tree
Showing 2 changed files with 428 additions and 10 deletions.
118 changes: 108 additions & 10 deletions src/Analyzer/NameResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Use_;
use PhpParser\NodeAbstract;
use PhpParser\NodeVisitorAbstract;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
Expand All @@ -34,6 +39,12 @@ class NameResolver extends NodeVisitorAbstract
/** @var bool Whether to parse DocBlock Custom Annotations */
protected $parseCustomAnnotations;

/** @var PhpDocParser */
protected $phpDocParser;

/** @var Lexer */
protected $phpDocLexer;

/**
* Constructs a name resolution visitor.
*
Expand All @@ -54,6 +65,11 @@ public function __construct(ErrorHandler $errorHandler = null, array $options =
$this->preserveOriginalNames = $options['preserveOriginalNames'] ?? false;
$this->replaceNodes = $options['replaceNodes'] ?? true;
$this->parseCustomAnnotations = $options['parseCustomAnnotations'] ?? true;

$typeParser = new TypeParser();
$constExprParser = new ConstExprParser();
$this->phpDocParser = new PhpDocParser($typeParser, $constExprParser);
$this->phpDocLexer = new Lexer();
}

/**
Expand Down Expand Up @@ -131,21 +147,25 @@ public function enterNode(Node $node)
}
$this->resolveAttrGroups($node);

$lexer = new Lexer();
$typeParser = new TypeParser();
$constExprParser = new ConstExprParser();
$phpDocParser = new PhpDocParser($typeParser, $constExprParser);
$phpDocNode = $this->getPhpDocNode($node);

if (null === $node->getDocComment()) {
if (null === $phpDocNode) {
return;
}

/** @var Doc $docComment */
$docComment = $node->getDocComment();
if ($this->isNodeOfTypeArray($node)) {
$arrayItemType = null;

$tokens = $lexer->tokenize($docComment->getText());
$tokenIterator = new TokenIterator($tokens);
$phpDocNode = $phpDocParser->parse($tokenIterator);
foreach ($phpDocNode->getVarTagValues() as $tagValue) {
$arrayItemType = $this->getArrayItemType($tagValue->type);
}

if (null !== $arrayItemType) {
$node->type = $this->resolveName(new Node\Name($arrayItemType), Use_::TYPE_NORMAL);

return;
}
}

foreach ($phpDocNode->getVarTagValues() as $tagValue) {
$type = $this->resolveName(new Node\Name((string) $tagValue->type), Use_::TYPE_NORMAL);
Expand Down Expand Up @@ -307,11 +327,38 @@ private function addAlias(Stmt\UseUse $use, int $type, Name $prefix = null): voi
/** @param Stmt\Function_|Stmt\ClassMethod|Expr\Closure|Expr\ArrowFunction $node */
private function resolveSignature($node): void
{
$phpDocNode = $this->getPhpDocNode($node);

foreach ($node->params as $param) {
$param->type = $this->resolveType($param->type);
$this->resolveAttrGroups($param);

if ($this->isNodeOfTypeArray($param) && null !== $phpDocNode) {
foreach ($phpDocNode->getParamTagValues() as $phpDocParam) {
if ($param->var instanceof Expr\Variable && \is_string($param->var->name) && $phpDocParam->parameterName === ('$'.$param->var->name)) {
$arrayItemType = $this->getArrayItemType($phpDocParam->type);

if (null !== $arrayItemType) {
$param->type = $this->resolveName(new Node\Name($arrayItemType), Use_::TYPE_NORMAL);
}
}
}
}
}

$node->returnType = $this->resolveType($node->returnType);

if ($node->returnType instanceof Node\Identifier && 'array' === $node->returnType->name && null !== $phpDocNode) {
$arrayItemType = null;

foreach ($phpDocNode->getReturnTagValues() as $tagValue) {
$arrayItemType = $this->getArrayItemType($tagValue->type);
}

if (null !== $arrayItemType) {
$node->returnType = $this->resolveName(new Node\Name($arrayItemType), Use_::TYPE_NORMAL);
}
}
}

/**
Expand Down Expand Up @@ -341,4 +388,55 @@ private function resolveType($node)

return $node;
}

private function getPhpDocNode(NodeAbstract $node): ?PhpDocNode
{
if (null === $node->getDocComment()) {
return null;
}

/** @var Doc $docComment */
$docComment = $node->getDocComment();

$tokens = $this->phpDocLexer->tokenize($docComment->getText());
$tokenIterator = new TokenIterator($tokens);

return $this->phpDocParser->parse($tokenIterator);
}

/**
* @param Node\Param|Stmt\Property $node
*/
private function isNodeOfTypeArray($node): bool
{
return null !== $node->type && isset($node->type->name) && 'array' === $node->type->name;
}

private function getArrayItemType(TypeNode $typeNode): ?string
{
$arrayItemType = null;

if ($typeNode instanceof GenericTypeNode) {
if (1 === \count($typeNode->genericTypes)) {
// this handles list<ClassName>
$arrayItemType = (string) $typeNode->genericTypes[0];
} elseif (2 === \count($typeNode->genericTypes)) {
// this handles array<int, ClassName>
$arrayItemType = (string) $typeNode->genericTypes[1];
}
}

if ($typeNode instanceof ArrayTypeNode) {
// this handles ClassName[]
$arrayItemType = (string) $typeNode->type;
}

$validFqcn = '/^[a-zA-Z_\x7f-\xff\\\\][a-zA-Z0-9_\x7f-\xff\\\\]*[a-zA-Z0-9_\x7f-\xff]$/';

if (null !== $arrayItemType && !(bool) preg_match($validFqcn, $arrayItemType)) {
return null;
}

return $arrayItemType;
}
}
Loading

0 comments on commit 8531eef

Please sign in to comment.