1
0
mirror of https://github.com/danog/phpdoc.git synced 2024-11-26 12:04:47 +01:00
This commit is contained in:
Daniil Gentili 2024-04-06 18:17:12 +02:00
parent 9112c1537f
commit 5e7cd94ec1
6 changed files with 114 additions and 191 deletions

View File

@ -13,15 +13,13 @@
"require": { "require": {
"php": "^8.0", "php": "^8.0",
"danog/class-finder": "^0.4", "danog/class-finder": "^0.4",
"phpdocumentor/reflection-docblock": "^5.2",
"phpstan/phpdoc-parser": "^1.2", "phpstan/phpdoc-parser": "^1.2",
"symfony/yaml": "^6.0" "symfony/yaml": "^6.0"
}, },
"require-dev": { "require-dev": {
"vimeo/psalm": "dev-master", "vimeo/psalm": "dev-master",
"amphp/php-cs-fixer-config": "dev-master", "amphp/php-cs-fixer-config": "dev-master",
"friendsofphp/php-cs-fixer": "^2", "friendsofphp/php-cs-fixer": "^2"
"phabel/phabel": "^1"
}, },
"authors": [ "authors": [
{ {
@ -51,12 +49,7 @@
"config": { "config": {
"allow-plugins": { "allow-plugins": {
"composer/package-versions-deprecated": true, "composer/package-versions-deprecated": true,
"phabel/phabel": true "phabel/phabel": false
}
},
"extra": {
"phabel": {
"revision": 0
} }
} }
} }

View File

@ -22,8 +22,12 @@ use danog\ClassFinder\ClassFinder;
use danog\PhpDoc\PhpDoc\ClassDoc; use danog\PhpDoc\PhpDoc\ClassDoc;
use danog\PhpDoc\PhpDoc\FunctionDoc; use danog\PhpDoc\PhpDoc\FunctionDoc;
use danog\PhpDoc\PhpDoc\GenericDoc; use danog\PhpDoc\PhpDoc\GenericDoc;
use phpDocumentor\Reflection\DocBlock\Tags\Author; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use phpDocumentor\Reflection\DocBlockFactory; 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 ReflectionClass; use ReflectionClass;
use ReflectionFunction; use ReflectionFunction;
use Symfony\Component\Yaml\Escaper; use Symfony\Component\Yaml\Escaper;
@ -44,9 +48,13 @@ class PhpDoc
*/ */
private int $mode = ClassFinder::ALLOW_ALL | ClassFinder::RECURSIVE_MODE; private int $mode = ClassFinder::ALLOW_ALL | ClassFinder::RECURSIVE_MODE;
/** /**
* Docblock factory. * PHPDOC parser.
*/ */
private DocBlockFactory $factory; private PhpDocParser $parser;
/**
* Lexer.
*/
private Lexer $lexer;
/** /**
* Authors. * Authors.
*/ */
@ -119,7 +127,14 @@ class PhpDoc
*/ */
private function __construct(string $namespace) private function __construct(string $namespace)
{ {
$this->factory = DocBlockFactory::createInstance(); $this->lexer = new Lexer();
$constExprParser = new ConstExprParser();
$typeParser = new TypeParser($constExprParser);
$this->parser = new PhpDocParser(
$typeParser,
$constExprParser,
textBetweenTagsBelongsToDescription: true
);
$this->namespace = $namespace; $this->namespace = $namespace;
$appRoot = new \danog\ClassFinder\AppConfig; $appRoot = new \danog\ClassFinder\AppConfig;
@ -131,7 +146,7 @@ class PhpDoc
$this->name = $json['name'] ?? ''; $this->name = $json['name'] ?? '';
$this->description = $json['description'] ?? ''; $this->description = $json['description'] ?? '';
foreach ($authors as $author) { foreach ($authors as $author) {
$this->authors []= new Author($author['name'], $author['email']); $this->authors []= "{$author['name']} <{$author['email']}>";
} }
if (!$this->namespace) { if (!$this->namespace) {
@ -445,7 +460,7 @@ class PhpDoc
} }
if ($reflectionClass instanceof ReflectionClass) { if ($reflectionClass instanceof ReflectionClass) {
array_map($this->addTypeAliases(...), $reflectionClass->getTraitNames()); \array_map($this->addTypeAliases(...), $reflectionClass->getTraitNames());
} }
} }
/** /**
@ -487,15 +502,13 @@ class PhpDoc
} }
/** /**
* Get docblock factory. * Parse phpdoc.
* *
* @internal * @internal
*
* @return DocBlockFactory
*/ */
public function getFactory(): DocBlockFactory public function parse(string $phpdoc): PhpDocNode
{ {
return $this->factory; return $this->parser->parse(new TokenIterator($this->lexer->tokenize($phpdoc)));
} }
/** /**
@ -511,7 +524,7 @@ class PhpDoc
/** /**
* Get authors. * Get authors.
* *
* @return Author[] * @return string[]
*/ */
public function getAuthors(): array public function getAuthors(): array
{ {
@ -521,7 +534,7 @@ class PhpDoc
/** /**
* Set authors. * Set authors.
* *
* @param Author[] $authors Authors * @param string[] $authors Authors
* *
* @return self * @return self
*/ */

View File

@ -3,10 +3,9 @@
namespace danog\PhpDoc\PhpDoc; namespace danog\PhpDoc\PhpDoc;
use danog\PhpDoc\PhpDoc; use danog\PhpDoc\PhpDoc;
use phpDocumentor\Reflection\DocBlock\Tags\Generic; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag; use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode;
use phpDocumentor\Reflection\DocBlock\Tags\Property; use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use phpDocumentor\Reflection\DocBlock\Tags\Var_;
use ReflectionClass; use ReflectionClass;
use ReflectionClassConstant; use ReflectionClassConstant;
use ReflectionMethod; use ReflectionMethod;
@ -40,7 +39,7 @@ class ClassDoc extends GenericDoc
$this->builder = $builder; $this->builder = $builder;
$this->name = $reflectionClass->getName(); $this->name = $reflectionClass->getName();
$doc = $reflectionClass->getDocComment() ?: '/** */'; $doc = $reflectionClass->getDocComment() ?: '/** */';
$doc = $this->builder->getFactory()->create($doc); $doc = $this->builder->parse($doc);
parent::__construct($doc, $reflectionClass); parent::__construct($doc, $reflectionClass);
@ -50,13 +49,15 @@ class ClassDoc extends GenericDoc
$type = (string) $type; $type = (string) $type;
$name = $property->getName(); $name = $property->getName();
$comment = ''; $comment = '';
foreach ($this->builder->getFactory()->create($property->getDocComment() ?: '/** */')->getTags() as $tag) { foreach ($this->builder->parse($property->getDocComment() ?: '/** */')->getTags() as $tag) {
if ($tag instanceof Var_) { if ($tag->name === '@var') {
$comment = $tag->getDescription(); $tag = $tag->value;
$type = $tag->getType() ?? 'mixed'; \assert($tag instanceof VarTagValueNode);
$type = (string) $tag->type;
$comment = $tag->description;
break; break;
} }
if ($tag instanceof Generic && $tag->getName() === 'internal') { if ($tag->name === 'internal') {
continue 2; continue 2;
} }
} }
@ -66,27 +67,17 @@ class ClassDoc extends GenericDoc
$docReflection .= " * @property $type \$$name $comment\n"; $docReflection .= " * @property $type \$$name $comment\n";
} }
$docReflection .= " */\n"; $docReflection .= " */\n";
$docReflection = $this->builder->getFactory()->create($docReflection); $docReflection = $this->builder->parse($docReflection);
$tags = \array_merge($docReflection->getTags(), $doc->getTags()); $tags = \array_merge($docReflection->getTags(), $doc->getTags());
foreach ($tags as $tag) { foreach ($tags as $tag) {
if ($tag instanceof Property && $tag->getVariableName()) { if ($tag->name === 'property') {
$tag = $tag->value;
\assert($tag instanceof PropertyTagValueNode);
/** @psalm-suppress InvalidPropertyAssignmentValue */ /** @psalm-suppress InvalidPropertyAssignmentValue */
$this->properties[$tag->getVariableName()] = [ $this->properties[$tag->propertyName] = [
$tag->getType(), (string) $tag->type,
$tag->getDescription() $tag->description
];
}
if ($tag instanceof InvalidTag && $tag->getName() === 'property') {
[$type, $description] = \explode(" $", $tag->render(), 2);
$description .= ' ';
[$varName, $description] = \explode(" ", $description, 2);
$type = \str_replace('@property ', '', $type);
$description ??= '';
/** @psalm-suppress InvalidPropertyAssignmentValue */
$this->properties[$varName] = [
$type,
$description
]; ];
} }
} }
@ -97,19 +88,17 @@ class ClassDoc extends GenericDoc
} }
$description = ''; $description = '';
if ($refl->getDocComment()) { if ($refl->getDocComment()) {
$docConst = $this->builder->getFactory()->create($refl->getDocComment());
if ($this->builder->shouldIgnore($refl->getDeclaringClass()->getName())) { if ($this->builder->shouldIgnore($refl->getDeclaringClass()->getName())) {
continue; continue;
} }
foreach ($docConst->getTags() as $tag) { $docConst = $this->builder->parse($refl->getDocComment());
if ($tag instanceof Generic && $tag->getName() === 'internal') { if ($docConst->getTagsByName('internal')) {
continue 2; continue;
}
} }
$description .= $docConst->getSummary(); foreach ($docConst->children as $node) {
if ($docConst->getDescription()) { if ($node instanceof PhpDocTextNode) {
$description .= "\n\n"; $description .= $node->text."\n";
$description .= $docConst->getDescription(); }
} }
} }
$description = \trim($description); $description = \trim($description);

View File

@ -3,15 +3,9 @@
namespace danog\PhpDoc\PhpDoc; namespace danog\PhpDoc\PhpDoc;
use danog\PhpDoc\PhpDoc; use danog\PhpDoc\PhpDoc;
use phpDocumentor\Reflection\DocBlock; use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
use phpDocumentor\Reflection\DocBlock\Description; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use phpDocumentor\Reflection\DocBlock\Tags\Author; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
use phpDocumentor\Reflection\DocBlock\Tags\Generic;
use phpDocumentor\Reflection\DocBlock\Tags\Reference\Fqsen;
use phpDocumentor\Reflection\DocBlock\Tags\Reference\Url;
use phpDocumentor\Reflection\DocBlock\Tags\See;
use phpDocumentor\Reflection\Fqsen as ReflectionFqsen;
use ReflectionClass; use ReflectionClass;
use ReflectionFunction; use ReflectionFunction;
@ -37,17 +31,17 @@ abstract class GenericDoc
/** /**
* Description. * Description.
*/ */
protected Description $description; protected string $description;
/** /**
* See also array. * See also array.
* *
* @var array<string, See> * @var array<string, GenericTagValueNode>
*/ */
protected array $seeAlso = []; protected array $seeAlso = [];
/** /**
* Authors. * Authors.
* *
* @var Author[] * @var string[]
*/ */
protected array $authors; protected array $authors;
/** /**
@ -69,34 +63,39 @@ abstract class GenericDoc
/** /**
* Constructor. * Constructor.
* *
* @param DocBlock $doc
* @param ReflectionClass|ReflectionFunction $reflectionClass * @param ReflectionClass|ReflectionFunction $reflectionClass
*/ */
public function __construct(DocBlock $doc, $reflectionClass) public function __construct(PhpDocNode $doc, $reflectionClass)
{ {
$empty = []; $empty = [];
$this->className = $reflectionClass->getName(); $this->className = $reflectionClass->getName();
$this->resolvedClassName = $this->builder->resolveTypeAlias($this->className, $this->className, $empty); $this->resolvedClassName = $this->builder->resolveTypeAlias($this->className, $this->className, $empty);
$this->namespace = \str_replace('/', '\\', \dirname(\str_replace('\\', '/', $this->className))); $this->namespace = \str_replace('/', '\\', \dirname(\str_replace('\\', '/', $this->className)));
$this->title = $doc->getSummary(); $description = '';
$this->description = $doc->getDescription(); foreach ($doc->children as $child) {
if ($child instanceof PhpDocTextNode) {
$description .= $child->text."\n";
}
}
[$this->title, $this->description] = \explode("\n", \trim($description)."\n", 2);
$tags = $doc->getTags(); $tags = $doc->getTags();
$this->authors = $this->builder->getAuthors(); $this->authors = $this->builder->getAuthors();
foreach ($tags as $tag) { foreach ($tags as $tag) {
if ($tag instanceof Author) { if ($tag->name === '@author') {
$this->authors []= $tag; $tag = $tag->value;
\assert($tag instanceof GenericTagValueNode);
$this->authors []= $tag->value;
continue;
} }
if ($tag instanceof Deprecated) { if ($tag->name === '@deprecated' || $tag->name === '@internal') {
$this->ignore = true; $this->ignore = true;
break; break;
} }
if ($tag instanceof Generic && $tag->getName() === 'internal') { if ($tag->name === '@see') {
$this->ignore = true; $tag = $tag->value;
break; \assert($tag instanceof GenericTagValueNode);
} $this->seeAlso[$tag->value] = $tag;
if ($tag instanceof See) {
$this->seeAlso[$tag->getReference()->__toString()] = $tag;
} }
} }
$this->authors = \array_unique($this->authors); $this->authors = \array_unique($this->authors);
@ -111,38 +110,9 @@ abstract class GenericDoc
{ {
$namespace = \explode('\\', $namespace); $namespace = \explode('\\', $namespace);
$empty = [];
$seeAlso = ''; $seeAlso = '';
foreach ($this->seeAlso as $see) { foreach ($this->seeAlso as $see) {
$ref = $see->getReference(); $seeAlso .= "* ".$see->value."\n";
if ($ref instanceof Fqsen) {
$ref = (string) $ref;
$ref = $this->builder->resolveTypeAlias($this->className, $ref, $empty);
$to = \explode("\\", $ref.".md");
if (\count($to) === 2 || !$this->builder->hasClass($ref)) {
$seeAlso .= "* `$ref`\n";
continue;
}
\array_shift($to);
\array_unshift($to, ...\array_fill(0, \count($namespace), '..'));
$relPath = $to;
$path = \implode('/', $relPath);
if (!$desc = $see->getDescription()) {
if ($desc = $this->builder->getTitle($ref)) {
$desc = "`$ref`: $desc";
} else {
$desc = $ref;
}
}
$seeAlso .= "* [$desc]($path)\n";
}
if ($ref instanceof Url) {
$desc = $see->getDescription() ?: $ref;
$seeAlso .= "* [$desc]($ref)\n";
}
} }
if ($seeAlso) { if ($seeAlso) {
$seeAlso = "\n#### See also: \n$seeAlso\n\n"; $seeAlso = "\n#### See also: \n$seeAlso\n\n";
@ -208,7 +178,7 @@ abstract class GenericDoc
continue; continue;
} }
try { try {
$this->seeAlso[$type] = new See(new Fqsen(new ReflectionFqsen($type))); $this->seeAlso[$type] = new GenericTagValueNode($type);
} catch (\Throwable $e) { } catch (\Throwable $e) {
} }
} }

View File

@ -3,10 +3,7 @@
namespace danog\PhpDoc\PhpDoc; namespace danog\PhpDoc\PhpDoc;
use danog\PhpDoc\PhpDoc; use danog\PhpDoc\PhpDoc;
use phpDocumentor\Reflection\DocBlock\Description; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use phpDocumentor\Reflection\DocBlock\Tags\Generic;
use phpDocumentor\Reflection\DocBlock\Tags\Param;
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
use ReflectionFunctionAbstract; use ReflectionFunctionAbstract;
use ReflectionMethod; use ReflectionMethod;
@ -17,10 +14,8 @@ use ReflectionMethod;
*/ */
class MethodDoc extends GenericDoc class MethodDoc extends GenericDoc
{ {
private Return_ $return; private ReturnTagValueNode $return;
private string $psalmReturn;
private array $params = []; private array $params = [];
private array $psalmParams = [];
/** /**
* Constructor. * Constructor.
* *
@ -32,16 +27,17 @@ class MethodDoc extends GenericDoc
$this->builder = $phpDocBuilder; $this->builder = $phpDocBuilder;
$this->name = $method->getName(); $this->name = $method->getName();
$doc = $method->getDocComment() ?: '/** */'; $doc = $method->getDocComment() ?: '/** */';
$doc = $this->builder->getFactory()->create($doc); $doc = $this->builder->parse($doc);
parent::__construct($doc, $method instanceof ReflectionMethod ? $method->getDeclaringClass() : $method); parent::__construct($doc, $method instanceof ReflectionMethod ? $method->getDeclaringClass() : $method);
$order = []; $order = [];
$optional = []; $optional = [];
$params = [];
$docReflection = "/**\n"; $docReflection = "/**\n";
foreach ($method->getParameters() as $param) { foreach ($method->getParameters() as $param) {
$order []= $param->getName(); $order []= '$'.$param->getName();
$opt = $param->isOptional() && !$param->isVariadic(); $opt = $param->isOptional() && !$param->isVariadic();
$default = ''; $default = '';
if ($opt) { if ($opt) {
@ -51,54 +47,38 @@ class MethodDoc extends GenericDoc
$default = \str_replace([PHP_EOL, 'array (', ')'], ['', '[', ']'], \var_export($param->getDefaultValue(), true)); $default = \str_replace([PHP_EOL, 'array (', ')'], ['', '[', ']'], \var_export($param->getDefaultValue(), true));
} }
} }
$optional[$param->getName()] = [$opt, $default]; $optional['$'.$param->getName()] = [$opt, $default];
$type = (string) ($param->getType() ?? 'mixed'); $type = (string) ($param->getType() ?? 'mixed');
$variadic = $param->isVariadic() ? '...' : ''; $params['$'.$param->getName()] = [
$docReflection .= " * @param $type $variadic\$".$param->getName()."\n"; $type,
'',
$param->isVariadic(),
$optional['$'.$param->getName()]
];
} }
$docReflection .= ' * @return '.($method->getReturnType() ?? 'mixed')."\n*/";
$docReflection = $this->builder->getFactory()->create($docReflection);
$params = []; foreach (['@param', '@psalm-param', '@phpstan-param'] as $t) {
$psalmParams = []; foreach ($doc->getParamTagValues($t) as $tag) {
$params[$tag->parameterName] ??= [
foreach ([...$doc->getTags(), ...$docReflection->getTags()] as $tag) { $tag->type,
if ($tag instanceof Param && !isset($params[$tag->getVariableName()])) { $tag->description,
$params[$tag->getVariableName()] = [ $tag->isVariadic,
$tag->getType(), $optional[$tag->parameterName]
$tag->getDescription(),
$tag->isVariadic(),
$optional[$tag->getVariableName()]
]; ];
} elseif ($tag instanceof Return_ && !isset($this->return) && $this->name !== '__construct') { $params[$tag->parameterName][0] = $tag->type;
$this->return = $tag; $params[$tag->parameterName][1] = $tag->description;
} elseif ($tag instanceof Generic && $tag->getName() === 'psalm-return') { }
$this->psalmReturn = $tag; }
} elseif ($tag instanceof Generic && $tag->getName() === 'psalm-param') { if ($this->name !== '__construct') {
$desc = $tag->getDescription(); foreach (['@return', '@psalm-return', '@phpstan-return'] as $t) {
$dollar = \strrpos($desc, '$'); foreach ($doc->getReturnTagValues($t) as $tag) {
$type = \substr($tag, 0, $dollar-1); $this->return = $tag;
$description = \substr($tag, $dollar+1);
$description .= ' ';
[$varName, $description] = \explode(" ", $description, 2);
if (!$description && isset($params[$varName])) {
$description = (string) $params[$varName][1];
} else {
$description = new Description($description);
} }
$psalmParams[$varName] = [
$type,
$description,
$optional[$varName]
];
} }
} }
foreach ($order as $param) { foreach ($order as $param) {
$this->params[$param] = $params[$param]; $this->params[$param] = $params[$param];
if (isset($psalmParams[$param])) {
$this->psalmParams[$param] = $psalmParams[$param];
}
} }
foreach ($this->params as &$param) { foreach ($this->params as &$param) {
@ -106,14 +86,6 @@ class MethodDoc extends GenericDoc
$param[0] = $this->resolveTypeAlias($param[0]); $param[0] = $this->resolveTypeAlias($param[0]);
} }
} }
foreach ($this->psalmParams as &$param) {
if (isset($param[0])) {
$param[0] = $this->resolveTypeAlias($param[0]);
}
}
if (isset($this->psalmReturn)) {
$this->psalmReturn = $this->resolveTypeAlias($this->psalmReturn);
}
} }
/** /**
@ -130,7 +102,7 @@ class MethodDoc extends GenericDoc
if ($variadic) { if ($variadic) {
$sig .= '...'; $sig .= '...';
} }
$sig .= "$".$var; $sig .= $var;
if ($optional) { if ($optional) {
$sig .= " = ".$default; $sig .= " = ".$default;
} }
@ -140,7 +112,7 @@ class MethodDoc extends GenericDoc
$sig .= ')'; $sig .= ')';
if (isset($this->return)) { if (isset($this->return)) {
$sig .= ': '; $sig .= ': ';
$sig .= $this->resolveTypeAlias($this->return); $sig .= $this->resolveTypeAlias((string) $this->return->type);
} }
return $sig; return $sig;
} }
@ -182,28 +154,16 @@ class MethodDoc extends GenericDoc
$sig .= "\n"; $sig .= "\n";
$sig .= \str_replace("\n", " \n", $this->description); $sig .= \str_replace("\n", " \n", $this->description);
$sig .= "\n"; $sig .= "\n";
if ($this->psalmParams || $this->params) { if ($this->params) {
$sig .= "\nParameters:\n\n"; $sig .= "\nParameters:\n\n";
foreach ($this->params as $name => [$type, $description, $variadic]) { foreach ($this->params as $name => [$type, $description, $variadic]) {
$variadic = $variadic ? '...' : ''; $variadic = $variadic ? '...' : '';
$sig .= "* `$variadic\$$name`: `$type` $description \n"; $sig .= "* `$variadic$name`: `$type` $description \n";
if (isset($this->psalmParams[$name])) {
[$psalmType] = $this->psalmParams[$name];
$psalmType = \trim(\str_replace("\n", "\n ", $psalmType));
$sig .= " Full type:\n";
$sig .= " ```\n";
$sig .= " $psalmType\n";
$sig .= " ```\n";
}
} }
$sig .= "\n"; $sig .= "\n";
} }
if (isset($this->return) && $this->return->getDescription() && $this->return->getDescription()->render()) { if (isset($this->return) && $this->return->description) {
$sig .= "\nReturn value: ".$this->return->getDescription()."\n"; $sig .= "\nReturn value: ".$this->return->description."\n";
}
if (isset($this->psalmReturn)) {
$sig .= "\nFully typed return value:\n```\n".$this->psalmReturn."\n```";
} }
$sig .= $this->seeAlso($namespace ?? $this->namespace); $sig .= $this->seeAlso($namespace ?? $this->namespace);
$sig .= "\n"; $sig .= "\n";

View File

@ -18,8 +18,6 @@
namespace danog\PhpDoc; namespace danog\PhpDoc;
use phpDocumentor\Reflection\DocBlock\Tags\Author;
/** /**
* PHP documentation builder. * PHP documentation builder.
*/ */
@ -52,7 +50,7 @@ final class PhpDocBuilder
/** /**
* Set authors. * Set authors.
* *
* @param Author[] $authors Authors * @param string[] $authors Authors
* *
* @return self * @return self
*/ */