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 self.references_to_in_files_cancellable(symbol, files, &|| false)
257 .unwrap_or_default()
258 }
259
260 /// Cancellable variant of [`Self::references_to_in_files`].
261 ///
262 /// `should_cancel` is polled at Phase-1 file boundaries and between
263 /// Phase-2 retry attempts; when it returns `true` the query aborts with
264 /// `None`. Without it a sustained write stream (rapid edits, background
265 /// indexing) keeps cancelling Phase 2's snapshot reads and the retry loop
266 /// spins with no way for the caller to abandon the now-stale request.
267 pub fn references_to_in_files_cancellable(
268 &self,
269 symbol: &crate::Name,
270 files: &[Arc<str>],
271 should_cancel: &(dyn Fn() -> bool + Sync),
272 ) -> Option<Vec<(Arc<str>, crate::Range)>> {
273 use crate::db::MirDatabase;
274 use rayon::prelude::*;
275 use std::panic::AssertUnwindSafe;
276
277 let key = symbol.codebase_key();
278
279 // Phase 1 (serial, no live snapshot held across the writes): fault in
280 // each file's class references. `analyze_file` resolves names, and an
281 // unresolved class triggers `load_class`, which mutates salsa inputs —
282 // doing that from inside the parallel phase would cancel the very
283 // snapshots it runs on. Each parse takes a scoped snapshot it drops
284 // before warming up. Runs ONCE, outside the retry loop: re-running its
285 // writes per retry would amplify cancellation (each write cancels
286 // sibling readers). Mirrors `reanalyze_dependents`.
287 //
288 // Files already warmed against their current text skip the parse +
289 // AST walk entirely (`prepared_files`), so a warm repeat query pays
290 // one map lookup per candidate instead of a serial parse sweep.
291 for path in files {
292 if should_cancel() {
293 return None;
294 }
295 self.prepare_file_for_analysis(path);
296 }
297
298 // Phase 2 (parallel, pure) under a `salsa::Cancelled` retry loop: every
299 // referenced class is now loaded, so this is a memoized read with no
300 // writes. An external writer (background indexer) bumping the revision
301 // still cancels in-flight reads; catch it, let the snapshot unwind and
302 // drop (holding one across the retry would deadlock the writer), and
303 // retry. Mirrors the host's `snapshot_query` retry on the read path.
304 loop {
305 let attempt = salsa::Cancelled::catch(AssertUnwindSafe(|| {
306 let db_main = self.snapshot_db();
307 files
308 .par_iter()
309 .map_with(db_main, |db, path| {
310 let Some(sf) = db.lookup_source_file(path.as_ref()) else {
311 return Vec::new();
312 };
313 let out = crate::db::analyze_file(&*db as &dyn MirDatabase, sf);
314 out.ref_locs
315 .iter()
316 .filter(|loc| loc.symbol_key.as_ref() == key.as_str())
317 .map(|loc| {
318 (
319 loc.file.clone(),
320 crate::Range {
321 start: crate::Position {
322 line: loc.line,
323 column: loc.col_start as u32,
324 },
325 end: crate::Position {
326 line: loc.line,
327 column: loc.col_end as u32,
328 },
329 },
330 )
331 })
332 .collect::<Vec<_>>()
333 })
334 .flatten()
335 .collect::<Vec<_>>()
336 }));
337 if let Ok(refs) = attempt {
338 return Some(refs);
339 }
340 // A write landed mid-read. Before retrying, let the caller decide
341 // whether the request is still worth answering.
342 if should_cancel() {
343 return None;
344 }
345 }
346 }
347
348 /// Files declaring transitive subclasses of `class_fqn`, computed from the
349 /// resolved inheritance graph (see [`crate::db::class_subtype_files`]).
350 /// Excludes `class_fqn`'s own declaring file — the caller adds it.
351 ///
352 /// Lets a reference-search caller scope a `protected` member to its class
353 /// hierarchy without reconstructing that hierarchy from declaration text:
354 /// subclasses are matched by resolved FQCN, so `extends \Ns\Base` and
355 /// aliased `use` forms are all found. Read-only; no input mutation.
356 pub fn subtype_files(&self, class_fqn: &str) -> Vec<Arc<str>> {
357 let db = self.snapshot_db();
358 let here = crate::db::Fqcn::from_str(&db, class_fqn);
359 crate::db::class_subtype_files(&db, here).to_vec()
360 }
361
362 /// Class-level issues (inheritance violations, abstract-method gaps, override
363 /// incompatibilities) for the given set of files.
364 ///
365 /// These checks are cross-file by nature and are not emitted by
366 /// [`crate::FileAnalyzer::analyze`]. Call this after ingesting or
367 /// re-analyzing a file and its dependents to get the full diagnostic picture.
368 ///
369 /// Circular-inheritance checks always run against the full workspace graph
370 /// regardless of the `files` filter — a cycle is a workspace-wide problem.
371 pub fn class_issues(&self, files: &[Arc<str>]) -> Vec<crate::Issue> {
372 let db = self.snapshot_db();
373 let file_set: HashSet<Arc<str>> = files.iter().cloned().collect();
374 // Read source texts through the snapshot already in hand — calling
375 // `source_of` here would re-enter the session RwLock while this
376 // snapshot is live, and a concurrent salsa write (which blocks new
377 // readers behind the fair write lock while waiting for existing
378 // snapshots to drop) turns that into a deadlock.
379 let file_data: Vec<(Arc<str>, Arc<str>)> = files
380 .iter()
381 .filter_map(|f| {
382 let sf = db.lookup_source_file(f)?;
383 Some((f.clone(), sf.text(&db as &dyn crate::db::MirDatabase)))
384 })
385 .collect();
386 crate::class::ClassAnalyzer::with_files(&db, file_set, &file_data).analyze_all()
387 }
388
389 /// All declarations defined in `file` as a **hierarchical tree**.
390 ///
391 /// Classes/interfaces/traits/enums are returned with their methods,
392 /// properties, and constants nested in `children`. Top-level functions
393 /// and constants are returned with empty `children`.
394 pub fn document_symbols(&self, file: &str) -> Vec<crate::symbol::DocumentSymbol> {
395 use crate::symbol::{DeclarationKind, DocumentSymbol};
396
397 let db = self.snapshot_db();
398 let Some(sf) = db.lookup_source_file(file) else {
399 return Vec::new();
400 };
401 let defs = crate::db::collect_file_definitions(&db, sf);
402 let mut out: Vec<DocumentSymbol> = Vec::new();
403
404 let class_children =
405 |methods: &indexmap::IndexMap<Arc<str>, Arc<mir_codebase::storage::MethodDef>>,
406 props: Option<&indexmap::IndexMap<Arc<str>, mir_codebase::storage::PropertyDef>>,
407 consts: &indexmap::IndexMap<Arc<str>, mir_codebase::storage::ConstantDef>,
408 is_enum: bool|
409 -> Vec<DocumentSymbol> {
410 let mut out: Vec<DocumentSymbol> = Vec::new();
411 for (_, m) in methods.iter() {
412 out.push(DocumentSymbol {
413 name: m.name.clone(),
414 kind: DeclarationKind::Method,
415 location: m.location.clone(),
416 children: Vec::new(),
417 });
418 }
419 if let Some(props) = props {
420 for (_, p) in props.iter() {
421 out.push(DocumentSymbol {
422 name: p.name.clone(),
423 kind: DeclarationKind::Property,
424 location: p.location.clone(),
425 children: Vec::new(),
426 });
427 }
428 }
429 let const_kind = if is_enum {
430 DeclarationKind::EnumCase
431 } else {
432 DeclarationKind::Constant
433 };
434 for (_, c) in consts.iter() {
435 out.push(DocumentSymbol {
436 name: c.name.clone(),
437 kind: const_kind,
438 location: c.location.clone(),
439 children: Vec::new(),
440 });
441 }
442 out
443 };
444
445 for c in defs.slice.classes.iter() {
446 out.push(DocumentSymbol {
447 name: c.fqcn.clone(),
448 kind: DeclarationKind::Class,
449 location: c.location.clone(),
450 children: class_children(
451 &c.own_methods,
452 Some(&c.own_properties),
453 &c.own_constants,
454 false,
455 ),
456 });
457 }
458 for i in defs.slice.interfaces.iter() {
459 out.push(DocumentSymbol {
460 name: i.fqcn.clone(),
461 kind: DeclarationKind::Interface,
462 location: i.location.clone(),
463 children: class_children(&i.own_methods, None, &i.own_constants, false),
464 });
465 }
466 for t in defs.slice.traits.iter() {
467 out.push(DocumentSymbol {
468 name: t.fqcn.clone(),
469 kind: DeclarationKind::Trait,
470 location: t.location.clone(),
471 children: class_children(
472 &t.own_methods,
473 Some(&t.own_properties),
474 &t.own_constants,
475 false,
476 ),
477 });
478 }
479 for e in defs.slice.enums.iter() {
480 let mut children = class_children(&e.own_methods, None, &e.own_constants, true);
481 for (_, case) in e.cases.iter() {
482 children.push(DocumentSymbol {
483 name: case.name.clone(),
484 kind: DeclarationKind::EnumCase,
485 location: case.location.clone(),
486 children: Vec::new(),
487 });
488 }
489 out.push(DocumentSymbol {
490 name: e.fqcn.clone(),
491 kind: DeclarationKind::Enum,
492 location: e.location.clone(),
493 children,
494 });
495 }
496 for f in defs.slice.functions.iter() {
497 out.push(DocumentSymbol {
498 name: f.fqn.clone(),
499 kind: DeclarationKind::Function,
500 location: f.location.clone(),
501 children: Vec::new(),
502 });
503 }
504 for (name, _) in defs.slice.constants.iter() {
505 out.push(DocumentSymbol {
506 name: name.clone(),
507 kind: DeclarationKind::Constant,
508 location: None,
509 children: Vec::new(),
510 });
511 }
512 out
513 }
514}