Skip to content

First iteration on option attributes #1231

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,10 @@
->set(GlobSearcher::class)
->set(ToctreeBuilder::class)
->set(InlineMarkupRule::class)

->set(\phpDocumentor\Guides\RestructuredText\Compiler\Passes\DirectiveProcessPass::class)
->arg('$directives', tagged_iterator('phpdoc.guides.directive'))
->tag('phpdoc.guides.compiler.nodeTransformers')
->set(DefaultCodeNodeOptionMapper::class)
->alias(CodeNodeOptionMapper::class, DefaultCodeNodeOptionMapper::class);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace phpDocumentor\Guides\RestructuredText\Compiler\Passes;

use phpDocumentor\Guides\Compiler\CompilerContext;
use phpDocumentor\Guides\Compiler\NodeTransformer;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\RestructuredText\Directives\BaseDirective as DirectiveHandler;
use phpDocumentor\Guides\RestructuredText\Directives\GeneralDirective;
use phpDocumentor\Guides\RestructuredText\Nodes\DirectiveNode;
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
use Psr\Log\LoggerInterface;

class DirectiveProcessPass implements NodeTransformer

Check failure on line 16 in packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Class phpDocumentor\Guides\RestructuredText\Compiler\Passes\DirectiveProcessPass implements generic interface phpDocumentor\Guides\Compiler\NodeTransformer but does not specify its types: T
{
/** @var array<string, DirectiveHandler> */
private array $directives;

/** @param iterable<DirectiveHandler> $directives */
public function __construct(
private readonly LoggerInterface $logger,

Check failure on line 23 in packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Property phpDocumentor\Guides\RestructuredText\Compiler\Passes\DirectiveProcessPass::$logger is never read, only written.
private readonly GeneralDirective $generalDirective,
iterable $directives = [],
) {
foreach ($directives as $directive) {
$this->registerDirective($directive);
}
}

private function registerDirective(DirectiveHandler $directive): void
{
$this->directives[strtolower($directive->getName())] = $directive;

Check failure on line 34 in packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Function strtolower() should not be referenced via a fallback global name, but via a use statement.
foreach ($directive->getAliases() as $alias) {
$this->directives[strtolower($alias)] = $directive;

Check failure on line 36 in packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Function strtolower() should not be referenced via a fallback global name, but via a use statement.
}
}

public function enterNode(Node $node, CompilerContext $compilerContext): Node
{
return $node;
}

public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null
{
return $this->getDirectiveHandler($node->getDirective())->createNode($node->getDirective());

Check failure on line 47 in packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Call to an undefined method phpDocumentor\Guides\Nodes\Node::getDirective().

Check failure on line 47 in packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Call to an undefined method phpDocumentor\Guides\Nodes\Node::getDirective().
}

private function getDirectiveHandler(Directive $directive): DirectiveHandler
{
return $this->directives[strtolower($directive->getName())] ?? $this->generalDirective;

Check failure on line 52 in packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Function strtolower() should not be referenced via a fallback global name, but via a use statement.
}

public function supports(Node $node): bool
{
return $node instanceof DirectiveNode;
}

public function getPriority(): int
{
return PHP_INT_MAX;

Check failure on line 62 in packages/guides-restructured-text/src/RestructuredText/Compiler/Passes/DirectiveProcessPass.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Constant PHP_INT_MAX should not be referenced via a fallback global name, but via a use statement.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace phpDocumentor\Guides\RestructuredText\Directives\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
final class Directive
{
public function __construct(

Check failure on line 12 in packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Directive.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Method phpDocumentor\Guides\RestructuredText\Directives\Attributes\Directive::__construct() has parameter $aliases with no value type specified in iterable type array.

Check failure on line 12 in packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Directive.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Method \phpDocumentor\Guides\RestructuredText\Directives\Attributes\Directive::__construct() does not have @param annotation for its traversable parameter $aliases.
public readonly string $name,
public readonly array $aliases = [],
) {

}

Check failure on line 17 in packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Directive.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Function closing brace must go on the next line following the body; found 1 blank lines before brace
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace phpDocumentor\Guides\RestructuredText\Directives\Attributes;

use Attribute;
use phpDocumentor\Guides\RestructuredText\Directives\OptionType;

#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class Option
{
public function __construct(
public readonly string $name,
public readonly OptionType $type = OptionType::String,
public readonly mixed $default = null,
public readonly string $description = '',
public readonly string|null $example = null,
) {
}
}

Check failure on line 21 in packages/guides-restructured-text/src/RestructuredText/Directives/Attributes/Option.php

View workflow job for this annotation

GitHub Actions / Coding Standards

Expected 1 blank line at end of file; 2 found

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use phpDocumentor\Guides\Nodes\GenericNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option;
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
use phpDocumentor\Guides\RestructuredText\Parser\DirectiveOption;
Expand All @@ -36,10 +37,32 @@
*/
abstract class BaseDirective
{
/** @var array<string, Option|null> Cache of Option attributes indexed by option name */
private array $optionAttributeCache;

private string $name;

private array $aliases;

Check failure on line 45 in packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Property phpDocumentor\Guides\RestructuredText\Directives\BaseDirective::$aliases type has no value type specified in iterable type array.

/**
* Get the directive name
*/
abstract public function getName(): string;
public function getName(): string
{
if (isset($this->name)) {
return $this->name;
}

$reflection = new \ReflectionClass($this);
$attributes = $reflection->getAttributes(Attributes\Directive::class);

if (count($attributes) === 0) {
throw new \LogicException('Directive class must have a Directive attribute');
}

$this->name = $attributes[0]->newInstance()->name;
return $this->name;
}

/**
* Allow a directive to be registered under multiple names.
Expand All @@ -50,7 +73,35 @@
*/
public function getAliases(): array
{
return [];
if (isset($this->aliases)) {
return $this->aliases;
}

$reflection = new \ReflectionClass($this);
$attributes = $reflection->getAttributes(Attributes\Directive::class);
$this->aliases = [];
if (count($attributes) !== 0) {
$this->aliases = $attributes[0]->newInstance()->aliases;
}

return $this->aliases;
}

/**
* Returns whether this directive has been upgraded to a new version.
*
* In the new version of directives, the processing is done during the compile phase.
* This method only exists to allow for backward compatibility with directives that
* were written before the upgrade.
*
* @internal
*/
final public function isUpgraded(): bool
{
$reflection = new \ReflectionClass($this);
$attributes = $reflection->getAttributes(Attributes\Directive::class);

return count($attributes) === 1;
}

/**
Expand Down Expand Up @@ -85,6 +136,11 @@
return new GenericNode($directive->getVariable(), $directive->getData());
}

public function createNode(Directive $directive): Node|null
{
return null;
}

/**
* @param DirectiveOption[] $options
*
Expand All @@ -94,4 +150,87 @@
{
return array_map(static fn (DirectiveOption $option): bool|float|int|string|null => $option->getValue(), $options);
}

/**
* Gets an option value from a directive based on attribute configuration.
*
* Looks up the option in the directive and returns its value converted to the
* appropriate type based on the Option attribute defined on this directive class.
* If the option is not present in the directive, returns the default value from the attribute.
*
* @param Directive $directive The directive containing the options
* @param string $optionName The name of the option to retrieve
*
* @return mixed The option value converted to the appropriate type, or the default value
*/
final protected function readOption(Directive $directive, string $optionName): mixed
{
$optionAttribute = $this->findOptionAttribute($optionName);

return $this->getOptionValue($directive, $optionAttribute);
}

final protected function readAllOptions(Directive $directive): array

Check failure on line 173 in packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Method phpDocumentor\Guides\RestructuredText\Directives\BaseDirective::readAllOptions() return type has no value type specified in iterable type array.
{
$this->initialize();

return array_map(
fn (Option $option) => $this->getOptionValue($directive, $option),

Check failure on line 178 in packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Parameter #1 $callback of function array_map expects (callable(phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option|null): mixed)|null, Closure(phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option): mixed given.
$this->optionAttributeCache
);
}

private function getOptionValue(Directive $directive, Option|null $option): mixed
{
if ($option === null) {
return null;
}

if (!$directive->hasOption($option->name)) {
return $option->default;
}

$directiveOption = $directive->getOption($option->name);
$value = $directiveOption->getValue();

return match ($option->type) {
OptionType::Integer => (int) $value,
OptionType::Boolean => $value === null || filter_var($value, FILTER_VALIDATE_BOOL),
OptionType::String => (string) $value,
OptionType::Array => (array) $value,

Check failure on line 200 in packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Match arm comparison between phpDocumentor\Guides\RestructuredText\Directives\OptionType::Array and phpDocumentor\Guides\RestructuredText\Directives\OptionType::Array is always true.
default => $value,

Check failure on line 201 in packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

View workflow job for this annotation

GitHub Actions / Static analysis / Static Code Analysis (8.2)

Match arm is unreachable because previous comparison is always true.
};
}


/**
* Finds the Option attribute for the given option name on the current class.
*
* @param string $optionName The option name to look for
*
* @return Option|null The Option attribute if found, null otherwise
*/
private function findOptionAttribute(string $optionName): ?Option
{
$this->initialize();

return $this->optionAttributeCache[$optionName] ?? null;
}

private function initialize(): void
{
if (isset($this->optionAttributeCache)) {
return;
}

$reflection = new \ReflectionClass($this);
$attributes = $reflection->getAttributes(Option::class);
$this->optionAttributeCache = [];
foreach ($attributes as $attribute) {
$option = $attribute->newInstance();
$this->optionAttributeCache[$option->name] = $option;
}
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use phpDocumentor\Guides\Nodes\CollectionNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\ReferenceResolvers\AnchorNormalizer;
use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option;
use phpDocumentor\Guides\RestructuredText\Nodes\ConfvalNode;
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
Expand All @@ -32,6 +33,11 @@
*
* https://sphinx-toolbox.readthedocs.io/en/stable/extensions/confval.html
*/
#[Option(name: 'name', description: 'Id of the configuration value, used for linking to it.')]
#[Option(name: 'type', description: 'Type of the configuration value, e.g. "string", "int", etc.')]
#[Option(name: 'required', type: OptionType::Boolean, default: false, description: 'Whether the configuration value is required or not.')]
#[Option(name: 'default', description: 'Default value of the configuration value, if any.')]
#[Option(name: 'noindex', type: OptionType::Boolean, default: false, description: 'Whether the configuration value should not be indexed.')]
final class ConfvalDirective extends SubDirective
{
public const NAME = 'confval';
Expand Down Expand Up @@ -80,16 +86,16 @@ protected function processSub(
}

if ($directive->hasOption('type')) {
$type = $this->inlineParser->parse($directive->getOptionString('type'), $blockContext);
$type = $this->inlineParser->parse($this->readOption($directive, 'type'), $blockContext);
}

$required = $directive->getOptionBool('required');
$required = $this->readOption($directive, 'required');

if ($directive->hasOption('default')) {
$default = $this->inlineParser->parse($directive->getOptionString('default'), $blockContext);
$default = $this->inlineParser->parse($this->readOption($directive, 'default'), $blockContext);
}

$noindex = $directive->getOptionBool('noindex');
$noindex = $this->readOption($directive, 'noindex');

foreach ($directive->getOptions() as $option) {
if (in_array($option->getName(), ['type', 'required', 'default', 'noindex', 'name'], true)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use phpDocumentor\Guides\Nodes\Menu\SectionMenuEntryNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\ReferenceResolvers\DocumentNameResolverInterface;
use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option;
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
use phpDocumentor\Guides\RestructuredText\Parser\Directive;

Expand All @@ -25,6 +26,8 @@
*
* Displays a table of content of the current page
*/
#[Option(name: 'local', type: OptionType::Boolean, description: 'If set, the table of contents will only include sections that are local to the current document.', default: false)]
#[Option(name: 'depth', description: 'The maximum depth of the table of contents.')]
final class ContentsDirective extends BaseDirective
{
public function __construct(
Expand All @@ -51,6 +54,6 @@ public function process(
return (new ContentMenuNode([new SectionMenuEntryNode($absoluteUrl)]))
->withOptions($this->optionsToArray($options))
->withCaption($directive->getDataNode())
->withLocal($directive->hasOption('local'));
->withLocal($this->readOption($directive, 'local'));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use phpDocumentor\Guides\Nodes\Table\TableColumn;
use phpDocumentor\Guides\Nodes\Table\TableRow;
use phpDocumentor\Guides\Nodes\TableNode;
use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option;
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
use phpDocumentor\Guides\RestructuredText\Parser\Productions\RuleContainer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
use phpDocumentor\Guides\Nodes\CollectionNode;
use phpDocumentor\Guides\Nodes\DocumentBlockNode;
use phpDocumentor\Guides\Nodes\Node;
use phpDocumentor\Guides\RestructuredText\Directives\Attributes\Option;
use phpDocumentor\Guides\RestructuredText\Parser\BlockContext;
use phpDocumentor\Guides\RestructuredText\Parser\Directive;

#[Option(name: 'identifier', description: 'The identifier of the document block')]
final class DocumentBlockDirective extends SubDirective
{
public function getName(): string
Expand All @@ -39,7 +41,7 @@ protected function processSub(

return new DocumentBlockNode(
$collectionNode->getChildren(),
$identifier,
$this->readOption($directive, 'identifier'),
);
}
}
Loading
Loading