From 44df90a4f7fe2a705f6a4f6e3b9de63995df8b08 Mon Sep 17 00:00:00 2001 From: Kirill Nesmeyanov Date: Wed, 3 Apr 2024 18:45:23 +0300 Subject: [PATCH 1/5] Add optional type and value content parsers --- src/Tag/Content.php | 38 +++++++++++++ .../Content/OptionalTypeParserApplicator.php | 47 ++++++++++++++++ src/Tag/Content/OptionalValueApplicator.php | 55 +++++++++++++++++++ src/Tag/Content/TypeParserApplicator.php | 1 + src/Tag/Content/ValueApplicator.php | 43 +++++++++++++++ src/Tag/Description.php | 6 ++ 6 files changed, 190 insertions(+) create mode 100644 src/Tag/Content/OptionalTypeParserApplicator.php create mode 100644 src/Tag/Content/OptionalValueApplicator.php create mode 100644 src/Tag/Content/ValueApplicator.php diff --git a/src/Tag/Content.php b/src/Tag/Content.php index 6fa44b3..a7ab1b9 100644 --- a/src/Tag/Content.php +++ b/src/Tag/Content.php @@ -4,10 +4,14 @@ namespace TypeLang\PHPDoc\Tag; +use TypeLang\Parser\Exception\ParserExceptionInterface; use TypeLang\Parser\Node\Stmt\TypeStatement; use TypeLang\Parser\ParserInterface as TypesParserInterface; use TypeLang\PHPDoc\Exception\InvalidTagException; use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface; +use TypeLang\PHPDoc\Tag\Content\OptionalTypeParserApplicator; +use TypeLang\PHPDoc\Tag\Content\ValueApplicator; +use TypeLang\PHPDoc\Tag\Content\OptionalValueApplicator; use TypeLang\PHPDoc\Tag\Content\OptionalVariableNameApplicator; use TypeLang\PHPDoc\Tag\Content\TypeParserApplicator; use TypeLang\PHPDoc\Tag\Content\VariableNameApplicator; @@ -67,6 +71,14 @@ public function nextType(string $tag, TypesParserInterface $parser): TypeStateme return $this->apply(new TypeParserApplicator($tag, $parser)); } + /** + * @api + */ + public function nextOptionalType(TypesParserInterface $parser): ?TypeStatement + { + return $this->apply(new OptionalTypeParserApplicator($parser)); + } + /** * @api * @param non-empty-string $tag @@ -86,6 +98,32 @@ public function nextOptionalVariable(): ?string return $this->apply(new OptionalVariableNameApplicator()); } + /** + * @template T of non-empty-string + * + * @api + * @param non-empty-string $tag + * @param T $value + * @return T + */ + public function nextValue(string $tag, string $value): string + { + return $this->apply(new ValueApplicator($tag, $value)); + } + + /** + * @template T of non-empty-string + * + * @api + * @param non-empty-string $tag + * @param T $value + * @return T|null + */ + public function nextOptionalValue(string $value): ?string + { + return $this->apply(new OptionalValueApplicator($value)); + } + /** * @template T of mixed * @param callable(Content):T $applicator diff --git a/src/Tag/Content/OptionalTypeParserApplicator.php b/src/Tag/Content/OptionalTypeParserApplicator.php new file mode 100644 index 0000000..8090b44 --- /dev/null +++ b/src/Tag/Content/OptionalTypeParserApplicator.php @@ -0,0 +1,47 @@ + + */ +final class OptionalTypeParserApplicator extends Applicator +{ + /** + * @param non-empty-string $tag + */ + public function __construct( + private readonly TypesParserInterface $parser, + ) {} + + /** + * {@inheritDoc} + * + * @throws \Throwable + * @throws InvalidTagException + */ + public function __invoke(Content $lexer): ?TypeStatement + { + try { + $type = $this->parser->parse($lexer->value); + } catch (ParserExceptionInterface $e) { + return null; + } + + /** + * @psalm-suppress MixedArgument + * @psalm-suppress NoInterfaceProperties + */ + $lexer->shift($this->parser->lastProcessedTokenOffset); + + return $type; + } +} diff --git a/src/Tag/Content/OptionalValueApplicator.php b/src/Tag/Content/OptionalValueApplicator.php new file mode 100644 index 0000000..992f5ef --- /dev/null +++ b/src/Tag/Content/OptionalValueApplicator.php @@ -0,0 +1,55 @@ + + */ +final class OptionalValueApplicator extends Applicator +{ + /** + * @param T $value + */ + public function __construct( + private readonly string $value, + ) {} + + /** + * @return T|null + */ + public function __invoke(Content $lexer): ?string + { + if (!\str_starts_with($lexer->value, $this->value)) { + return null; + } + + $expectedLength = \strlen($this->value); + + // In case of end of string + if ($expectedLength === \strlen(\rtrim($lexer->value))) { + return $this->apply($lexer); + } + + // In case of separated by whitespace + if (\ctype_space($lexer->value[$expectedLength])) { + return $this->apply($lexer); + } + + return null; + } + + /** + * @return T + */ + private function apply(Content $lexer): string + { + $lexer->shift(\strlen($this->value)); + + return $this->value; + } +} diff --git a/src/Tag/Content/TypeParserApplicator.php b/src/Tag/Content/TypeParserApplicator.php index 891fa28..5fdd32a 100644 --- a/src/Tag/Content/TypeParserApplicator.php +++ b/src/Tag/Content/TypeParserApplicator.php @@ -44,6 +44,7 @@ public function __invoke(Content $lexer): TypeStatement /** * @psalm-suppress MixedArgument + * @psalm-suppress NoInterfaceProperties */ $lexer->shift($this->parser->lastProcessedTokenOffset); diff --git a/src/Tag/Content/ValueApplicator.php b/src/Tag/Content/ValueApplicator.php new file mode 100644 index 0000000..5e96dcd --- /dev/null +++ b/src/Tag/Content/ValueApplicator.php @@ -0,0 +1,43 @@ + + */ +final class ValueApplicator extends Applicator +{ + private readonly OptionalValueApplicator $identifier; + + /** + * @param non-empty-string $tag + * @param T $value + */ + public function __construct( + private readonly string $tag, + private readonly string $value + ) { + $this->identifier = new OptionalValueApplicator($value); + } + + /** + * @return T + * + * @throws InvalidTagException + */ + public function __invoke(Content $lexer): string + { + return ($this->identifier)($lexer) + ?? throw $lexer->getTagException(\sprintf( + 'Tag @%s contains an incorrect identifier value "%s"', + $this->tag, + $this->value, + )); + } +} diff --git a/src/Tag/Description.php b/src/Tag/Description.php index 8b0519a..29d1b05 100644 --- a/src/Tag/Description.php +++ b/src/Tag/Description.php @@ -25,6 +25,9 @@ public function __construct( $this->bootTagProvider($tags); } + /** + * @return ($description is DescriptionInterface ? DescriptionInterface : self) + */ public static function fromStringable(string|\Stringable $description): DescriptionInterface { if ($description instanceof DescriptionInterface) { @@ -34,6 +37,9 @@ public static function fromStringable(string|\Stringable $description): Descript return new self($description); } + /** + * @return ($description is DescriptionInterface ? DescriptionInterface : self|null) + */ public static function fromStringableOrNull(string|\Stringable|null $description): ?DescriptionInterface { if ($description === null) { From 516b84f7193b900e714b9633c37a637a57aad840 Mon Sep 17 00:00:00 2001 From: Kirill Nesmeyanov Date: Wed, 3 Apr 2024 18:50:18 +0300 Subject: [PATCH 2/5] Fix psalm errors --- composer.json | 1 + src/Tag/Content.php | 4 ++-- src/Tag/Content/ValueApplicator.php | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 660d67d..f9c233e 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ } }, "require-dev": { + "type-lang/parser": "^1.0", "friendsofphp/php-cs-fixer": "^3.42", "phpunit/phpunit": "^10.5", "rector/rector": "^1.0", diff --git a/src/Tag/Content.php b/src/Tag/Content.php index a7ab1b9..782a413 100644 --- a/src/Tag/Content.php +++ b/src/Tag/Content.php @@ -4,7 +4,6 @@ namespace TypeLang\PHPDoc\Tag; -use TypeLang\Parser\Exception\ParserExceptionInterface; use TypeLang\Parser\Node\Stmt\TypeStatement; use TypeLang\Parser\ParserInterface as TypesParserInterface; use TypeLang\PHPDoc\Exception\InvalidTagException; @@ -108,6 +107,7 @@ public function nextOptionalVariable(): ?string */ public function nextValue(string $tag, string $value): string { + /** @var T */ return $this->apply(new ValueApplicator($tag, $value)); } @@ -115,12 +115,12 @@ public function nextValue(string $tag, string $value): string * @template T of non-empty-string * * @api - * @param non-empty-string $tag * @param T $value * @return T|null */ public function nextOptionalValue(string $value): ?string { + /** @var T|null */ return $this->apply(new OptionalValueApplicator($value)); } diff --git a/src/Tag/Content/ValueApplicator.php b/src/Tag/Content/ValueApplicator.php index 5e96dcd..35d344c 100644 --- a/src/Tag/Content/ValueApplicator.php +++ b/src/Tag/Content/ValueApplicator.php @@ -33,6 +33,7 @@ public function __construct( */ public function __invoke(Content $lexer): string { + /** @var T */ return ($this->identifier)($lexer) ?? throw $lexer->getTagException(\sprintf( 'Tag @%s contains an incorrect identifier value "%s"', From a10a69c412fa9d437061dbc8e6326276c6cd2764 Mon Sep 17 00:00:00 2001 From: Kirill Nesmeyanov Date: Wed, 3 Apr 2024 19:23:54 +0300 Subject: [PATCH 3/5] Add invalid tags support --- src/Exception/ParsingException.php | 2 +- src/Parser.php | 2 ++ src/Parser/Tag/TagParser.php | 10 ++++++- src/Tag/Content.php | 6 ++--- src/Tag/Factory/TagFactory.php | 30 +++++++++++---------- src/Tag/InvalidTag.php | 43 ++++++++++++++++++++++++++++++ 6 files changed, 74 insertions(+), 19 deletions(-) create mode 100644 src/Tag/InvalidTag.php diff --git a/src/Exception/ParsingException.php b/src/Exception/ParsingException.php index 1ce47c6..761da92 100644 --- a/src/Exception/ParsingException.php +++ b/src/Exception/ParsingException.php @@ -47,7 +47,7 @@ public function withSource(string $source, int $offset): self offset: $offset, message: $this->message, code: $this->code, - previous: $this->getPrevious(), + previous: $this, ); } diff --git a/src/Parser.php b/src/Parser.php index 452071e..55409b0 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -5,6 +5,8 @@ namespace TypeLang\PHPDoc; use JetBrains\PhpStorm\Language; +use TypeLang\PHPDoc\Exception\InvalidTagException; +use TypeLang\PHPDoc\Exception\InvalidTagNameException; use TypeLang\PHPDoc\Exception\ParsingException; use TypeLang\PHPDoc\Exception\RuntimeExceptionInterface; use TypeLang\PHPDoc\Parser\Comment\CommentParserInterface; diff --git a/src/Parser/Tag/TagParser.php b/src/Parser/Tag/TagParser.php index cc3f4c0..ba6edb4 100644 --- a/src/Parser/Tag/TagParser.php +++ b/src/Parser/Tag/TagParser.php @@ -9,6 +9,7 @@ use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface; use TypeLang\PHPDoc\Tag\Factory\FactoryInterface; use TypeLang\PHPDoc\Tag\Content; +use TypeLang\PHPDoc\Tag\InvalidTag; use TypeLang\PHPDoc\Tag\TagInterface; final class TagParser implements TagParserInterface @@ -66,7 +67,14 @@ private function getTagName(string $content): string */ public function parse(string $tag, DescriptionParserInterface $parser): TagInterface { - $name = $this->getTagName($tag); + try { + $name = $this->getTagName($tag); + } catch (InvalidTagNameException $e) { + return new InvalidTag($e, $parser->parse( + description: \substr($tag, 1), + )); + } + /** @var non-empty-string $name */ $name = \substr($name, 1); diff --git a/src/Tag/Content.php b/src/Tag/Content.php index 782a413..4f0dc09 100644 --- a/src/Tag/Content.php +++ b/src/Tag/Content.php @@ -17,7 +17,7 @@ class Content implements \Stringable { - private readonly string $original; + public readonly string $source; /** * @var int<0, max> @@ -28,7 +28,7 @@ class Content implements \Stringable public function __construct( public string $value, ) { - $this->original = $this->value; + $this->source = $this->value; } /** @@ -54,7 +54,7 @@ public function shift(int $offset, bool $ltrim = true): void public function getTagException(string $message, \Throwable $previous = null): InvalidTagException { return new InvalidTagException( - source: $this->original, + source: $this->source, offset: $this->offset, message: $message, previous: $previous, diff --git a/src/Tag/Factory/TagFactory.php b/src/Tag/Factory/TagFactory.php index c62cddb..cd6a118 100644 --- a/src/Tag/Factory/TagFactory.php +++ b/src/Tag/Factory/TagFactory.php @@ -8,6 +8,7 @@ use TypeLang\PHPDoc\Exception\ParsingException; use TypeLang\PHPDoc\Exception\RuntimeExceptionInterface; use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface; +use TypeLang\PHPDoc\Tag\InvalidTag; use TypeLang\PHPDoc\Tag\Tag; use TypeLang\PHPDoc\Tag\Content; use TypeLang\PHPDoc\Tag\TagInterface; @@ -34,21 +35,22 @@ public function create(string $name, Content $content, DescriptionParserInterfac if ($delegate !== null) { try { - return $delegate->create($name, $content, $descriptions); - } catch (ParsingException $e) { - throw $e; + try { + return $delegate->create($name, $content, $descriptions); + } catch (RuntimeExceptionInterface $e) { + throw $e; + } catch (\Throwable $e) { + throw InvalidTagException::fromCreatingTag( + tag: $name, + source: $content->value, + prev: $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->value, - prev: $e, + return new InvalidTag( + reason: $e, + description: $descriptions->parse($content->source), + name: $name, ); } } diff --git a/src/Tag/InvalidTag.php b/src/Tag/InvalidTag.php new file mode 100644 index 0000000..a74f26c --- /dev/null +++ b/src/Tag/InvalidTag.php @@ -0,0 +1,43 @@ +'; + + /** + * @param non-empty-string $name + */ + public function __construct( + protected readonly \Throwable $reason, + \Stringable|string|null $description = null, + string $name = self::DEFAULT_UNKNOWN_TAG_NAME, + ) { + parent::__construct($name, $description); + } + + public function getReason(): \Throwable + { + return $this->reason; + } + + public function __toString(): string + { + $name = $this->name === self::DEFAULT_UNKNOWN_TAG_NAME ? '' : $this->name; + + if ($this->description === null) { + return \sprintf('@%s', $name); + } + + return \rtrim(\vsprintf('@%s %s', [ + $name, + (string) $this->description, + ])); + } +} From 7b0577232c8ef8a207bf0c3a0fabf8e04f02387e Mon Sep 17 00:00:00 2001 From: Kirill Nesmeyanov Date: Wed, 3 Apr 2024 20:19:41 +0300 Subject: [PATCH 4/5] Add identifier content applicators --- src/Tag/Content.php | 21 ++++++++++ src/Tag/Content/IdentifierApplicator.php | 39 +++++++++++++++++++ .../Content/OptionalIdentifierApplicator.php | 33 ++++++++++++++++ .../OptionalVariableNameProviderInterface.php | 2 +- src/Tag/VariableNameProviderInterface.php | 2 +- 5 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/Tag/Content/IdentifierApplicator.php create mode 100644 src/Tag/Content/OptionalIdentifierApplicator.php diff --git a/src/Tag/Content.php b/src/Tag/Content.php index 4f0dc09..9e9e96c 100644 --- a/src/Tag/Content.php +++ b/src/Tag/Content.php @@ -8,6 +8,8 @@ use TypeLang\Parser\ParserInterface as TypesParserInterface; use TypeLang\PHPDoc\Exception\InvalidTagException; use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface; +use TypeLang\PHPDoc\Tag\Content\IdentifierApplicator; +use TypeLang\PHPDoc\Tag\Content\OptionalIdentifierApplicator; use TypeLang\PHPDoc\Tag\Content\OptionalTypeParserApplicator; use TypeLang\PHPDoc\Tag\Content\ValueApplicator; use TypeLang\PHPDoc\Tag\Content\OptionalValueApplicator; @@ -78,6 +80,25 @@ public function nextOptionalType(TypesParserInterface $parser): ?TypeStatement return $this->apply(new OptionalTypeParserApplicator($parser)); } + /** + * @api + * @param non-empty-string $tag + * @return non-empty-string + */ + public function nextIdentifier(string $tag): string + { + return $this->apply(new IdentifierApplicator($tag)); + } + + /** + * @api + * @return non-empty-string|null + */ + public function nextOptionalIdentifier(): ?string + { + return $this->apply(new OptionalIdentifierApplicator()); + } + /** * @api * @param non-empty-string $tag diff --git a/src/Tag/Content/IdentifierApplicator.php b/src/Tag/Content/IdentifierApplicator.php new file mode 100644 index 0000000..e1794d4 --- /dev/null +++ b/src/Tag/Content/IdentifierApplicator.php @@ -0,0 +1,39 @@ + + */ +final class IdentifierApplicator extends Applicator +{ + private readonly OptionalIdentifierApplicator $id; + + /** + * @param non-empty-string $tag + */ + public function __construct( + private readonly string $tag, + ) { + $this->id = new OptionalIdentifierApplicator(); + } + + /** + * @return non-empty-string + * + * @throws InvalidTagException + */ + public function __invoke(Content $lexer): string + { + return ($this->id)($lexer) + ?? throw $lexer->getTagException(\sprintf( + 'Tag @%s contains an incorrect identifier value', + $this->tag, + )); + } +} diff --git a/src/Tag/Content/OptionalIdentifierApplicator.php b/src/Tag/Content/OptionalIdentifierApplicator.php new file mode 100644 index 0000000..0742dba --- /dev/null +++ b/src/Tag/Content/OptionalIdentifierApplicator.php @@ -0,0 +1,33 @@ + + */ +final class OptionalIdentifierApplicator extends Applicator +{ + /** + * @return non-empty-string|null + */ + public function __invoke(Content $lexer): ?string + { + if ($lexer->value === '') { + return null; + } + + \preg_match('/([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\b/u', $lexer->value, $matches); + + if (\count($matches) !== 2 || $matches[1] === '') { + return null; + } + + $lexer->shift(\strlen($matches[0])); + + return $matches[1]; + } +} diff --git a/src/Tag/OptionalVariableNameProviderInterface.php b/src/Tag/OptionalVariableNameProviderInterface.php index c38aad0..8c6052c 100644 --- a/src/Tag/OptionalVariableNameProviderInterface.php +++ b/src/Tag/OptionalVariableNameProviderInterface.php @@ -15,5 +15,5 @@ interface OptionalVariableNameProviderInterface * * @return non-empty-string|null */ - public function getVariable(): ?string; + public function getVariableName(): ?string; } diff --git a/src/Tag/VariableNameProviderInterface.php b/src/Tag/VariableNameProviderInterface.php index 3c29c23..b39e76c 100644 --- a/src/Tag/VariableNameProviderInterface.php +++ b/src/Tag/VariableNameProviderInterface.php @@ -14,5 +14,5 @@ interface VariableNameProviderInterface extends OptionalVariableNameProviderInte * * @return non-empty-string */ - public function getVariable(): string; + public function getVariableName(): string; } From 3f15d8474ce6775af93bed4f28370899d34f7851 Mon Sep 17 00:00:00 2001 From: Kirill Nesmeyanov Date: Wed, 3 Apr 2024 20:37:14 +0300 Subject: [PATCH 5/5] Add transactional content method --- src/Tag/Content.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Tag/Content.php b/src/Tag/Content.php index 9e9e96c..de7dca8 100644 --- a/src/Tag/Content.php +++ b/src/Tag/Content.php @@ -53,6 +53,20 @@ public function shift(int $offset, bool $ltrim = true): void $this->offset += $size - \strlen($this->value); } + /** + * @param callable(self):bool $context + */ + public function transactional(callable $context): void + { + $offset = $this->offset; + $value = $this->value; + + if ($context($this) === false) { + $this->offset = $offset; + $this->value = $value; + } + } + public function getTagException(string $message, \Throwable $previous = null): InvalidTagException { return new InvalidTagException(