Skip to main content

mir_analyzer/session/
queries.rs

1use super::*;
2
3impl AnalysisSession {
4    /// Resolve a top-level symbol (class or function) to its declaration
5    /// location. Powers go-to-definition.
6    ///
7    /// **Side effects:** if the symbol isn't yet known, this may invoke the
8    /// configured [`crate::SourceProvider`] to fault in additional files and
9    /// mutate the salsa input set. Use [`Self::definition_of_cached`] for a
10    /// pure variant that only consults already-loaded state.
11    ///
12    /// Returns:
13    /// - `Ok(Location)` — symbol found with a source location
14    /// - `Err(NotFound)` — no such symbol in the codebase
15    /// - `Err(NoSourceLocation)` — symbol exists but has no recorded span
16    ///   (e.g. some stub-only declarations)
17    pub fn definition_of(
18        &self,
19        symbol: &crate::Name,
20    ) -> Result<mir_types::Location, crate::SymbolLookupError> {
21        // Trigger any necessary lazy-load mutations before snapshotting.
22        match symbol {
23            crate::Name::Class(fqcn) => {
24                let _ = self.load_class(fqcn.as_ref());
25            }
26            crate::Name::Function(fqn) => {
27                let _ = self.load_class(fqn.as_ref());
28            }
29            crate::Name::Method { class, .. }
30            | crate::Name::Property { class, .. }
31            | crate::Name::ClassConstant { class, .. } => {
32                let _ = self.load_class(class.as_ref());
33            }
34            _ => {}
35        }
36        self.definition_of_cached(symbol)
37    }
38
39    /// Pure variant of [`Self::definition_of`]. Never invokes the
40    /// [`crate::SourceProvider`] and never mutates salsa inputs; resolves
41    /// only against state already loaded by `set_file_text` / `ingest_file`.
42    /// Returns `Err(NotFound)` when the symbol isn't in the loaded set, even
43    /// if a resolver could in principle map it.
44    pub fn definition_of_cached(
45        &self,
46        symbol: &crate::Name,
47    ) -> Result<mir_types::Location, crate::SymbolLookupError> {
48        let db = self.snapshot_db();
49        match symbol {
50            crate::Name::Class(fqcn) => {
51                let here = crate::db::Fqcn::from_str(&db, fqcn.as_ref());
52                let class = crate::db::find_class_like(&db, here)
53                    .ok_or(crate::SymbolLookupError::NotFound)?;
54                class
55                    .location()
56                    .cloned()
57                    .ok_or(crate::SymbolLookupError::NoSourceLocation)
58            }
59            crate::Name::Function(fqn) => {
60                let here = crate::db::Fqcn::from_str(&db, fqn.as_ref());
61                let f = crate::db::find_function(&db, here)
62                    .ok_or(crate::SymbolLookupError::NotFound)?;
63                f.location
64                    .clone()
65                    .ok_or(crate::SymbolLookupError::NoSourceLocation)
66            }
67            crate::Name::Method { class, name }
68            | crate::Name::Property { class, name }
69            | crate::Name::ClassConstant { class, name } => {
70                crate::db::member_location(&db, class, name)
71                    .ok_or(crate::SymbolLookupError::NotFound)
72            }
73            crate::Name::GlobalConstant(_) => Err(crate::SymbolLookupError::NoSourceLocation),
74        }
75    }
76
77    /// Hover information for a symbol: type, docstring, and definition location.
78    ///
79    /// Use [`crate::FileAnalysis::symbol_at`] to find the symbol at a cursor
80    /// position, then build a [`crate::Name`] from its `kind`. This method
81    /// assembles the displayable hover data.
82    ///
83    /// **Side effects:** when `symbol`'s owning class isn't yet loaded, this
84    /// may invoke the configured [`crate::SourceProvider`] to fault in
85    /// dependencies. Use [`Self::hover_cached`] for a pure variant.
86    ///
87    /// Returns `Err(NotFound)` if the symbol doesn't exist. May still return
88    /// `Ok` with `docstring: None` or `definition: None` if those specific
89    /// pieces aren't available.
90    pub fn hover(
91        &self,
92        symbol: &crate::Name,
93    ) -> Result<crate::HoverInfo, crate::SymbolLookupError> {
94        // Trigger lazy loading for class-rooted symbols before snapshotting.
95        // No-op when the class is already known; ensures inherited member
96        // lookups have the chain present.
97        match symbol {
98            crate::Name::Class(fqcn) => {
99                self.load_class(fqcn.as_ref());
100            }
101            crate::Name::Method { class, .. }
102            | crate::Name::Property { class, .. }
103            | crate::Name::ClassConstant { class, .. } => {
104                // Fault in the owning class for navigation if the background
105                // indexer hasn't reached it yet. Its inheritance ancestors
106                // resolve through the (eagerly-built) workspace symbol index.
107                self.load_class(class.as_ref());
108            }
109            _ => {}
110        }
111        self.hover_cached(symbol)
112    }
113
114    /// Pure variant of [`Self::hover`]. Never invokes the
115    /// [`crate::SourceProvider`]; consults only the already-loaded db.
116    pub fn hover_cached(
117        &self,
118        symbol: &crate::Name,
119    ) -> Result<crate::HoverInfo, crate::SymbolLookupError> {
120        use mir_types::{Atomic, Type};
121        let db = self.snapshot_db();
122        match symbol {
123            crate::Name::Function(fqn) => {
124                let here = crate::db::Fqcn::from_str(&db, fqn.as_ref());
125                let f = crate::db::find_function(&db, here)
126                    .ok_or(crate::SymbolLookupError::NotFound)?;
127                let ty = f
128                    .return_type
129                    .as_deref()
130                    .cloned()
131                    .unwrap_or_else(Type::mixed);
132                let docstring = f.docstring.as_ref().map(|s| s.to_string());
133                Ok(crate::HoverInfo {
134                    ty,
135                    docstring,
136                    definition: f.location.clone(),
137                })
138            }
139            crate::Name::Method { class, name } => {
140                let here = crate::db::Fqcn::from_str(&db, class.as_ref());
141                let (_, m) = crate::db::find_method_in_chain(&db, here, name)
142                    .ok_or(crate::SymbolLookupError::NotFound)?;
143                let ty = m
144                    .return_type
145                    .as_deref()
146                    .cloned()
147                    .unwrap_or_else(Type::mixed);
148                let docstring = m.docstring.as_ref().map(|s| s.to_string());
149                Ok(crate::HoverInfo {
150                    ty,
151                    docstring,
152                    definition: m.location.clone(),
153                })
154            }
155            crate::Name::Class(fqcn) => {
156                let here = crate::db::Fqcn::from_str(&db, fqcn.as_ref());
157                let class = crate::db::find_class_like(&db, here)
158                    .ok_or(crate::SymbolLookupError::NotFound)?;
159                let ty = Type::single(Atomic::TNamedObject {
160                    fqcn: mir_types::Name::from(fqcn.as_ref()),
161                    type_params: mir_types::union::empty_type_params(),
162                });
163                Ok(crate::HoverInfo {
164                    ty,
165                    docstring: None,
166                    definition: class.location().cloned(),
167                })
168            }
169            crate::Name::Property { class, name } => {
170                let here = crate::db::Fqcn::from_str(&db, class.as_ref());
171                let (_, p) = crate::db::find_property_in_chain(&db, here, name)
172                    .ok_or(crate::SymbolLookupError::NotFound)?;
173                let ty = p.ty.as_deref().cloned().unwrap_or_else(Type::mixed);
174                Ok(crate::HoverInfo {
175                    ty,
176                    docstring: None,
177                    definition: p.location.clone(),
178                })
179            }
180            crate::Name::ClassConstant { class, name } => {
181                let here = crate::db::Fqcn::from_str(&db, class.as_ref());
182                let (_, c) = crate::db::find_class_constant_in_chain(&db, here, name)
183                    .ok_or(crate::SymbolLookupError::NotFound)?;
184                Ok(crate::HoverInfo {
185                    ty: c.ty.clone(),
186                    docstring: None,
187                    definition: c.location.clone(),
188                })
189            }
190            crate::Name::GlobalConstant(fqn) => {
191                let here = crate::db::Fqcn::from_str(&db, fqn.as_ref());
192                let ty = crate::db::find_global_constant(&db, here)
193                    .ok_or(crate::SymbolLookupError::NotFound)?;
194                Ok(crate::HoverInfo {
195                    ty: (*ty).clone(),
196                    docstring: None,
197                    definition: None,
198                })
199            }
200        }
201    }
202
203    /// Raw reference locations indexed by string symbol key, kept for tests
204    /// that use the legacy stringly-typed API. Prefer [`Self::references_to`]
205    /// with a typed [`crate::Name`].
206    #[doc(hidden)]
207    pub fn reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
208        use crate::db::MirDatabase;
209        let db = self.snapshot_db();
210        db.reference_locations(symbol)
211    }
212
213    /// Every recorded reference to `symbol` with its source location as a Range.
214    /// Use [`crate::FileAnalysis::symbol_at`] to find the symbol at a cursor,
215    /// build a [`crate::Name`] from it, and pass it here.
216    pub fn references_to(&self, symbol: &crate::Name) -> Vec<(Arc<str>, crate::Range)> {
217        let db = self.snapshot_db();
218        let key = symbol.codebase_key();
219        db.reference_locations(&key)
220            .into_iter()
221            .map(|(file, line, col_start, col_end)| {
222                let range = crate::Range {
223                    start: crate::Position {
224                        line,
225                        column: col_start as u32,
226                    },
227                    end: crate::Position {
228                        line,
229                        column: col_end as u32,
230                    },
231                };
232                (file, range)
233            })
234            .collect()
235    }
236
237    /// Every recorded reference to `symbol` that originates in one of `files`,
238    /// computed directly from the memoized [`crate::db::analyze_file`] query.
239    ///
240    /// Unlike [`Self::references_to`] — which reads the imperatively-maintained
241    /// reverse index and therefore requires the files to have been
242    /// `ingest_file`d first — this analyzes the given files on demand via salsa:
243    /// warm files are memo hits, cold files analyze exactly once, and nothing
244    /// mutates the shared reference index. Repeated queries don't churn
245    /// reverse-deps or evict caches, so per-request cost stays flat across a
246    /// session regardless of how much of the workspace has been touched.
247    ///
248    /// `files` are source paths; any not registered as a `SourceFile` input are
249    /// silently skipped (their refs are simply absent — the caller's text
250    /// pre-filter already scoped the set).
251    pub fn references_to_in_files(
252        &self,
253        symbol: &crate::Name,
254        files: &[Arc<str>],
255    ) -> Vec<(Arc<str>, crate::Range)> {
256        use crate::db::MirDatabase;
257        use rayon::prelude::*;
258        use std::panic::AssertUnwindSafe;
259
260        let key = symbol.codebase_key();
261
262        // Phase 1 (serial, no live snapshot held across the writes): fault in
263        // each file's class references. `analyze_file` resolves names, and an
264        // unresolved class triggers `load_class`, which mutates salsa inputs —
265        // doing that from inside the parallel phase would cancel the very
266        // snapshots it runs on. Each parse takes a scoped snapshot it drops
267        // before warming up. Runs ONCE, outside the retry loop: re-running its
268        // writes per retry would amplify cancellation (each write cancels
269        // sibling readers). Mirrors `reanalyze_dependents`.
270        for path in files {
271            let parsed = {
272                let db = self.snapshot_db();
273                let Some(sf) = db.lookup_source_file(path.as_ref()) else {
274                    continue;
275                };
276                crate::db::parse_file(&db as &dyn MirDatabase, sf).0
277            };
278            self.prepare_ast_for_analysis(&parsed.program, path.as_ref());
279        }
280
281        // Phase 2 (parallel, pure) under a `salsa::Cancelled` retry loop: every
282        // referenced class is now loaded, so this is a memoized read with no
283        // writes. An external writer (background indexer) bumping the revision
284        // still cancels in-flight reads; catch it, let the snapshot unwind and
285        // drop (holding one across the retry would deadlock the writer), and
286        // retry. Mirrors the host's `snapshot_query` retry on the read path.
287        loop {
288            let attempt = salsa::Cancelled::catch(AssertUnwindSafe(|| {
289                let db_main = self.snapshot_db();
290                files
291                    .par_iter()
292                    .map_with(db_main, |db, path| {
293                        let Some(sf) = db.lookup_source_file(path.as_ref()) else {
294                            return Vec::new();
295                        };
296                        let out = crate::db::analyze_file(&*db as &dyn MirDatabase, sf);
297                        out.ref_locs
298                            .iter()
299                            .filter(|loc| loc.symbol_key.as_ref() == key.as_str())
300                            .map(|loc| {
301                                (
302                                    loc.file.clone(),
303                                    crate::Range {
304                                        start: crate::Position {
305                                            line: loc.line,
306                                            column: loc.col_start as u32,
307                                        },
308                                        end: crate::Position {
309                                            line: loc.line,
310                                            column: loc.col_end as u32,
311                                        },
312                                    },
313                                )
314                            })
315                            .collect::<Vec<_>>()
316                    })
317                    .flatten()
318                    .collect::<Vec<_>>()
319            }));
320            if let Ok(refs) = attempt {
321                return refs;
322            }
323        }
324    }
325
326    /// Class-level issues (inheritance violations, abstract-method gaps, override
327    /// incompatibilities) for the given set of files.
328    ///
329    /// These checks are cross-file by nature and are not emitted by
330    /// [`crate::FileAnalyzer::analyze`]. Call this after ingesting or
331    /// re-analyzing a file and its dependents to get the full diagnostic picture.
332    ///
333    /// Circular-inheritance checks always run against the full workspace graph
334    /// regardless of the `files` filter — a cycle is a workspace-wide problem.
335    pub fn class_issues(&self, files: &[Arc<str>]) -> Vec<crate::Issue> {
336        let db = self.snapshot_db();
337        let file_set: HashSet<Arc<str>> = files.iter().cloned().collect();
338        let file_data: Vec<(Arc<str>, Arc<str>)> = files
339            .iter()
340            .filter_map(|f| Some((f.clone(), self.source_of(f)?)))
341            .collect();
342        crate::class::ClassAnalyzer::with_files(&db, file_set, &file_data).analyze_all()
343    }
344
345    /// All declarations defined in `file` as a **hierarchical tree**.
346    ///
347    /// Classes/interfaces/traits/enums are returned with their methods,
348    /// properties, and constants nested in `children`. Top-level functions
349    /// and constants are returned with empty `children`.
350    pub fn document_symbols(&self, file: &str) -> Vec<crate::symbol::DocumentSymbol> {
351        use crate::symbol::{DeclarationKind, DocumentSymbol};
352
353        let db = self.snapshot_db();
354        let Some(sf) = db.lookup_source_file(file) else {
355            return Vec::new();
356        };
357        let defs = crate::db::collect_file_definitions(&db, sf);
358        let mut out: Vec<DocumentSymbol> = Vec::new();
359
360        let class_children =
361            |methods: &indexmap::IndexMap<Arc<str>, Arc<mir_codebase::storage::MethodDef>>,
362             props: Option<&indexmap::IndexMap<Arc<str>, mir_codebase::storage::PropertyDef>>,
363             consts: &indexmap::IndexMap<Arc<str>, mir_codebase::storage::ConstantDef>,
364             is_enum: bool|
365             -> Vec<DocumentSymbol> {
366                let mut out: Vec<DocumentSymbol> = Vec::new();
367                for (_, m) in methods.iter() {
368                    out.push(DocumentSymbol {
369                        name: m.name.clone(),
370                        kind: DeclarationKind::Method,
371                        location: m.location.clone(),
372                        children: Vec::new(),
373                    });
374                }
375                if let Some(props) = props {
376                    for (_, p) in props.iter() {
377                        out.push(DocumentSymbol {
378                            name: p.name.clone(),
379                            kind: DeclarationKind::Property,
380                            location: p.location.clone(),
381                            children: Vec::new(),
382                        });
383                    }
384                }
385                let const_kind = if is_enum {
386                    DeclarationKind::EnumCase
387                } else {
388                    DeclarationKind::Constant
389                };
390                for (_, c) in consts.iter() {
391                    out.push(DocumentSymbol {
392                        name: c.name.clone(),
393                        kind: const_kind,
394                        location: c.location.clone(),
395                        children: Vec::new(),
396                    });
397                }
398                out
399            };
400
401        for c in defs.slice.classes.iter() {
402            out.push(DocumentSymbol {
403                name: c.fqcn.clone(),
404                kind: DeclarationKind::Class,
405                location: c.location.clone(),
406                children: class_children(
407                    &c.own_methods,
408                    Some(&c.own_properties),
409                    &c.own_constants,
410                    false,
411                ),
412            });
413        }
414        for i in defs.slice.interfaces.iter() {
415            out.push(DocumentSymbol {
416                name: i.fqcn.clone(),
417                kind: DeclarationKind::Interface,
418                location: i.location.clone(),
419                children: class_children(&i.own_methods, None, &i.own_constants, false),
420            });
421        }
422        for t in defs.slice.traits.iter() {
423            out.push(DocumentSymbol {
424                name: t.fqcn.clone(),
425                kind: DeclarationKind::Trait,
426                location: t.location.clone(),
427                children: class_children(
428                    &t.own_methods,
429                    Some(&t.own_properties),
430                    &t.own_constants,
431                    false,
432                ),
433            });
434        }
435        for e in defs.slice.enums.iter() {
436            let mut children = class_children(&e.own_methods, None, &e.own_constants, true);
437            for (_, case) in e.cases.iter() {
438                children.push(DocumentSymbol {
439                    name: case.name.clone(),
440                    kind: DeclarationKind::EnumCase,
441                    location: case.location.clone(),
442                    children: Vec::new(),
443                });
444            }
445            out.push(DocumentSymbol {
446                name: e.fqcn.clone(),
447                kind: DeclarationKind::Enum,
448                location: e.location.clone(),
449                children,
450            });
451        }
452        for f in defs.slice.functions.iter() {
453            out.push(DocumentSymbol {
454                name: f.fqn.clone(),
455                kind: DeclarationKind::Function,
456                location: f.location.clone(),
457                children: Vec::new(),
458            });
459        }
460        for (name, _) in defs.slice.constants.iter() {
461            out.push(DocumentSymbol {
462                name: name.clone(),
463                kind: DeclarationKind::Constant,
464                location: None,
465                children: Vec::new(),
466            });
467        }
468        out
469    }
470}