Skip to content

Commit 89535d8

Browse files
committed
Optimize tag parser
1 parent 17d4716 commit 89535d8

5 files changed

Lines changed: 55 additions & 25 deletions

File tree

libs/phpdoc/src/DocBlockParser.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
use TypeLang\PhpDoc\Parser\SourceMap;
1717
use TypeLang\PhpDoc\Parser\Splitter\SplitterInterface;
1818
use TypeLang\PhpDoc\Parser\Splitter\StringSplitter;
19-
use TypeLang\PhpDoc\Parser\Tag\RegexTagParser;
19+
use TypeLang\PhpDoc\Parser\Tag\StringTagParser;
2020
use TypeLang\PhpDoc\Parser\Tag\TagParserInterface;
2121

2222
final readonly class DocBlockParser implements DocBlockParserInterface
@@ -46,7 +46,7 @@ private function createTagFactory(): TagFactoryInterface
4646

4747
private function createTagParser(TagFactoryInterface $factory): TagParserInterface
4848
{
49-
return new RegexTagParser($factory);
49+
return new StringTagParser($factory);
5050
}
5151

5252
private function createDescriptionParser(TagParserInterface $parser): DescriptionParserInterface

libs/phpdoc/src/Parser/Tag/RegexTagParser.php renamed to libs/phpdoc/src/Parser/Tag/StringTagParser.php

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,54 @@
1212
use TypeLang\PhpDoc\Exception\InvalidTagPrefixException;
1313
use TypeLang\PhpDoc\Parser\Description\DescriptionParserInterface;
1414

15-
final readonly class RegexTagParser implements TagParserInterface
15+
/**
16+
* Parses a tag definition into a {@see TagInterface}.
17+
*
18+
* A definition is an "@" followed by a name and an optional, whitespace
19+
* separated description:
20+
*
21+
* ```
22+
*
23+
* @name
24+
* @name the description text
25+
* ```
26+
*
27+
* A definition that does not start with an "@", or whose "@" is not followed
28+
* by a name, is an {@see InvalidTag}.
29+
*/
30+
final readonly class StringTagParser implements TagParserInterface
1631
{
1732
/**
33+
* The ASCII characters allowed inside a tag name.
34+
*
1835
* @var non-empty-string
1936
*/
20-
private const string PATTERN_TAG = '\G@[\w\-\_\\\\:]++';
37+
private const string ASCII_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz'
38+
. 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
39+
. '0123456789'
40+
. '_-\\:';
2141

22-
/**
23-
* @var non-empty-string
24-
*/
25-
private string $pattern;
42+
private string $nameTerminators;
2643

2744
public function __construct(
2845
private TagFactoryInterface $factory,
2946
) {
30-
$this->pattern = \sprintf('/^%s/isum', \addcslashes(self::PATTERN_TAG, '/'));
47+
$this->nameTerminators = self::createTerminatorMask();
48+
}
49+
50+
private static function createTerminatorMask(): string
51+
{
52+
$mask = '';
53+
54+
for ($byte = 0x00; $byte <= 0x7F; ++$byte) {
55+
$char = \chr($byte);
56+
57+
if (!\str_contains(self::ASCII_NAME_CHARS, $char)) {
58+
$mask .= $char;
59+
}
60+
}
61+
62+
return $mask;
3163
}
3264

3365
private static function createForEmptyTagLine(): InvalidTag
@@ -67,17 +99,15 @@ public function parse(string $definition, DescriptionParserInterface $descriptio
6799
return self::createForInvalidTagPrefix($definition, $descriptions);
68100
}
69101

70-
\preg_match($this->pattern, $definition, $matches);
71-
$prefixedTagName = $matches[0] ?? null;
102+
$length = \strcspn($definition, $this->nameTerminators, 1);
103+
$name = \substr($definition, 1, $length);
72104

73-
if ($prefixedTagName === null) {
105+
if ($name === '') {
74106
return self::createForInvalidTagName($definition, $descriptions);
75107
}
76108

77-
/** @var non-empty-string $tagName */
78-
$tagName = \substr($prefixedTagName, 1);
79-
$tagSuffix = \ltrim(\substr($definition, \strlen($prefixedTagName)));
109+
$suffix = \ltrim(\substr($definition, 1 + $length));
80110

81-
return $this->factory->create($tagName, $tagSuffix, $descriptions);
111+
return $this->factory->create($name, $suffix, $descriptions);
82112
}
83113
}

libs/phpdoc/tests/Bench/TagParser/RegexTagParserBench.php renamed to libs/phpdoc/tests/Bench/TagParser/StringTagParserBench.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@
1010
use PhpBench\Attributes\Revs;
1111
use PhpBench\Attributes\Warmup;
1212
use TypeLang\PhpDoc\DocBlock\Tag\TagFactory;
13-
use TypeLang\PhpDoc\Parser\Tag\RegexTagParser;
13+
use TypeLang\PhpDoc\Parser\Tag\StringTagParser;
1414
use TypeLang\PhpDoc\Parser\Tag\TagParserInterface;
1515

1616
#[Revs(20), Warmup(5), Iterations(15), BeforeMethods('prepare'), RetryThreshold(2)]
17-
final class RegexTagParserBench extends TagParserBench
17+
final class StringTagParserBench extends TagParserBench
1818
{
1919
protected TagParserInterface $parser;
2020

2121
#[\Override]
2222
public function prepare(): void
2323
{
24-
$this->parser = new RegexTagParser(new TagFactory());
24+
$this->parser = new StringTagParser(new TagFactory());
2525

2626
parent::prepare();
2727
}

libs/phpdoc/tests/Parser/DescriptionParserTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
use TypeLang\PhpDoc\DocBlock\Tag\TagInterface;
1414
use TypeLang\PhpDoc\Parser\Description\BalancedBraceAwareParser;
1515
use TypeLang\PhpDoc\Parser\Description\DescriptionParserInterface;
16-
use TypeLang\PhpDoc\Parser\Tag\RegexTagParser;
16+
use TypeLang\PhpDoc\Parser\Tag\StringTagParser;
1717
use TypeLang\PhpDoc\Tests\TestCase;
1818

1919
final class DescriptionParserTest extends TestCase
@@ -24,7 +24,7 @@ final class DescriptionParserTest extends TestCase
2424
public static function parserDataProvider(): iterable
2525
{
2626
yield 'BalancedBraceAwareParser' => [
27-
new BalancedBraceAwareParser(new RegexTagParser(new TagFactory())),
27+
new BalancedBraceAwareParser(new StringTagParser(new TagFactory())),
2828
];
2929
}
3030

libs/phpdoc/tests/Parser/TagParserTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
use TypeLang\PhpDoc\Exception\ParsingExceptionInterface;
1919
use TypeLang\PhpDoc\Parser\Description\BalancedBraceAwareParser;
2020
use TypeLang\PhpDoc\Parser\Description\DescriptionParserInterface;
21-
use TypeLang\PhpDoc\Parser\Tag\RegexTagParser;
21+
use TypeLang\PhpDoc\Parser\Tag\StringTagParser;
2222
use TypeLang\PhpDoc\Parser\Tag\TagParserInterface;
2323
use TypeLang\PhpDoc\Tests\TestCase;
2424

@@ -29,8 +29,8 @@ final class TagParserTest extends TestCase
2929
*/
3030
public static function parserDataProvider(): iterable
3131
{
32-
yield 'RegexTagParser' => [
33-
new RegexTagParser(new TagFactory()),
32+
yield 'StringTagParser' => [
33+
new StringTagParser(new TagFactory()),
3434
];
3535
}
3636

@@ -249,7 +249,7 @@ public function unicodeTagNameIsSupported(TagParserInterface $parser): void
249249
*/
250250
private static function descriptions(): DescriptionParserInterface
251251
{
252-
return new BalancedBraceAwareParser(new RegexTagParser(new TagFactory()));
252+
return new BalancedBraceAwareParser(new StringTagParser(new TagFactory()));
253253
}
254254

255255
/**

0 commit comments

Comments
 (0)