1use std::fmt::Write;
2
3use cairo_lang_defs::ids::{ImplItemId, LookupItemId, ModuleId, ModuleItemId, TraitItemId};
4use cairo_lang_filesystem::ids::{CrateId, FileId};
5use cairo_lang_semantic::db::SemanticGroup;
6use cairo_lang_utils::Upcast;
7use itertools::{Itertools, intersperse};
8
9use crate::documentable_item::DocumentableItemId;
10use crate::location_links::LocationLink;
11use crate::parser::{DocumentationCommentParser, DocumentationCommentToken};
12
13#[salsa::query_group(DocDatabase)]
14pub trait DocGroup: SemanticGroup + Upcast<dyn SemanticGroup> {
15 fn get_item_documentation(&self, item_id: DocumentableItemId) -> Option<String>;
20
21 fn get_item_documentation_as_tokens(
23 &self,
24 item_id: DocumentableItemId,
25 ) -> Option<Vec<DocumentationCommentToken>>;
26
27 #[salsa::invoke(crate::documentable_formatter::get_item_signature)]
29 fn get_item_signature(&self, item_id: DocumentableItemId) -> Option<String>;
30
31 #[salsa::invoke(crate::documentable_formatter::get_item_signature_with_links)]
34 fn get_item_signature_with_links(
35 &self,
36 item_id: DocumentableItemId,
37 ) -> (Option<String>, Vec<LocationLink>);
38}
39
40fn get_item_documentation(db: &dyn DocGroup, item_id: DocumentableItemId) -> Option<String> {
41 let tokens = get_item_documentation_as_tokens(db, item_id)?;
42 let mut buff = String::new();
43 for doc_token in &tokens {
44 match doc_token {
45 DocumentationCommentToken::Content(content) => buff.push_str(content),
46 DocumentationCommentToken::Link(link) => {
47 write!(&mut buff, "[{}]", link.label).ok()?;
48 if let Some(path) = &link.path {
49 write!(&mut buff, "({path})").ok()?;
50 }
51 }
52 }
53 }
54 Some(buff)
55}
56
57fn get_item_documentation_as_tokens(
58 db: &dyn DocGroup,
59 item_id: DocumentableItemId,
60) -> Option<Vec<DocumentationCommentToken>> {
61 let (outer_comment, inner_comment, module_level_comment) = match item_id {
62 DocumentableItemId::Crate(crate_id) => {
63 (None, None, get_crate_root_module_documentation(db, crate_id))
64 }
65 item_id => (
66 extract_item_outer_documentation(db, item_id),
69 extract_item_inner_documentation(db, item_id),
74 extract_item_module_level_documentation(db, item_id),
75 ),
76 };
77
78 let doc_parser = DocumentationCommentParser::new(db);
79
80 let outer_comment_tokens =
81 outer_comment.map(|comment| doc_parser.parse_documentation_comment(item_id, comment));
82 let inner_comment_tokens =
83 inner_comment.map(|comment| doc_parser.parse_documentation_comment(item_id, comment));
84 let module_level_comment_tokens = module_level_comment
85 .map(|comment| doc_parser.parse_documentation_comment(item_id, comment));
86
87 let mut result: Vec<Vec<DocumentationCommentToken>> =
88 [module_level_comment_tokens, outer_comment_tokens, inner_comment_tokens]
89 .into_iter()
90 .flatten()
91 .collect();
92 result.retain(|v| !v.is_empty());
93 if result.is_empty() {
94 return None;
95 }
96 let separator_token = vec![DocumentationCommentToken::Content(" ".to_string())];
97 Some(intersperse(result, separator_token).flatten().collect())
98}
99
100fn get_crate_root_module_documentation(db: &dyn DocGroup, crate_id: CrateId) -> Option<String> {
102 let module_file_id = db.module_main_file(ModuleId::CrateRoot(crate_id)).ok()?;
103 extract_item_module_level_documentation_from_file(db, module_file_id)
104}
105
106fn extract_item_inner_documentation(
108 db: &dyn DocGroup,
109 item_id: DocumentableItemId,
110) -> Option<String> {
111 if matches!(
112 item_id,
113 DocumentableItemId::LookupItem(
114 LookupItemId::ModuleItem(ModuleItemId::FreeFunction(_) | ModuleItemId::Submodule(_))
115 | LookupItemId::ImplItem(ImplItemId::Function(_))
116 | LookupItemId::TraitItem(TraitItemId::Function(_))
117 )
118 ) {
119 let raw_text = item_id
120 .stable_location(db)?
121 .syntax_node(db)
122 .get_text_without_inner_commentable_children(db);
123 Some(extract_item_inner_documentation_from_raw_text(raw_text))
124 } else {
125 None
126 }
127}
128
129fn extract_item_outer_documentation(
131 db: &dyn DocGroup,
132 item_id: DocumentableItemId,
133) -> Option<String> {
134 let raw_text = item_id.stable_location(db)?.syntax_node(db).get_text(db);
136 Some(
137 raw_text
138 .lines()
139 .filter(|line| !line.trim().is_empty())
140 .take_while_ref(|line| is_comment_line(line) || line.trim_start().starts_with("#"))
143 .filter_map(|line| extract_comment_from_code_line(line, &["///"]))
144 .join("\n"),
145 )
146}
147
148fn extract_item_module_level_documentation(
150 db: &dyn DocGroup,
151 item_id: DocumentableItemId,
152) -> Option<String> {
153 match item_id {
154 DocumentableItemId::LookupItem(LookupItemId::ModuleItem(ModuleItemId::Submodule(
155 submodule_id,
156 ))) => {
157 if db.is_submodule_inline(submodule_id) {
158 return None;
159 }
160 let module_file_id = db.module_main_file(ModuleId::Submodule(submodule_id)).ok()?;
161 extract_item_module_level_documentation_from_file(db, module_file_id)
162 }
163 _ => None,
164 }
165}
166
167fn extract_item_inner_documentation_from_raw_text(raw_text: String) -> String {
169 raw_text
170 .lines()
171 .filter(|line| !line.trim().is_empty())
172 .skip_while(|line| is_comment_line(line))
173 .filter_map(|line| extract_comment_from_code_line(line, &["//!"]))
174 .join("\n")
175}
176
177fn extract_item_module_level_documentation_from_file(
179 db: &dyn DocGroup,
180 file_id: FileId,
181) -> Option<String> {
182 let file_content = db.file_content(file_id)?.to_string();
183 Some(
184 file_content
185 .lines()
186 .filter(|line| !line.trim().is_empty())
187 .take_while_ref(|line| is_comment_line(line))
188 .filter_map(|line| extract_comment_from_code_line(line, &["//!"]))
189 .join("\n"),
190 )
191}
192
193fn extract_comment_from_code_line(line: &str, comment_markers: &[&'static str]) -> Option<String> {
199 let dedent = line.trim_start();
201 for comment_marker in comment_markers {
203 if let Some(content) = dedent.strip_prefix(*comment_marker) {
204 if content.starts_with('/') {
210 return None;
211 }
212 return Some(content.strip_prefix(' ').unwrap_or(content).to_string());
213 }
214 }
215 None
216}
217
218fn is_comment_line(line: &str) -> bool {
220 line.trim_start().starts_with("//")
221}