diff --git a/src/DocBlock.php b/src/DocBlock.php index 16533fb..02ca692 100644 --- a/src/DocBlock.php +++ b/src/DocBlock.php @@ -10,9 +10,13 @@ use TypeLang\PHPDoc\Tag\TagProviderInterface; /** - * @template-implements \IteratorAggregate + * @template-implements \IteratorAggregate, Tag> + * @template-implements \ArrayAccess, Tag|null> */ -final class DocBlock implements TagProviderInterface, \IteratorAggregate +final class DocBlock implements + TagProviderInterface, + \IteratorAggregate, + \ArrayAccess { use TagProvider; @@ -26,6 +30,26 @@ public function __construct( $this->bootTagProvider($tags); } + public function offsetExists(mixed $offset): bool + { + return isset($this->tags[$offset]); + } + + public function offsetGet(mixed $offset): ?Tag + { + return $this->tags[$offset] ?? null; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + throw new \BadMethodCallException(self::class . ' objects are immutable'); + } + + public function offsetUnset(mixed $offset): void + { + throw new \BadMethodCallException(self::class . ' objects are immutable'); + } + public function getDescription(): Description { return $this->description; diff --git a/src/Exception/InvalidTagException.php b/src/Exception/InvalidTagException.php index aa0a268..f2d69c7 100644 --- a/src/Exception/InvalidTagException.php +++ b/src/Exception/InvalidTagException.php @@ -4,4 +4,26 @@ namespace TypeLang\PHPDoc\Exception; -class InvalidTagException extends ParsingException {} +class InvalidTagException extends ParsingException +{ + final public const ERROR_CODE_PARSING = 0x01 + parent::CODE_LAST; + + protected const CODE_LAST = self::ERROR_CODE_PARSING; + + /** + * Occurs when a tag contain creation error. + * + * @param non-empty-string $tag + * @param int<0, max> $offset + */ + public static function fromCreatingTag( + string $tag, + string $source, + int $offset = 0, + \Throwable $prev = null, + ): static { + $message = \sprintf('Error while parsing tag @%s', $tag); + + return new static($source, $offset, $message, self::ERROR_CODE_PARSING, $prev); + } +} diff --git a/src/Factory.php b/src/Factory.php deleted file mode 100644 index e4835d9..0000000 --- a/src/Factory.php +++ /dev/null @@ -1,36 +0,0 @@ - $factories - */ - public function __construct( - private array $factories = [], - ) {} - - public function add(array|string $tags, FactoryInterface $delegate): void - { - foreach ((array) $tags as $tag) { - $this->factories[$tag] = $delegate; - } - } - - public function create(string $name, string $content, DescriptionParserInterface $descriptions): Tag - { - $delegate = $this->factories[$name] ?? null; - - if ($delegate !== null) { - return $delegate->create($name, $content, $descriptions); - } - - return new Tag($name, $descriptions->parse($content)); - } -} diff --git a/src/Parser.php b/src/Parser.php index 477b99f..e46d038 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -15,6 +15,8 @@ use TypeLang\PHPDoc\Parser\SourceMap; use TypeLang\PHPDoc\Parser\Tag\TagParser; use TypeLang\PHPDoc\Parser\Tag\TagParserInterface; +use TypeLang\PHPDoc\Tag\TagFactory; +use TypeLang\PHPDoc\Tag\FactoryInterface; /** * @psalm-suppress UndefinedAttributeClass : JetBrains language attribute may not be available @@ -28,7 +30,7 @@ class Parser implements ParserInterface private readonly TagParserInterface $tags; public function __construct( - FactoryInterface $tags = new Factory(), + FactoryInterface $tags = new TagFactory(), ) { $this->tags = new TagParser($tags); $this->descriptions = new SprintfDescriptionReader($this->tags); diff --git a/src/Parser/Comment/RegexCommentParser.php b/src/Parser/Comment/RegexCommentParser.php index 759a4c7..0cf870b 100644 --- a/src/Parser/Comment/RegexCommentParser.php +++ b/src/Parser/Comment/RegexCommentParser.php @@ -14,7 +14,7 @@ final class RegexCommentParser implements CommentParserInterface . '|(?:(?:\h*\*\/)(*MARK:T_COMMENT_END))' . '|(?:(?:^\h*\*\h*)(*MARK:T_COMMENT_PREFIX))' . '|(?:(?:\r\n|\n)(*MARK:T_NEWLINE))' - . '|(?:(?:.+?(?:\r\n|\n|$))(*MARK:T_TEXT))' + . '|(?:(?:.+?(?:(?=\*+\/)|(?:\r\n|\n|$)))(*MARK:T_TEXT))' . ')/Ssum'; /** diff --git a/src/Parser/Tag/TagParser.php b/src/Parser/Tag/TagParser.php index 512af85..9d36d2b 100644 --- a/src/Parser/Tag/TagParser.php +++ b/src/Parser/Tag/TagParser.php @@ -5,8 +5,9 @@ namespace TypeLang\PHPDoc\Parser\Tag; use TypeLang\PHPDoc\Exception\InvalidTagNameException; -use TypeLang\PHPDoc\FactoryInterface; +use TypeLang\PHPDoc\Exception\RuntimeExceptionInterface; use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface; +use TypeLang\PHPDoc\Tag\FactoryInterface; use TypeLang\PHPDoc\Tag\Tag; final class TagParser implements TagParserInterface @@ -59,7 +60,8 @@ private function getTagName(string $content): string } /** - * @throws InvalidTagNameException + * @throws \Throwable + * @throws RuntimeExceptionInterface */ public function parse(string $tag, DescriptionParserInterface $parser): Tag { @@ -67,9 +69,16 @@ public function parse(string $tag, DescriptionParserInterface $parser): Tag /** @var non-empty-string $name */ $name = \substr($name, 1); - $content = \substr($tag, \strlen($name) + 1); - $content = \ltrim($content); + $content = \substr($tag, $offset = \strlen($name) + 1); + $trimmed = \ltrim($content); + + try { + return $this->tags->create($name, $trimmed, $parser); + } catch (RuntimeExceptionInterface $e) { + /** @var int<0, max> */ + $offset += \strlen($content) - \strlen($trimmed); - return $this->tags->create($name, $content, $parser); + throw $e->withSource($tag, $offset); + } } } diff --git a/src/Tag/Description.php b/src/Tag/Description.php index 4eb4f6e..7175d13 100644 --- a/src/Tag/Description.php +++ b/src/Tag/Description.php @@ -5,9 +5,12 @@ namespace TypeLang\PHPDoc\Tag; /** - * @template-implements \IteratorAggregate + * @template-implements \IteratorAggregate, Tag> */ -class Description implements \Stringable, TagProviderInterface, \IteratorAggregate +class Description implements + TagProviderInterface, + \IteratorAggregate, + \Stringable { use TagProvider; diff --git a/src/FactoryInterface.php b/src/Tag/FactoryInterface.php similarity index 61% rename from src/FactoryInterface.php rename to src/Tag/FactoryInterface.php index 41858bd..634bde2 100644 --- a/src/FactoryInterface.php +++ b/src/Tag/FactoryInterface.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace TypeLang\PHPDoc; +namespace TypeLang\PHPDoc\Tag; +use TypeLang\PHPDoc\Exception\RuntimeExceptionInterface; use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface; -use TypeLang\PHPDoc\Tag\Tag; interface FactoryInterface { @@ -13,6 +13,9 @@ interface FactoryInterface * Returns a tag object with the specified name and description. * * @param non-empty-string $name + * + * @throws RuntimeExceptionInterface In case of parsing error occurs. + * @throws \Throwable In case of internal error occurs. */ public function create(string $name, string $content, DescriptionParserInterface $descriptions): Tag; } diff --git a/src/MutableFactoryInterface.php b/src/Tag/MutableFactoryInterface.php similarity index 60% rename from src/MutableFactoryInterface.php rename to src/Tag/MutableFactoryInterface.php index 9d4277b..75aa9f9 100644 --- a/src/MutableFactoryInterface.php +++ b/src/Tag/MutableFactoryInterface.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace TypeLang\PHPDoc; +namespace TypeLang\PHPDoc\Tag; interface MutableFactoryInterface extends FactoryInterface { /** * @param non-empty-string|list $tags */ - public function add(string|array $tags, FactoryInterface $delegate): void; + public function register(string|array $tags, FactoryInterface $delegate): void; } diff --git a/src/Tag/PrefixedTagFactory.php b/src/Tag/PrefixedTagFactory.php new file mode 100644 index 0000000..35c4e40 --- /dev/null +++ b/src/Tag/PrefixedTagFactory.php @@ -0,0 +1,32 @@ + $prefixes + */ + public function __construct( + private readonly MutableFactoryInterface $delegate, + private readonly array $prefixes, + ) {} + + public function register(array|string $tags, FactoryInterface $delegate): void + { + foreach ($this->prefixes as $prefix) { + foreach ((array) $tags as $tag) { + $this->delegate->register($prefix . $tag, $delegate); + } + } + } + + public function create(string $name, string $content, DescriptionParserInterface $descriptions): Tag + { + return $this->delegate->create($name, $content, $descriptions); + } +} diff --git a/src/Tag/TagFactory.php b/src/Tag/TagFactory.php new file mode 100644 index 0000000..f1a8258 --- /dev/null +++ b/src/Tag/TagFactory.php @@ -0,0 +1,55 @@ + $factories + */ + public function __construct( + private array $factories = [], + ) {} + + public function register(array|string $tags, FactoryInterface $delegate): void + { + foreach ((array) $tags as $tag) { + $this->factories[$tag] = $delegate; + } + } + + public function create(string $name, string $content, DescriptionParserInterface $descriptions): Tag + { + $delegate = $this->factories[$name] ?? null; + + if ($delegate !== null) { + try { + return $delegate->create($name, $content, $descriptions); + } catch (ParsingException $e) { + throw $e; + } catch (RuntimeExceptionInterface $e) { + throw InvalidTagException::fromCreatingTag( + tag: $name, + source: $e->getSource(), + offset: $e->getOffset(), + prev: $e, + ); + } catch (\Throwable $e) { + throw InvalidTagException::fromCreatingTag( + tag: $name, + source: $content, + prev: $e, + ); + } + } + + return new Tag($name, $descriptions->parse($content)); + } +} diff --git a/src/Tag/TagProvider.php b/src/Tag/TagProvider.php index fa1d512..766dad4 100644 --- a/src/Tag/TagProvider.php +++ b/src/Tag/TagProvider.php @@ -42,7 +42,7 @@ public function getTags(): array } /** - * @return \Traversable + * @return \Traversable, Tag> */ public function getIterator(): \Traversable { diff --git a/src/Tag/TagProviderInterface.php b/src/Tag/TagProviderInterface.php index 5cdfdd9..5b120b6 100644 --- a/src/Tag/TagProviderInterface.php +++ b/src/Tag/TagProviderInterface.php @@ -5,7 +5,7 @@ namespace TypeLang\PHPDoc\Tag; /** - * @template-extends \Traversable + * @template-extends \Traversable, Tag> * * @internal This is an internal library interface, please do not use it in your code. * @psalm-internal TypeLang\PHPDoc\Tag diff --git a/src/Tag/TypedTag.php b/src/Tag/TypedTag.php new file mode 100644 index 0000000..03e0255 --- /dev/null +++ b/src/Tag/TypedTag.php @@ -0,0 +1,26 @@ +