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(self.as_dyn_database(), node_descendant_files, node)
116    }
117}
118
119impl<T: Database + ?Sized> CommonGroup for T {}
120
121/// Finds the most specific [`SyntaxNode`] at the given [`TextOffset`] in the file.
122#[salsa::tracked]
123fn find_syntax_node_at_offset<'db>(
124    db: &'db dyn Database,
125    file: FileId<'db>,
126    offset: TextOffset,
127) -> Option<SyntaxNode<'db>> {
128    Some(db.file_syntax(file).to_option()?.lookup_offset(db, offset))
129}
130
131#[salsa::tracked(returns(ref))]
132fn file_and_subfiles_with_corresponding_modules_without_inline<'db>(
133    db: &'db dyn Database,
134    file: FileId<'db>,
135) -> Option<(OrderedHashSet<FileId<'db>>, OrderedHashSet<ModuleId<'db>>)> {
136    // This will fail if crate is still not loaded.
137    let file_modules = db.file_modules(file).ok()?;
138
139    let mut modules: OrderedHashSet<_> = file_modules.iter().copied().collect();
140    let mut files = OrderedHashSet::from_iter([file]);
141    // Collect descendants of `file`
142    // and modules from all virtual files that are descendants of `file`.
143    //
144    // Caveat: consider a situation `file1` --(child)--> `file2` with file contents:
145    // - `file1`: `mod file2_origin_module { #[file2]fn sth() {} }`
146    // - `file2`: `mod mod_from_file2 { }`
147    //  It is important that `file2` content contains a module.
148    //
149    // Problem: in this situation it is not enough to call `db.file_modules(file1_id)` since
150    //  `mod_from_file2` won't be in the result of this query.
151    // Solution: we can find file id of `file2`
152    //  (note that we only have file id of `file1` at this point)
153    //  in `db.module_files(mod_from_file1_from_which_file2_origins)`.
154    //  Then we can call `db.file_modules(file2_id)` to obtain module id of `mod_from_file2`.
155    //  We repeat this procedure until there is nothing more to collect.
156    let mut modules_queue: VecDeque<_> = modules.iter().copied().collect();
157    while let Some(module_id) = modules_queue.pop_front() {
158        let Ok(module_files) = db.module_files(module_id) else {
159            continue;
160        };
161
162        for &file_id in module_files {
163            if files.insert(file_id)
164                && let Ok(file_modules) = db.file_modules(file_id)
165            {
166                for module_id in file_modules {
167                    if modules.insert(*module_id) {
168                        modules_queue.push_back(*module_id);
169                    }
170                }
171            }
172        }
173    }
174    Some((files, modules))
175}
176
177#[salsa::tracked(returns(ref))]
178fn file_and_subfiles_with_corresponding_modules<'db>(
179    db: &'db dyn Database,
180    file: FileId<'db>,
181) -> Option<(OrderedHashSet<FileId<'db>>, OrderedHashSet<ModuleId<'db>>)> {
182    let (mut files, modules) = db
183        .file_and_subfiles_with_corresponding_modules_without_inline(file)?
184        .clone();
185
186    for &module_id in modules.iter() {
187        let inline_macro_files = db.inline_macro_expansion_files(module_id);
188
189        let mut path_buffer = vec![];
190
191        for &inline_macro_file in inline_macro_files {
192            let mut inline_macro_file = inline_macro_file;
193            while let Some((parent, _)) = get_parent_and_mapping(db, inline_macro_file) {
194                path_buffer.push(inline_macro_file);
195
196                if files.contains(&parent.file_id) {
197                    files.extend(path_buffer.iter().copied());
198                    break;
199                }
200
201                inline_macro_file = parent.file_id;
202            }
203            path_buffer.clear();
204        }
205    }
206    Some((files, modules))
207}
208
209#[tracing::instrument(skip_all)]
210#[salsa::tracked(returns(ref))]
211fn get_node_resultants<'db>(
212    db: &'db dyn Database,
213    _: (),
214    node: SyntaxNode<'db>,
215) -> Option<Vec<SyntaxNode<'db>>> {
216    let main_file = node.stable_ptr(db).file_id(db);
217
218    let (files, _) = db.file_and_subfiles_with_corresponding_modules(main_file)?;
219
220    let files: Arc<[FileId]> = files
221        .iter()
222        .filter(|file| **file != main_file)
223        .copied()
224        .collect();
225    let resultants = db.find_generated_nodes(files, node);
226
227    Some(resultants.into_iter().copied().collect())
228}
229
230#[tracing::instrument(skip_all)]
231#[salsa::tracked(returns(ref))]
232/// See [`get_node_resultants`].
233fn find_generated_nodes<'db>(
234    db: &'db dyn Database,
235    node_descendant_files: Arc<[FileId<'db>]>,
236    node: SyntaxNode<'db>,
237) -> OrderedHashSet<SyntaxNode<'db>> {
238    let start_file = node.stable_ptr(db).file_id(db);
239
240    let mut result = OrderedHashSet::default();
241
242    let mut is_replaced = false;
243
244    for file in node_descendant_files.iter().cloned() {
245        let Some((parent, mappings)) = get_parent_and_mapping(db, file) else {
246            continue;
247        };
248
249        if parent.file_id != start_file {
250            continue;
251        }
252
253        let Ok(file_syntax) = db.file_syntax(file) else {
254            continue;
255        };
256
257        let mappings: Vec<_> = mappings
258            .iter()
259            .filter(|mapping| match mapping.origin {
260                // This is in fact default mapping containing whole file
261                // Skip it as whole file content `ModuleItemList` or `SyntaxFile` node is found otherwise
262                CodeOrigin::CallSite(_) => false,
263                CodeOrigin::Start(start) => start == node.span(db).start,
264                CodeOrigin::Span(span) => node.span(db).contains(span),
265            })
266            .cloned()
267            .collect();
268        if mappings.is_empty() {
269            continue;
270        }
271
272        let is_replacing_og_item = match file.long(db) {
273            FileLongId::Virtual(vfs) => vfs.original_item_removed,
274            FileLongId::External(id) => ext_as_virtual(db, *id).original_item_removed,
275            _ => unreachable!(),
276        };
277
278        let mut new_nodes: OrderedHashSet<_> = Default::default();
279
280        for mapping in &mappings {
281            let node_by_offset = file_syntax.lookup_offset(db, mapping.span.start);
282            let node_parent = node_by_offset.parent(db);
283            node_parent
284                .unwrap_or(node_by_offset)
285                .for_each_terminal(db, |terminal| {
286                    // Skip end of the file terminal, which is also a syntax tree leaf.
287                    // As `ModuleItemList` and `TerminalEndOfFile` have the same parent,
288                    // which is the `SyntaxFile`, so we don't want to take the `SyntaxFile`
289                    // as an additional resultant.
290                    if terminal.kind(db) == SyntaxKind::TerminalEndOfFile {
291                        return;
292                    }
293                    let nodes: Vec<_> = terminal
294                        .ancestors_with_self(db)
295                        .map_while(|new_node| {
296                            translate_location(&mappings, new_node.span_without_trivia(db))
297                                .map(|span_in_parent| (new_node, span_in_parent))
298                        })
299                        .take_while(|(_, span_in_parent)| {
300                            node.span_without_trivia(db).contains(*span_in_parent)
301                        })
302                        .collect();
303
304                    if let Some((last_node, _)) = nodes.last().cloned() {
305                        let (new_node, _) = nodes
306                            .into_iter()
307                            .rev()
308                            .take_while(|(node, _)| node.span(db) == last_node.span(db))
309                            .last()
310                            .unwrap();
311
312                        new_nodes.insert(new_node);
313                    }
314                });
315        }
316
317        // If there is no node found, don't mark it as potentially replaced.
318        if !new_nodes.is_empty() {
319            is_replaced = is_replaced || is_replacing_og_item;
320        }
321
322        for new_node in new_nodes {
323            result.extend(
324                find_generated_nodes(db, Arc::clone(&node_descendant_files), new_node)
325                    .into_iter()
326                    .copied(),
327            );
328        }
329    }
330
331    if !is_replaced {
332        result.insert(node);
333    }
334
335    result
336}