Skip to content

Commit 55a60e1

Browse files
committed
Replace description (and sprintf template) to separate simple and tagged descriptions.
1 parent 9c6f941 commit 55a60e1

16 files changed

+367
-202
lines changed

Diff for: src/DocBlock.php

+66-9
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,34 @@
44

55
namespace TypeLang\PHPDoc;
66

7-
use TypeLang\PHPDoc\Tag\Description;
8-
use TypeLang\PHPDoc\Tag\DescriptionInterface;
9-
use TypeLang\PHPDoc\Tag\OptionalDescriptionProviderInterface;
7+
use TypeLang\PHPDoc\Tag\Description\Description;
8+
use TypeLang\PHPDoc\Tag\Description\DescriptionInterface;
9+
use TypeLang\PHPDoc\Tag\Description\OptionalDescriptionProviderInterface;
1010
use TypeLang\PHPDoc\Tag\TagInterface;
11-
use TypeLang\PHPDoc\Tag\TagsProvider;
1211
use TypeLang\PHPDoc\Tag\TagsProviderInterface;
1312

1413
/**
1514
* This class represents structure containing a description and a set of tags
1615
* that describe an arbitrary DocBlock Comment in the code.
1716
*
1817
* @template-implements \ArrayAccess<int<0, max>, TagInterface|null>
18+
* @template-implements \IteratorAggregate<int<0, max>, TagInterface>
1919
*/
2020
final class DocBlock implements
2121
OptionalDescriptionProviderInterface,
2222
TagsProviderInterface,
23-
\ArrayAccess
23+
\IteratorAggregate,
24+
\ArrayAccess,
25+
\Countable
2426
{
25-
use TagsProvider;
26-
2727
private readonly DescriptionInterface $description;
2828

29+
/**
30+
* @var list<TagInterface>
31+
* @psalm-suppress PropertyNotSetInConstructor
32+
*/
33+
private readonly array $tags;
34+
2935
/**
3036
* @param iterable<array-key, TagInterface> $tags List of all tags contained in
3137
* a docblock object.
@@ -40,12 +46,63 @@ public function __construct(
4046
iterable $tags = [],
4147
) {
4248
$this->description = Description::fromStringable($description);
43-
44-
$this->bootTagProvider($tags);
49+
$this->tags = \array_values([...$tags]);
4550
}
4651

4752
public function getDescription(): DescriptionInterface
4853
{
4954
return $this->description;
5055
}
56+
57+
public function getTags(): array
58+
{
59+
return $this->tags;
60+
}
61+
62+
public function offsetExists(mixed $offset): bool
63+
{
64+
assert(\is_int($offset));
65+
66+
return isset($this->tags[$offset]);
67+
}
68+
69+
public function offsetGet(mixed $offset): ?TagInterface
70+
{
71+
assert(\is_int($offset));
72+
73+
return $this->tags[$offset] ?? null;
74+
}
75+
76+
/**
77+
* {@inheritDoc}
78+
*
79+
* @throws \BadMethodCallException
80+
*/
81+
public function offsetSet(mixed $offset, mixed $value): void
82+
{
83+
throw new \BadMethodCallException(self::class . ' objects are immutable');
84+
}
85+
86+
/**
87+
* {@inheritDoc}
88+
*
89+
* @throws \BadMethodCallException
90+
*/
91+
public function offsetUnset(mixed $offset): void
92+
{
93+
throw new \BadMethodCallException(self::class . ' objects are immutable');
94+
}
95+
96+
public function getIterator(): \Traversable
97+
{
98+
return new \ArrayIterator($this->tags);
99+
}
100+
101+
/**
102+
* @return int<0, max>
103+
*/
104+
public function count(): int
105+
{
106+
return \count($this->tags);
107+
}
51108
}

Diff for: src/Parser.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use TypeLang\PHPDoc\Parser\Comment\RegexCommentParser;
1212
use TypeLang\PHPDoc\Parser\Comment\Segment;
1313
use TypeLang\PHPDoc\Parser\Description\DescriptionParserInterface;
14-
use TypeLang\PHPDoc\Parser\Description\SprintfDescriptionParser;
14+
use TypeLang\PHPDoc\Parser\Description\RegexDescriptionParser;
1515
use TypeLang\PHPDoc\Parser\SourceMap;
1616
use TypeLang\PHPDoc\Parser\Tag\RegexTagParser;
1717
use TypeLang\PHPDoc\Parser\Tag\TagParserInterface;
@@ -30,7 +30,7 @@ public function __construct(
3030
FactoryInterface $tags = new TagFactory(),
3131
) {
3232
$this->tags = new RegexTagParser($tags);
33-
$this->descriptions = new SprintfDescriptionParser($this->tags);
33+
$this->descriptions = new RegexDescriptionParser($this->tags);
3434
$this->comments = new RegexCommentParser();
3535
}
3636

Diff for: src/Parser/Description/DescriptionParser.php

-103
This file was deleted.

Diff for: src/Parser/Description/DescriptionParserInterface.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace TypeLang\PHPDoc\Parser\Description;
66

7-
use TypeLang\PHPDoc\Tag\DescriptionInterface;
7+
use TypeLang\PHPDoc\Tag\Description\DescriptionInterface;
88

99
interface DescriptionParserInterface
1010
{

Diff for: src/Parser/Description/RegexDescriptionParser.php

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PHPDoc\Parser\Description;
6+
7+
use TypeLang\PHPDoc\Parser\Tag\RegexTagParser;
8+
use TypeLang\PHPDoc\Parser\Tag\TagParserInterface;
9+
use TypeLang\PHPDoc\Tag\Description\Description;
10+
use TypeLang\PHPDoc\Tag\Description\DescriptionInterface;
11+
use TypeLang\PHPDoc\Tag\Description\TaggedDescription;
12+
use TypeLang\PHPDoc\Tag\TagInterface;
13+
14+
class RegexDescriptionParser implements DescriptionParserInterface
15+
{
16+
public function __construct(
17+
private readonly TagParserInterface $tags = new RegexTagParser(),
18+
) {}
19+
20+
public function parse(string $description): DescriptionInterface
21+
{
22+
$components = [];
23+
$last = null;
24+
25+
foreach ($this->getDocBlockChunks($description) as $chunk) {
26+
$component = $this->createFromChunk($chunk);
27+
28+
// In case of `[N => Description, N+1 => Description]` we should
29+
// merge them into one.
30+
if ($component instanceof Description && $last instanceof Description) {
31+
// Remove [N] description object from the
32+
// end of the components list.
33+
$previous = \array_pop($components);
34+
35+
// Merge current component from the previous one
36+
// and replace current component that should be added to
37+
// components list with the merged one.
38+
$component = new Description($previous . $component);
39+
}
40+
41+
$components[] = $last = $component;
42+
}
43+
44+
return match (\count($components)) {
45+
0 => new Description(),
46+
1 => $components[0],
47+
default => new TaggedDescription($components),
48+
};
49+
}
50+
51+
private function createFromChunk(string $chunk): DescriptionInterface|TagInterface
52+
{
53+
if ($chunk === '@') {
54+
return new Description('{' . $chunk . '}');
55+
}
56+
57+
if (\str_starts_with($chunk, '@')) {
58+
try {
59+
return $this->tags->parse($chunk, $this);
60+
} catch (\Throwable) {
61+
return new Description('{' . $chunk . '}');
62+
}
63+
}
64+
65+
return new Description($chunk);
66+
}
67+
68+
/**
69+
* @return list<non-empty-string>
70+
*/
71+
private function getDocBlockChunks(string $contents): array
72+
{
73+
$result = [];
74+
$offset = 0;
75+
76+
foreach ($this->getDocBlockTags($contents) as [$tag, $at]) {
77+
if ($offset !== $at) {
78+
$result[] = \substr($contents, $offset, $at - $offset);
79+
}
80+
81+
$result[] = \substr($tag, 1, -1);
82+
83+
$offset = $at + \strlen($tag);
84+
}
85+
86+
if ($offset < \strlen($contents)) {
87+
$result[] = \substr($contents, $offset);
88+
}
89+
90+
/** @var list<non-empty-string> */
91+
return \array_filter($result);
92+
}
93+
94+
/**
95+
* @return list<array{non-empty-string, int<0, max>}>
96+
*/
97+
private function getDocBlockTags(string $contents): array
98+
{
99+
if (!\str_contains($contents, '{@')) {
100+
return [];
101+
}
102+
103+
\preg_match_all(
104+
pattern: '/\{@[^}{]*+(?:(?R)[^}{]*)*+}/',
105+
subject: $contents,
106+
matches: $matches,
107+
flags: \PREG_OFFSET_CAPTURE,
108+
);
109+
110+
/** @var list<array{non-empty-string, int<0, max>}> */
111+
return $matches[0] ?? [];
112+
}
113+
}

Diff for: src/Parser/Description/SprintfDescriptionParser.php

-26
This file was deleted.

Diff for: src/Tag/Content.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
use TypeLang\PHPDoc\Tag\Content\IdentifierApplicator;
1212
use TypeLang\PHPDoc\Tag\Content\OptionalIdentifierApplicator;
1313
use TypeLang\PHPDoc\Tag\Content\OptionalTypeParserApplicator;
14-
use TypeLang\PHPDoc\Tag\Content\ValueApplicator;
1514
use TypeLang\PHPDoc\Tag\Content\OptionalValueApplicator;
1615
use TypeLang\PHPDoc\Tag\Content\OptionalVariableNameApplicator;
1716
use TypeLang\PHPDoc\Tag\Content\TypeParserApplicator;
17+
use TypeLang\PHPDoc\Tag\Content\ValueApplicator;
1818
use TypeLang\PHPDoc\Tag\Content\VariableNameApplicator;
19+
use TypeLang\PHPDoc\Tag\Description\DescriptionInterface;
1920

2021
class Content implements \Stringable
2122
{

0 commit comments

Comments
 (0)