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    Trait,
213    TraitProperty,
214    TraitMethod,
215    TraitIndexSignature,
216    TraitAssociatedType,
217    ExtensionMethod,
218    ImplMethod,
219    Enum,
220    EnumVariant,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224pub struct DocTarget {
225    pub kind: DocTargetKind,
226    pub path: String,
227    pub span: Span,
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231pub struct DocEntry {
232    pub target: DocTarget,
233    pub comment: DocComment,
234}
235
236#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
237pub struct ProgramDocs {
238    pub entries: Vec<DocEntry>,
239}
240
241impl ProgramDocs {
242    pub fn entry_for_path(&self, path: &str) -> Option<&DocEntry> {
243        self.entries.iter().find(|entry| entry.target.path == path)
244    }
245
246    pub fn entry_for_span(&self, span: Span) -> Option<&DocEntry> {
247        self.entries
248            .iter()
249            .find(|entry| entry.target.span == span && !span.is_dummy())
250    }
251
252    pub fn comment_for_path(&self, path: &str) -> Option<&DocComment> {
253        self.entry_for_path(path).map(|entry| &entry.comment)
254    }
255
256    pub fn comment_for_span(&self, span: Span) -> Option<&DocComment> {
257        self.entry_for_span(span).map(|entry| &entry.comment)
258    }
259}
260
261pub fn qualify_doc_owner_path(module_path: &[String], owner: &str) -> String {
262    if module_path.is_empty() {
263        owner.to_string()
264    } else {
265        format!("{}::{}", module_path.join("::"), owner)
266    }
267}
268
269pub fn type_name_doc_path(type_name: &TypeName) -> String {
270    match type_name {
271        TypeName::Simple(name) => name.to_string(),
272        TypeName::Generic { name, type_args } => {
273            let args = type_args
274                .iter()
275                .map(type_annotation_doc_path)
276                .collect::<Vec<_>>()
277                .join(", ");
278            format!("{name}<{args}>")
279        }
280    }
281}
282
283pub fn type_annotation_doc_path(annotation: &TypeAnnotation) -> String {
284    annotation.to_type_string()
285}
286
287pub fn extend_method_doc_path(
288    module_path: &[String],
289    target_type: &TypeName,
290    method_name: &str,
291) -> String {
292    let owner = qualify_doc_owner_path(module_path, &type_name_doc_path(target_type));
293    format!("{owner}::{method_name}")
294}
295
296pub fn impl_method_doc_path(
297    module_path: &[String],
298    trait_name: &TypeName,
299    target_type: &TypeName,
300    method_name: &str,
301) -> String {
302    let target = qualify_doc_owner_path(module_path, &type_name_doc_path(target_type));
303    let trait_name = qualify_doc_owner_path(module_path, &type_name_doc_path(trait_name));
304    format!("{target}::{trait_name}::{method_name}")
305}