Skip to content

Commit

Permalink
Add simple content lexer for external tag rules
Browse files Browse the repository at this point in the history
  • Loading branch information
SerafimArts committed Apr 3, 2024
1 parent 89cd39f commit 4cb76d4
Show file tree
Hide file tree
Showing 15 changed files with 289 additions and 16 deletions.
19 changes: 19 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
cacheDirectory="vendor/.cache.psalm"
>
<issueHandlers>
<UndefinedDocblockClass>
<errorLevel type="suppress">
<referencedClass name="TypeLang\Parser\Node\Stmt\TypeStatement" />
</errorLevel>
</UndefinedDocblockClass>
<UndefinedClass>
<errorLevel type="suppress">
<referencedClass name="TypeLang\Parser\Node\Stmt\TypeStatement" />
<referencedClass name="TypeLang\Parser\ParserInterface" />
<referencedClass name="TypeLang\Parser\Exception\ParserExceptionInterface" />
</errorLevel>
</UndefinedClass>
<UndefinedAttributeClass>
<errorLevel type="suppress">
<referencedClass name="JetBrains\PhpStorm\Language" />
</errorLevel>
</UndefinedAttributeClass>
</issueHandlers>
<projectFiles>
<directory name="src" />
<ignoreFiles>
Expand Down
11 changes: 10 additions & 1 deletion src/DocBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use TypeLang\PHPDoc\Tag\Description;
use TypeLang\PHPDoc\Tag\DescriptionInterface;
use TypeLang\PHPDoc\Tag\DescriptionProviderInterface;
use TypeLang\PHPDoc\Tag\OptionalDescriptionProviderInterface;
use TypeLang\PHPDoc\Tag\TagInterface;
use TypeLang\PHPDoc\Tag\TagsProvider;
Expand Down Expand Up @@ -57,11 +56,21 @@ public function offsetGet(mixed $offset): ?TagInterface
return $this->tags[$offset] ?? null;
}

/**
* {@inheritDoc}
*
* @throws \BadMethodCallException
*/
public function offsetSet(mixed $offset, mixed $value): void
{
throw new \BadMethodCallException(self::class . ' objects are immutable');
}

/**
* {@inheritDoc}
*
* @throws \BadMethodCallException
*/
public function offsetUnset(mixed $offset): void
{
throw new \BadMethodCallException(self::class . ' objects are immutable');
Expand Down
3 changes: 0 additions & 3 deletions src/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@
use TypeLang\PHPDoc\Tag\Factory\FactoryInterface;
use TypeLang\PHPDoc\Tag\Factory\TagFactory;

/**
* @psalm-suppress UndefinedAttributeClass : JetBrains language attribute may not be available
*/
class Parser implements ParserInterface
{
private readonly CommentParserInterface $comments;
Expand Down
4 changes: 2 additions & 2 deletions src/Parser/Tag/TagParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use TypeLang\PHPDoc\Exception\RuntimeExceptionInterface;
use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface;
use TypeLang\PHPDoc\Tag\Factory\FactoryInterface;
use TypeLang\PHPDoc\Tag\Tag;
use TypeLang\PHPDoc\Tag\Content;
use TypeLang\PHPDoc\Tag\TagInterface;

final class TagParser implements TagParserInterface
Expand Down Expand Up @@ -74,7 +74,7 @@ public function parse(string $tag, DescriptionParserInterface $parser): TagInter
$trimmed = \ltrim($content);

try {
return $this->tags->create($name, $trimmed, $parser);
return $this->tags->create($name, new Content($trimmed), $parser);
} catch (RuntimeExceptionInterface $e) {
/** @var int<0, max> */
$offset += \strlen($content) - \strlen($trimmed);
Expand Down
3 changes: 0 additions & 3 deletions src/ParserInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@

use JetBrains\PhpStorm\Language;

/**
* @psalm-suppress UndefinedAttributeClass : JetBrains language attribute may not be available
*/
interface ParserInterface
{
/**
Expand Down
108 changes: 108 additions & 0 deletions src/Tag/Content.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

declare(strict_types=1);

namespace TypeLang\PHPDoc\Tag;

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\OptionalVariableNameApplicator;
use TypeLang\PHPDoc\Tag\Content\TypeParserApplicator;
use TypeLang\PHPDoc\Tag\Content\VariableNameApplicator;

class Content implements \Stringable
{
private readonly string $original;

/**
* @var int<0, max>
* @psalm-readonly-allow-private-mutation
*/
public int $offset = 0;

public function __construct(
public string $value,
) {
$this->original = $this->value;
}

/**
* @param int<0, max> $offset
*/
public function shift(int $offset, bool $ltrim = true): void
{
if ($offset <= 0) {
return;
}

$size = \strlen($this->value);
$this->value = \substr($this->value, $offset);

if ($ltrim) {
$this->value = \ltrim($this->value);
}

/** @psalm-suppress InvalidPropertyAssignmentValue */
$this->offset += $size - \strlen($this->value);
}

public function getTagException(string $message, \Throwable $previous = null): InvalidTagException
{
return new InvalidTagException(
source: $this->original,
offset: $this->offset,
message: $message,
previous: $previous,
);
}

/**
* @api
* @param non-empty-string $tag
*/
public function nextType(string $tag, TypesParserInterface $parser): TypeStatement
{
return $this->apply(new TypeParserApplicator($tag, $parser));
}

/**
* @api
* @param non-empty-string $tag
* @return non-empty-string
*/
public function nextVariable(string $tag): string
{
return $this->apply(new VariableNameApplicator($tag));
}

/**
* @api
* @return non-empty-string|null
*/
public function nextOptionalVariable(): ?string
{
return $this->apply(new OptionalVariableNameApplicator());
}

/**
* @template T of mixed
* @param callable(Content):T $applicator
* @return T
*/
public function apply(callable $applicator): mixed
{
return $applicator($this);
}

public function toDescription(DescriptionParserInterface $descriptions): DescriptionInterface
{
return $descriptions->parse($this->value);
}

public function __toString(): string
{
return $this->value;
}
}
18 changes: 18 additions & 0 deletions src/Tag/Content/Applicator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace TypeLang\PHPDoc\Tag\Content;

use TypeLang\PHPDoc\Tag\Content;

/**
* @template T of mixed
*/
abstract class Applicator
{
/**
* @return T
*/
abstract public function __invoke(Content $lexer): mixed;
}
33 changes: 33 additions & 0 deletions src/Tag/Content/OptionalVariableNameApplicator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace TypeLang\PHPDoc\Tag\Content;

use TypeLang\PHPDoc\Tag\Content;

/**
* @template-extends Applicator<non-empty-string|null>
*/
final class OptionalVariableNameApplicator extends Applicator
{
/**
* @return non-empty-string|null
*/
public function __invoke(Content $lexer): ?string
{
if (!\str_starts_with($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];
}
}
52 changes: 52 additions & 0 deletions src/Tag/Content/TypeParserApplicator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace TypeLang\PHPDoc\Tag\Content;

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\Tag\Content;

/**
* @template-extends Applicator<TypeStatement>
*/
final class TypeParserApplicator extends Applicator
{
/**
* @param non-empty-string $tag
*/
public function __construct(
private readonly string $tag,
private readonly TypesParserInterface $parser,
) {}

/**
* {@inheritDoc}
*
* @throws \Throwable
* @throws InvalidTagException
*/
public function __invoke(Content $lexer): TypeStatement
{
try {
/** @var TypeStatement $type */
$type = $this->parser->parse($lexer->value);
} catch (ParserExceptionInterface $e) {
/** @psalm-suppress InvalidArgument */
throw $lexer->getTagException(
message: \sprintf('Tag @%s contains an incorrect type', $this->tag),
previous: $e,
);
}

/**
* @psalm-suppress MixedArgument
*/
$lexer->shift($this->parser->lastProcessedTokenOffset);

return $type;
}
}
39 changes: 39 additions & 0 deletions src/Tag/Content/VariableNameApplicator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace TypeLang\PHPDoc\Tag\Content;

use TypeLang\PHPDoc\Exception\InvalidTagException;
use TypeLang\PHPDoc\Tag\Content;

/**
* @template-extends Applicator<non-empty-string>
*/
final class VariableNameApplicator extends Applicator
{
private readonly OptionalVariableNameApplicator $var;

/**
* @param non-empty-string $tag
*/
public function __construct(
private readonly string $tag,
) {
$this->var = new OptionalVariableNameApplicator();
}

/**
* @return non-empty-string
*
* @throws InvalidTagException
*/
public function __invoke(Content $lexer): string
{
return ($this->var)($lexer)
?? throw $lexer->getTagException(\sprintf(
'Tag @%s contains an incorrect variable name',
$this->tag,
));
}
}
3 changes: 2 additions & 1 deletion src/Tag/Factory/FactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use TypeLang\PHPDoc\Exception\RuntimeExceptionInterface;
use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface;
use TypeLang\PHPDoc\Tag\Content;
use TypeLang\PHPDoc\Tag\TagInterface;

interface FactoryInterface
Expand All @@ -18,5 +19,5 @@ interface FactoryInterface
* @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): TagInterface;
public function create(string $name, Content $content, DescriptionParserInterface $descriptions): TagInterface;
}
3 changes: 2 additions & 1 deletion src/Tag/Factory/PrefixedTagFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace TypeLang\PHPDoc\Tag\Factory;

use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface;
use TypeLang\PHPDoc\Tag\Content;
use TypeLang\PHPDoc\Tag\TagInterface;

final class PrefixedTagFactory implements MutableFactoryInterface
Expand All @@ -31,7 +32,7 @@ public function register(array|string $tags, FactoryInterface $delegate): void
}
}

public function create(string $name, string $content, DescriptionParserInterface $descriptions): TagInterface
public function create(string $name, Content $content, DescriptionParserInterface $descriptions): TagInterface
{
return $this->delegate->create($name, $content, $descriptions);
}
Expand Down
Loading

0 comments on commit 4cb76d4

Please sign in to comment.