Skip to content

Commit 60b4baf

Browse files
committed
Add example tag
1 parent f046d9a commit 60b4baf

5 files changed

Lines changed: 246 additions & 2 deletions

File tree

libs/phpdoc/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,8 @@ its description.
226226
> `"@copyright" [ <Description> ]`
227227
- [x] `@deprecated` — Marks an _Element_ as deprecated, optionally since a given version
228228
> `"@deprecated" [ <Version> ] [ <Description> ]`
229-
- [ ] `@example` — Points to an example source file illustrating the use of an _Element_
230-
> `"@example" [ <Location> [ <StartLine> [ <LineCount> ] ] ] [ <Description> ]`
229+
- [x] `@example` — Points to an example source file illustrating the use of an _Element_
230+
> `"@example" (<URL> | <URI>) [ <StartLine> [ <LineCount> ] ] [ <Description> ]`
231231
- [x] `@extends` — Allows to extend templated classes and interfaces
232232
> `"@extends" <Type> [ <Description> ]`
233233
- [x] `@filesource` — Tells documentation tooling to include the source of the current file in its output
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PhpDoc\DocBlock\Tag\ExampleTag;
6+
7+
use TypeLang\PhpDoc\DocBlock\Description\DescriptionInterface;
8+
use TypeLang\PhpDoc\DocBlock\Reference\UriReference;
9+
use TypeLang\PhpDoc\DocBlock\Tag\Tag;
10+
11+
/**
12+
* The "@example" tag points at an external source that illustrates the use of
13+
* the documented element, optionally narrowing it to a range of lines starting
14+
* at a line and spanning a number of lines.
15+
*/
16+
final class ExampleTag extends Tag
17+
{
18+
public function __construct(
19+
string $name,
20+
/**
21+
* The location of the illustrating source.
22+
*/
23+
public readonly UriReference $location,
24+
/**
25+
* @var int<0, max>|null
26+
*/
27+
public readonly ?int $start = null,
28+
/**
29+
* @var int<0, max>|null
30+
*/
31+
public readonly ?int $count = null,
32+
?DescriptionInterface $description = null,
33+
) {
34+
parent::__construct($name, $description);
35+
}
36+
37+
#[\Override]
38+
public function __toString(): string
39+
{
40+
$result = \sprintf('@%s %s', $this->name, $this->location);
41+
42+
if ($this->start !== null) {
43+
$result .= ' ' . $this->start;
44+
}
45+
46+
if ($this->count !== null) {
47+
$result .= ' ' . $this->count;
48+
}
49+
50+
if ($this->description !== null) {
51+
$result .= ' ' . $this->description;
52+
}
53+
54+
return $result;
55+
}
56+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PhpDoc\DocBlock\Tag\ExampleTag;
6+
7+
use TypeLang\PhpDoc\DocBlock\Combinator\DescriptionCombinator;
8+
use TypeLang\PhpDoc\DocBlock\Combinator\IntegerCombinator;
9+
use TypeLang\PhpDoc\DocBlock\Combinator\UriCombinator;
10+
use TypeLang\PhpDoc\DocBlock\Combinator\UrlCombinator;
11+
use TypeLang\PhpDoc\DocBlock\Description\DescriptionInterface;
12+
use TypeLang\PhpDoc\DocBlock\Reference\UriReference;
13+
use TypeLang\PhpDoc\DocBlock\TagDefinition\Spec;
14+
use TypeLang\PhpDoc\DocBlock\TagDefinition\TagDefinition;
15+
use TypeLang\PhpDoc\DocBlock\TagDefinition\TagPayload;
16+
17+
/**
18+
* The "`@example`" tag points at an external source illustrating the use of the
19+
* documented element, or describes such an example inline.
20+
*
21+
* ```
22+
* "@example" (<URL> | <URI>) [ <Start> [ <Count> ] ] [ <Description> ]
23+
* ```
24+
*/
25+
final class ExampleTagDefinition extends TagDefinition
26+
{
27+
public const string NAME = 'example';
28+
29+
public function __construct()
30+
{
31+
parent::__construct(
32+
name: self::NAME,
33+
spec: Spec::sequence(
34+
Spec::oneOf(
35+
Spec::rule(UrlCombinator::NAME, 'location'),
36+
Spec::rule(UriCombinator::NAME, 'location'),
37+
),
38+
Spec::maybe(
39+
Spec::sequence(
40+
Spec::rule(IntegerCombinator::NAME, 'start'),
41+
Spec::maybe(
42+
Spec::rule(IntegerCombinator::NAME, 'count'),
43+
),
44+
),
45+
),
46+
Spec::maybe(
47+
Spec::rule(DescriptionCombinator::NAME, 'description'),
48+
),
49+
),
50+
isInline: false,
51+
);
52+
}
53+
54+
public function create(string $name, TagPayload $result): ExampleTag
55+
{
56+
/** @var UriReference $location */
57+
$location = $result->get('location');
58+
59+
/** @var int<0, max>|null $start */
60+
$start = $result->find('start');
61+
62+
/** @var int<0, max>|null $count */
63+
$count = $result->find('count');
64+
65+
/** @var DescriptionInterface|null $description */
66+
$description = $result->find('description');
67+
68+
return new ExampleTag($name, $location, $start, $count, $description);
69+
}
70+
}

libs/phpdoc/src/DocBlockParser.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use TypeLang\PhpDoc\DocBlock\Tag\CategoryTag\CategoryTagDefinition;
2727
use TypeLang\PhpDoc\DocBlock\Tag\CopyrightTag\CopyrightTagDefinition;
2828
use TypeLang\PhpDoc\DocBlock\Tag\DeprecatedTag\DeprecatedTagDefinition;
29+
use TypeLang\PhpDoc\DocBlock\Tag\ExampleTag\ExampleTagDefinition;
2930
use TypeLang\PhpDoc\DocBlock\Tag\FilesourceTag\FilesourceTagDefinition;
3031
use TypeLang\PhpDoc\DocBlock\Tag\FinalTag\FinalTagDefinition;
3132
use TypeLang\PhpDoc\DocBlock\Tag\GlobalTag\GlobalTagDefinition;
@@ -222,6 +223,7 @@ private function createDefaultTagDefinitions(): array
222223
SuppressTagDefinition::NAME => new SuppressTagDefinition(),
223224
SourceTagDefinition::NAME => new SourceTagDefinition(),
224225
MethodTagDefinition::NAME => new MethodTagDefinition(),
226+
ExampleTagDefinition::NAME => new ExampleTagDefinition(),
225227
];
226228
}
227229

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypeLang\PhpDoc\Tests\DocBlock\Tag;
6+
7+
use PHPUnit\Framework\Attributes\Test;
8+
use TypeLang\PhpDoc\DocBlock\Combinator\DescriptionCombinator;
9+
use TypeLang\PhpDoc\DocBlock\Combinator\IntegerCombinator;
10+
use TypeLang\PhpDoc\DocBlock\Combinator\UriCombinator;
11+
use TypeLang\PhpDoc\DocBlock\Combinator\UrlCombinator;
12+
use TypeLang\PhpDoc\DocBlock\Tag\ExampleTag\ExampleTag;
13+
use TypeLang\PhpDoc\DocBlock\Tag\ExampleTag\ExampleTagDefinition;
14+
use TypeLang\PhpDoc\DocBlock\Tag\InvalidTag;
15+
use TypeLang\PhpDoc\DocBlockParser;
16+
use TypeLang\PhpDoc\TagFactory;
17+
use TypeLang\PhpDoc\Tests\TestCase;
18+
19+
final class ExampleTagTest extends TestCase
20+
{
21+
#[Test]
22+
public function parsesLocationWithStartCountAndDescription(): void
23+
{
24+
$tag = self::factory()->create('example', 'https://example.com/demo.php 12 30 A relevant excerpt.');
25+
26+
self::assertInstanceOf(ExampleTag::class, $tag);
27+
self::assertNotNull($tag->location);
28+
self::assertSame('https://example.com/demo.php', (string) $tag->location);
29+
self::assertSame(12, $tag->start);
30+
self::assertSame(30, $tag->count);
31+
self::assertSame('A relevant excerpt.', (string) $tag->description);
32+
self::assertSame('@example https://example.com/demo.php 12 30 A relevant excerpt.', (string) $tag);
33+
}
34+
35+
#[Test]
36+
public function parsesLocationWithStartOnly(): void
37+
{
38+
$tag = self::factory()->create('example', 'demo.php 7');
39+
40+
self::assertInstanceOf(ExampleTag::class, $tag);
41+
self::assertNotNull($tag->location);
42+
self::assertSame('demo.php', (string) $tag->location);
43+
self::assertSame(7, $tag->start);
44+
self::assertNull($tag->count);
45+
self::assertNull($tag->description);
46+
self::assertSame('@example demo.php 7', (string) $tag);
47+
}
48+
49+
#[Test]
50+
public function parsesLocationOnly(): void
51+
{
52+
$tag = self::factory()->create('example', 'demo.php');
53+
54+
self::assertInstanceOf(ExampleTag::class, $tag);
55+
self::assertNotNull($tag->location);
56+
self::assertSame('demo.php', (string) $tag->location);
57+
self::assertNull($tag->start);
58+
self::assertNull($tag->count);
59+
self::assertNull($tag->description);
60+
self::assertSame('@example demo.php', (string) $tag);
61+
}
62+
63+
/**
64+
* Any ordinary word is a valid relative URI, so a leading word is captured
65+
* as the location and the remainder becomes the description.
66+
*/
67+
#[Test]
68+
public function treatsLeadingWordAsLocation(): void
69+
{
70+
$tag = self::factory()->create('example', 'demo.php the bundled snippet below.');
71+
72+
self::assertInstanceOf(ExampleTag::class, $tag);
73+
self::assertNotNull($tag->location);
74+
self::assertSame('demo.php', (string) $tag->location);
75+
self::assertSame('the bundled snippet below.', (string) $tag->description);
76+
self::assertSame('@example demo.php the bundled snippet below.', (string) $tag);
77+
}
78+
79+
/**
80+
* A location is mandatory, so an input whose first word cannot be a URI is
81+
* malformed.
82+
*/
83+
#[Test]
84+
public function rejectsMissingLocation(): void
85+
{
86+
$tag = self::factory()->create('example', '// inline example');
87+
88+
self::assertInstanceOf(InvalidTag::class, $tag);
89+
}
90+
91+
#[Test]
92+
public function resolvesThroughTheRealParser(): void
93+
{
94+
$block = new DocBlockParser()->parse('/** @example demo.php 3 5 */');
95+
96+
self::assertInstanceOf(ExampleTag::class, $block->tags[0]);
97+
self::assertSame('demo.php', (string) $block->tags[0]->location);
98+
self::assertSame(3, $block->tags[0]->start);
99+
self::assertSame(5, $block->tags[0]->count);
100+
}
101+
102+
private static function factory(): TagFactory
103+
{
104+
return new TagFactory(
105+
definitions: [
106+
ExampleTagDefinition::NAME => new ExampleTagDefinition(),
107+
],
108+
combinators: [
109+
UrlCombinator::NAME => new UrlCombinator(),
110+
UriCombinator::NAME => new UriCombinator(),
111+
IntegerCombinator::NAME => new IntegerCombinator(),
112+
DescriptionCombinator::NAME => new DescriptionCombinator(self::createDescriptionParser()),
113+
],
114+
);
115+
}
116+
}

0 commit comments

Comments
 (0)