cairo_lang_doc/
db.rs

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    // 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<'db>(&'db self, item_id: DocumentableItemId<'db>) -> Option<String> {
20        get_item_documentation(self.as_dyn_database(), (), item_id)
21    }
22
23    /// Gets the documentation of a certain as a vector of continuous tokens.
24    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    /// Gets the signature of an item (i.e., item without its body).
32    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    /// Gets the signature of an item and a list of [`LocationLink`]s to enable mapping
37    /// signature slices on documentable items.
38    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            // We check for different types of comments for the item. Even modules can have both
85            // inner and module level comments.
86            extract_item_outer_documentation(db, item_id),
87            // In case if item_id is a module, there are 2 possible cases:
88            // 1. Inline module: It could have inner comments, but not the module_level.
89            // 2. Non-inline Module (module as a file): It could have module level comments, but
90            //    not the inner ones.
91            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
118/// Gets the crate level documentation.
119fn 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
127/// Gets the "//!" inner comment of the item (if only item supports inner comments).
128fn 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
150/// Only gets the doc comments above the item.
151fn extract_item_outer_documentation<'db>(
152    db: &'db dyn Database,
153    item_id: DocumentableItemId<'db>,
154) -> Option<String> {
155    // Get the text of the item (trivia + definition)
156    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        // Takes all the lines before the definition.
162        // Anything other than doc comments will be filtered out later.
163        .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
169/// Gets the module level comments of the item.
170fn 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
188/// Only gets the comments inside the item.
189fn 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
198/// Gets the module level comments of certain file.
199fn 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
214/// This function does 2 things to the line of comment:
215/// 1. Removes indentation
216/// 2. If it starts with one of the passed prefixes, removes the given prefixes (including the space
217///    after the prefix).
218/// 3. If the comment starts with a slash, returns None.
219fn extract_comment_from_code_line(line: &str, comment_markers: &[&'static str]) -> Option<String> {
220    // Remove indentation.
221    let dedent = line.trim_start();
222    // Check if this is a doc comment.
223    for comment_marker in comment_markers {
224        if let Some(content) = dedent.strip_prefix(*comment_marker) {
225            // TODO(mkaput): The way how removing this indentation is performed is probably
226            //   wrong. The code should probably learn how many spaces are used at the first
227            //   line of comments block, and then remove the same amount of spaces in the
228            //   block, instead of assuming just one space.
229            // Remove inner indentation if one exists.
230            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
239/// Check whether the code line is a comment line.
240fn is_comment_line(line: &str) -> bool {
241    line.trim_start().starts_with("//")
242}