Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ jobs:
strategy:
matrix:
php-version:
- "7.3"
- "7.4"
- "8.0"
- "8.1"
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [Unreleased]
### Added
- PHPDoc type names that depend on `use` imports (e.g. in `@param`, `@return`, `@var`, `@throws`, `@property`, `@method`, `@mixin`, and `@phpstan-*` tags) are now resolved to their fully qualified form in generated stubs, while template names, array/object shape keys, and reserved/built-in types are left untouched.
### Changed
- Minimum supported PHP version raised to 7.4 (required by `phpstan/phpdoc-parser` 2.x).

## [0.5] - 2018-04-13
### Added
- `--nullify-globals` option: converts all global variable assignments to `null`.
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@
"bin/generate-stubs"
],
"require": {
"php": "^7.3 || ^8.0",
"php": "^7.4 || ^8.0",
"nikic/php-parser": "^4.18 || ^5.5",
"phpstan/phpdoc-parser": "^2.3",
"symfony/console": "^5.1 || ^6.0 || ^7.0",
"symfony/filesystem": "^5.0 || ^6.0 || ^7.0",
"symfony/finder": "^5.0 || ^6.0 || ^7.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "3.4.0 || ^3.12",
"mikey179/vfsstream": "^1.6",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "^1.0 || ^2.0",
"phpstan/phpstan-symfony": "^1.0 || ^2.0",
Expand Down
109 changes: 99 additions & 10 deletions src/NodeVisitor.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<?php
namespace StubsGenerator;

use function count;
use function defined;
use function is_string;
use function ltrim;
use PhpParser\Comment\Doc;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\ArrayDimFetch;
Expand All @@ -19,17 +24,18 @@
use PhpParser\Node\Stmt\Enum_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\GroupUse;
use PhpParser\Node\Stmt\If_;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Property;

use PhpParser\Node\Stmt\Trait_;
use PhpParser\Node\Stmt\Use_;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;

use function count;
use function defined;
use function is_string;
use function sprintf;
use function strtolower;

/**
* On node traversal, this visitor converts any AST to one just containing stub
Expand Down Expand Up @@ -59,6 +65,10 @@ class NodeVisitor extends NodeVisitorAbstract
private $nullifyGlobals;
/** @var bool */
private $includeInaccessibleClassNodes;
/** @var array<string, string> */
private $useAliases = [];
/** @var PhpDocFqcnRewriter */
private $fqcnRewriter;

/**
* @var array<Node>
Expand Down Expand Up @@ -118,18 +128,23 @@ public function init(int $symbols = StubsGenerator::DEFAULT, array $config = [])
$this->includeInaccessibleClassNodes = ($config['include_inaccessible_class_nodes'] ?? false) === true;

$this->globalNamespace = new Namespace_();
$this->fqcnRewriter = new PhpDocFqcnRewriter();
}

public function beforeTraverse(array $nodes)
{
$this->stack = [];
$this->useAliases = [];
return null;
}

public function enterNode(Node $node)
{
$this->stack[] = $node;

$this->trackUseStatements($node);
$this->rewriteImportedNames($node);

if ($node instanceof Namespace_) {
// We always need to parse the children of namespaces.
return null;
Expand All @@ -156,15 +171,17 @@ public function enterNode(Node $node)
// signatures are fully qualified by the `NameResolver` visitor.
// (This will already be `true` if it's a ClassMethod.)
$this->isInDeclaration = true;
} elseif ($node instanceof Expression
} elseif (
$node instanceof Expression
&& $node->expr instanceof Assign
) {
// Since we don't parse any the bodies of any statements which can
// hold variable assignments---other than namespaces---we know these
// assigns are for globals. Check if we are assigning to `$GLOBALS`
// with a simple string that's a valid variable identifier. If so,
// convert it to a normal variable assignment.
if (count($this->stack) === 1
if (
count($this->stack) === 1
&& $node->expr->var instanceof ArrayDimFetch
&& $node->expr->var->var instanceof Variable
&& $node->expr->var->var->name === 'GLOBALS'
Expand Down Expand Up @@ -203,6 +220,77 @@ public function enterNode(Node $node)
return null;
}

private function trackUseStatements(Node $node): void
{
if ($node instanceof Namespace_) {
$this->useAliases = [];
return;
}

if ($node instanceof Use_) {
foreach ($node->uses as $use) {
$this->addAlias($use, $node->type, '');
}
return;
}

if (!($node instanceof GroupUse)) {
return;
}

$prefix = sprintf('%s\\', $node->prefix->toString());
foreach ($node->uses as $use) {
$this->addAlias($use, $node->type, $prefix);
}
}

/**
* @param \PhpParser\Node\UseItem $useItem
*/
private function addAlias(Node $useItem, int $type, string $prefix): void
{
if ($useItem->type !== Use_::TYPE_UNKNOWN) {
$type = $useItem->type;
}

if ($type !== Use_::TYPE_NORMAL) {
return;
}

$alias = strtolower($useItem->getAlias()->toString());
$fullyQualifiedName = ltrim(sprintf('%s%s', $prefix, $useItem->name->toString()), '\\');

$this->useAliases[$alias] = sprintf('\\%s', $fullyQualifiedName);
}

private function rewriteImportedNames(Node $node): void
{
if (
!($node instanceof Function_)
&& !($node instanceof ClassMethod)
&& !($node instanceof Property)
&& !($node instanceof ClassLike)
) {
return;
}

$docComment = $node->getDocComment();
if (!($docComment instanceof Doc)) {
return;
}

if ($this->useAliases === []) {
return;
}

$newText = $this->fqcnRewriter->rewrite($docComment->getText(), $this->useAliases);
if ($newText === $docComment->getText()) {
return;
}

$node->setDocComment(new Doc($newText, $docComment->getStartLine(), $docComment->getStartFilePos()));
}

public function leaveNode(Node $node, bool $preserveStack = false)
{
if (!$preserveStack) {
Expand Down Expand Up @@ -258,8 +346,9 @@ public function leaveNode(Node $node, bool $preserveStack = false)
// either a method, property, or constant, or enum case, or its part
// of the declaration itself (e.g., `extends`).

if (!$this->includeInaccessibleClassNodes && ($parent instanceof Class_ || $parent instanceof Enum_) && ($node instanceof ClassMethod || $node instanceof ClassConst || $node instanceof Property)) {
if ($node->isPrivate()
if (!$this->includeInaccessibleClassNodes && ($parent instanceof Class_ || $parent instanceof Enum_) && ($node instanceof ClassMethod || $node instanceof ClassConst || $node instanceof Property)) {
if (
$node->isPrivate()
|| ($parent instanceof Class_ && $parent->isFinal() && $node->isProtected())
|| ($parent instanceof Enum_ && $node->isProtected())
) {
Expand Down Expand Up @@ -406,7 +495,7 @@ function (\PhpParser\Node\Const_ $const) {
!isset($this->counts['constants'][$fullyQualifiedName])
) {
return $this->count('constants', $fullyQualifiedName)
&& !defined($fullyQualifiedName);
&& !defined($fullyQualifiedName);
}
}
);
Expand All @@ -429,7 +518,7 @@ function (\PhpParser\Node\Const_ $const) {
!isset($this->counts['constants'][$fullyQualifiedName])
) {
return $this->count('constants', $fullyQualifiedName)
&& !defined($fullyQualifiedName);
&& !defined($fullyQualifiedName);
}
}
}
Expand Down
70 changes: 70 additions & 0 deletions src/PhpDocFqcnRewriter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types = 1);
namespace StubsGenerator;

use PHPStan\PhpDocParser\Ast\NodeTraverser;
use PHPStan\PhpDocParser\Ast\NodeVisitor\CloningVisitor;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
use PHPStan\PhpDocParser\ParserConfig;
use PHPStan\PhpDocParser\Printer\Printer;

use function strtolower;

final class PhpDocFqcnRewriter
{
private Lexer $lexer;
private Printer $printer;
private PhpDocParser $docParser;

public function __construct()
{
$config = new ParserConfig(['lines' => true, 'indexes' => true, 'comments' => true]);
$constExprParser = new ConstExprParser($config);

$this->lexer = new Lexer($config);
$this->printer = new Printer();
$this->docParser = new PhpDocParser($config, new TypeParser($config, $constExprParser), $constExprParser);
}

/**
* @param array<string, string> $imports
*/
public function rewrite(string $docComment, array $imports): string
{
if ($imports === []) {
return $docComment;
}

$aliases = [];
foreach ($imports as $alias => $fqcn) {
$aliases[strtolower($alias)] = $fqcn;
}

try {
$tokens = new TokenIterator($this->lexer->tokenize($docComment));
$original = $this->docParser->parse($tokens);
} catch (\Throwable $e) {
return $docComment;
}

$rewritten = $this->cloningTraverser()->traverse([$original])[0];
(new NodeTraverser([new PhpDocTypeNameResolver($aliases)]))->traverse([$rewritten]);

if (! $rewritten instanceof PhpDocNode) {
return $docComment;
}

return $this->printer->printFormatPreserving($rewritten, $original, $tokens);
}

private function cloningTraverser(): NodeTraverser
{
return new NodeTraverser([new CloningVisitor()]);
}
}
Loading