1pub mod docstring;
20pub mod godoc;
21pub mod javadoc;
22pub mod jsdoc;
23pub mod rustdoc;
24
25pub use docstring::DocstringParser;
26pub use godoc::{GoDocExtensions, GodocParser};
27pub use javadoc::{JavadocExtensions, JavadocParser};
28pub use jsdoc::{JsDocParser, TsDocExtensions, TsDocParser};
29pub use rustdoc::{RustDocExtensions, RustdocParser};
30
31use crate::annotate::{AnnotationType, Suggestion, SuggestionSource};
32
33#[derive(Debug, Clone, Default)]
36pub struct ParsedDocumentation {
37 pub summary: Option<String>,
39
40 pub description: Option<String>,
42
43 pub deprecated: Option<String>,
45
46 pub see_refs: Vec<String>,
48
49 pub todos: Vec<String>,
51
52 pub params: Vec<(String, Option<String>, Option<String>)>,
54
55 pub returns: Option<(Option<String>, Option<String>)>,
57
58 pub throws: Vec<(String, Option<String>)>,
60
61 pub examples: Vec<String>,
63
64 pub since: Option<String>,
66
67 pub author: Option<String>,
69
70 pub custom_tags: Vec<(String, String)>,
72
73 pub notes: Vec<String>,
75}
76
77impl ParsedDocumentation {
78 pub fn new() -> Self {
80 Self::default()
81 }
82
83 pub fn is_empty(&self) -> bool {
85 self.summary.is_none()
86 && self.description.is_none()
87 && self.deprecated.is_none()
88 && self.see_refs.is_empty()
89 && self.todos.is_empty()
90 && self.params.is_empty()
91 && self.returns.is_none()
92 && self.throws.is_empty()
93 && self.examples.is_empty()
94 && self.notes.is_empty()
95 && self.custom_tags.is_empty()
96 }
97
98 pub fn get_visibility(&self) -> Option<&str> {
100 self.custom_tags
101 .iter()
102 .find(|(k, _)| k == "visibility")
103 .map(|(_, v)| v.as_str())
104 }
105
106 pub fn get_module(&self) -> Option<&str> {
108 self.custom_tags
109 .iter()
110 .find(|(k, _)| k == "module")
111 .map(|(_, v)| v.as_str())
112 }
113
114 pub fn get_category(&self) -> Option<&str> {
116 self.custom_tags
117 .iter()
118 .find(|(k, _)| k == "category")
119 .map(|(_, v)| v.as_str())
120 }
121
122 pub fn is_readonly(&self) -> bool {
124 self.custom_tags
125 .iter()
126 .any(|(k, v)| k == "readonly" && v == "true")
127 }
128}
129
130pub trait DocStandardParser: Send + Sync {
132 fn parse(&self, raw_comment: &str) -> ParsedDocumentation;
134
135 fn standard_name(&self) -> &'static str;
137
138 fn to_suggestions(
143 &self,
144 parsed: &ParsedDocumentation,
145 target: &str,
146 line: usize,
147 ) -> Vec<Suggestion> {
148 let mut suggestions = Vec::new();
149
150 if let Some(summary) = &parsed.summary {
152 let truncated = truncate_summary(summary, 100);
153 suggestions.push(Suggestion::summary(
154 target,
155 line,
156 truncated,
157 SuggestionSource::Converted,
158 ));
159 }
160
161 if let Some(msg) = &parsed.deprecated {
163 suggestions.push(Suggestion::deprecated(
164 target,
165 line,
166 msg,
167 SuggestionSource::Converted,
168 ));
169 }
170
171 for see_ref in &parsed.see_refs {
173 suggestions.push(Suggestion::new(
174 target,
175 line,
176 AnnotationType::Ref,
177 see_ref,
178 SuggestionSource::Converted,
179 ));
180 }
181
182 for todo in &parsed.todos {
184 suggestions.push(Suggestion::new(
185 target,
186 line,
187 AnnotationType::Hack,
188 format!("reason=\"{}\"", todo),
189 SuggestionSource::Converted,
190 ));
191 }
192
193 if let Some(visibility) = parsed.get_visibility() {
195 let lock_level = match visibility {
196 "private" | "internal" => "restricted",
197 "protected" => "normal",
198 _ => "normal",
199 };
200 suggestions.push(Suggestion::lock(
201 target,
202 line,
203 lock_level,
204 SuggestionSource::Converted,
205 ));
206 }
207
208 if let Some(module_name) = parsed.get_module() {
210 suggestions.push(Suggestion::new(
211 target,
212 line,
213 AnnotationType::Module,
214 module_name,
215 SuggestionSource::Converted,
216 ));
217 }
218
219 if let Some(category) = parsed.get_category() {
221 suggestions.push(Suggestion::domain(
222 target,
223 line,
224 category.to_lowercase(),
225 SuggestionSource::Converted,
226 ));
227 }
228
229 if !parsed.throws.is_empty() {
231 let throws_list: Vec<String> = parsed.throws.iter().map(|(t, _)| t.clone()).collect();
232 suggestions.push(Suggestion::ai_hint(
233 target,
234 line,
235 format!("throws {}", throws_list.join(", ")),
236 SuggestionSource::Converted,
237 ));
238 }
239
240 if parsed.is_readonly() {
242 suggestions.push(Suggestion::ai_hint(
243 target,
244 line,
245 "readonly",
246 SuggestionSource::Converted,
247 ));
248 }
249
250 if !parsed.examples.is_empty() {
252 suggestions.push(Suggestion::ai_hint(
253 target,
254 line,
255 "has examples",
256 SuggestionSource::Converted,
257 ));
258 }
259
260 for note in &parsed.notes {
262 suggestions.push(Suggestion::ai_hint(
263 target,
264 line,
265 note,
266 SuggestionSource::Converted,
267 ));
268 }
269
270 suggestions
271 }
272}
273
274fn truncate_summary(summary: &str, max_len: usize) -> String {
276 let trimmed = summary.trim();
277 if trimmed.len() <= max_len {
278 trimmed.to_string()
279 } else {
280 let truncate_at = trimmed[..max_len].rfind(' ').unwrap_or(max_len);
282 format!("{}...", &trimmed[..truncate_at])
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn test_parsed_documentation_is_empty() {
292 let empty = ParsedDocumentation::new();
293 assert!(empty.is_empty());
294
295 let mut with_summary = ParsedDocumentation::new();
296 with_summary.summary = Some("Test".to_string());
297 assert!(!with_summary.is_empty());
298 }
299
300 #[test]
301 fn test_truncate_summary() {
302 assert_eq!(truncate_summary("Short", 100), "Short");
303 assert_eq!(
304 truncate_summary("This is a very long summary that needs to be truncated", 20),
305 "This is a very long..."
306 );
307 }
308
309 #[test]
310 fn test_parsed_documentation_getters() {
311 let mut doc = ParsedDocumentation::new();
312 doc.custom_tags
313 .push(("visibility".to_string(), "private".to_string()));
314 doc.custom_tags
315 .push(("module".to_string(), "TestModule".to_string()));
316 doc.custom_tags
317 .push(("category".to_string(), "Security".to_string()));
318 doc.custom_tags
319 .push(("readonly".to_string(), "true".to_string()));
320
321 assert_eq!(doc.get_visibility(), Some("private"));
322 assert_eq!(doc.get_module(), Some("TestModule"));
323 assert_eq!(doc.get_category(), Some("Security"));
324 assert!(doc.is_readonly());
325 }
326}