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

View File

@ -22,8 +22,12 @@ use danog\ClassFinder\ClassFinder;
use danog\PhpDoc\PhpDoc\ClassDoc;
use danog\PhpDoc\PhpDoc\FunctionDoc;
use danog\PhpDoc\PhpDoc\GenericDoc;
use phpDocumentor\Reflection\DocBlock\Tags\Author;
use phpDocumentor\Reflection\DocBlockFactory;
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 ReflectionClass;
use ReflectionFunction;
use Symfony\Component\Yaml\Escaper;
@ -44,9 +48,13 @@ class PhpDoc
*/
private int $mode = ClassFinder::ALLOW_ALL | ClassFinder::RECURSIVE_MODE;
/**
* Docblock factory.
* PHPDOC parser.
*/
private DocBlockFactory $factory;
private PhpDocParser $parser;
/**
* Lexer.
*/
private Lexer $lexer;
/**
* Authors.
*/
@ -119,7 +127,14 @@ class PhpDoc
*/
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;
$appRoot = new \danog\ClassFinder\AppConfig;
@ -131,7 +146,7 @@ class PhpDoc
$this->name = $json['name'] ?? '';
$this->description = $json['description'] ?? '';
foreach ($authors as $author) {
$this->authors []= new Author($author['name'], $author['email']);
$this->authors []= "{$author['name']} <{$author['email']}>";
}
if (!$this->namespace) {
@ -445,7 +460,7 @@ class PhpDoc
}
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
*
* @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.
*
* @return Author[]
* @return string[]
*/
public function getAuthors(): array
{
@ -521,7 +534,7 @@ class PhpDoc
/**
* Set authors.
*
* @param Author[] $authors Authors
* @param string[] $authors Authors
*
* @return self
*/

View File

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

View File

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

View File

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

View File

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