phpdoc_parser/lib.rs
1//! Structural PHPDoc comment parser.
2//!
3//! Parses `/** ... */` documentation blocks into a structured AST with accurate
4//! spans and support for inline tags. Designed for type checkers, linters, IDEs,
5//! and documentation generators.
6//!
7//! The crate is **agnostic** — it does not interpret tag semantics or parse type
8//! expressions. Tag bodies are exposed as raw [`PhpDocText`], letting tools apply
9//! their own type parsers and validation rules.
10//!
11//! # Quick start
12//!
13//! ```
14//! use phpdoc_parser::parse;
15//!
16//! let text = "/** @param int $x The value */";
17//! let doc = parse(text);
18//! assert_eq!(doc.tags.len(), 1);
19//! assert_eq!(doc.tags[0].name, "param");
20//! ```
21//!
22//! # Common patterns
23//!
24//! ### Read a tag body
25//!
26//! ```
27//! use phpdoc_parser::{parse, find_tags, body_text};
28//!
29//! let doc = parse("/** @param int $x The mapping */");
30//! for param in find_tags(&doc, "param") {
31//! let body = body_text(param.body.as_ref()).unwrap_or_default();
32//! // body is "int $x The mapping" — parse the type yourself
33//! }
34//! ```
35//!
36//! ### Find inline references
37//!
38//! ```
39//! use phpdoc_parser::{parse, inline_tags};
40//!
41//! let doc = parse("/** See {@link User::load()} for details. */");
42//! if let Some(desc) = &doc.description {
43//! for tag in inline_tags(desc) {
44//! if tag.name == "link" {
45//! // Process the reference to User::load()
46//! }
47//! }
48//! }
49//! ```
50//!
51//! # Serialization
52//!
53//! With the `serde` feature (off by default), the AST implements `serde::Serialize`;
54//! its serialized shape is not covered by semver.
55
56#![warn(missing_docs)]
57
58pub(crate) mod ast;
59pub(crate) mod parser;
60
61pub use ast::{InlineTag, PhpDoc, PhpDocTag, PhpDocText, TextSegment};
62pub use parser::parse;
63/// Spans are byte offsets into the docblock text passed to [`parse`], not into the PHP file.
64pub use php_ast::Span;
65
66// =============================================================================
67// Utility functions for common tasks
68// =============================================================================
69
70/// Find the first tag with the given name.
71///
72/// # Example
73/// ```
74/// use phpdoc_parser::{parse, find_tag};
75/// let doc = parse("/** @param int $x @return string */");
76/// assert!(find_tag(&doc, "param").is_some());
77/// assert!(find_tag(&doc, "throws").is_none());
78/// ```
79pub fn find_tag<'d>(doc: &'d PhpDoc, name: &str) -> Option<&'d PhpDocTag> {
80 doc.tags.iter().find(|t| t.name == name)
81}
82
83/// Find all tags with the given name.
84///
85/// # Example
86/// ```
87/// use phpdoc_parser::{parse, find_tags};
88/// let doc = parse("/**\n * @param int $x\n * @param string $y\n */");
89/// assert_eq!(find_tags(&doc, "param").count(), 2);
90/// ```
91pub fn find_tags<'d, 'n>(
92 doc: &'d PhpDoc,
93 name: &'n str,
94) -> impl Iterator<Item = &'d PhpDocTag> + use<'d, 'n> {
95 doc.tags.iter().filter(move |t| t.name == name)
96}
97
98/// Reconstruct the text content of a `PhpDocText`, including inline tags.
99///
100/// Inline tags are reconstructed as `{@name body}` format.
101///
102/// # Example
103/// ```
104/// use phpdoc_parser::{parse, text_content};
105/// let doc = parse("/** See {@link Foo} for details. */");
106/// let summary = text_content(doc.summary.as_ref().unwrap());
107/// assert!(summary.contains("@link"));
108/// ```
109pub fn text_content(text: &PhpDocText) -> String {
110 text.segments
111 .iter()
112 .map(|seg| match seg {
113 TextSegment::Text(t) => t.clone(),
114 TextSegment::InlineTag(tag) => {
115 format!(
116 "{{@{}{}}}",
117 tag.name,
118 tag.body
119 .as_ref()
120 .map(|b| format!(" {}", b))
121 .unwrap_or_default()
122 )
123 }
124 })
125 .collect()
126}
127
128/// Get the text content of a tag body, if present.
129///
130/// # Example
131/// ```
132/// use phpdoc_parser::{parse, find_tag, body_text};
133/// let doc = parse("/** @param int $x */");
134/// let param = find_tag(&doc, "param").unwrap();
135/// let text = body_text(param.body.as_ref()).unwrap();
136/// assert!(text.contains("$x"));
137/// ```
138pub fn body_text(body: Option<&PhpDocText>) -> Option<String> {
139 body.map(text_content)
140}
141
142/// Extract all inline tags from a text segment.
143///
144/// # Example
145/// ```
146/// use phpdoc_parser::{parse, inline_tags};
147/// let doc = parse("/** See {@link Foo} and {@link Bar}. */");
148/// let tags = inline_tags(doc.summary.as_ref().unwrap());
149/// assert_eq!(tags.count(), 2);
150/// ```
151pub fn inline_tags(text: &PhpDocText) -> impl Iterator<Item = &InlineTag> {
152 text.segments.iter().filter_map(|seg| match seg {
153 TextSegment::InlineTag(tag) => Some(tag),
154 _ => None,
155 })
156}