Skip to content

Commit 4cb76d4

Browse files
committed
Add simple content lexer for external tag rules
1 parent 89cd39f commit 4cb76d4

15 files changed

+289
-16
lines changed

psalm.xml

+19
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,25 @@
99
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
1010
cacheDirectory="vendor/.cache.psalm"
1111
>
12+
<issueHandlers>
13+
<UndefinedDocblockClass>
14+
<errorLevel type="suppress">
15+
<referencedClass name="TypeLang\Parser\Node\Stmt\TypeStatement" />
16+
</errorLevel>
17+
</UndefinedDocblockClass>
18+
<UndefinedClass>
19+
<errorLevel type="suppress">
20+
<referencedClass name="TypeLang\Parser\Node\Stmt\TypeStatement" />
21+
<referencedClass name="TypeLang\Parser\ParserInterface" />
22+
<referencedClass name="TypeLang\Parser\Exception\ParserExceptionInterface" />
23+
</errorLevel>
24+
</UndefinedClass>
25+
<UndefinedAttributeClass>
26+
<errorLevel type="suppress">
27+
<referencedClass name="JetBrains\PhpStorm\Language" />
28+
</errorLevel>
29+
</UndefinedAttributeClass>
30+
</issueHandlers>
1231
<projectFiles>
1332
<directory name="src" />
1433
<ignoreFiles>

src/DocBlock.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
use TypeLang\PHPDoc\Tag\Description;
88
use TypeLang\PHPDoc\Tag\DescriptionInterface;
9-
use TypeLang\PHPDoc\Tag\DescriptionProviderInterface;
109
use TypeLang\PHPDoc\Tag\OptionalDescriptionProviderInterface;
1110
use TypeLang\PHPDoc\Tag\TagInterface;
1211
use TypeLang\PHPDoc\Tag\TagsProvider;
@@ -57,11 +56,21 @@ public function offsetGet(mixed $offset): ?TagInterface
5756
return $this->tags[$offset] ?? null;
5857
}
5958

59+
/**
60+
* {@inheritDoc}
61+
*
62+
* @throws \BadMethodCallException
63+
*/
6064
public function offsetSet(mixed $offset, mixed $value): void
6165
{
6266
throw new \BadMethodCallException(self::class . ' objects are immutable');
6367
}
6468

69+
/**
70+
* {@inheritDoc}
71+
*
72+
* @throws \BadMethodCallException
73+
*/
6574
public function offsetUnset(mixed $offset): void
6675
{
6776
throw new \BadMethodCallException(self::class . ' objects are immutable');

src/Parser.php

-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@
1818
use TypeLang\PHPDoc\Tag\Factory\FactoryInterface;
1919
use TypeLang\PHPDoc\Tag\Factory\TagFactory;
2020

21-
/**
22-
* @psalm-suppress UndefinedAttributeClass : JetBrains language attribute may not be available
23-
*/
2421
class Parser implements ParserInterface
2522
{
2623
private readonly CommentParserInterface $comments;

src/Parser/Tag/TagParser.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use TypeLang\PHPDoc\Exception\RuntimeExceptionInterface;
99
use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface;
1010
use TypeLang\PHPDoc\Tag\Factory\FactoryInterface;
11-
use TypeLang\PHPDoc\Tag\Tag;
11+
use TypeLang\PHPDoc\Tag\Content;
1212
use TypeLang\PHPDoc\Tag\TagInterface;
1313

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

7676
try {
77-
return $this->tags->create($name, $trimmed, $parser);
77+
return $this->tags->create($name, new Content($trimmed), $parser);
7878
} catch (RuntimeExceptionInterface $e) {
7979
/** @var int<0, max> */
8080
$offset += \strlen($content) - \strlen($trimmed);

src/ParserInterface.php

-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@
66

77
use JetBrains\PhpStorm\Language;
88

9-
/**
10-
* @psalm-suppress UndefinedAttributeClass : JetBrains language attribute may not be available
11-
*/
129
interface ParserInterface
1310
{
1411
/**

src/Tag/Content.php

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tag;
6+
7+
use TypeLang\Parser\Node\Stmt\TypeStatement;
8+
use TypeLang\Parser\ParserInterface as TypesParserInterface;
9+
use TypeLang\PHPDoc\Exception\InvalidTagException;
10+
use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface;
11+
use TypeLang\PHPDoc\Tag\Content\OptionalVariableNameApplicator;
12+
use TypeLang\PHPDoc\Tag\Content\TypeParserApplicator;
13+
use TypeLang\PHPDoc\Tag\Content\VariableNameApplicator;
14+
15+
class Content implements \Stringable
16+
{
17+
private readonly string $original;
18+
19+
/**
20+
* @var int<0, max>
21+
* @psalm-readonly-allow-private-mutation
22+
*/
23+
public int $offset = 0;
24+
25+
public function __construct(
26+
public string $value,
27+
) {
28+
$this->original = $this->value;
29+
}
30+
31+
/**
32+
* @param int<0, max> $offset
33+
*/
34+
public function shift(int $offset, bool $ltrim = true): void
35+
{
36+
if ($offset <= 0) {
37+
return;
38+
}
39+
40+
$size = \strlen($this->value);
41+
$this->value = \substr($this->value, $offset);
42+
43+
if ($ltrim) {
44+
$this->value = \ltrim($this->value);
45+
}
46+
47+
/** @psalm-suppress InvalidPropertyAssignmentValue */
48+
$this->offset += $size - \strlen($this->value);
49+
}
50+
51+
public function getTagException(string $message, \Throwable $previous = null): InvalidTagException
52+
{
53+
return new InvalidTagException(
54+
source: $this->original,
55+
offset: $this->offset,
56+
message: $message,
57+
previous: $previous,
58+
);
59+
}
60+
61+
/**
62+
* @api
63+
* @param non-empty-string $tag
64+
*/
65+
public function nextType(string $tag, TypesParserInterface $parser): TypeStatement
66+
{
67+
return $this->apply(new TypeParserApplicator($tag, $parser));
68+
}
69+
70+
/**
71+
* @api
72+
* @param non-empty-string $tag
73+
* @return non-empty-string
74+
*/
75+
public function nextVariable(string $tag): string
76+
{
77+
return $this->apply(new VariableNameApplicator($tag));
78+
}
79+
80+
/**
81+
* @api
82+
* @return non-empty-string|null
83+
*/
84+
public function nextOptionalVariable(): ?string
85+
{
86+
return $this->apply(new OptionalVariableNameApplicator());
87+
}
88+
89+
/**
90+
* @template T of mixed
91+
* @param callable(Content):T $applicator
92+
* @return T
93+
*/
94+
public function apply(callable $applicator): mixed
95+
{
96+
return $applicator($this);
97+
}
98+
99+
public function toDescription(DescriptionParserInterface $descriptions): DescriptionInterface
100+
{
101+
return $descriptions->parse($this->value);
102+
}
103+
104+
public function __toString(): string
105+
{
106+
return $this->value;
107+
}
108+
}

src/Tag/Content/Applicator.php

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tag\Content;
6+
7+
use TypeLang\PHPDoc\Tag\Content;
8+
9+
/**
10+
* @template T of mixed
11+
*/
12+
abstract class Applicator
13+
{
14+
/**
15+
* @return T
16+
*/
17+
abstract public function __invoke(Content $lexer): mixed;
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tag\Content;
6+
7+
use TypeLang\PHPDoc\Tag\Content;
8+
9+
/**
10+
* @template-extends Applicator<non-empty-string|null>
11+
*/
12+
final class OptionalVariableNameApplicator extends Applicator
13+
{
14+
/**
15+
* @return non-empty-string|null
16+
*/
17+
public function __invoke(Content $lexer): ?string
18+
{
19+
if (!\str_starts_with($lexer->value, '$')) {
20+
return null;
21+
}
22+
23+
\preg_match('/\$([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\b/u', $lexer->value, $matches);
24+
25+
if (\count($matches) !== 2 || $matches[1] === '') {
26+
return null;
27+
}
28+
29+
$lexer->shift(\strlen($matches[0]));
30+
31+
return $matches[1];
32+
}
33+
}
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tag\Content;
6+
7+
use TypeLang\Parser\Exception\ParserExceptionInterface;
8+
use TypeLang\Parser\Node\Stmt\TypeStatement;
9+
use TypeLang\Parser\ParserInterface as TypesParserInterface;
10+
use TypeLang\PHPDoc\Exception\InvalidTagException;
11+
use TypeLang\PHPDoc\Tag\Content;
12+
13+
/**
14+
* @template-extends Applicator<TypeStatement>
15+
*/
16+
final class TypeParserApplicator extends Applicator
17+
{
18+
/**
19+
* @param non-empty-string $tag
20+
*/
21+
public function __construct(
22+
private readonly string $tag,
23+
private readonly TypesParserInterface $parser,
24+
) {}
25+
26+
/**
27+
* {@inheritDoc}
28+
*
29+
* @throws \Throwable
30+
* @throws InvalidTagException
31+
*/
32+
public function __invoke(Content $lexer): TypeStatement
33+
{
34+
try {
35+
/** @var TypeStatement $type */
36+
$type = $this->parser->parse($lexer->value);
37+
} catch (ParserExceptionInterface $e) {
38+
/** @psalm-suppress InvalidArgument */
39+
throw $lexer->getTagException(
40+
message: \sprintf('Tag @%s contains an incorrect type', $this->tag),
41+
previous: $e,
42+
);
43+
}
44+
45+
/**
46+
* @psalm-suppress MixedArgument
47+
*/
48+
$lexer->shift($this->parser->lastProcessedTokenOffset);
49+
50+
return $type;
51+
}
52+
}
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tag\Content;
6+
7+
use TypeLang\PHPDoc\Exception\InvalidTagException;
8+
use TypeLang\PHPDoc\Tag\Content;
9+
10+
/**
11+
* @template-extends Applicator<non-empty-string>
12+
*/
13+
final class VariableNameApplicator extends Applicator
14+
{
15+
private readonly OptionalVariableNameApplicator $var;
16+
17+
/**
18+
* @param non-empty-string $tag
19+
*/
20+
public function __construct(
21+
private readonly string $tag,
22+
) {
23+
$this->var = new OptionalVariableNameApplicator();
24+
}
25+
26+
/**
27+
* @return non-empty-string
28+
*
29+
* @throws InvalidTagException
30+
*/
31+
public function __invoke(Content $lexer): string
32+
{
33+
return ($this->var)($lexer)
34+
?? throw $lexer->getTagException(\sprintf(
35+
'Tag @%s contains an incorrect variable name',
36+
$this->tag,
37+
));
38+
}
39+
}

src/Tag/Factory/FactoryInterface.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use TypeLang\PHPDoc\Exception\RuntimeExceptionInterface;
88
use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface;
9+
use TypeLang\PHPDoc\Tag\Content;
910
use TypeLang\PHPDoc\Tag\TagInterface;
1011

1112
interface FactoryInterface
@@ -18,5 +19,5 @@ interface FactoryInterface
1819
* @throws RuntimeExceptionInterface In case of parsing error occurs.
1920
* @throws \Throwable In case of internal error occurs.
2021
*/
21-
public function create(string $name, string $content, DescriptionParserInterface $descriptions): TagInterface;
22+
public function create(string $name, Content $content, DescriptionParserInterface $descriptions): TagInterface;
2223
}

src/Tag/Factory/PrefixedTagFactory.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace TypeLang\PHPDoc\Tag\Factory;
66

77
use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface;
8+
use TypeLang\PHPDoc\Tag\Content;
89
use TypeLang\PHPDoc\Tag\TagInterface;
910

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

34-
public function create(string $name, string $content, DescriptionParserInterface $descriptions): TagInterface
35+
public function create(string $name, Content $content, DescriptionParserInterface $descriptions): TagInterface
3536
{
3637
return $this->delegate->create($name, $content, $descriptions);
3738
}

0 commit comments

Comments
 (0)