Skip to content

Commit ab44fb7

Browse files
committed
Add phpdoc package
1 parent c992f41 commit ab44fb7

226 files changed

Lines changed: 7733 additions & 119 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/Writerside/tl.tree

Lines changed: 276 additions & 49 deletions
Large diffs are not rendered by default.

docs/Writerside/topics/phpdoc.md

Lines changed: 169 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1-
# The PHPDoc Parser Component
1+
# PHPDoc Parser Component
22

33
<primary-label ref="phpdoc-component"/>
44
<show-structure for="chapter" depth="2"/>
55

6-
The printer package is responsible for visualizing the AST as string
7-
formats.
6+
Most PHP code carries part of its type information not in the signature
7+
itself, but in the docblock above it — a `@param` narrowing an `array`
8+
argument down to `list<User>`, a `@throws` PHP has no native syntax for at
9+
all, a `@template` turning a plain class into a generic one. The PHPDoc
10+
component reads that comment and turns it into a small, immutable object
11+
graph: one entry per line, each already parsed according to what that
12+
particular tag means.
13+
14+
Every type a tag carries — a `@param`'s argument type, a `@return`'s result,
15+
a `@template`'s bound — is parsed with the very same
16+
[TypeLang grammar](introduction.md) used everywhere else in this project.
17+
A type written inside a docblock is therefore described exactly as precisely
18+
as one written directly in PHP code; nothing is lost by moving it into a
19+
comment.
820

921
## Installation
1022

@@ -15,22 +27,163 @@ formats.
1527
</p>
1628
</tldr>
1729

30+
**Requirements:**
31+
* `PHP >= 8.4`
32+
* `ext-mbstring` <sup>optional</sup>
33+
34+
<note>
35+
Every tag this component understands — including the generic
36+
(<code>@template</code>) and structural (<code>@param</code>,
37+
<code>@property</code>, ...) families — ships in this single package.
38+
Earlier releases split those out into separate
39+
<code>type-lang/phpdoc-standard-tags</code> and
40+
<code>type-lang/phpdoc-template-tags</code> extensions; both have since been
41+
folded back in, and installing <code>type-lang/phpdoc</code> alone is
42+
enough.
43+
</note>
44+
45+
## Usage
46+
47+
Parsing a comment is a single call: hand `DocBlockParser::parse()` the raw
48+
`/** ... */` text, and it hands back a `DocBlock`.
49+
50+
```php
51+
use TypeLang\PhpDoc\DocBlockParser;
52+
53+
$parser = new DocBlockParser();
54+
55+
$block = $parser->parse(<<<'PHPDOC'
56+
/**
57+
* Sends a notification to the given recipient.
58+
*
59+
* @see Mailer::send() The underlying transport.
60+
* @link https://example.com/docs Delivery documentation.
61+
* @return bool
62+
*/
63+
PHPDOC);
64+
```
65+
66+
The description — everything written before the first tag — is available as
67+
`$block->description`, and the tags themselves form an ordered, countable,
68+
iterable collection:
69+
70+
```php
71+
echo $block->description;
72+
// "Sends a notification to the given recipient."
73+
74+
echo count($block); // 3
75+
76+
foreach ($block as $tag) {
77+
echo $tag->name; // "see", "link", "return"
78+
}
79+
80+
$see = $block[0]; // SeeTag
81+
echo $see->reference; // "Mailer::send()"
82+
```
83+
84+
A single malformed tag never brings the rest of the comment down with it.
85+
A tag whose name isn't recognized, or whose body doesn't match the grammar
86+
expected for its name, is not thrown as an exception — it is returned as an
87+
`InvalidTag`, carrying the failure reason alongside it, so that the other,
88+
well-formed tags around it are still parsed normally:
89+
90+
```php
91+
$block = $parser->parse(<<<'PHPDOC'
92+
/**
93+
* @param int $wellFormed
94+
* @param not a valid type here $because
95+
*/
96+
PHPDOC);
97+
98+
$broken = $block[1]; // object(InvalidTag)
99+
100+
echo $broken->name; // "param"
101+
102+
// Malformed "@param" tag, expected:
103+
// <Type> <Variable> [ <Description> ]
104+
echo $broken->reason->getMessage();
105+
```
106+
18107
<tip>
19-
In addition, extensions are available that complement the
20-
functionality of the component.
21-
<ul>
22-
<li><a href="phpdoc-standard-tags.md">PHPDoc Standard Tags</a></li>
23-
<li><a href="phpdoc-template-tags.md">PHPDoc Template Tags</a></li>
24-
</ul>
108+
A <code>DocBlockParser</code> instance builds its internal tag and type
109+
grammar once, in its constructor, and is otherwise stateless. Construct it
110+
once and reuse (or share) it across an entire application, rather than
111+
building a new instance per docblock.
25112
</tip>
26113

27-
**Requirements:**
28-
* `PHP >= 8.1`
114+
## How a Comment Is Read
29115

30-
## Usage
116+
Parsing happens in two passes that mirror the two logical parts of a
117+
comment: first the whole text is split into the leading description and one
118+
segment per tag line, and only then is each of those segments parsed on its
119+
own — the description checked for inline tags, each tag line checked against
120+
the grammar declared for its name.
121+
122+
```mermaid
123+
graph TD
124+
C["/** ... */ comment"] -- split --> D[Description segment]
125+
C -- split --> T1["@tag segment #1"]
126+
C -- split --> T2["@tag segment #2"]
127+
D -- parsed for inline tags --> DB[DocBlock]
128+
T1 -- parsed against its grammar --> DB
129+
T2 -- parsed against its grammar --> DB
130+
```
131+
132+
### DocBlock
133+
134+
A `DocBlock` is a read-only snapshot of the whole comment: an optional
135+
`description` and the ordered `tags` list. It additionally behaves as a
136+
collection of its own tags — countable, iterable, and indexable by integer
137+
offset — so `count($block)`, `foreach ($block as $tag)` and `$block[0]` all
138+
work directly on it without reaching for `->tags` explicitly.
139+
140+
### Description
141+
142+
Everything before the first tag becomes the description. Most of the time
143+
that is just text, represented by a plain `Description` object exposing a
144+
single `$value` string. When the text additionally contains one or more
145+
**inline tags** — a `{@tag ...}` sequence with balanced braces, such as
146+
`{@see Mailer::send()}` written in the middle of a sentence — it is
147+
represented as a `TaggedDescription` instead: an ordered mix of plain-text
148+
fragments and the nested tags found among them.
149+
150+
```
151+
/**
152+
* Hello world {@see Mailer::send()} and more text after it.
153+
└────┬────┘ └─────────┬─────────┘ └──────────┬──────────┘
154+
text nested tag text
155+
*/
156+
```
157+
158+
Only a handful of tags — [@see](see-tag.md), [@link](link-tag.md),
159+
[@internal](internal-tag.md) and [@inheritdoc](inheritdoc-tag.md) — are
160+
ever recognized this way. A `{@param}` written in running text, for instance, is never lifted
161+
out: `@param` only makes sense as a whole tag line, so the braces around it
162+
stay exactly as written, as plain text.
163+
164+
### Tag
165+
166+
Every tag, whether it stood on its own line or was found nested inside a
167+
description, carries at minimum a `$name` (without the leading `@`) and an
168+
optional `$description` (whatever text follows its own body). A tag
169+
recognized by name exposes further, tag-specific parts on top of that — a
170+
`ParamTag` additionally exposes the argument's `$type` and `$variable`, a
171+
`SeeTag` exposes what it `$reference`s, and so on. A tag whose name has no
172+
registered definition at all — including every tag still marked "Not Implemented"
173+
in the sidebar — falls back to a plain `Tag`, with its entire suffix folded
174+
unparsed into the description.
175+
176+
## Where to Go Next
31177

32-
<secondary-label ref="wip"/>
178+
Every tag this component recognizes has its own page in the sidebar,
179+
grouped by where it comes from: Standard, Advanced, phpDocumentor, or a
180+
specific static analyzer (Psalm, PHPStan, Phan, PhpStorm, PHP
181+
CodeSniffer) for the ones not yet implemented.
33182

34-
<warning>
35-
WIP: Documentation is not complete
36-
</warning>
183+
<deflist>
184+
<def title="Extending">
185+
How a tag's own grammar is declared, how to add a tag of your own,
186+
and how to write a new grammar building block (a "combinator") for
187+
one. See <a href="custom-tags.md">Extending</a>.
188+
</def>
189+
</deflist>
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# Combinators
2+
3+
<primary-label ref="phpdoc-component"/>
4+
<show-structure for="chapter" depth="2"/>
5+
6+
Every named building block a [tag's grammar](custom-tags.md) is assembled
7+
from — a type, a variable, a URI, an email address — is a **combinator**:
8+
a small object that knows how to read exactly one piece of syntax and
9+
nothing else. `Spec::rule(TypeCombinator::NAME, 'type')` in
10+
[`@param`](param-tag.md)'s definition, for instance, refers to the
11+
combinator below by name.
12+
13+
```php
14+
/**
15+
* @template-covariant TResult of mixed = mixed
16+
*/
17+
interface CombinatorInterface
18+
{
19+
/**
20+
* @return TResult
21+
*/
22+
public function __invoke(Cursor $cursor): mixed;
23+
}
24+
```
25+
26+
Given a `Cursor` positioned at the start of whatever text is still left to
27+
parse, a combinator either consumes the part that belongs to it and returns
28+
the value it read, or throws `NoMatchException` and leaves the cursor
29+
exactly where it found it. That second case matters: a combinator that
30+
partially consumes input before deciding it does not fit would corrupt
31+
whichever alternative the surrounding grammar tries next — see
32+
[`Spec::oneOf()`](custom-tags.md), which relies on being able to roll back
33+
cleanly.
34+
35+
The `Cursor` itself offers the handful of reading operations most
36+
combinators need, so a combinator rarely has to touch its raw position by
37+
hand: `peek()` and `read()` for a fixed number of bytes, `readWhile()` /
38+
`readUntil()` for a run of (or up to) a set of characters, `readWord()` for
39+
the next whitespace-delimited word, `readPhpIdentifier()` /
40+
`readPhpQualifiedName()` for a name (with or without namespace separators),
41+
`readLiteral()` for an exact piece of text, and `readRemainder()` for
42+
whatever is left entirely.
43+
44+
## Built-in Combinators
45+
46+
Thirteen combinators cover every built-in tag's grammar between them:
47+
48+
<table style="both">
49+
<tr>
50+
<td width="140">Name</td>
51+
<td width="160">Returns</td>
52+
<td>Reads</td>
53+
</tr>
54+
<tr>
55+
<td><code>Access</code></td>
56+
<td><code>Visibility</code></td>
57+
<td>One of <code>public</code>, <code>protected</code> or <code>private</code>.</td>
58+
</tr>
59+
<tr>
60+
<td><code>AuthorName</code></td>
61+
<td><code>string</code></td>
62+
<td>Everything up to an optional <code>"&lt;"</code>.</td>
63+
</tr>
64+
<tr>
65+
<td><code>CallableType</code></td>
66+
<td><code>TypeReference</code></td>
67+
<td>A <a href="callable-types.md">type</a>, accepted only when it is a callable.</td>
68+
</tr>
69+
<tr>
70+
<td><code>Description</code></td>
71+
<td><code>DescriptionInterface</code></td>
72+
<td>Everything left, recursively parsed for inline tags.</td>
73+
</tr>
74+
<tr>
75+
<td><code>Email</code></td>
76+
<td><code>string</code></td>
77+
<td>An address, up to its closing <code>">"</code>.</td>
78+
</tr>
79+
<tr>
80+
<td><code>Integer</code></td>
81+
<td><code>int</code></td>
82+
<td>A non-negative integer.</td>
83+
</tr>
84+
<tr>
85+
<td><code>IssueName</code></td>
86+
<td><code>string</code></td>
87+
<td>Letters, digits, <code>_</code>, <code>-</code> and <code>.</code>.</td>
88+
</tr>
89+
<tr>
90+
<td><code>Name</code></td>
91+
<td><code>string</code></td>
92+
<td>A single identifier.</td>
93+
</tr>
94+
<tr>
95+
<td><code>Reference</code></td>
96+
<td><code>CodeReference</code></td>
97+
<td>A class, function, method, constant, property or variable.</td>
98+
</tr>
99+
<tr>
100+
<td><code>Type</code></td>
101+
<td><code>TypeReference</code></td>
102+
<td>A full <a href="introduction.md">TypeLang type</a>.</td>
103+
</tr>
104+
<tr>
105+
<td><code>URI</code></td>
106+
<td><code>UriReference</code></td>
107+
<td>A well-formed word, per RFC 3986.</td>
108+
</tr>
109+
<tr>
110+
<td><code>URL</code></td>
111+
<td><code>UrlReference</code></td>
112+
<td>A word that additionally carries a scheme.</td>
113+
</tr>
114+
<tr>
115+
<td><code>Variable</code></td>
116+
<td><code>string</code></td>
117+
<td>A <code>$name</code>, without the leading <code>$</code>.</td>
118+
</tr>
119+
</table>
120+
121+
## Writing One of Your Own
122+
123+
A combinator that reads one of a fixed set of keywords looks much like the
124+
built-in `Access` combinator above. This one reads a `low` / `medium` /
125+
`high` priority level:
126+
127+
```php
128+
enum Priority: string
129+
{
130+
case Low = 'low';
131+
case Medium = 'medium';
132+
case High = 'high';
133+
}
134+
135+
final readonly class PriorityCombinator implements CombinatorInterface
136+
{
137+
public const string NAME = 'Priority';
138+
139+
public function __invoke(Cursor $cursor): Priority
140+
{
141+
$priority = Priority::tryFrom($cursor->readWord());
142+
143+
if ($priority === null) {
144+
throw new NoMatchException('Expected a priority level');
145+
}
146+
147+
return $priority;
148+
}
149+
}
150+
```
151+
152+
Three habits keep a combinator well-behaved inside a larger grammar:
153+
154+
* **Fail before consuming.** Throw `NoMatchException` the moment the input
155+
is found not to fit, before reading anything that would belong to what
156+
comes next — a combinator that consumes first and validates afterwards
157+
leaves the cursor somewhere a sibling alternative can no longer make sense
158+
of.
159+
* **Consume only your own syntax.** Whitespace meant to separate one grammar
160+
element from the next is handled by `Spec::sequence()`, not by the
161+
combinator itself; reading past it leaves nothing for the rest of the
162+
grammar to match.
163+
* **Fail rather than guess.** When more than one reading of the same input
164+
would be plausible, throwing and letting a different alternative in a
165+
surrounding `Spec::oneOf()` take over is safer than silently picking one.
166+
167+
Once written, a combinator with a `NAME` constant is used from a tag's
168+
`$spec` exactly like a built-in one — see
169+
[Declaring a Tag's Grammar](custom-tags.md).

0 commit comments

Comments
 (0)