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", ¶ms));
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}