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 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}