Skip to main content

cairo_language_common/
db.rs

1use std::collections::VecDeque;
2use std::sync::Arc;
3
4use crate::syntax_ext::SyntaxNodeExt;
5use cairo_lang_defs::db::DefsGroup;
6use cairo_lang_defs::ids::ModuleId;
7use cairo_lang_diagnostics::ToOption;
8use cairo_lang_filesystem::db::{ext_as_virtual, get_parent_and_mapping, translate_location};
9use cairo_lang_filesystem::ids::{CodeOrigin, FileId, FileLongId};
10use cairo_lang_filesystem::span::TextOffset;
11use cairo_lang_parser::db::ParserGroup;
12use cairo_lang_semantic::lsp_helpers::LspHelpers;
13use cairo_lang_syntax::node::SyntaxNode;
14use cairo_lang_syntax::node::kind::SyntaxKind;
15use cairo_lang_utils::ordered_hash_set::OrderedHashSet;
16use salsa::Database;
17
18pub trait CommonGroup: Database {
19    /// Finds the most specific [`SyntaxNode`] at the given [`TextOffset`] in the file.
20    fn find_syntax_node_at_offset<'db>(
21        &'db self,
22        file: FileId<'db>,
23        offset: TextOffset,
24    ) -> Option<SyntaxNode<'db>> {
25        find_syntax_node_at_offset(self.as_dyn_database(), file, offset)
26    }
27
28    /// Collects `file` and all its descendants together with modules from all these files. Does *not* includes inline macro expansions.
29    fn file_and_subfiles_with_corresponding_modules_without_inline<'db>(
30        &'db self,
31        file: FileId<'db>,
32    ) -> Option<&'db (OrderedHashSet<FileId<'db>>, OrderedHashSet<ModuleId<'db>>)> {
33        file_and_subfiles_with_corresponding_modules_without_inline(self.as_dyn_database(), file)
34            .as_ref()
35    }
36
37    /// Collects `file` and all its descendants together with modules from all these files. Includes inline macro expansions.
38    fn file_and_subfiles_with_corresponding_modules<'db>(
39        &'db self,
40        file: FileId<'db>,
41    ) -> Option<&'db (OrderedHashSet<FileId<'db>>, OrderedHashSet<ModuleId<'db>>)> {
42        file_and_subfiles_with_corresponding_modules(self.as_dyn_database(), file).as_ref()
43    }
44
45    /// We use the term `resultants` to refer to generated nodes that are mapped to the original node and are not deleted.
46    /// Effectively (user nodes + generated nodes - removed nodes) set always contains resultants for any user defined node.
47    /// Semantic data may be available only for resultants.
48    ///
49    /// Consider the following foundry code as an example:
50    /// ```ignore
51    /// #[test]
52    /// #[available_gas(123)]
53    /// fn test_fn(){
54    /// }
55    /// ```
56    /// This code expands to something like:
57    /// ```ignore
58    /// #[available_gas(123)]
59    /// fn test_fn(){
60    ///     if is_config_run {
61    ///         // do config check
62    ///         return;
63    ///     }
64    /// }
65    /// ```
66    /// It then further transforms to:
67    /// ```ignore
68    /// fn test_fn(){
69    ///     if is_config_run {
70    ///         // do config check
71    ///         set_available_gas(123);
72    ///         return;
73    ///     }
74    /// }
75    /// ```
76    ///
77    /// Let's label these as files 1, 2 and 3, respectively. The macros used here are attribute proc macros. They delete old code and generate new code.
78    /// In this process, `test_fn` from file 1 is deleted. However, `test_fn` from file 2 is mapped to it.
79    /// Therefore, we should ignore `test_fn` from file 1 as it no longer exists and
80    /// should use `test_fn` from file 2. But then, `test_fn` from file 2 is effectively replaced by `test_fn` from file 3, so `test_fn` from file 2 is now deleted.
81    ///
82    /// In this scenario, only `test_fn` from file 3 is a resultant. Both `test_fn` from files 1 and 2 were deleted.
83    ///
84    /// So for input being `test_fn` from file 1, only `test_fn` from file 3 is returned
85    ///
86    /// Now, consider another example:
87    ///
88    /// The `generate_trait` macro is a builtin macro that does not remove the original code. Thus, we have the following code:
89    ///
90    /// ```ignore
91    /// #[generate_trait]
92    /// impl FooImpl for FooTrait {}
93    /// ```
94    /// This code generates the following:
95    /// ```ignore
96    /// trait FooTrait {}
97    /// ```
98    ///
99    /// Both the original and the generated files are considered when calculating semantics, since original `FooTrait` was not removed.
100    /// Additionally `FooTrait` from file 2 is mapped to `FooTrait` from file 1.
101    ///
102    /// Therefore for `FooTrait` from file 1, `FooTrait` from file 1 and `FooTrait` from file 2 are returned.
103    fn get_node_resultants<'db>(
104        &'db self,
105        node: SyntaxNode<'db>,
106    ) -> Option<&'db Vec<SyntaxNode<'db>>> {
107        get_node_resultants(self.as_dyn_database(), (), node).as_ref()
108    }
109
110    fn find_generated_nodes<'db>(
111        &'db self,
112        node_descendant_files: Arc<[FileId<'db>]>,
113        node: SyntaxNode<'db>,
114    ) -> &'db OrderedHashSet<SyntaxNode<'db>> {
115        find_generated_nodes(
116            self.as_dyn_database(),
117            node_descendant_files.iter().cloned().collect(),
118            node,
119        )
120    }
121}
122
123impl<T: Database + ?Sized> CommonGroup for T {}
124
125/// Finds the most specific [`SyntaxNode`] at the given [`TextOffset`] in the file.
126#[salsa::tracked]
127fn find_syntax_node_at_offset<'db>(
128    db: &'db dyn Database,
129    file: FileId<'db>,
130    offset: TextOffset,
131) -> Option<SyntaxNode<'db>> {
132    Some(db.file_syntax(file).to_option()?.lookup_offset(db, offset))
133}
134
135#[salsa::tracked(returns(ref))]
136fn file_and_subfiles_with_corresponding_modules_without_inline<'db>(
137    db: &'db dyn Database,
138    file: FileId<'db>,
139) -> Option<(OrderedHashSet<FileId<'db>>, OrderedHashSet<ModuleId<'db>>)> {
140    // This will fail if crate is still not loaded.
141    let file_modules = db.file_modules(file).ok()?;
142
143    let mut modules: OrderedHashSet<_> = file_modules.iter().copied().collect();
144    let mut files = OrderedHashSet::from_iter([file]);
145    // Collect descendants of `file`
146    // and modules from all virtual files that are descendants of `file`.
147    //
148    // Caveat: consider a situation `file1` --(child)--> `file2` with file contents:
149    // - `file1`: `mod file2_origin_module { #[file2]fn sth() {} }`
150    // - `file2`: `mod mod_from_file2 { }`
151    //  It is important that `file2` content contains a module.
152    //
153    // Problem: in this situation it is not enough to call `db.file_modules(file1_id)` since
154    //  `mod_from_file2` won't be in the result of this query.
155    // Solution: we can find file id of `file2`
156    //  (note that we only have file id of `file1` at this point)
157    //  in `db.module_files(mod_from_file1_from_which_file2_origins)`.
158    //  Then we can call `db.file_modules(file2_id)` to obtain module id of `mod_from_file2`.
159    //  We repeat this procedure until there is nothing more to collect.
160    let mut modules_queue: VecDeque<_> = modules.iter().copied().collect();
161    while let Some(module_id) = modules_queue.pop_front() {
162        let Ok(module_files) = db.module_files(module_id) else {
163            continue;
164        };
165
166        for &file_id in module_files {
167            if files.insert(file_id)
168                && let Ok(file_modules) = db.file_modules(file_id)
169            {
170                for module_id in file_modules {
171                    if modules.insert(*module_id) {
172                        modules_queue.push_back(*module_id);
173                    }
174                }
175            }
176        }
177    }
178    Some((files, modules))
179}
180
181#[salsa::tracked(returns(ref))]
182fn file_and_subfiles_with_corresponding_modules<'db>(
183    db: &'db dyn Database,
184    file: FileId<'db>,
185) -> Option<(OrderedHashSet<FileId<'db>>, OrderedHashSet<ModuleId<'db>>)> {
186    let (mut files, modules) = db
187        .file_and_subfiles_with_corresponding_modules_without_inline(file)?
188        .clone();
189
190    for &module_id in modules.iter() {
191        let inline_macro_files = db.inline_macro_expansion_files(module_id);
192
193        let mut path_buffer = vec![];
194
195        for &inline_macro_file in inline_macro_files {
196            let mut inline_macro_file = inline_macro_file;
197            while let Some((parent, _)) = get_parent_and_mapping(db, inline_macro_file) {
198                path_buffer.push(inline_macro_file);
199
200                if files.contains(&parent.file_id) {
201                    files.extend(path_buffer.iter().copied());
202                    break;
203                }
204
205                inline_macro_file = parent.file_id;
206            }
207            path_buffer.clear();
208        }
209    }
210    Some((files, modules))
211}
212
213#[tracing::instrument(skip_all)]
214#[salsa::tracked(returns(ref))]
215fn get_node_resultants<'db>(
216    db: &'db dyn Database,
217    _: (),
218    node: SyntaxNode<'db>,
219) -> Option<Vec<SyntaxNode<'db>>> {
220    let main_file = node.stable_ptr(db).file_id(db);
221
222    let (files, _) = db.file_and_subfiles_with_corresponding_modules(main_file)?;
223
224    let files: Arc<[FileId]> = files
225        .iter()
226        .filter(|file| **file != main_file)
227        .copied()
228        .collect();
229    let resultants = db.find_generated_nodes(files, node);
230
231    Some(resultants.into_iter().copied().collect())
232}
233
234#[tracing::instrument(skip_all)]
235#[salsa::tracked(returns(ref))]
236/// See [`get_node_resultants`].
237fn find_generated_nodes<'db>(
238    db: &'db dyn Database,
239    node_descendant_files: Vec<FileId<'db>>,
240    node: SyntaxNode<'db>,
241) -> OrderedHashSet<SyntaxNode<'db>> {
242    let start_file = node.stable_ptr(db).file_id(db);
243
244    let mut result = OrderedHashSet::default();
245
246    let mut is_replaced = false;
247
248    for file in node_descendant_files.iter().cloned() {
249        let Some((parent, mappings)) = get_parent_and_mapping(db, file) else {
250            continue;
251        };
252
253        if parent.file_id != start_file {
254            continue;
255        }
256
257        let Ok(file_syntax) = db.file_syntax(file) else {
258            continue;
259        };
260
261        let mappings: Vec<_> = mappings
262            .iter()
263            .filter(|mapping| match mapping.origin {
264                // This is in fact default mapping containing whole file
265                // Skip it as whole file content `ModuleItemList` or `SyntaxFile` node is found otherwise
266                CodeOrigin::CallSite(_) => false,
267                CodeOrigin::Start(start) => start == node.span(db).start,
268                CodeOrigin::Span(span) => node.span(db).contains(span),
269            })
270            .cloned()
271            .collect();
272        if mappings.is_empty() {
273            continue;
274        }
275
276        let is_replacing_og_item = match file.long(db) {
277            FileLongId::Virtual(vfs) => vfs.original_item_removed,
278            FileLongId::External(id) => ext_as_virtual(db, *id).original_item_removed,
279            _ => unreachable!(),
280        };
281
282        let mut new_nodes: OrderedHashSet<_> = Default::default();
283
284        for mapping in &mappings {
285            let node_by_offset = file_syntax.lookup_offset(db, mapping.span.start);
286            let node_parent = node_by_offset.parent(db);
287            node_parent
288                .unwrap_or(node_by_offset)
289                .for_each_terminal(db, |terminal| {
290                    // Skip end of the file terminal, which is also a syntax tree leaf.
291                    // As `ModuleItemList` and `TerminalEndOfFile` have the same parent,
292                    // which is the `SyntaxFile`, so we don't want to take the `SyntaxFile`
293                    // as an additional resultant.
294                    if terminal.kind(db) == SyntaxKind::TerminalEndOfFile {
295                        return;
296                    }
297                    let nodes: Vec<_> = terminal
298                        .ancestors_with_self(db)
299                        .map_while(|new_node| {
300                            translate_location(&mappings, new_node.span_without_trivia(db))
301                                .map(|span_in_parent| (new_node, span_in_parent))
302                        })
303                        .take_while(|(_, span_in_parent)| {
304                            node.span_without_trivia(db).contains(*span_in_parent)
305                        })
306                        .collect();
307
308                    if let Some((last_node, _)) = nodes.last().cloned() {
309                        let (new_node, _) = nodes
310                            .into_iter()
311                            .rev()
312                            .take_while(|(node, _)| node.span(db) == last_node.span(db))
313                            .last()
314                            .unwrap();
315
316                        new_nodes.insert(new_node);
317                    }
318                });
319        }
320
321        // If there is no node found, don't mark it as potentially replaced.
322        if !new_nodes.is_empty() {
323            is_replaced = is_replaced || is_replacing_og_item;
324        }
325
326        for new_node in new_nodes {
327            result.extend(
328                find_generated_nodes(db, node_descendant_files.clone(), new_node)
329                    .into_iter()
330                    .copied(),
331            );
332        }
333    }
334
335    if !is_replaced {
336        result.insert(node);
337    }
338
339    result
340}