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/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 6fa44b3..de7dca8 100644 --- a/src/Tag/Content.php +++ b/src/Tag/Content.php @@ -8,13 +8,18 @@ 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; use TypeLang\PHPDoc\Tag\Content\OptionalVariableNameApplicator; use TypeLang\PHPDoc\Tag\Content\TypeParserApplicator; use TypeLang\PHPDoc\Tag\Content\VariableNameApplicator; class Content implements \Stringable { - private readonly string $original; + public readonly string $source; /** * @var int<0, max> @@ -25,7 +30,7 @@ class Content implements \Stringable public function __construct( public string $value, ) { - $this->original = $this->value; + $this->source = $this->value; } /** @@ -48,10 +53,24 @@ 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( - source: $this->original, + source: $this->source, offset: $this->offset, message: $message, previous: $previous, @@ -67,6 +86,33 @@ 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 + * @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 @@ -86,6 +132,33 @@ 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 + { + /** @var T */ + return $this->apply(new ValueApplicator($tag, $value)); + } + + /** + * @template T of non-empty-string + * + * @api + * @param T $value + * @return T|null + */ + public function nextOptionalValue(string $value): ?string + { + /** @var T|null */ + return $this->apply(new OptionalValueApplicator($value)); + } + /** * @template T of mixed * @param callable(Content):T $applicator 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/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..35d344c --- /dev/null +++ b/src/Tag/Content/ValueApplicator.php @@ -0,0 +1,44 @@ + + */ +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 + { + /** @var T */ + 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) { 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, + ])); + } +} 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; }