1use oxc_ast::ast::Comment;
9
10pub struct DocComments<'a> {
12 comments: &'a [Comment],
13 source: &'a str,
14}
15
16impl<'a> DocComments<'a> {
17 pub fn new(comments: &'a [Comment], source: &'a str) -> Self {
18 Self { comments, source }
19 }
20
21 pub fn for_span(&self, span_start: u32) -> Option<String> {
26 let jsdoc = self
29 .comments
30 .iter()
31 .rev()
32 .find(|c| c.attached_to == span_start && c.is_jsdoc())?;
33
34 let content_span = jsdoc.content_span();
35 let raw = &self.source[content_span.start as usize..content_span.end as usize];
36
37 Some(clean_jsdoc(raw))
38 }
39}
40
41fn clean_jsdoc(raw: &str) -> String {
49 let lines: Vec<&str> = raw.lines().collect();
50 let mut cleaned: Vec<&str> = Vec::new();
51
52 for line in &lines {
53 let trimmed = line.trim();
54 let stripped = if let Some(rest) = trimmed.strip_prefix("* ") {
56 rest
57 } else if let Some(rest) = trimmed.strip_prefix('*') {
58 rest
59 } else {
60 trimmed
61 };
62 cleaned.push(stripped);
63 }
64
65 while cleaned.first().is_some_and(|l| l.is_empty()) {
67 cleaned.remove(0);
68 }
69 while cleaned.last().is_some_and(|l| l.is_empty()) {
70 cleaned.pop();
71 }
72
73 convert_jsdoc_tags(&cleaned)
74}
75
76fn convert_jsdoc_tags(lines: &[&str]) -> String {
81 let mut description: Vec<String> = Vec::new();
82 let mut params: Vec<String> = Vec::new();
83 let mut returns: Option<String> = None;
84 let mut examples: Vec<Vec<String>> = Vec::new();
85
86 let mut i = 0;
87 while i < lines.len() {
88 let line = lines[i];
89
90 if let Some(rest) = line.strip_prefix("@param ") {
91 params.push(format_param(rest));
93 } else if let Some(rest) = line
94 .strip_prefix("@returns ")
95 .or_else(|| line.strip_prefix("@return "))
96 {
97 returns = Some(rest.to_string());
98 } else if line == "@example" {
99 let mut code_lines = Vec::new();
101 i += 1;
102 while i < lines.len() && !lines[i].starts_with('@') {
103 code_lines.push(lines[i].to_string());
104 i += 1;
105 }
106 while code_lines.first().is_some_and(|l| l.is_empty()) {
108 code_lines.remove(0);
109 }
110 while code_lines.last().is_some_and(|l| l.is_empty()) {
111 code_lines.pop();
112 }
113 if !code_lines.is_empty() {
114 examples.push(code_lines);
115 }
116 continue; } else if line.starts_with('@') {
118 description.push(line.to_string());
120 } else {
121 description.push(line.to_string());
122 }
123
124 i += 1;
125 }
126
127 let mut out: Vec<String> = Vec::new();
129
130 out.extend(description);
132
133 if !params.is_empty() {
135 if !out.is_empty() && !out.last().is_none_or(|l| l.is_empty()) {
137 out.push(String::new());
138 }
139 out.push("## Arguments".to_string());
140 out.push(String::new());
141 for p in ¶ms {
142 out.push(p.clone());
143 }
144 }
145
146 if let Some(ret) = &returns {
148 if !out.is_empty() && !out.last().is_none_or(|l| l.is_empty()) {
149 out.push(String::new());
150 }
151 out.push("## Returns".to_string());
152 out.push(String::new());
153 out.push(ret.clone());
154 }
155
156 for example in &examples {
158 if !out.is_empty() && !out.last().is_none_or(|l| l.is_empty()) {
159 out.push(String::new());
160 }
161 out.push("## Example".to_string());
162 out.push(String::new());
163 out.push("```js".to_string());
164 for line in example {
165 out.push(line.clone());
166 }
167 out.push("```".to_string());
168 }
169
170 while out.last().is_some_and(|l| l.is_empty()) {
172 out.pop();
173 }
174
175 out.join("\n")
176}
177
178fn format_param(rest: &str) -> String {
187 let rest = rest.trim();
188
189 let rest = if rest.starts_with('{') {
191 if let Some(end) = rest.find('}') {
192 rest[end + 1..].trim()
193 } else {
194 rest
195 }
196 } else {
197 rest
198 };
199
200 if let Some((name, desc)) = rest.split_once(" - ") {
202 format!("* `{}` - {}", name.trim(), desc.trim())
203 } else if let Some((name, desc)) = rest.split_once(' ') {
204 format!("* `{}` - {}", name.trim(), desc.trim())
205 } else {
206 format!("* `{rest}`")
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn test_clean_single_line() {
216 assert_eq!(
217 clean_jsdoc(" A simple description "),
218 "A simple description"
219 );
220 }
221
222 #[test]
223 fn test_clean_multi_line() {
224 let raw = "\n * First line\n * Second line\n ";
225 assert_eq!(clean_jsdoc(raw), "First line\nSecond line");
226 }
227
228 #[test]
229 fn test_param_conversion() {
230 let raw = "\n * Does a thing.\n * @param x - the value\n * @returns the result\n ";
231 assert_eq!(
232 clean_jsdoc(raw),
233 "Does a thing.\n\n## Arguments\n\n* `x` - the value\n\n## Returns\n\nthe result"
234 );
235 }
236
237 #[test]
238 fn test_param_without_dash() {
239 let raw = "\n * Hello.\n * @param source Source code to parse\n ";
240 assert_eq!(
241 clean_jsdoc(raw),
242 "Hello.\n\n## Arguments\n\n* `source` - Source code to parse"
243 );
244 }
245
246 #[test]
247 fn test_multiple_params() {
248 let raw = "\n * Parse it.\n * @param source Source code\n * @param name Optional name\n * @returns The parsed result.\n ";
249 assert_eq!(
250 clean_jsdoc(raw),
251 "Parse it.\n\n## Arguments\n\n* `source` - Source code\n* `name` - Optional name\n\n## Returns\n\nThe parsed result."
252 );
253 }
254
255 #[test]
256 fn test_example_block() {
257 let raw = "\n * Do something.\n * @example\n * const x = foo();\n * console.log(x);\n ";
258 assert_eq!(
259 clean_jsdoc(raw),
260 "Do something.\n\n## Example\n\n```js\nconst x = foo();\nconsole.log(x);\n```"
261 );
262 }
263
264 #[test]
265 fn test_multiple_examples() {
266 let raw = "\n * Thing.\n * @example\n * foo();\n * @example\n * bar();\n ";
267 assert_eq!(
268 clean_jsdoc(raw),
269 "Thing.\n\n## Example\n\n```js\nfoo();\n```\n\n## Example\n\n```js\nbar();\n```"
270 );
271 }
272
273 #[test]
274 fn test_param_with_jsdoc_type() {
275 assert_eq!(
276 format_param("{string} name - the name"),
277 "* `name` - the name"
278 );
279 }
280
281 #[test]
282 fn test_description_only() {
283 let raw = "\n * Just a description with `inline code`.\n ";
284 assert_eq!(clean_jsdoc(raw), "Just a description with `inline code`.");
285 }
286
287 #[test]
288 fn test_example_between_tags() {
289 let raw = "\n * Desc.\n * @example\n * code();\n * @returns result\n ";
290 assert_eq!(
291 clean_jsdoc(raw),
292 "Desc.\n\n## Returns\n\nresult\n\n## Example\n\n```js\ncode();\n```"
293 );
294 }
295}