From 493d73635fbf40801973600d9ba31ef6039951aa Mon Sep 17 00:00:00 2001 From: Kirill Nesmeyanov Date: Tue, 2 Apr 2024 22:52:27 +0300 Subject: [PATCH 01/10] Add ArrayAccess support of the DocBlock object --- src/DocBlock.php | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) 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; From 23f99971fbbf3784f61b1cdf0e27a44528f8f0b4 Mon Sep 17 00:00:00 2001 From: Kirill Nesmeyanov Date: Tue, 2 Apr 2024 22:52:57 +0300 Subject: [PATCH 02/10] Improve type-hints of the tag containers --- src/Tag/Description.php | 7 +++++-- src/Tag/TagProviderInterface.php | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) 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/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 From ca97bbb1ce39552ee70df574b2ba35d7bb84c258 Mon Sep 17 00:00:00 2001 From: Kirill Nesmeyanov Date: Tue, 2 Apr 2024 22:53:31 +0300 Subject: [PATCH 03/10] Fix inline tags parsing --- src/Parser/Comment/RegexCommentParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'; /** From 17c9b408c628063ed3ddb45a6919bfaea7f6a86c Mon Sep 17 00:00:00 2001 From: Kirill Nesmeyanov Date: Tue, 2 Apr 2024 22:54:15 +0300 Subject: [PATCH 04/10] Calculating the correct position in external tag parser errors --- src/Parser/Tag/TagParser.php | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Parser/Tag/TagParser.php b/src/Parser/Tag/TagParser.php index 512af85..4144da3 100644 --- a/src/Parser/Tag/TagParser.php +++ b/src/Parser/Tag/TagParser.php @@ -5,6 +5,7 @@ namespace TypeLang\PHPDoc\Parser\Tag; use TypeLang\PHPDoc\Exception\InvalidTagNameException; +use TypeLang\PHPDoc\Exception\RuntimeExceptionInterface; use TypeLang\PHPDoc\FactoryInterface; use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface; use TypeLang\PHPDoc\Tag\Tag; @@ -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); + } } } From 64ba1fbf82b342f1b2a94d8208b1198f84caf7a0 Mon Sep 17 00:00:00 2001 From: Kirill Nesmeyanov Date: Tue, 2 Apr 2024 22:54:58 +0300 Subject: [PATCH 05/10] Add external error handling support and rename "add" method "register" --- src/Factory.php | 24 ++++++++++++++++++++++-- src/FactoryInterface.php | 4 ++++ src/MutableFactoryInterface.php | 2 +- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/Factory.php b/src/Factory.php index e4835d9..2dad848 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -4,6 +4,9 @@ namespace TypeLang\PHPDoc; +use TypeLang\PHPDoc\Exception\InvalidTagException; +use TypeLang\PHPDoc\Exception\ParsingException; +use TypeLang\PHPDoc\Exception\RuntimeExceptionInterface; use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface; use TypeLang\PHPDoc\Tag\Tag; @@ -16,7 +19,7 @@ public function __construct( private array $factories = [], ) {} - public function add(array|string $tags, FactoryInterface $delegate): void + public function register(array|string $tags, FactoryInterface $delegate): void { foreach ((array) $tags as $tag) { $this->factories[$tag] = $delegate; @@ -28,7 +31,24 @@ public function create(string $name, string $content, DescriptionParserInterface $delegate = $this->factories[$name] ?? null; if ($delegate !== null) { - return $delegate->create($name, $content, $descriptions); + 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/FactoryInterface.php b/src/FactoryInterface.php index 41858bd..3991f50 100644 --- a/src/FactoryInterface.php +++ b/src/FactoryInterface.php @@ -4,6 +4,7 @@ namespace TypeLang\PHPDoc; +use TypeLang\PHPDoc\Exception\RuntimeExceptionInterface; use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface; use TypeLang\PHPDoc\Tag\Tag; @@ -13,6 +14,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/MutableFactoryInterface.php index 9d4277b..93fd2d3 100644 --- a/src/MutableFactoryInterface.php +++ b/src/MutableFactoryInterface.php @@ -9,5 +9,5 @@ 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; } From 933340c48d3bbc1520a688c9bdedbc49cfd04f93 Mon Sep 17 00:00:00 2001 From: Kirill Nesmeyanov Date: Tue, 2 Apr 2024 22:55:30 +0300 Subject: [PATCH 06/10] Add InvalidTagException::fromCreatingTag method --- src/Exception/InvalidTagException.php | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) 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); + } +} From b11617d86088b2ba0b631194bc58336ec9e21581 Mon Sep 17 00:00:00 2001 From: Kirill Nesmeyanov Date: Tue, 2 Apr 2024 23:14:08 +0300 Subject: [PATCH 07/10] Add TypedTag value object --- src/Tag/TypedTag.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/Tag/TypedTag.php diff --git a/src/Tag/TypedTag.php b/src/Tag/TypedTag.php new file mode 100644 index 0000000..2b838cf --- /dev/null +++ b/src/Tag/TypedTag.php @@ -0,0 +1,24 @@ + Date: Tue, 2 Apr 2024 23:14:36 +0300 Subject: [PATCH 08/10] Move tags Factory to Tag namespace --- src/Parser.php | 2 ++ src/Parser/Tag/TagParser.php | 2 +- src/{ => Tag}/Factory.php | 3 +-- src/{ => Tag}/FactoryInterface.php | 3 +-- src/{ => Tag}/MutableFactoryInterface.php | 2 +- src/Tag/PrefixedFactory.php | 32 +++++++++++++++++++++++ 6 files changed, 38 insertions(+), 6 deletions(-) rename src/{ => Tag}/Factory.php (96%) rename src/{ => Tag}/FactoryInterface.php (90%) rename src/{ => Tag}/MutableFactoryInterface.php (89%) create mode 100644 src/Tag/PrefixedFactory.php diff --git a/src/Parser.php b/src/Parser.php index 477b99f..04e7ac2 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\Factory; +use TypeLang\PHPDoc\Tag\FactoryInterface; /** * @psalm-suppress UndefinedAttributeClass : JetBrains language attribute may not be available diff --git a/src/Parser/Tag/TagParser.php b/src/Parser/Tag/TagParser.php index 4144da3..9d36d2b 100644 --- a/src/Parser/Tag/TagParser.php +++ b/src/Parser/Tag/TagParser.php @@ -6,8 +6,8 @@ use TypeLang\PHPDoc\Exception\InvalidTagNameException; use TypeLang\PHPDoc\Exception\RuntimeExceptionInterface; -use TypeLang\PHPDoc\FactoryInterface; use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface; +use TypeLang\PHPDoc\Tag\FactoryInterface; use TypeLang\PHPDoc\Tag\Tag; final class TagParser implements TagParserInterface diff --git a/src/Factory.php b/src/Tag/Factory.php similarity index 96% rename from src/Factory.php rename to src/Tag/Factory.php index 2dad848..6b156b7 100644 --- a/src/Factory.php +++ b/src/Tag/Factory.php @@ -2,13 +2,12 @@ declare(strict_types=1); -namespace TypeLang\PHPDoc; +namespace TypeLang\PHPDoc\Tag; use TypeLang\PHPDoc\Exception\InvalidTagException; use TypeLang\PHPDoc\Exception\ParsingException; use TypeLang\PHPDoc\Exception\RuntimeExceptionInterface; use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface; -use TypeLang\PHPDoc\Tag\Tag; final class Factory implements MutableFactoryInterface { diff --git a/src/FactoryInterface.php b/src/Tag/FactoryInterface.php similarity index 90% rename from src/FactoryInterface.php rename to src/Tag/FactoryInterface.php index 3991f50..634bde2 100644 --- a/src/FactoryInterface.php +++ b/src/Tag/FactoryInterface.php @@ -2,11 +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 { diff --git a/src/MutableFactoryInterface.php b/src/Tag/MutableFactoryInterface.php similarity index 89% rename from src/MutableFactoryInterface.php rename to src/Tag/MutableFactoryInterface.php index 93fd2d3..75aa9f9 100644 --- a/src/MutableFactoryInterface.php +++ b/src/Tag/MutableFactoryInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace TypeLang\PHPDoc; +namespace TypeLang\PHPDoc\Tag; interface MutableFactoryInterface extends FactoryInterface { diff --git a/src/Tag/PrefixedFactory.php b/src/Tag/PrefixedFactory.php new file mode 100644 index 0000000..5268d83 --- /dev/null +++ b/src/Tag/PrefixedFactory.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); + } +} From 46d862cd9bc6b24ed30c821a3e998bd3fd7a2f58 Mon Sep 17 00:00:00 2001 From: Kirill Nesmeyanov Date: Tue, 2 Apr 2024 23:22:04 +0300 Subject: [PATCH 09/10] Fix psalm errors --- src/Parser.php | 4 ++-- src/Tag/{PrefixedFactory.php => PrefixedTagFactory.php} | 4 ++-- src/Tag/{Factory.php => TagFactory.php} | 2 +- src/Tag/TagProvider.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) rename src/Tag/{PrefixedFactory.php => PrefixedTagFactory.php} (87%) rename src/Tag/{Factory.php => TagFactory.php} (96%) diff --git a/src/Parser.php b/src/Parser.php index 04e7ac2..e46d038 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -15,7 +15,7 @@ use TypeLang\PHPDoc\Parser\SourceMap; use TypeLang\PHPDoc\Parser\Tag\TagParser; use TypeLang\PHPDoc\Parser\Tag\TagParserInterface; -use TypeLang\PHPDoc\Tag\Factory; +use TypeLang\PHPDoc\Tag\TagFactory; use TypeLang\PHPDoc\Tag\FactoryInterface; /** @@ -30,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/Tag/PrefixedFactory.php b/src/Tag/PrefixedTagFactory.php similarity index 87% rename from src/Tag/PrefixedFactory.php rename to src/Tag/PrefixedTagFactory.php index 5268d83..35c4e40 100644 --- a/src/Tag/PrefixedFactory.php +++ b/src/Tag/PrefixedTagFactory.php @@ -6,14 +6,14 @@ use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface; -final class PrefixedFactory implements MutableFactoryInterface +final class PrefixedTagFactory implements MutableFactoryInterface { /** * @param non-empty-list $prefixes */ public function __construct( private readonly MutableFactoryInterface $delegate, - private readonly array $prefixes = [], + private readonly array $prefixes, ) {} public function register(array|string $tags, FactoryInterface $delegate): void diff --git a/src/Tag/Factory.php b/src/Tag/TagFactory.php similarity index 96% rename from src/Tag/Factory.php rename to src/Tag/TagFactory.php index 6b156b7..f1a8258 100644 --- a/src/Tag/Factory.php +++ b/src/Tag/TagFactory.php @@ -9,7 +9,7 @@ use TypeLang\PHPDoc\Exception\RuntimeExceptionInterface; use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface; -final class Factory implements MutableFactoryInterface +final class TagFactory implements MutableFactoryInterface { /** * @param array $factories 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 { From a65e221496a1e65b073341626e00837dce01b192 Mon Sep 17 00:00:00 2001 From: Kirill Nesmeyanov Date: Tue, 2 Apr 2024 23:29:05 +0300 Subject: [PATCH 10/10] Mark TypeStatement is optional in TypedTag --- src/Tag/TypedTag.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Tag/TypedTag.php b/src/Tag/TypedTag.php index 2b838cf..03e0255 100644 --- a/src/Tag/TypedTag.php +++ b/src/Tag/TypedTag.php @@ -8,6 +8,8 @@ /** * Requires a `type-lang/parser` dependency for {@see TypeStatement} support. + * + * @psalm-suppress UndefinedClass : Expects optional `type-lang/parser` dependency `type-lang/parser`. */ abstract class TypedTag extends Tag {