1use std::fmt::Write;
2
3use cairo_lang_defs::db::DefsGroup;
4use cairo_lang_defs::ids::{ImplItemId, LookupItemId, ModuleId, ModuleItemId, TraitItemId};
5use cairo_lang_filesystem::db::FilesGroup;
6use cairo_lang_filesystem::ids::{CrateId, FileId, Tracked};
7use itertools::{Itertools, intersperse};
8use salsa::Database;
9
10use crate::documentable_item::DocumentableItemId;
11use crate::location_links::LocationLink;
12use crate::parser::{DocumentationCommentParser, DocumentationCommentToken};
13
14pub trait DocGroup: Database {
15 fn get_item_documentation<'db>(&'db self, item_id: DocumentableItemId<'db>) -> Option<String> {
20 get_item_documentation(self.as_dyn_database(), (), item_id)
21 }
22
23 fn get_item_documentation_as_tokens<'db>(
25 &'db self,
26 item_id: DocumentableItemId<'db>,
27 ) -> Option<Vec<DocumentationCommentToken<'db>>> {
28 get_item_documentation_as_tokens(self.as_dyn_database(), (), item_id)
29 }
30
31 fn get_item_signature<'db>(&'db self, item_id: DocumentableItemId<'db>) -> Option<String> {
33 self.get_item_signature_with_links(item_id).0
34 }
35
36 fn get_item_signature_with_links<'db>(
39 &'db self,
40 item_id: DocumentableItemId<'db>,
41 ) -> (Option<String>, Vec<LocationLink<'db>>) {
42 crate::documentable_formatter::get_item_signature_with_links(
43 self.as_dyn_database(),
44 (),
45 item_id,
46 )
47 }
48}
49impl<T: Database + ?Sized> DocGroup for T {}
50
51#[salsa::tracked]
52fn get_item_documentation<'db>(
53 db: &'db dyn Database,
54 _tracked: Tracked,
55 item_id: DocumentableItemId<'db>,
56) -> Option<String> {
57 let tokens = db.get_item_documentation_as_tokens(item_id)?;
58 let mut buff = String::new();
59 for doc_token in &tokens {
60 match doc_token {
61 DocumentationCommentToken::Content(content) => buff.push_str(content.as_str()),
62 DocumentationCommentToken::Link(link) => {
63 write!(&mut buff, "[{}]", link.label).ok()?;
64 if let Some(path) = &link.path {
65 write!(&mut buff, "({path})").ok()?;
66 }
67 }
68 }
69 }
70 Some(buff)
71}
72
73#[salsa::tracked]
74fn get_item_documentation_as_tokens<'db>(
75 db: &'db dyn Database,
76 _tracked: Tracked,
77 item_id: DocumentableItemId<'db>,
78) -> Option<Vec<DocumentationCommentToken<'db>>> {
79 let (outer_comment, inner_comment, module_level_comment) = match item_id {
80 DocumentableItemId::Crate(crate_id) => {
81 (None, None, get_crate_root_module_documentation(db, crate_id))
82 }
83 item_id => (
84 extract_item_outer_documentation(db, item_id),
87 extract_item_inner_documentation(db, item_id),
92 extract_item_module_level_documentation(db, item_id),
93 ),
94 };
95
96 let doc_parser: DocumentationCommentParser<'db> = DocumentationCommentParser::new(db);
97
98 let outer_comment_tokens =
99 outer_comment.map(|comment| doc_parser.parse_documentation_comment(item_id, comment));
100 let inner_comment_tokens =
101 inner_comment.map(|comment| doc_parser.parse_documentation_comment(item_id, comment));
102 let module_level_comment_tokens = module_level_comment
103 .map(|comment| doc_parser.parse_documentation_comment(item_id, comment));
104
105 let mut result: Vec<Vec<DocumentationCommentToken<'db>>> =
106 [module_level_comment_tokens, outer_comment_tokens, inner_comment_tokens]
107 .into_iter()
108 .flatten()
109 .collect();
110 result.retain(|v| !v.is_empty());
111 if result.is_empty() {
112 return None;
113 }
114 let separator_token = vec![DocumentationCommentToken::Content(" ".to_string())];
115 Some(intersperse(result, separator_token).flatten().collect())
116}
117
118fn get_crate_root_module_documentation<'db>(
120 db: &'db dyn Database,
121 crate_id: CrateId<'db>,
122) -> Option<String> {
123 let module_id = db.module_main_file(ModuleId::CrateRoot(crate_id)).ok()?;
124 extract_item_module_level_documentation_from_file(db, module_id)
125}
126
127fn extract_item_inner_documentation<'db>(
129 db: &'db dyn Database,
130 item_id: DocumentableItemId<'db>,
131) -> Option<String> {
132 if matches!(
133 item_id,
134 DocumentableItemId::LookupItem(
135 LookupItemId::ModuleItem(ModuleItemId::FreeFunction(_) | ModuleItemId::Submodule(_))
136 | LookupItemId::ImplItem(ImplItemId::Function(_))
137 | LookupItemId::TraitItem(TraitItemId::Function(_))
138 )
139 ) {
140 let raw_text = item_id
141 .stable_location(db)?
142 .syntax_node(db)
143 .get_text_without_inner_commentable_children(db);
144 Some(extract_item_inner_documentation_from_raw_text(raw_text))
145 } else {
146 None
147 }
148}
149
150fn extract_item_outer_documentation<'db>(
152 db: &'db dyn Database,
153 item_id: DocumentableItemId<'db>,
154) -> Option<String> {
155 let raw_text = item_id.stable_location(db)?.syntax_node(db).get_text(db);
157 Some(
158 raw_text
159 .lines()
160 .filter(|line| !line.trim().is_empty())
161 .take_while_ref(|line| is_comment_line(line) || line.trim_start().starts_with("#"))
164 .filter_map(|line| extract_comment_from_code_line(line, &["///"]))
165 .join("\n"),
166 )
167}
168
169fn extract_item_module_level_documentation<'db>(
171 db: &'db dyn Database,
172 item_id: DocumentableItemId<'db>,
173) -> Option<String> {
174 match item_id {
175 DocumentableItemId::LookupItem(LookupItemId::ModuleItem(ModuleItemId::Submodule(
176 submodule_id,
177 ))) => {
178 if db.is_submodule_inline(submodule_id) {
179 return None;
180 }
181 let module_id = db.module_main_file(ModuleId::Submodule(submodule_id)).ok()?;
182 extract_item_module_level_documentation_from_file(db, module_id)
183 }
184 _ => None,
185 }
186}
187
188fn extract_item_inner_documentation_from_raw_text(raw_text: String) -> String {
190 raw_text
191 .lines()
192 .filter(|line| !line.trim().is_empty())
193 .skip_while(|line| is_comment_line(line))
194 .filter_map(|line| extract_comment_from_code_line(line, &["//!"]))
195 .join("\n")
196}
197
198fn extract_item_module_level_documentation_from_file<'db>(
200 db: &'db dyn Database,
201 file_id: FileId<'db>,
202) -> Option<String> {
203 let file_content = db.file_content(file_id)?.to_string();
204 Some(
205 file_content
206 .lines()
207 .filter(|line| !line.trim().is_empty())
208 .take_while_ref(|line| is_comment_line(line))
209 .filter_map(|line| extract_comment_from_code_line(line, &["//!"]))
210 .join("\n"),
211 )
212}
213
214fn extract_comment_from_code_line(line: &str, comment_markers: &[&'static str]) -> Option<String> {
220 let dedent = line.trim_start();
222 for comment_marker in comment_markers {
224 if let Some(content) = dedent.strip_prefix(*comment_marker) {
225 if content.starts_with('/') {
231 return None;
232 }
233 return Some(content.strip_prefix(' ').unwrap_or(content).to_string());
234 }
235 }
236 None
237}
238
239fn is_comment_line(line: &str) -> bool {
241 line.trim_start().starts_with("//")
242}