Skip to main content

shape_ast/ast/
docs.rs

1use super::span::Span;
2use super::types::{TypeAnnotation, TypeName};
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6pub enum DocTagKind {
7    Module,
8    TypeParam,
9    Param,
10    Returns,
11    Throws,
12    Deprecated,
13    Requires,
14    Since,
15    See,
16    Link,
17    Note,
18    Example,
19    Unknown(String),
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct DocLink {
24    pub target: String,
25    #[serde(default)]
26    pub target_span: Span,
27    pub label: Option<String>,
28    #[serde(default)]
29    pub label_span: Option<Span>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct DocTag {
34    pub kind: DocTagKind,
35    #[serde(default)]
36    pub span: Span,
37    #[serde(default)]
38    pub kind_span: Span,
39    pub name: Option<String>,
40    #[serde(default)]
41    pub name_span: Option<Span>,
42    pub body: String,
43    #[serde(default)]
44    pub body_span: Option<Span>,
45    pub link: Option<DocLink>,
46}
47
48#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
49pub struct DocComment {
50    #[serde(default)]
51    pub span: Span,
52    pub summary: String,
53    pub body: String,
54    pub tags: Vec<DocTag>,
55}
56
57impl DocComment {
58    pub fn is_empty(&self) -> bool {
59        self.summary.is_empty() && self.body.is_empty() && self.tags.is_empty()
60    }
61
62    pub fn param_doc(&self, name: &str) -> Option<&str> {
63        self.tags.iter().find_map(|tag| match &tag.kind {
64            DocTagKind::Param if tag.name.as_deref() == Some(name) => Some(tag.body.as_str()),
65            _ => None,
66        })
67    }
68
69    pub fn type_param_doc(&self, name: &str) -> Option<&str> {
70        self.tags.iter().find_map(|tag| match &tag.kind {
71            DocTagKind::TypeParam if tag.name.as_deref() == Some(name) => Some(tag.body.as_str()),
72            _ => None,
73        })
74    }
75
76    pub fn returns_doc(&self) -> Option<&str> {
77        self.tags.iter().find_map(|tag| match tag.kind {
78            DocTagKind::Returns => Some(tag.body.as_str()),
79            _ => None,
80        })
81    }
82
83    pub fn deprecated_doc(&self) -> Option<&str> {
84        self.tags.iter().find_map(|tag| match tag.kind {
85            DocTagKind::Deprecated => Some(tag.body.as_str()),
86            _ => None,
87        })
88    }
89
90    pub fn example_doc(&self) -> Option<&str> {
91        self.tags.iter().find_map(|tag| match tag.kind {
92            DocTagKind::Example => Some(tag.body.as_str()),
93            _ => None,
94        })
95    }
96
97    pub fn since_doc(&self) -> Option<&str> {
98        self.tags.iter().find_map(|tag| match tag.kind {
99            DocTagKind::Since => Some(tag.body.as_str()),
100            _ => None,
101        })
102    }
103
104    pub fn to_markdown(&self) -> String {
105        let mut sections = Vec::new();
106        if !self.body.is_empty() {
107            sections.push(self.body.clone());
108        } else if !self.summary.is_empty() {
109            sections.push(self.summary.clone());
110        }
111
112        let type_params: Vec<_> = self
113            .tags
114            .iter()
115            .filter(|tag| matches!(tag.kind, DocTagKind::TypeParam))
116            .collect();
117        if !type_params.is_empty() {
118            sections.push(render_named_section("Type Parameters", &type_params));
119        }
120
121        let params: Vec<_> = self
122            .tags
123            .iter()
124            .filter(|tag| matches!(tag.kind, DocTagKind::Param))
125            .collect();
126        if !params.is_empty() {
127            sections.push(render_named_section("Parameters", &params));
128        }
129
130        if let Some(returns) = self.returns_doc() {
131            sections.push(format!("**Returns**\n{}", returns));
132        }
133
134        if let Some(deprecated) = self.deprecated_doc() {
135            sections.push(format!("**Deprecated**\n{}", deprecated));
136        }
137
138        if let Some(since) = self.since_doc() {
139            sections.push(format!("**Since**\n{}", since));
140        }
141
142        let notes: Vec<_> = self
143            .tags
144            .iter()
145            .filter(|tag| matches!(tag.kind, DocTagKind::Note))
146            .map(|tag| tag.body.as_str())
147            .filter(|body| !body.trim().is_empty())
148            .collect();
149        if !notes.is_empty() {
150            sections.push(format!(
151                "**Notes**\n{}",
152                notes
153                    .into_iter()
154                    .map(|body| format!("- {}", body))
155                    .collect::<Vec<_>>()
156                    .join("\n")
157            ));
158        }
159
160        let related: Vec<_> = self
161            .tags
162            .iter()
163            .filter_map(|tag| match &tag.kind {
164                DocTagKind::See | DocTagKind::Link => tag.link.as_ref(),
165                _ => None,
166            })
167            .map(|link| match &link.label {
168                Some(label) => format!("- `{}` ({})", link.target, label),
169                None => format!("- `{}`", link.target),
170            })
171            .collect();
172        if !related.is_empty() {
173            sections.push(format!("**See Also**\n{}", related.join("\n")));
174        }
175
176        if let Some(example) = self.example_doc() {
177            sections.push(format!("**Example**\n```shape\n{}\n```", example));
178        }
179
180        sections
181            .into_iter()
182            .filter(|section| !section.trim().is_empty())
183            .collect::<Vec<_>>()
184            .join("\n\n")
185    }
186}
187
188fn render_named_section(title: &str, tags: &[&DocTag]) -> String {
189    let lines = tags
190        .iter()
191        .map(|tag| {
192            let name = tag.name.as_deref().unwrap_or("_");
193            format!("- `{}`: {}", name, tag.body)
194        })
195        .collect::<Vec<_>>()
196        .join("\n");
197    format!("**{}**\n{}", title, lines)
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
201pub enum DocTargetKind {
202    Module,
203    Function,
204    Annotation,
205    ForeignFunction,
206    BuiltinFunction,
207    BuiltinType,
208    TypeParam,
209    TypeAlias,
210    Struct,
211    StructField,
212    Interface,
213    InterfaceProperty,
214    InterfaceMethod,
215    InterfaceIndexSignature,
216    Trait,
217    TraitMethod,
218    TraitAssociatedType,
219    ExtensionMethod,
220    ImplMethod,
221    Enum,
222    EnumVariant,
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226pub struct DocTarget {
227    pub kind: DocTargetKind,
228    pub path: String,
229    pub span: Span,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233pub struct DocEntry {
234    pub target: DocTarget,
235    pub comment: DocComment,
236}
237
238#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
239pub struct ProgramDocs {
240    pub entries: Vec<DocEntry>,
241}
242
243impl ProgramDocs {
244    pub fn entry_for_path(&self, path: &str) -> Option<&DocEntry> {
245        self.entries.iter().find(|entry| entry.target.path == path)
246    }
247
248    pub fn entry_for_span(&self, span: Span) -> Option<&DocEntry> {
249        self.entries
250            .iter()
251            .find(|entry| entry.target.span == span && !span.is_dummy())
252    }
253
254    pub fn comment_for_path(&self, path: &str) -> Option<&DocComment> {
255        self.entry_for_path(path).map(|entry| &entry.comment)
256    }
257
258    pub fn comment_for_span(&self, span: Span) -> Option<&DocComment> {
259        self.entry_for_span(span).map(|entry| &entry.comment)
260    }
261}
262
263pub fn qualify_doc_owner_path(module_path: &[String], owner: &str) -> String {
264    if module_path.is_empty() {
265        owner.to_string()
266    } else {
267        format!("{}::{}", module_path.join("::"), owner)
268    }
269}
270
271pub fn type_name_doc_path(type_name: &TypeName) -> String {
272    match type_name {
273        TypeName::Simple(name) => name.to_string(),
274        TypeName::Generic { name, type_args } => {
275            let args = type_args
276                .iter()
277                .map(type_annotation_doc_path)
278                .collect::<Vec<_>>()
279                .join(", ");
280            format!("{name}<{args}>")
281        }
282    }
283}
284
285pub fn type_annotation_doc_path(annotation: &TypeAnnotation) -> String {
286    annotation.to_type_string()
287}
288
289pub fn extend_method_doc_path(
290    module_path: &[String],
291    target_type: &TypeName,
292    method_name: &str,
293) -> String {
294    let owner = qualify_doc_owner_path(module_path, &type_name_doc_path(target_type));
295    format!("{owner}::{method_name}")
296}
297
298pub fn impl_method_doc_path(
299    module_path: &[String],
300    trait_name: &TypeName,
301    target_type: &TypeName,
302    method_name: &str,
303) -> String {
304    let target = qualify_doc_owner_path(module_path, &type_name_doc_path(target_type));
305    let trait_name = qualify_doc_owner_path(module_path, &type_name_doc_path(trait_name));
306    format!("{target}::{trait_name}::{method_name}")
307}