cairo_lang_doc/
db.rs

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    // TODO(mkaput): Support #[doc] attribute. This will be a bigger chunk of work because it would
16    //   be the best to convert all /// comments to #[doc] attrs before processing items by plugins,
17    //   so that plugins would get a nice and clean syntax of documentation to manipulate further.
18    /// Gets the documentation of an item.
19    fn get_item_documentation(&self, item_id: DocumentableItemId) -> Option<String>;
20
21    /// Gets the documentation of a certain as a vector of continuous tokens.
22    fn get_item_documentation_as_tokens(
23        &self,
24        item_id: DocumentableItemId,
25    ) -> Option<Vec<DocumentationCommentToken>>;
26
27    /// Gets the signature of an item (i.e., item without its body).
28    #[salsa::invoke(crate::documentable_formatter::get_item_signature)]
29    fn get_item_signature(&self, item_id: DocumentableItemId) -> Option<String>;
30
31    /// Gets the signature of an item and a list of [`LocationLink`]s to enable mapping
32    /// signature slices on documentable items.
33    #[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            // We check for different types of comments for the item. Even modules can have both
67            // inner and module level comments.
68            extract_item_outer_documentation(db, item_id),
69            // In case if item_id is a module, there are 2 possible cases:
70            // 1. Inline module: It could have inner comments, but not the module_level.
71            // 2. Non-inline Module (module as a file): It could have module level comments, but
72            //    not the inner ones.
73            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
100/// Gets the crate level documentation.
101fn 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
106/// Gets the "//!" inner comment of the item (if only item supports inner comments).
107fn 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
129/// Only gets the doc comments above the item.
130fn extract_item_outer_documentation(
131    db: &dyn DocGroup,
132    item_id: DocumentableItemId,
133) -> Option<String> {
134    // Get the text of the item (trivia + definition)
135    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        // Takes all the lines before the definition.
141        // Anything other than doc comments will be filtered out later.
142        .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
148/// Gets the module level comments of the item.
149fn 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
167/// Only gets the comments inside the item.
168fn 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
177/// Gets the module level comments of certain file.
178fn 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
193/// This function does 2 things to the line of comment:
194/// 1. Removes indentation
195/// 2. If it starts with one of the passed prefixes, removes the given prefixes (including the space
196///    after the prefix).
197/// 3. If the comment starts with a slash, returns None.
198fn extract_comment_from_code_line(line: &str, comment_markers: &[&'static str]) -> Option<String> {
199    // Remove indentation.
200    let dedent = line.trim_start();
201    // Check if this is a doc comment.
202    for comment_marker in comment_markers {
203        if let Some(content) = dedent.strip_prefix(*comment_marker) {
204            // TODO(mkaput): The way how removing this indentation is performed is probably
205            //   wrong. The code should probably learn how many spaces are used at the first
206            //   line of comments block, and then remove the same amount of spaces in the
207            //   block, instead of assuming just one space.
208            // Remove inner indentation if one exists.
209            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
218/// Check whether the code line is a comment line.
219fn is_comment_line(line: &str) -> bool {
220    line.trim_start().starts_with("//")
221}