Skip to content

Commit 3637313

Browse files
committed
Improve "@see" tag behaviour
1 parent 72ac67f commit 3637313

32 files changed

+832
-73
lines changed

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

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+
}

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);

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
}

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

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(

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,

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
{

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
}

0 commit comments

Comments
 (0)