Skip to content

Commit 3637313

Browse files
committedMar 11, 2025
Improve "@see" tag behaviour
1 parent 72ac67f commit 3637313

32 files changed

+832
-73
lines changed
 

Diff for: ‎composer.json

+7-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
"composer/spdx-licenses": "^1.5",
3030
"friendsofphp/php-cs-fixer": "^3.70",
3131
"jetbrains/phpstorm-attributes": "^1.0",
32+
"phpbench/phpbench": "^1.4",
33+
"phpdocumentor/reflection-docblock": "^5.6",
34+
"phpstan/phpdoc-parser": "^2.1",
3235
"phpstan/phpstan": "^2.1",
3336
"phpstan/phpstan-strict-rules": "^2.0",
3437
"phpunit/phpunit": "^10.5|^11.0|^12.0",
@@ -70,7 +73,10 @@
7073
"linter:baseline": "phpstan analyse --configuration phpstan.neon --generate-baseline",
7174
"phpcs": "@phpcs:check",
7275
"phpcs:check": "php-cs-fixer fix --config=.php-cs-fixer.php --allow-risky=yes --dry-run --verbose --diff",
73-
"phpcs:fix": "php-cs-fixer fix --config=.php-cs-fixer.php --allow-risky=yes --verbose --diff"
76+
"phpcs:fix": "php-cs-fixer fix --config=.php-cs-fixer.php --allow-risky=yes --verbose --diff",
77+
"bench": [
78+
"phpbench run --report=default --tag=current --progress=none --filter=benchDocBlockParsing"
79+
]
7480
},
7581
"minimum-stability": "dev",
7682
"prefer-stable": true

Diff for: ‎phpbench.json

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"$schema": "./vendor/phpbench/phpbench/phpbench.schema.json",
3+
"runner.bootstrap": "vendor/autoload.php",
4+
"runner.path": "tests/Bench",
5+
"runner.progress": "plain",
6+
"runner.file_pattern": "*Bench.php",
7+
"runner.php_config": {
8+
"opcache.enable": 1,
9+
"opcache.enable_cli": 1,
10+
"opcache.jit_buffer_size": "128M",
11+
"opcache.jit": "1255",
12+
"xdebug.mode": "off"
13+
},
14+
"storage.xml_storage_path": "vendor/.cache.phpbench",
15+
"storage.store_binary": true,
16+
"storage.driver": "xml",
17+
"report.generators": {
18+
"default": {
19+
"extends": "overview",
20+
"tabbed": false,
21+
"components": [
22+
{
23+
"component": "section",
24+
"tabbed": true,
25+
"tab_labels": [
26+
"Time",
27+
"Memory"
28+
],
29+
"components": [
30+
{
31+
"component": "section",
32+
"title": "Results",
33+
"components": [
34+
{
35+
"component": "table_aggregate",
36+
"title": "Aggregation Table ({{ first(frame.suite_tag) }})",
37+
"partition": [
38+
"benchmark_name",
39+
"subject_name",
40+
"variant_name"
41+
],
42+
"row": {
43+
"benchmark": "first(partition['benchmark_name'])",
44+
"memory": "first(partition['result_mem_peak']) as memory",
45+
"min": "min(partition['result_time_avg']) as time",
46+
"max": "max(partition['result_time_avg']) as time",
47+
"mode": "mode(partition['result_time_avg']) as time",
48+
"rstdev": "rstdev(partition['result_time_avg'])"
49+
}
50+
}
51+
]
52+
}
53+
]
54+
}
55+
]
56+
}
57+
}
58+
}

Diff for: ‎src/DocBlock/Tag/LinkTag/LinkTag.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace TypeLang\PHPDoc\DocBlock\Tag\LinkTag;
66

7+
use TypeLang\PHPDoc\DocBlock\Tag\Shared\Reference\ReferenceInterface;
78
use TypeLang\PHPDoc\DocBlock\Tag\Shared\Reference\UriReference;
89
use TypeLang\PHPDoc\DocBlock\Tag\Tag;
910

@@ -17,7 +18,7 @@
1718
* relation defined by this occurrence.
1819
*
1920
* ```
20-
* "@link" [URI] [<description>]
21+
* "@link" [<URI> | <reference>] [<description>]
2122
* ```
2223
*
2324
* @link https://www.ietf.org/rfc/rfc2396.txt RFC2396
@@ -26,7 +27,7 @@ final class LinkTag extends Tag
2627
{
2728
public function __construct(
2829
string $name,
29-
public readonly UriReference $uri,
30+
public readonly ReferenceInterface $reference,
3031
\Stringable|string|null $description = null,
3132
) {
3233
parent::__construct($name, $description);

Diff for: ‎src/DocBlock/Tag/LinkTag/LinkTagFactory.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
namespace TypeLang\PHPDoc\DocBlock\Tag\LinkTag;
66

77
use TypeLang\PHPDoc\DocBlock\Tag\Factory\TagFactoryInterface;
8+
use TypeLang\PHPDoc\DocBlock\Tag\Shared\Reference\TypeElementReference;
9+
use TypeLang\PHPDoc\Parser\Content\ElementReferenceReader;
810
use TypeLang\PHPDoc\Parser\Content\Stream;
11+
use TypeLang\PHPDoc\Parser\Content\TypeReader;
912
use TypeLang\PHPDoc\Parser\Content\UriReferenceReader;
1013
use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface;
1114

@@ -20,9 +23,15 @@ public function create(string $tag, string $content, DescriptionParserInterface
2023
{
2124
$stream = new Stream($tag, $content);
2225

26+
try {
27+
$reference = $stream->apply(new UriReferenceReader());
28+
} catch (\Throwable) {
29+
$reference = $stream->apply(new ElementReferenceReader());
30+
}
31+
2332
return new LinkTag(
2433
name: $tag,
25-
uri: $stream->apply(new UriReferenceReader()),
34+
uri: $reference,
2635
description: $stream->toOptionalDescription($descriptions),
2736
);
2837
}

Diff for: ‎src/DocBlock/Tag/SeeTag/SeeTag.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
* The "`@see`" tag cannot refer to a namespace element.
2626
*
2727
* ```
28-
* "@see" [URI | FQN] [<description>]
28+
* "@see" [<URI> | <reference> | <type>] [<description>]
2929
* ```
3030
*
3131
* @link https://www.ietf.org/rfc/rfc2396.txt RFC2396

Diff for: ‎src/DocBlock/Tag/SeeTag/SeeTagFactory.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
use TypeLang\Parser\Parser as TypesParser;
88
use TypeLang\Parser\ParserInterface as TypesParserInterface;
99
use TypeLang\PHPDoc\DocBlock\Tag\Factory\TagFactoryInterface;
10+
use TypeLang\PHPDoc\DocBlock\Tag\Shared\Reference\TypeElementReference;
1011
use TypeLang\PHPDoc\Parser\Content\ElementReferenceReader;
1112
use TypeLang\PHPDoc\Parser\Content\Stream;
13+
use TypeLang\PHPDoc\Parser\Content\TypeReader;
1214
use TypeLang\PHPDoc\Parser\Content\UriReferenceReader;
1315
use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface;
1416

@@ -30,7 +32,13 @@ public function create(string $tag, string $content, DescriptionParserInterface
3032
try {
3133
$reference = $stream->apply(new UriReferenceReader());
3234
} catch (\Throwable) {
33-
$reference = $stream->apply(new ElementReferenceReader($this->parser));
35+
try {
36+
$reference = $stream->apply(new ElementReferenceReader());
37+
} catch (\Throwable) {
38+
$reference = new TypeElementReference(
39+
type: $stream->apply(new TypeReader($this->parser))
40+
);
41+
}
3442
}
3543

3644
return new SeeTag(

Diff for: ‎src/DocBlock/Tag/Shared/Reference/FunctionReference.php renamed to ‎src/DocBlock/Tag/Shared/Reference/FunctionElementReference.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
/**
1010
* Related to internal function reference
1111
*/
12-
final class FunctionReference extends ElementReference
12+
final class FunctionElementReference extends ElementReference
1313
{
1414
public function __construct(
1515
public readonly Name $function,

Diff for: ‎src/Parser.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ protected function createCommentParser(): CommentParserInterface
6767
/**
6868
* Facade method of {@see MutableTagFactoryInterface::register()}
6969
*
70-
* @param non-empty-string|list<non-empty-string> $tags
70+
* @param non-empty-lowercase-string|list<non-empty-lowercase-string> $tags
7171
*/
7272
public function register(string|array $tags, TagFactoryInterface $delegate): void
7373
{

Diff for: ‎src/Parser/Content/ElementReferenceReader.php

+101-54
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44

55
namespace TypeLang\PHPDoc\Parser\Content;
66

7+
use TypeLang\Parser\Node\FullQualifiedName;
78
use TypeLang\Parser\Node\Literal\VariableLiteralNode;
8-
use TypeLang\Parser\Node\Stmt\CallableTypeNode;
9-
use TypeLang\Parser\Node\Stmt\ClassConstNode;
9+
use TypeLang\Parser\Node\Name;
1010
use TypeLang\Parser\Node\Stmt\NamedTypeNode;
1111
use TypeLang\Parser\ParserInterface as TypesParserInterface;
1212
use TypeLang\PHPDoc\DocBlock\Tag\Shared\Reference\ClassConstantElementReference;
1313
use TypeLang\PHPDoc\DocBlock\Tag\Shared\Reference\ClassMethodElementReference;
1414
use TypeLang\PHPDoc\DocBlock\Tag\Shared\Reference\ClassPropertyElementReference;
1515
use TypeLang\PHPDoc\DocBlock\Tag\Shared\Reference\ElementReference;
16-
use TypeLang\PHPDoc\DocBlock\Tag\Shared\Reference\FunctionReference;
16+
use TypeLang\PHPDoc\DocBlock\Tag\Shared\Reference\FunctionElementReference;
1717
use TypeLang\PHPDoc\DocBlock\Tag\Shared\Reference\TypeElementReference;
1818
use TypeLang\PHPDoc\DocBlock\Tag\Shared\Reference\VariableReference;
1919

@@ -22,44 +22,66 @@
2222
*/
2323
final class ElementReferenceReader implements ReaderInterface
2424
{
25-
private OptionalTypeReader $types;
26-
27-
public function __construct(TypesParserInterface $parser)
28-
{
29-
$this->types = new OptionalTypeReader($parser);
30-
}
25+
private const T_FQN = '(?:[\\\\]?+[a-zA-Z\x80-\xFF_][0-9a-zA-Z\x80-\xFF_-]*+)++';
26+
private const T_IDENTIFIER = '[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*+';
27+
28+
private const SIMPLE_TOKENIZER_PCRE = '/^(?'
29+
. '|(?:(?P<T_CLASS>'. self::T_FQN . ')::(?:'
30+
. '(?:\\$[a-zA-Z_\\x80-\\xff][a-zA-Z0-9_\\x80-\\xff]*+)(*MARK:T_CLASS_PROPERTY)'
31+
. '|(?:[a-zA-Z_\\x80-\\xff][a-zA-Z0-9_\\x80-\\xff]*+\(\))(*MARK:T_CLASS_METHOD)'
32+
. '|(?:[a-zA-Z_\\x80-\\xff][a-zA-Z0-9_\\x80-\\xff]*+)(*MARK:T_CLASS_CONSTANT)'
33+
. '))'
34+
. '|(?:(?:\\$'. self::T_IDENTIFIER . ')(*MARK:T_VARIABLE))'
35+
. '|(?:(?:'. self::T_FQN . '\(\))(*MARK:T_FUNCTION))'
36+
. '|(?:(?:'. self::T_FQN . ')(*MARK:T_IDENTIFIER))'
37+
. ')(?:\s|$)/Ssum';
3138

3239
public function __invoke(Stream $stream): ElementReference
3340
{
34-
$type = ($this->types)($stream);
35-
36-
if ($type instanceof CallableTypeNode) {
37-
return $this->createFromFunction($type);
38-
}
39-
40-
if ($type instanceof NamedTypeNode) {
41-
return $this->createFromNamedType($type, $stream);
42-
}
43-
44-
if (\str_starts_with($stream->value, '$')) {
45-
return new VariableReference(VariableLiteralNode::parse(
46-
value: $stream->apply(new VariableNameReader()),
47-
));
48-
}
49-
50-
if ($type instanceof ClassConstNode) {
51-
/**
52-
* @var non-empty-string $identifier
53-
*
54-
* @phpstan-ignore-next-line constant cannot be null
55-
*/
56-
$identifier = $type->constant->toString();
57-
58-
if (\str_starts_with($stream->value, '()')) {
59-
return new ClassMethodElementReference($type->class, $identifier);
60-
}
61-
62-
return new ClassConstantElementReference($type->class, $identifier);
41+
\preg_match(self::SIMPLE_TOKENIZER_PCRE, $stream->value, $matches);
42+
43+
if ($matches !== []) {
44+
/** @var non-empty-string $body */
45+
$body = \rtrim($matches[0]);
46+
$isFullyQualified = $body[0] === '\\';
47+
48+
$result = match ($matches['MARK']) {
49+
'T_FUNCTION' => new FunctionElementReference($isFullyQualified
50+
? new FullQualifiedName(\substr($body, 0, -2))
51+
: new Name(\substr($body, 0, -2)),
52+
),
53+
'T_IDENTIFIER' => new TypeElementReference(
54+
type: new NamedTypeNode($isFullyQualified
55+
? new FullQualifiedName($body)
56+
: new Name($body)
57+
),
58+
),
59+
'T_VARIABLE' => new VariableReference(
60+
variable: new VariableLiteralNode($body),
61+
),
62+
'T_CLASS_CONSTANT' => new ClassConstantElementReference(
63+
class: $isFullyQualified
64+
? new FullQualifiedName($matches['T_CLASS'])
65+
: new Name($matches['T_CLASS']),
66+
constant: \substr($body, \strlen($matches['T_CLASS']) + 2),
67+
),
68+
'T_CLASS_METHOD' => new ClassMethodElementReference(
69+
class: $isFullyQualified
70+
? new FullQualifiedName($matches['T_CLASS'])
71+
: new Name($matches['T_CLASS']),
72+
method: \substr($body, \strlen($matches['T_CLASS']) + 2, -2),
73+
),
74+
'T_CLASS_PROPERTY' => new ClassPropertyElementReference(
75+
class: $isFullyQualified
76+
? new FullQualifiedName($matches['T_CLASS'])
77+
: new Name($matches['T_CLASS']),
78+
property: \substr($body, \strlen($matches['T_CLASS']) + 3),
79+
),
80+
};
81+
82+
$stream->shift(\strlen($matches[0]));
83+
84+
return $result;
6385
}
6486

6587
throw $stream->toException(\sprintf(
@@ -68,38 +90,63 @@ public function __invoke(Stream $stream): ElementReference
6890
));
6991
}
7092

71-
private function createFromFunction(CallableTypeNode $type): ElementReference
93+
private function getReference(string $context, Stream $stream): ElementReference
7294
{
73-
if ($type->type !== null || $type->parameters->items !== []) {
74-
return new TypeElementReference($type);
75-
}
95+
$class = new Name($context);
7696

77-
return new FunctionReference($type->name);
78-
}
79-
80-
private function createFromNamedType(NamedTypeNode $type, Stream $stream): ElementReference
81-
{
8297
if (\str_starts_with($stream->value, '::')) {
83-
if ($type->arguments === null && $type->fields === null) {
84-
// Skip "::" delimiter
85-
$stream->shift(2);
98+
$stream->shift(2, false);
99+
100+
if (\str_starts_with($stream->value, '$')) {
101+
$stream->shift(1, false);
86102

87-
$variable = $stream->apply(new OptionalVariableNameReader());
103+
$variable = $this->fetchIdentifier($stream->value);
88104

89105
if ($variable !== null) {
90106
return new ClassPropertyElementReference(
91-
class: $type->name,
107+
class: $class,
92108
property: $variable,
93109
);
94110
}
111+
112+
throw $stream->toException(\sprintf(
113+
'Tag @%s contains invalid property name after class reference',
114+
$stream->tag,
115+
));
116+
}
117+
118+
$identifier = $this->fetchIdentifier($stream->value);
119+
120+
if ($identifier !== null) {
121+
$stream->shift(\strlen($identifier), false);
122+
123+
if (\str_starts_with($stream->value, '()')) {
124+
return new ClassMethodElementReference(
125+
class: $class,
126+
method: $identifier,
127+
);
128+
}
129+
130+
return new ClassConstantElementReference(
131+
class: $class,
132+
constant: $identifier,
133+
);
95134
}
96135

97136
throw $stream->toException(\sprintf(
98-
'Tag @%s expects the FQN reference to be defined',
137+
'Tag @%s contains invalid method or constant name after class reference',
99138
$stream->tag,
100139
));
101140
}
102141

103-
return new TypeElementReference($type);
142+
if (\str_starts_with($stream->value, '()')) {
143+
$stream->shift(2, false);
144+
145+
return new FunctionElementReference($class);
146+
}
147+
148+
return new TypeElementReference(
149+
type: new NamedTypeNode($class),
150+
);
104151
}
105152
}

Diff for: ‎src/Parser/Content/OptionalTypeReader.php

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
namespace TypeLang\PHPDoc\Parser\Content;
66

77
use TypeLang\Parser\Exception\ParserExceptionInterface;
8+
use TypeLang\Parser\Node\FullQualifiedName;
9+
use TypeLang\Parser\Node\Name;
10+
use TypeLang\Parser\Node\Stmt\NamedTypeNode;
811
use TypeLang\Parser\Node\Stmt\TypeStatement;
912
use TypeLang\Parser\ParserInterface as TypesParserInterface;
1013
use TypeLang\PHPDoc\Exception\InvalidTagException;
@@ -14,6 +17,8 @@
1417
*/
1518
final class OptionalTypeReader implements OptionalReaderInterface
1619
{
20+
private const IS_SIMPLE_TYPE_PCRE = '/^[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*+(?:\s|$)/Ssu';
21+
1722
public function __construct(
1823
private readonly TypesParserInterface $parser,
1924
) {}

Diff for: ‎src/Parser/Content/Stream.php

+9-8
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ public function __construct(
3232
*/
3333
public function shift(int $offset, bool $ltrim = true): void
3434
{
35-
if ($offset <= 0) {
36-
return;
37-
}
38-
3935
$size = \strlen($this->value);
4036
$this->value = \substr($this->value, $offset);
4137

@@ -49,18 +45,23 @@ public function shift(int $offset, bool $ltrim = true): void
4945

5046
/**
5147
* @api
52-
*
53-
* @param callable(self):bool $context
48+
* @template TArgResult of mixed
49+
* @param callable(self):TArgResult $context
50+
* @return TArgResult
5451
*/
55-
public function lookahead(callable $context): void
52+
public function lookahead(callable $context): mixed
5653
{
5754
$offset = $this->offset;
5855
$value = $this->value;
5956

60-
if ($context($this) === false) {
57+
$result = $context($this);
58+
59+
if ($result === null) {
6160
$this->offset = $offset;
6261
$this->value = $value;
6362
}
63+
64+
return $result;
6465
}
6566

6667
/**

Diff for: ‎src/Parser/Content/TypeReader.php

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
namespace TypeLang\PHPDoc\Parser\Content;
66

77
use TypeLang\Parser\Exception\ParserExceptionInterface;
8+
use TypeLang\Parser\Node\FullQualifiedName;
9+
use TypeLang\Parser\Node\Name;
10+
use TypeLang\Parser\Node\Stmt\NamedTypeNode;
811
use TypeLang\Parser\Node\Stmt\TypeStatement;
912
use TypeLang\Parser\ParserInterface as TypesParserInterface;
1013
use TypeLang\PHPDoc\Exception\InvalidTagException;

Diff for: ‎src/Platform/EmptyPlatform.php

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Platform;
6+
7+
use TypeLang\Parser\ParserInterface as TypesParserInterface;
8+
9+
final class EmptyPlatform extends Platform
10+
{
11+
public function getName(): string
12+
{
13+
return 'Empty';
14+
}
15+
16+
protected function load(TypesParserInterface $types): iterable
17+
{
18+
return [];
19+
}
20+
}

Diff for: ‎src/Platform/Platform.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public function getTags(): iterable
3232
}
3333

3434
/**
35-
* @return iterable<non-empty-string|iterable<mixed, non-empty-string>, TagFactoryInterface>
35+
* @return iterable<non-empty-lowercase-string|iterable<mixed, non-empty-lowercase-string>, TagFactoryInterface>
3636
*/
3737
abstract protected function load(TypesParserInterface $types): iterable;
3838
}

Diff for: ‎src/Platform/PlatformInterface.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public function getName(): string;
1818
/**
1919
* Returns a list of registered tags for the specified platform.
2020
*
21-
* @return iterable<non-empty-string, TagFactoryInterface>
21+
* @return iterable<non-empty-lowercase-string, TagFactoryInterface>
2222
*/
2323
public function getTags(): iterable;
2424
}

Diff for: ‎src/Platform/StandardPlatform.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ protected function load(TypesParserInterface $types): iterable
7676
}
7777

7878
/**
79-
* @return iterable<non-empty-string|iterable<mixed, non-empty-string>, TagFactoryInterface>
79+
* @return iterable<non-empty-lowercase-string|iterable<mixed, non-empty-lowercase-string>, TagFactoryInterface>
8080
*/
8181
protected function loadAliases(TypesParserInterface $types): iterable
8282
{
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tests\Bench\DocBlockParser;
6+
7+
abstract class DocBlockParserBenchCase
8+
{
9+
/**
10+
* @var non-empty-list<non-empty-string>
11+
*/
12+
protected readonly array $samples;
13+
14+
public function __construct()
15+
{
16+
$this->samples = \json_decode(
17+
json: \file_get_contents(__DIR__ . '/samples.json'),
18+
associative: true,
19+
flags: \JSON_THROW_ON_ERROR,
20+
);
21+
}
22+
23+
abstract public function benchDocBlockParsing(): void;
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tests\Bench\DocBlockParser;
6+
7+
use PhpBench\Attributes\Iterations;
8+
use PhpBench\Attributes\Revs;
9+
use PhpBench\Attributes\Warmup;
10+
use phpDocumentor\Reflection\DocBlockFactory;
11+
use phpDocumentor\Reflection\DocBlockFactoryInterface;
12+
13+
#[Revs(1), Warmup(1), Iterations(2)]
14+
final class PHPDocumentorDocBlockParserBench extends DocBlockParserBenchCase
15+
{
16+
private readonly DocBlockFactoryInterface $parser;
17+
18+
public function __construct()
19+
{
20+
parent::__construct();
21+
22+
$this->parser = DocBlockFactory::createInstance();
23+
}
24+
25+
public function benchDocBlockParsing(): void
26+
{
27+
foreach ($this->samples as $sample) {
28+
$this->parser->create($sample);
29+
}
30+
}
31+
}
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tests\Bench\DocBlockParser;
6+
7+
use PhpBench\Attributes\Iterations;
8+
use PhpBench\Attributes\Revs;
9+
use PhpBench\Attributes\Warmup;
10+
use PHPStan\PhpDocParser\Lexer\Lexer;
11+
use PHPStan\PhpDocParser\Parser\ConstExprParser;
12+
use PHPStan\PhpDocParser\Parser\PhpDocParser;
13+
use PHPStan\PhpDocParser\Parser\TokenIterator;
14+
use PHPStan\PhpDocParser\Parser\TypeParser;
15+
use PHPStan\PhpDocParser\ParserConfig;
16+
17+
#[Revs(1), Warmup(1), Iterations(2)]
18+
final class PHPStanDocBlockParserBench extends DocBlockParserBenchCase
19+
{
20+
private readonly Lexer $lexer;
21+
private readonly PhpDocParser $parser;
22+
23+
public function __construct()
24+
{
25+
parent::__construct();
26+
27+
$config = new ParserConfig(usedAttributes: [
28+
'lines' => true,
29+
'indexes' => true,
30+
'comments' => true,
31+
]);
32+
$constExprParser = new ConstExprParser($config);
33+
$typeParser = new TypeParser($config, $constExprParser);
34+
$this->lexer = new Lexer($config);
35+
$this->parser = new PhpDocParser($config, $typeParser, $constExprParser);
36+
}
37+
38+
public function benchDocBlockParsing(): void
39+
{
40+
foreach ($this->samples as $sample) {
41+
$tokens = new TokenIterator($this->lexer->tokenize($sample));
42+
$this->parser->parse($tokens);
43+
}
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tests\Bench\DocBlockParser;
6+
7+
use PhpBench\Attributes\Iterations;
8+
use PhpBench\Attributes\Revs;
9+
use PhpBench\Attributes\Warmup;
10+
use TypeLang\PHPDoc\Parser;
11+
12+
#[Revs(1), Warmup(1), Iterations(2)]
13+
final class TypeLangDocBlockParserBench extends DocBlockParserBenchCase
14+
{
15+
private readonly Parser $parser;
16+
17+
public function __construct()
18+
{
19+
parent::__construct();
20+
21+
$this->parser = new Parser();
22+
}
23+
24+
public function benchDocBlockParsing(): void
25+
{
26+
foreach ($this->samples as $sample) {
27+
$this->parser->parse($sample);
28+
}
29+
}
30+
}

Diff for: ‎tests/Bench/DocBlockParser/samples.json

+1
Large diffs are not rendered by default.

Diff for: ‎tests/Bench/FullQualified/FullQualifiedBenchCase.php

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tests\Bench\FullQualified;
6+
7+
abstract class FullQualifiedBenchCase
8+
{
9+
public const PARSING_SAMPLES = [
10+
'number_of() Testing',
11+
'number_of()',
12+
'Path\To\Function\number_of() Description',
13+
'MyClass::$items Property',
14+
'Path\To\MyClass::$items For the property whose items are counted.',
15+
'MyClass::setItems() To set the items for this collection.',
16+
'Example\MyClass::setItems()',
17+
'https://example.com/my/bar link',
18+
'Some::ANY_* test',
19+
'non-empty-string\ololo::ANY_* test',
20+
'Some::ANY_*_OLOLO test42',
21+
'PSR\Documentation\API',
22+
'doc://getting-started/index Getting started document.',
23+
];
24+
25+
abstract public function benchIdentifierReading(): void;
26+
}

Diff for: ‎tests/Bench/FullQualified/FullTokenizerBench.php

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tests\Bench\FullQualified;
6+
7+
use PhpBench\Attributes\Iterations;
8+
use PhpBench\Attributes\Revs;
9+
use PhpBench\Attributes\Warmup;
10+
11+
#[Revs(10), Warmup(5), Iterations(100)]
12+
final class FullTokenizerBench extends FullQualifiedBenchCase
13+
{
14+
private const T_FQN = '(?:[\\]?+[a-zA-Z\x80-\xFF_][0-9a-zA-Z\x80-\xFF_-]*+)++';
15+
private const T_IDENTIFIER = '[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*+';
16+
17+
private const SIMPLE_TOKENIZER_PCRE = '/\\G(?'
18+
. '|(?:(?:'. self::T_FQN . '::\\$' . self::T_IDENTIFIER . ')(*MARK:T_PROPERTY))'
19+
. '|(?:(?:'. self::T_FQN . '::' . self::T_IDENTIFIER . '\(\))(*MARK:T_METHOD))'
20+
. '|(?:(?:'. self::T_FQN . '::' . self::T_IDENTIFIER . ')(*MARK:T_CONSTANT))'
21+
. '|(?:(?:\\$'. self::T_IDENTIFIER . ')(*MARK:T_VARIABLE))'
22+
. '|(?:(?:'. self::T_FQN . '\(\))(*MARK:T_FUNCTION))'
23+
. '|(?:(?:'. self::T_FQN . ')(*MARK:T_IDENTIFIER))'
24+
. ')/Ssum';
25+
26+
public function benchIdentifierReading(): void
27+
{
28+
foreach (self::PARSING_SAMPLES as $sample) {
29+
\preg_match_all(self::SIMPLE_TOKENIZER_PCRE, $sample, $matches);
30+
31+
if ($matches[0] !== []) {
32+
$__result = $matches[0][0];
33+
}
34+
}
35+
}
36+
}

Diff for: ‎tests/Bench/FullQualified/InArrayBench.php

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tests\Bench\FullQualified;
6+
7+
use PhpBench\Attributes\Iterations;
8+
use PhpBench\Attributes\Revs;
9+
use PhpBench\Attributes\Warmup;
10+
11+
#[Revs(10), Warmup(5), Iterations(100)]
12+
final class InArrayBench extends FullQualifiedBenchCase
13+
{
14+
public const CHARSET = [
15+
'_',
16+
'-',
17+
'\\',
18+
'A',
19+
'B',
20+
'C',
21+
'D',
22+
'E',
23+
'F',
24+
'G',
25+
'H',
26+
'I',
27+
'J',
28+
'K',
29+
'L',
30+
'M',
31+
'N',
32+
'O',
33+
'P',
34+
'Q',
35+
'R',
36+
'S',
37+
'T',
38+
'U',
39+
'V',
40+
'W',
41+
'X',
42+
'Y',
43+
'Z',
44+
'a',
45+
'b',
46+
'c',
47+
'd',
48+
'e',
49+
'f',
50+
'g',
51+
'h',
52+
'i',
53+
'j',
54+
'k',
55+
'l',
56+
'm',
57+
'n',
58+
'o',
59+
'p',
60+
'q',
61+
'r',
62+
's',
63+
't',
64+
'u',
65+
'v',
66+
'w',
67+
'x',
68+
'y',
69+
'z',
70+
'0',
71+
'1',
72+
'2',
73+
'3',
74+
'4',
75+
'5',
76+
'6',
77+
'7',
78+
'8',
79+
'9',
80+
];
81+
82+
public function benchIdentifierReading(): void
83+
{
84+
foreach (self::PARSING_SAMPLES as $sample) {
85+
for ($offset = 0, $length = \strlen($sample); $offset < $length; $offset++) {
86+
if (!\in_array($sample[$offset], self::CHARSET, true)) {
87+
break;
88+
}
89+
}
90+
91+
$__result = \substr($sample, 0, $offset);
92+
}
93+
}
94+
}

Diff for: ‎tests/Bench/FullQualified/IssetBench.php

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tests\Bench\FullQualified;
6+
7+
use PhpBench\Attributes\Iterations;
8+
use PhpBench\Attributes\Revs;
9+
use PhpBench\Attributes\Warmup;
10+
11+
#[Revs(10), Warmup(5), Iterations(100)]
12+
final class IssetBench extends FullQualifiedBenchCase
13+
{
14+
public const CHARSET = [
15+
'_' => true,
16+
'-' => true,
17+
'\\' => true,
18+
'A' => true,
19+
'B' => true,
20+
'C' => true,
21+
'D' => true,
22+
'E' => true,
23+
'F' => true,
24+
'G' => true,
25+
'H' => true,
26+
'I' => true,
27+
'J' => true,
28+
'K' => true,
29+
'L' => true,
30+
'M' => true,
31+
'N' => true,
32+
'O' => true,
33+
'P' => true,
34+
'Q' => true,
35+
'R' => true,
36+
'S' => true,
37+
'T' => true,
38+
'U' => true,
39+
'V' => true,
40+
'W' => true,
41+
'X' => true,
42+
'Y' => true,
43+
'Z' => true,
44+
'a' => true,
45+
'b' => true,
46+
'c' => true,
47+
'd' => true,
48+
'e' => true,
49+
'f' => true,
50+
'g' => true,
51+
'h' => true,
52+
'i' => true,
53+
'j' => true,
54+
'k' => true,
55+
'l' => true,
56+
'm' => true,
57+
'n' => true,
58+
'o' => true,
59+
'p' => true,
60+
'q' => true,
61+
'r' => true,
62+
's' => true,
63+
't' => true,
64+
'u' => true,
65+
'v' => true,
66+
'w' => true,
67+
'x' => true,
68+
'y' => true,
69+
'z' => true,
70+
'0' => true,
71+
'1' => true,
72+
'2' => true,
73+
'3' => true,
74+
'4' => true,
75+
'5' => true,
76+
'6' => true,
77+
'7' => true,
78+
'8' => true,
79+
'9' => true,
80+
];
81+
82+
public function benchIdentifierReading(): void
83+
{
84+
foreach (self::PARSING_SAMPLES as $sample) {
85+
for ($offset = 0, $length = \strlen($sample); $offset < $length; $offset++) {
86+
if (!isset(self::CHARSET[$sample[$offset]])) {
87+
break;
88+
}
89+
}
90+
91+
$__result = \substr($sample, 0, $offset);
92+
}
93+
}
94+
}

Diff for: ‎tests/Bench/FullQualified/LTrimBench.php

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tests\Bench\FullQualified;
6+
7+
use PhpBench\Attributes\Iterations;
8+
use PhpBench\Attributes\Revs;
9+
use PhpBench\Attributes\Warmup;
10+
11+
#[Revs(10), Warmup(5), Iterations(100)]
12+
final class LTrimBench extends FullQualifiedBenchCase
13+
{
14+
public const CHARSET = '_-\\'
15+
. 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
16+
. 'abcdefghijklmnopqrstuvwxyz'
17+
. '0123456789';
18+
19+
public function benchIdentifierReading(): void
20+
{
21+
foreach (self::PARSING_SAMPLES as $sample) {
22+
$length = \strlen($sample);
23+
$after = \strlen(\ltrim($sample, self::CHARSET));
24+
$__result = \substr($sample, $length, $after);
25+
}
26+
}
27+
}

Diff for: ‎tests/Bench/FullQualified/PhpTokenizerBench.php

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tests\Bench\FullQualified;
6+
7+
use PhpBench\Attributes\Iterations;
8+
use PhpBench\Attributes\Revs;
9+
use PhpBench\Attributes\Warmup;
10+
11+
#[Revs(10), Warmup(5), Iterations(100)]
12+
final class PhpTokenizerBench extends FullQualifiedBenchCase
13+
{
14+
public function benchIdentifierReading(): void
15+
{
16+
foreach (self::PARSING_SAMPLES as $sample) {
17+
foreach (\PhpToken::tokenize('<?php ' . $sample) as $token) {
18+
if ($token->id === \T_OPEN_TAG) {
19+
continue;
20+
}
21+
22+
if ($token->id === \T_NAME_QUALIFIED) {
23+
$__result = $token->text;
24+
break;
25+
}
26+
27+
break;
28+
}
29+
30+
}
31+
}
32+
}

Diff for: ‎tests/Bench/FullQualified/RegexBench.php

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tests\Bench\FullQualified;
6+
7+
use PhpBench\Attributes\Iterations;
8+
use PhpBench\Attributes\Revs;
9+
use PhpBench\Attributes\Warmup;
10+
11+
#[Revs(10), Warmup(5), Iterations(100)]
12+
final class RegexBench extends FullQualifiedBenchCase
13+
{
14+
private const PCRE = '/^'
15+
. '\\\\?[a-zA-Z\\x80-\\xFF_][a-zA-Z0-9\\x80-\\xFF\\-_]'
16+
.'*(?:\\\\[a-zA-Z\\x80-\\xFF_][a-zA-Z0-9\\x80-\\xFF\\-_]*)*'
17+
. '/u';
18+
19+
public function benchIdentifierReading(): void
20+
{
21+
foreach (self::PARSING_SAMPLES as $sample) {
22+
\preg_match(self::PCRE, $sample, $__result);
23+
}
24+
}
25+
}

Diff for: ‎tests/Bench/FullQualified/RegexPHPStanLikeBench.php

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tests\Bench\FullQualified;
6+
7+
use PhpBench\Attributes\Iterations;
8+
use PhpBench\Attributes\Revs;
9+
use PhpBench\Attributes\Warmup;
10+
11+
#[Revs(10), Warmup(5), Iterations(100)]
12+
final class RegexPHPStanLikeBench extends FullQualifiedBenchCase
13+
{
14+
private const PCRE = '/^(?:[\\]?+[a-zA-Z\x80-\xFF_][0-9a-zA-Z\x80-\xFF_-]*+)++/u';
15+
16+
public function benchIdentifierReading(): void
17+
{
18+
foreach (self::PARSING_SAMPLES as $sample) {
19+
\preg_match(self::PCRE, $sample, $__result);
20+
}
21+
}
22+
}

Diff for: ‎tests/Bench/FullQualified/RegexTokenizerBench.php

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tests\Bench\FullQualified;
6+
7+
use PhpBench\Attributes\Iterations;
8+
use PhpBench\Attributes\Revs;
9+
use PhpBench\Attributes\Warmup;
10+
11+
#[Revs(10), Warmup(5), Iterations(100)]
12+
final class RegexTokenizerBench extends FullQualifiedBenchCase
13+
{
14+
private const SIMPLE_TOKENIZER_PCRE = '/\\G(?'
15+
. '|(?:(?:\\$[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*)(*MARK:T_VARIABLE))'
16+
. '|(?:(?:[a-zA-Z_\\x80-\\xff][a-zA-Z0-9\\-_\\x80-\\xff]*)(*MARK:T_NAME))'
17+
. '|(?:(?:\\()(*MARK:T_PARENTHESIS_OPEN))'
18+
. '|(?:(?:\\))(*MARK:T_PARENTHESIS_CLOSE))'
19+
. '|(?:(?:::)(*MARK:T_DOUBLE_COLON))'
20+
. '|(?:(?:\\\\)(*MARK:T_NS_DELIMITER))'
21+
. '|(?:(?:\\|)(*MARK:T_OR))'
22+
. '|(?:(?:(\\/\\/|#).+?$)(*MARK:T_COMMENT))'
23+
. '|(?:(?:\\/\\*.*?\\*\\/)(*MARK:T_DOC_COMMENT))'
24+
. '|(?:(?:(\\xfe\\xff|\\x20|\\x09|\\x0a|\\x0d)+)(*MARK:T_WHITESPACE))'
25+
. '|(?:(?:.+?)(*MARK:T_OTHER))'
26+
. ')/Ssum';
27+
28+
public function benchIdentifierReading(): void
29+
{
30+
foreach (self::PARSING_SAMPLES as $sample) {
31+
\preg_match_all(self::SIMPLE_TOKENIZER_PCRE, $sample, $matches, \PREG_SET_ORDER);
32+
$__result = '';
33+
34+
foreach ($matches as ['MARK' => $name, 0 => $value]) {
35+
if ($name === 'T_NAME' || $name === 'T_NS_DELIMITER') {
36+
continue;
37+
}
38+
39+
$__result .= $value;
40+
41+
break;
42+
}
43+
}
44+
}
45+
}

Diff for: ‎tests/Bench/FullQualified/TypeLangLexerBench.php

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tests\Bench\FullQualified;
6+
7+
use PhpBench\Attributes\Iterations;
8+
use PhpBench\Attributes\Revs;
9+
use PhpBench\Attributes\Warmup;
10+
use Phplrt\Lexer\Lexer;
11+
use TypeLang\Parser\Parser;
12+
13+
#[Revs(10), Warmup(5), Iterations(100)]
14+
final class TypeLangLexerBench extends FullQualifiedBenchCase
15+
{
16+
private readonly Lexer $lexer;
17+
18+
public function __construct()
19+
{
20+
$parser = new Parser();
21+
22+
$this->lexer = (new \ReflectionClass(Parser::class))
23+
->getProperty('lexer')
24+
->getValue($parser);
25+
}
26+
27+
public function benchIdentifierReading(): void
28+
{
29+
foreach (self::PARSING_SAMPLES as $sample) {
30+
foreach ($this->lexer->lex($sample) as $token) {
31+
$isValidToken = $token->getName() === 'T_NAME'
32+
|| $token->getName() === 'T_NAME_WITH_SPACE'
33+
|| $token->getName() === 'T_NS_DELIMITER';
34+
35+
if (!$isValidToken) {
36+
break;
37+
}
38+
}
39+
}
40+
}
41+
}

Diff for: ‎tests/Bench/FullQualified/TypeLangParserBench.php

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Tests\Bench\FullQualified;
6+
7+
use PhpBench\Attributes\Iterations;
8+
use PhpBench\Attributes\Revs;
9+
use PhpBench\Attributes\Warmup;
10+
use TypeLang\Parser\Parser;
11+
12+
#[Revs(10), Warmup(5), Iterations(100)]
13+
final class TypeLangParserBench extends FullQualifiedBenchCase
14+
{
15+
private readonly Parser $parser;
16+
17+
public function __construct()
18+
{
19+
$this->parser = new Parser(tolerant: true);
20+
}
21+
22+
public function benchIdentifierReading(): void
23+
{
24+
foreach (self::PARSING_SAMPLES as $sample) {
25+
$__result = $this->parser->parse($sample);
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)
Please sign in to comment.