diff --git a/packages/documentator/docs/Commands/preprocess.md b/packages/documentator/docs/Commands/preprocess.md index cad4ca461..1325009ee 100644 --- a/packages/documentator/docs/Commands/preprocess.md +++ b/packages/documentator/docs/Commands/preprocess.md @@ -33,8 +33,8 @@ Where: * `` - File path. * `` - additional parameters - * `summary` - Include the class summary? (default `true`) - * `description` - Include the class description? (default `true`) + * `summary: bool = true` - Include the class summary? + * `description: bool = true` - Include the class description? Includes the docblock of the first PHP class/interface/trait/enum/etc from `` file. Inline tags include as is except `@see`/`@link` @@ -44,8 +44,8 @@ which will be replaced to FQCN (if possible). Other tags are ignored. * `` - Directory path. * `` - additional parameters - * `depth` - Default is `0` (no nested directories). The `null` removes limits. - * `template` - Blade template + * `depth: array|string|int|null = 0` - [Directory Depth](https://symfony.com/doc/current/components/finder.html#directory-depth) (eg the `0` means no nested directories, the `null` removes limits). + * `template: string = 'default'` - Blade template. Returns the list of `*.md` files in the `` directory. Each file must have `# Header` as the first construction. The first paragraph @@ -79,7 +79,7 @@ Includes the `` file. * `` - Directory path. * `` - additional parameters - * `template` - Blade template + * `template: string = 'default'` - Blade template. Generates package list from `` directory. The readme file will be used to determine package name and summary. @@ -88,7 +88,7 @@ used to determine package name and summary. * `` - File path. * `` - additional parameters - * `data` - Array of variables (`${name}`) to replace (required). + * `data: array` - Array of variables (`${name}`) to replace. Includes the `` as a template. diff --git a/packages/documentator/src/Commands/Preprocess.php b/packages/documentator/src/Commands/Preprocess.php index 2d9b4249b..790da022c 100644 --- a/packages/documentator/src/Commands/Preprocess.php +++ b/packages/documentator/src/Commands/Preprocess.php @@ -8,17 +8,28 @@ use LastDragon_ru\LaraASP\Documentator\Package; use LastDragon_ru\LaraASP\Documentator\Preprocessor\Contracts\ParameterizableInstruction; use LastDragon_ru\LaraASP\Documentator\Preprocessor\Preprocessor; +use LastDragon_ru\LaraASP\Documentator\Utils\PhpDoc; +use LastDragon_ru\LaraASP\Serializer\Contracts\Serializable; use Override; +use ReflectionClass; +use ReflectionProperty; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; +use function array_map; +use function explode; use function getcwd; +use function gettype; use function implode; use function is_a; +use function is_scalar; use function ksort; +use function rtrim; +use function str_replace; use function strtr; use function trim; +use function var_export; /** * @see Preprocessor @@ -116,7 +127,7 @@ protected function getInstructionsHelp(Preprocessor $preprocessor): string { $desc = $instruction::getDescription(); $target = $instruction::getTargetDescription(); $params = is_a($instruction, ParameterizableInstruction::class, true) - ? $instruction::getParametersDescription() + ? $this->getInstructionParameters($instruction) : null; if ($target !== null && $params !== null) { @@ -160,4 +171,76 @@ protected function getInstructionsHelp(Preprocessor $preprocessor): string { return implode("\n\n", $help); } + + /** + * @template T of Serializable + * + * @param class-string> $instruction + * + * @return array + */ + protected function getInstructionParameters(string $instruction): array { + // Explicit? (deprecated) + $parameters = $instruction::getParametersDescription(); + + if ($parameters) { + return $parameters; + } + + // Nope + $class = new ReflectionClass($instruction::getParameters()); + $properties = $class->getProperties(ReflectionProperty::IS_PUBLIC); + $parameters = []; + + foreach ($properties as $property) { + // Ignored? + if (!$property->isPublic() || $property->isStatic()) { + continue; + } + + // Name + $name = $property->getName(); + $definition = $name; + $hasDefault = $property->hasDefaultValue(); + $theDefault = $hasDefault + ? $property->getDefaultValue() + : null; + + if ($property->hasType()) { + $definition = "{$definition}: {$property->getType()}"; + } + + if ($property->isPromoted()) { + foreach ($class->getConstructor()?->getParameters() ?? [] as $parameter) { + if ($parameter->getName() === $name) { + $hasDefault = $parameter->isDefaultValueAvailable(); + $theDefault = $hasDefault + ? $parameter->getDefaultValue() + : null; + + break; + } + } + } + + if ($hasDefault) { + $default = is_scalar($theDefault) ? var_export($theDefault, true) : '<'.gettype($theDefault).'>'; + $definition = "{$definition} = {$default}"; + } else { + // empty + } + + // Description + $doc = new PhpDoc($property->getDocComment() ?: null); + $description = $doc->getSummary() ?: '_No description provided_.'; + $description = trim( + implode(' ', array_map(rtrim(...), explode("\n", str_replace("\r\n", "\n", $description)))), + ); + + // Add + $parameters[$definition] = $description; + } + + return $parameters; + } } diff --git a/packages/documentator/src/Commands/PreprocessTest.php b/packages/documentator/src/Commands/PreprocessTest.php new file mode 100644 index 000000000..7a96b4dcb --- /dev/null +++ b/packages/documentator/src/Commands/PreprocessTest.php @@ -0,0 +1,109 @@ + 'Description.', + 'publicPropertyWithDefaultValue: float = 123.0' => '_No description provided_.', + 'publicPromotedPropertyWithoutDefaultValue: int' => 'Description.', + 'publicPromotedPropertyWithDefaultValue: int = 321' => '_No description provided_.', + ], + $command->getInstructionParameters( + PreprocessTest__ParameterizableInstruction::class, + ), + ); + } +} + +// @phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses +// @phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps + +/** + * @internal + * @noinspection PhpMultipleClassesDeclarationsInOneFile + * + * @implements ParameterizableInstruction + */ +class PreprocessTest__ParameterizableInstruction implements ParameterizableInstruction { + #[Override] + public static function getName(): string { + return 'test:parameterizable-instruction'; + } + + #[Override] + public static function getDescription(): string { + return 'Description description description description description.'; + } + + #[Override] + public static function getTargetDescription(): ?string { + return 'Target target target target target.'; + } + + #[Override] + public static function getParameters(): string { + return PreprocessTest__ParameterizableInstructionParameters::class; + } + + /** + * @inheritDoc + */ + #[Override] + public static function getParametersDescription(): array { + return []; + } + + #[Override] + public function process(string $path, string $target, Serializable $parameters): string { + return $path; + } +} + +/** + * @internal + * @noinspection PhpMultipleClassesDeclarationsInOneFile + */ +class PreprocessTest__ParameterizableInstructionParameters implements Serializable { + public static bool $publicStaticProperty = true; + + /** + * Description. + */ + public int $publicPropertyWithoutDefaultValue; + public float $publicPropertyWithDefaultValue = 123; + + public function __construct( + /** + * Description. + */ + public int $publicPromotedPropertyWithoutDefaultValue, + public int $publicPromotedPropertyWithDefaultValue = 321, + protected bool $protectedProperty = true, + protected bool $privateProperty = true, + ) { + $this->publicPropertyWithoutDefaultValue = 0; + } +} diff --git a/packages/documentator/src/Preprocessor/Contracts/ParameterizableInstruction.php b/packages/documentator/src/Preprocessor/Contracts/ParameterizableInstruction.php index c08122610..3047bcd2a 100644 --- a/packages/documentator/src/Preprocessor/Contracts/ParameterizableInstruction.php +++ b/packages/documentator/src/Preprocessor/Contracts/ParameterizableInstruction.php @@ -14,7 +14,9 @@ interface ParameterizableInstruction extends Instruction { public static function getParameters(): string; /** - * @return non-empty-array + * @deprecated %{VERSION} Use docblock instead. + * + * @return array */ public static function getParametersDescription(): array; diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludeDocBlock/Instruction.php b/packages/documentator/src/Preprocessor/Instructions/IncludeDocBlock/Instruction.php index 7cdef0542..d0d84a590 100644 --- a/packages/documentator/src/Preprocessor/Instructions/IncludeDocBlock/Instruction.php +++ b/packages/documentator/src/Preprocessor/Instructions/IncludeDocBlock/Instruction.php @@ -7,6 +7,7 @@ use LastDragon_ru\LaraASP\Documentator\Preprocessor\Contracts\ParameterizableInstruction; use LastDragon_ru\LaraASP\Documentator\Preprocessor\Exceptions\TargetIsNotFile; use LastDragon_ru\LaraASP\Documentator\Preprocessor\Exceptions\TargetIsNotValidPhpFile; +use LastDragon_ru\LaraASP\Documentator\Utils\PhpDoc; use LastDragon_ru\LaraASP\Serializer\Contracts\Serializable; use Override; use PhpParser\NameContext; @@ -17,18 +18,9 @@ use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; use PhpParser\ParserFactory; -use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode as PhpDocBlockNode; -use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode; -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 function array_slice; + use function dirname; use function file_get_contents; -use function implode; use function preg_replace_callback; use function trim; @@ -71,10 +63,7 @@ public static function getParameters(): string { */ #[Override] public static function getParametersDescription(): array { - return [ - 'summary' => 'Include the class summary? (default `true`)', - 'description' => 'Include the class description? (default `true`)', - ]; + return []; } #[Override] @@ -94,32 +83,19 @@ public function process(string $path, string $target, Serializable $parameters): return ''; } - // DocBlock? - $node = $this->getDocNode($class); - - if (!$node) { - return ''; - } - // Parse $eol = "\n"; - $text = $this->getDocText($node); + $doc = new PhpDoc($class->getDocComment()?->getText(), $eol.$eol); $result = ''; - if ($parameters->summary) { - $summary = trim(implode($eol.$eol, array_slice($text, 0, 1))); - - if ($summary) { - $result .= $summary.$eol.$eol; - } - } - - if ($parameters->description) { - $description = trim(implode($eol.$eol, array_slice($text, 1))); - - if ($description) { - $result .= $description.$eol.$eol; - } + if ($parameters->summary && $parameters->description) { + $result .= trim($doc->getText()); + } elseif ($parameters->summary) { + $result .= trim($doc->getSummary()); + } elseif ($parameters->description) { + $result .= trim($doc->getDescription()); + } else { + // empty } if ($result) { @@ -152,43 +128,6 @@ private function getClass(string $content, string $path, string $target): ?array : null; } - private function getDocNode(ClassLike $class): ?PhpDocBlockNode { - // Comment? - $comment = $class->getDocComment(); - - if (!$comment || trim($comment->getText()) === '') { - return null; - } - - // Parse - $lexer = new Lexer(); - $parser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser()); - $tokens = new TokenIterator($lexer->tokenize($comment->getText())); - $node = $parser->parse($tokens); - - // Return - return $node; - } - - /** - * @return list - */ - private function getDocText(PhpDocBlockNode $node): array { - $nodes = []; - - foreach ($node->children as $child) { - if ($child instanceof PhpDocTextNode) { - if (trim($child->text) !== '') { - $nodes[] = $child->text; - } - } else { - break; - } - } - - return $nodes; - } - /** * @return array */ diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludeDocBlock/Parameters.php b/packages/documentator/src/Preprocessor/Instructions/IncludeDocBlock/Parameters.php index 20a6e497a..45713d8ee 100644 --- a/packages/documentator/src/Preprocessor/Instructions/IncludeDocBlock/Parameters.php +++ b/packages/documentator/src/Preprocessor/Instructions/IncludeDocBlock/Parameters.php @@ -6,7 +6,13 @@ class Parameters implements Serializable { public function __construct( + /** + * Include the class summary? + */ public readonly bool $summary = true, + /** + * Include the class description? + */ public readonly bool $description = true, ) { // empty diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludeDocumentList/Instruction.php b/packages/documentator/src/Preprocessor/Instructions/IncludeDocumentList/Instruction.php index 138b29f67..a591cca1f 100644 --- a/packages/documentator/src/Preprocessor/Instructions/IncludeDocumentList/Instruction.php +++ b/packages/documentator/src/Preprocessor/Instructions/IncludeDocumentList/Instruction.php @@ -58,10 +58,7 @@ public static function getParameters(): string { */ #[Override] public static function getParametersDescription(): array { - return [ - 'depth' => 'Default is `0` (no nested directories). The `null` removes limits.', - 'template' => 'Blade template', - ]; + return []; } #[Override] diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludeDocumentList/Parameters.php b/packages/documentator/src/Preprocessor/Instructions/IncludeDocumentList/Parameters.php index 9452a9286..147c57510 100644 --- a/packages/documentator/src/Preprocessor/Instructions/IncludeDocumentList/Parameters.php +++ b/packages/documentator/src/Preprocessor/Instructions/IncludeDocumentList/Parameters.php @@ -8,10 +8,16 @@ class Parameters implements Serializable { public function __construct( /** + * [Directory Depth](https://symfony.com/doc/current/components/finder.html#directory-depth) + * (eg the `0` means no nested directories, the `null` removes limits). + * * @see Finder::depth() * @var array|string|int|null */ public readonly array|string|int|null $depth = 0, + /** + * Blade template. + */ public readonly string $template = 'default', ) { // empty diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludePackageList/Instruction.php b/packages/documentator/src/Preprocessor/Instructions/IncludePackageList/Instruction.php index 65f77e40c..da81f23a2 100644 --- a/packages/documentator/src/Preprocessor/Instructions/IncludePackageList/Instruction.php +++ b/packages/documentator/src/Preprocessor/Instructions/IncludePackageList/Instruction.php @@ -66,9 +66,7 @@ public static function getParameters(): string { */ #[Override] public static function getParametersDescription(): array { - return [ - 'template' => 'Blade template', - ]; + return []; } #[Override] diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludePackageList/Parameters.php b/packages/documentator/src/Preprocessor/Instructions/IncludePackageList/Parameters.php index 24347b023..299c40e13 100644 --- a/packages/documentator/src/Preprocessor/Instructions/IncludePackageList/Parameters.php +++ b/packages/documentator/src/Preprocessor/Instructions/IncludePackageList/Parameters.php @@ -6,6 +6,9 @@ class Parameters implements Serializable { public function __construct( + /** + * Blade template. + */ public readonly string $template = 'default', ) { // empty diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludeTemplate/Instruction.php b/packages/documentator/src/Preprocessor/Instructions/IncludeTemplate/Instruction.php index c5917bf2b..dd46bb763 100644 --- a/packages/documentator/src/Preprocessor/Instructions/IncludeTemplate/Instruction.php +++ b/packages/documentator/src/Preprocessor/Instructions/IncludeTemplate/Instruction.php @@ -55,9 +55,7 @@ public static function getParameters(): string { */ #[Override] public static function getParametersDescription(): array { - return [ - 'data' => 'Array of variables (`${name}`) to replace (required).', - ]; + return []; } #[Override] diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludeTemplate/InstructionTest.php b/packages/documentator/src/Preprocessor/Instructions/IncludeTemplate/InstructionTest.php index 98c066e0c..8e42bb4b4 100644 --- a/packages/documentator/src/Preprocessor/Instructions/IncludeTemplate/InstructionTest.php +++ b/packages/documentator/src/Preprocessor/Instructions/IncludeTemplate/InstructionTest.php @@ -57,7 +57,7 @@ public function testProcessAbsolute(): void { public function testProcessNoData(): void { $file = self::getTestData()->file('.md'); - $params = new Parameters(); + $params = new Parameters([]); $instance = Container::getInstance()->make(Instruction::class); self::expectExceptionObject( diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludeTemplate/Parameters.php b/packages/documentator/src/Preprocessor/Instructions/IncludeTemplate/Parameters.php index 3e3f3486c..38f48f58f 100644 --- a/packages/documentator/src/Preprocessor/Instructions/IncludeTemplate/Parameters.php +++ b/packages/documentator/src/Preprocessor/Instructions/IncludeTemplate/Parameters.php @@ -7,9 +7,11 @@ class Parameters implements Serializable { public function __construct( /** + * Array of variables (`${name}`) to replace. + * * @var array */ - public readonly array $data = [], + public readonly array $data, ) { // empty } diff --git a/packages/documentator/src/Utils/PhpDoc.php b/packages/documentator/src/Utils/PhpDoc.php new file mode 100644 index 000000000..d0266508e --- /dev/null +++ b/packages/documentator/src/Utils/PhpDoc.php @@ -0,0 +1,89 @@ + + */ + private readonly array $text; + + public function __construct( + private readonly ?string $comment, + private readonly string $eol = "\n\n", + ) { + $this->node = $this->parse($this->comment); + $this->text = $this->getTextNodes($this->node); + } + + public function getText(): string { + return $this->join($this->text); + } + + public function getSummary(): string { + return $this->text[0] ?? ''; + } + + public function getDescription(): string { + return $this->join(array_slice($this->text, 1)); + } + + /** + * @param array $strings + */ + private function join(array $strings): string { + return implode($this->eol, $strings); + } + + private function parse(?string $comment): ?PhpDocNode { + // Empty? + if (!$comment || trim($comment) === '') { + return null; + } + + // Parse + $lexer = new Lexer(); + $parser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser()); + $tokens = new TokenIterator($lexer->tokenize($comment)); + $node = $parser->parse($tokens); + + // Return + return $node; + } + + /** + * @return list + */ + private function getTextNodes(?PhpDocNode $node): array { + $nodes = []; + + foreach (($node->children ?? []) as $child) { + if ($child instanceof PhpDocTextNode) { + if (trim($child->text) !== '') { + $nodes[] = $child->text; + } + } else { + break; + } + } + + return $nodes; + } +}