From ebc708f1985f456c7481da8074821604cb90f0b8 Mon Sep 17 00:00:00 2001 From: Aleksei Lebedev <1329824+LastDragon-ru@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:35:47 +0400 Subject: [PATCH] fix(documentator): Parsing `{@see}` inline tags while help generation for `lara-asp-documentator:preprocess` command. --- .../documentator/docs/Commands/preprocess.md | 5 +- .../documentator/src/Commands/Preprocess.php | 26 +++---- .../src/Commands/PreprocessTest.php | 6 ++ .../Processor/Metadata/PhpClassMarkdown.php | 33 +------- packages/documentator/src/Utils/PhpDoc.php | 36 +++++++++ .../src/Utils/PhpDocumentFactory.php | 75 +++++++++++++++++++ 6 files changed, 135 insertions(+), 46 deletions(-) create mode 100644 packages/documentator/src/Utils/PhpDocumentFactory.php diff --git a/packages/documentator/docs/Commands/preprocess.md b/packages/documentator/docs/Commands/preprocess.md index 60553d5e..1fdaca17 100644 --- a/packages/documentator/docs/Commands/preprocess.md +++ b/packages/documentator/docs/Commands/preprocess.md @@ -84,7 +84,7 @@ after the Header will be used as a summary. * `` - File path. Includes contents of the `` file as an example wrapped into -` ```code block``` `. If {@see Runner} bound, it will be called to execute +` ```code block``` `. If [`Runner`][code-links/f9077a28b352f84b] bound, it will be called to execute the example. Its return value will be added right after the code block. By default, the `Runner` return value will be included as ` ```plain text``` ` @@ -164,6 +164,9 @@ Glob(s) to exclude. [//]: # (start: code-links) [//]: # (warning: Generated automatically. Do not edit.) +[code-links/f9077a28b352f84b]: ../../src/Processor/Tasks/Preprocess/Instructions/IncludeExample/Contracts/Runner.php + "\LastDragon_ru\LaraASP\Documentator\Processor\Tasks\Preprocess\Instructions\IncludeExample\Contracts\Runner" + [code-links/7e5c66e8748c6ff8]: ../../src/Utils/SortOrder.php "\LastDragon_ru\LaraASP\Documentator\Utils\SortOrder" diff --git a/packages/documentator/src/Commands/Preprocess.php b/packages/documentator/src/Commands/Preprocess.php index 73c33721..35036e51 100644 --- a/packages/documentator/src/Commands/Preprocess.php +++ b/packages/documentator/src/Commands/Preprocess.php @@ -6,7 +6,6 @@ use LastDragon_ru\LaraASP\Core\Path\DirectoryPath; use LastDragon_ru\LaraASP\Core\Path\FilePath; use LastDragon_ru\LaraASP\Core\Utils\Cast; -use LastDragon_ru\LaraASP\Documentator\Markdown\Document; use LastDragon_ru\LaraASP\Documentator\Markdown\Mutations\HeadingsLevel; use LastDragon_ru\LaraASP\Documentator\Markdown\Mutations\Move; use LastDragon_ru\LaraASP\Documentator\Package; @@ -18,7 +17,7 @@ use LastDragon_ru\LaraASP\Documentator\Processor\Tasks\Preprocess\Contracts\Instruction; use LastDragon_ru\LaraASP\Documentator\Processor\Tasks\Preprocess\Contracts\Parameters; use LastDragon_ru\LaraASP\Documentator\Processor\Tasks\Preprocess\Task as PreprocessTask; -use LastDragon_ru\LaraASP\Documentator\Utils\PhpDoc; +use LastDragon_ru\LaraASP\Documentator\Utils\PhpDocumentFactory; use LastDragon_ru\LaraASP\Documentator\Utils\Text; use LastDragon_ru\LaraASP\Formatter\Formatter; use LastDragon_ru\LaraASP\Serializer\Contracts\Serializable; @@ -76,6 +75,8 @@ class Preprocess extends Command { %tasks% HELP; + private ?PhpDocumentFactory $phpDocumentFactory = null; + public function __construct( protected readonly Factory $factory, ) { @@ -92,7 +93,7 @@ public function __invoke(Formatter $formatter): void { Result::Failed => ['FAIL', 'red', OutputInterface::VERBOSITY_NORMAL], Result::Success => ['DONE', 'green', OutputInterface::VERBOSITY_NORMAL], Result::Skipped => ['SKIP', 'gray', OutputInterface::VERBOSITY_VERBOSE], - Result::Missed => ['MISS', 'gray', OutputInterface::VERBOSITY_VERBOSE], + Result::Missed => ['MISS', 'yellow', OutputInterface::VERBOSITY_VERBOSE], }; $duration = $formatter->duration($duration); @@ -113,9 +114,13 @@ public function __invoke(Formatter $formatter): void { #[Override] public function getProcessedHelp(): string { - return strtr(parent::getProcessedHelp(), [ - '%tasks%' => trim($this->getProcessedHelpTasks(3)), - ]); + try { + return strtr(parent::getProcessedHelp(), [ + '%tasks%' => trim($this->getProcessedHelpTasks(3)), + ]); + } finally { + $this->phpDocumentFactory = null; + } } protected function getProcessedHelpTasks(int $level): string { @@ -316,13 +321,8 @@ private function getDocBlock( ?int $level = null, ): string { // Load - $path = match (true) { - $object instanceof ReflectionProperty => $object->getDeclaringClass()->getFileName(), - default => $object->getFileName(), - }; - $path = $path !== false ? new FilePath($path) : null; - $help = (new PhpDoc((string) $object->getDocComment()))->getText(); - $help = new Document($help, $path); + $this->phpDocumentFactory ??= $this->laravel->make(PhpDocumentFactory::class); + $help = ($this->phpDocumentFactory)($object); // Move to cwd $cwd = new DirectoryPath((string) getcwd()); diff --git a/packages/documentator/src/Commands/PreprocessTest.php b/packages/documentator/src/Commands/PreprocessTest.php index 3c4d2944..69ad639f 100644 --- a/packages/documentator/src/Commands/PreprocessTest.php +++ b/packages/documentator/src/Commands/PreprocessTest.php @@ -38,6 +38,8 @@ public function getProcessedHelpTaskPreprocessInstructions(PreprocessTask $task, } }; + $command->setLaravel($this->app()); + self::assertEquals( <<<'MARKDOWN' ### `[test:instruction]: ` @@ -94,6 +96,8 @@ public function getProcessedHelpTaskPreprocessInstructionTarget( } }; + $command->setLaravel($this->app()); + self::assertEquals( <<<'MARKDOWN' Target target target target target. @@ -122,6 +126,8 @@ public function getProcessedHelpTaskPreprocessParameters( } }; + $command->setLaravel($this->app()); + self::assertEquals( <<<'MARKDOWN' * `publicPropertyWithoutDefaultValue`: `int` - Description. diff --git a/packages/documentator/src/Processor/Metadata/PhpClassMarkdown.php b/packages/documentator/src/Processor/Metadata/PhpClassMarkdown.php index 5ac0dee6..0df2aac5 100644 --- a/packages/documentator/src/Processor/Metadata/PhpClassMarkdown.php +++ b/packages/documentator/src/Processor/Metadata/PhpClassMarkdown.php @@ -7,13 +7,6 @@ use LastDragon_ru\LaraASP\Documentator\Processor\FileSystem\File; use LastDragon_ru\LaraASP\Documentator\Processor\Tasks\CodeLinks\Contracts\LinkFactory; use Override; -use PhpParser\NameContext; -use PhpParser\Node\Name; - -use function preg_replace_callback; -use function trim; - -use const PREG_UNMATCHED_AS_NULL; /** * @implements Metadata @@ -39,32 +32,8 @@ public function __invoke(File $file): mixed { } // Parse - $content = $this->preprocess($comment->context, $comment->comment->getText()); - $document = new Document(trim($content), $file->getPath()); + $document = $comment->comment->getDocument($this->factory, $comment->context, $file->getPath()); return $document; } - - private function preprocess(NameContext $context, string $string): string { - return (string) preg_replace_callback( - pattern : '/\{@(?:see|link)\s+(?P[^}\s]+)\s?}/imu', - callback: function (array $matches) use ($context): string { - $result = $matches[0]; - $reference = $this->factory->create( - $matches['reference'], - static function (string $class) use ($context): string { - return (string) $context->getResolvedClassName(new Name($class)); - }, - ); - - if ($reference !== null) { - $result = "`{$reference}`"; - } - - return $result; - }, - subject : $string, - flags : PREG_UNMATCHED_AS_NULL, - ); - } } diff --git a/packages/documentator/src/Utils/PhpDoc.php b/packages/documentator/src/Utils/PhpDoc.php index f295f7f4..91f7b691 100644 --- a/packages/documentator/src/Utils/PhpDoc.php +++ b/packages/documentator/src/Utils/PhpDoc.php @@ -2,6 +2,11 @@ namespace LastDragon_ru\LaraASP\Documentator\Utils; +use LastDragon_ru\LaraASP\Core\Path\FilePath; +use LastDragon_ru\LaraASP\Documentator\Markdown\Document; +use LastDragon_ru\LaraASP\Documentator\Processor\Tasks\CodeLinks\Contracts\LinkFactory; +use PhpParser\NameContext; +use PhpParser\Node\Name; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; use PHPStan\PhpDocParser\Lexer\Lexer; @@ -12,8 +17,11 @@ use function array_slice; use function implode; +use function preg_replace_callback; use function trim; +use const PREG_UNMATCHED_AS_NULL; + /** * @internal */ @@ -49,6 +57,34 @@ public function isDeprecated(): bool { return $this->node !== null && $this->node->getDeprecatedTagValues() !== []; } + public function getDocument(LinkFactory $factory, NameContext $context, ?FilePath $path = null): Document { + return new Document( + trim( + (string) preg_replace_callback( + pattern : '/\{@(?:see|link)\s+(?P[^}\s]+)\s?}/imu', + callback: static function (array $matches) use ($context, $factory): string { + $result = $matches[0]; + $reference = $factory->create( + $matches['reference'], + static function (string $class) use ($context): string { + return (string) $context->getResolvedClassName(new Name($class)); + }, + ); + + if ($reference !== null) { + $result = "`{$reference}`"; + } + + return $result; + }, + subject : $this->getText(), + flags : PREG_UNMATCHED_AS_NULL, + ), + ), + $path, + ); + } + /** * @param array $strings */ diff --git a/packages/documentator/src/Utils/PhpDocumentFactory.php b/packages/documentator/src/Utils/PhpDocumentFactory.php new file mode 100644 index 00000000..b1da6982 --- /dev/null +++ b/packages/documentator/src/Utils/PhpDocumentFactory.php @@ -0,0 +1,75 @@ + + */ + private array $context = []; + + public function __construct( + protected readonly LinkFactory $factory, + ) { + // empty + } + + /** + * @param ReflectionClass|ReflectionProperty $object + */ + public function __invoke(ReflectionClass|ReflectionProperty $object): Document { + $document = null; + $path = match (true) { + $object instanceof ReflectionProperty => $object->getDeclaringClass()->getFileName(), + default => $object->getFileName(), + }; + + if ($path !== false) { + $phpdoc = new PhpDoc((string) $object->getDocComment()); + $context = $this->getContext($path); + $document = $phpdoc->getDocument($this->factory, $context, new FilePath($path)); + } else { + $phpdoc = new PhpDoc((string) $object->getDocComment()); + $document = new Document($phpdoc->getText(), null); + } + + return $document; + } + + private function getContext(string $path): NameContext { + // Resolved? + if (isset($this->context[$path])) { + return $this->context[$path]; + } + + // Resolve + $resolver = new NameResolver(); + $traverser = new NodeTraverser(); + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $stmts = (array) $parser->parse((string) file_get_contents($path)); + + $traverser->addVisitor($resolver); + $traverser->traverse($stmts); + + // Save + $this->context[$path] = $resolver->getNameContext(); + + // Return + return $this->context[$path]; + } +}