mir_analyzer/session/incremental.rs
1use super::*;
2
3impl AnalysisSession {
4 /// Retrieve the source text the session has registered for `file`, if
5 /// any. Returns `None` when the file has never been ingested. Used by
6 /// the parallel re-analysis path to re-feed dependents to body analysis without
7 /// the caller having to track sources independently.
8 pub fn source_of(&self, file: &str) -> Option<Arc<str>> {
9 let db = self.snapshot_db();
10 let sf = db.lookup_source_file(file)?;
11 Some(sf.text(&db).clone())
12 }
13
14 /// Re-analyze every transitive dependent of `file` in parallel.
15 ///
16 /// When the user saves a file that other files depend on (e.g. editing
17 /// a base class, an interface, or a trait), those dependents may have
18 /// new diagnostics. This method computes them in parallel using rayon
19 /// and returns the per-file analysis results so the LSP server can
20 /// publish updated diagnostics in one batch.
21 ///
22 /// Source text for dependents is retrieved from the session's salsa
23 /// inputs (set by previous `ingest_file` calls) — the caller doesn't
24 /// need to track or re-read files. Files for which the session has no
25 /// source are silently skipped (returns the analyzable subset).
26 ///
27 /// Cross-file inferred return types are resolved on demand via salsa.
28 pub fn reanalyze_dependents(&self, file: &str) -> Vec<(Arc<str>, crate::FileAnalysis)> {
29 self.reanalyze_dependents_cancellable(file, &crate::IndexCancel::new())
30 }
31
32 /// Cancellable variant of [`Self::reanalyze_dependents`].
33 ///
34 /// The consumer flips `cancel` (typically because a newer edit arrived) to
35 /// abandon the re-analysis; the flag is checked at each file boundary. Salsa
36 /// cannot unwind the plain-Rust body-analysis walk mid-flight, so a file
37 /// already in progress finishes, but no further files are started. Files
38 /// skipped due to cancellation are simply absent from the returned vec —
39 /// the consumer should drop a stale flag and start fresh work on each edit.
40 pub fn reanalyze_dependents_cancellable(
41 &self,
42 file: &str,
43 cancel: &crate::IndexCancel,
44 ) -> Vec<(Arc<str>, crate::FileAnalysis)> {
45 if cancel.is_cancelled() {
46 return Vec::new();
47 }
48
49 // Phase 1: compute dependents outside the analysis loop.
50 let dependents = self.dependency_graph().transitive_dependents(file);
51 if dependents.is_empty() {
52 return Vec::new();
53 }
54 let dependents: Vec<Arc<str>> = dependents
55 .into_iter()
56 .map(|path| Arc::from(path.as_str()))
57 .collect();
58 self.reanalyze_file_set(dependents, cancel)
59 }
60
61 /// Re-analyze an explicit file set — typically the editor's currently
62 /// open files — after an edit elsewhere in the workspace.
63 ///
64 /// This is the rust-analyzer diagnostics model: instead of computing the
65 /// edited file's transitive dependents (an O(all-ingested-files) graph
66 /// rebuild on every keystroke), the caller passes the handful of files it
67 /// actually publishes diagnostics for, and salsa memoization makes the
68 /// unaffected ones ~free — `analyze_file` re-validates each file's memo
69 /// against what actually changed and only re-executes bodies the edit
70 /// reaches. Per-edit cost is O(open files), independent of workspace size.
71 ///
72 /// Files the session has no source for are silently skipped. Cancellation
73 /// semantics match [`Self::reanalyze_dependents_cancellable`].
74 pub fn reanalyze_files_cancellable(
75 &self,
76 files: &[Arc<str>],
77 cancel: &crate::IndexCancel,
78 ) -> Vec<(Arc<str>, crate::FileAnalysis)> {
79 if cancel.is_cancelled() || files.is_empty() {
80 return Vec::new();
81 }
82 self.settle_workspace_index();
83 self.reanalyze_file_set(files.to_vec(), cancel)
84 }
85
86 /// Shared body of [`Self::reanalyze_dependents_cancellable`] and
87 /// [`Self::reanalyze_files_cancellable`]: warm up, analyze in parallel,
88 /// commit reference locations.
89 fn reanalyze_file_set(
90 &self,
91 files: Vec<Arc<str>>,
92 cancel: &crate::IndexCancel,
93 ) -> Vec<(Arc<str>, crate::FileAnalysis)> {
94 use rayon::prelude::*;
95
96 let dependents = files;
97
98 // Phase 2a: fault in each dependent's direct class references if the
99 // background indexer hasn't reached them yet (mirrors the FileAnalyzer
100 // warm-up behavior, avoiding transient false `UndefinedClass` during
101 // index warm-up).
102 //
103 // This runs SERIALLY and *before* the parallel analyze loop below:
104 // `prepare_ast_for_analysis` resolves and loads classes, and loading
105 // mutates the shared session salsa storage (`load_class` →
106 // `ingest_file` sets salsa inputs). Salsa input mutation cancels and
107 // blocks until every other database handle is released, so it must run
108 // with NO live snapshot in scope:
109 //
110 // - in parallel (the v0.37.0 regression), sibling rayon workers held
111 // live snapshot clones mid-`analyze_file`, so the first warm-up
112 // write blocked on them forever — under high dependent fan-out this
113 // deadlocked the whole runtime; and
114 // - even serially, a snapshot held across the loop (e.g. one taken to
115 // parse the dependents) blocks the very first write.
116 //
117 // `prepare_file_for_analysis` takes a *scoped* snapshot to fetch the
118 // parsed AST, drops it (the `Arc<ParseResult>` is owned), and only
119 // then warms up. Files already prepared against their current text
120 // skip the parse + AST walk entirely — hosts on the
121 // `ingest_file_prepared` write path pre-pay this per edit, making the
122 // whole loop a map-lookup sweep.
123 {
124 // One revision bump for the whole warm-up sweep instead of one per
125 // lazily-loaded class (each bump cancels every in-flight salsa
126 // reader). Closed before `commit_gen` is read below, so commits
127 // are stamped with the post-load generation as before.
128 let _deferred_bumps = self.defer_revision_bumps();
129 for file in &dependents {
130 if cancel.is_cancelled() {
131 return Vec::new();
132 }
133 self.prepare_file_for_analysis(file);
134 }
135 }
136
137 // Phase 2b: drive each dependent through the `analyze_file` tracked
138 // query in parallel. Salsa's memo validation does the real work
139 // here: after a body-only edit, a dependent whose tracked inputs are
140 // structurally unchanged (`FileDefinitions` backdating) returns its
141 // cached output without re-running body analysis — re-analysis cost
142 // scales with what actually changed, not with dependent count.
143 //
144 // The snapshot is taken AFTER the warm-up above so each worker observes
145 // the freshly-loaded classes. This loop is read-only on salsa: no
146 // worker mutates inputs, so the snapshots never contend on a write.
147 //
148 // Dependents' `FileAnalysis::symbols` are empty on this path:
149 // per-expression symbols are intentionally not memoized (a typical
150 // file resolves thousands; caching them balloons memory), and
151 // diagnostics consumers don't read them. Hover / go-to-definition
152 // flows analyze the open file directly via [`crate::FileAnalyzer`].
153 //
154 // Each worker short-circuits when cancellation has been requested.
155 // Generation before the snapshot: a file add racing the sweep leaves
156 // the commits stale (self-healing), never wrongly fresh.
157 let commit_gen = self.index_generation();
158 // Freeze on the pass-scoped snapshot: warm-up (2a) completed every
159 // lazy load, and a concurrent index write cancels the pass, so the
160 // frozen view is never stale. Same discipline as the batch body pass.
161 let mut db_main = self.snapshot_db();
162 db_main.freeze_workspace_index();
163 // Sweeps are the steady-state population path for the mention index:
164 // every analyzed file gets a current mention scan alongside its
165 // postings, so later reference-gate checks are set lookups.
166 let mention_scanner = db_main.class_mention_scanner();
167 type Analyzed = (
168 Arc<str>,
169 Arc<str>,
170 std::sync::Arc<crate::db::AnalyzeOutput>,
171 Vec<crate::db::SubtypeEntry>,
172 Option<super::RefCachePut>,
173 Option<Box<[mir_types::Name]>>,
174 );
175 let mut results: Vec<Analyzed> = dependents
176 .into_par_iter()
177 .map_with(db_main, |db, file| {
178 if cancel.is_cancelled() {
179 return None;
180 }
181 let sf = db.lookup_source_file(file.as_ref())?;
182 // Capture the text the analysis ran against: the freshness
183 // marks below must record exactly this Arc, so a text write
184 // racing the sweep leaves the file dirty rather than
185 // wrongly marked fresh.
186 let text = sf.text(&*db as &dyn crate::db::MirDatabase).clone();
187 let out = crate::db::analyze_file(&*db as &dyn crate::db::MirDatabase, sf).clone();
188 let defs =
189 crate::db::collect_file_definitions(&*db as &dyn crate::db::MirDatabase, sf);
190 let entries = crate::db::subtype_index::entries_from_slice(&defs.slice);
191 // Stage the disk-cache write only when the postings commit
192 // below will actually rewrite — a no-op re-sweep (current
193 // commit) adds no hashing or parse-walk cost per file.
194 let put = if self.ref_commit_is_current(file.as_ref(), &text, &out) {
195 None
196 } else {
197 self.stage_ref_cache_put(
198 &*db as &dyn crate::db::MirDatabase,
199 sf,
200 file.as_ref(),
201 &text,
202 &out,
203 )
204 };
205 let mentions = mention_scanner.as_ref().and_then(|s| {
206 (!db.class_mentions_current(file.as_ref(), &text, s.epoch()))
207 .then(|| s.scan(&text))
208 });
209 Some((file, text, out, entries, put, mentions))
210 })
211 .flatten()
212 .collect();
213
214 // Serial commit: each dependent's output is its complete reference
215 // set, so replace rather than append. Both inverted indexes and their
216 // freshness marks update here — this is what keeps read queries
217 // lookup-shaped instead of re-validating every candidate memo.
218 // Unchanged files (same text, same memoized output) skip the rebuild
219 // entirely, so a no-op re-sweep is a pointer compare per file.
220 {
221 let guard = self.db.salsa.read();
222 let mut dependency_graph_changed = false;
223 for (file, text, out, entries, put, mentions) in results.iter_mut() {
224 // Pointer-identical memo ⇒ identical postings: skip the
225 // index rewrite. The mark is re-stamped unconditionally so a
226 // no-op sweep still advances the commit's generation.
227 if !self.ref_commit_is_current(file.as_ref(), text, out) {
228 guard.set_file_reference_locations(file.as_ref(), out.ref_locs.to_vec());
229 dependency_graph_changed = true;
230 }
231 if let (Some(s), Some(m)) = (&mention_scanner, mentions.take()) {
232 guard.set_file_class_mentions(file, text, s.epoch(), m);
233 }
234 if let Some(put) = put.take() {
235 self.apply_ref_cache_put(file.as_ref(), out, put);
236 }
237 self.mark_ref_committed(
238 file,
239 text,
240 Some(out),
241 commit_gen,
242 !out.has_unresolved_names(),
243 );
244 if !self.is_defs_committed(file.as_ref(), text) {
245 guard.set_file_class_edges(file, entries.clone());
246 self.mark_defs_committed(file, text);
247 dependency_graph_changed = true;
248 }
249 }
250 if dependency_graph_changed {
251 self.clear_dependency_graph_cache();
252 }
253 }
254
255 results
256 .into_iter()
257 .map(|(file, _, out, _, _, _)| {
258 (
259 file,
260 crate::FileAnalysis {
261 issues: out.issues.to_vec(),
262 symbols: Vec::new(),
263 },
264 )
265 })
266 .collect()
267 }
268
269 /// FQCNs that `file` imports via `use` statements but that aren't yet
270 /// loaded in the session.
271 ///
272 /// Designed as the input to background prefetching: after the LSP server
273 /// Return the `use`-import alias map for a file: a list of `(alias, fqcn)`
274 /// pairs where `alias` is the local name (e.g. `"Str"`) and `fqcn` is the
275 /// fully-qualified name (e.g. `"Illuminate\\Support\\Str"`).
276 ///
277 /// Completion handlers can use this to expand a short class name written
278 /// before `::` into its FQN before looking up static members, mirroring the
279 /// same alias expansion that go-to-definition already performs via
280 /// `symbol_at` + `definition_of`.
281 ///
282 /// Returns an empty Vec if the file has not been ingested or has no use
283 /// imports.
284 pub fn class_imports(&self, file: &str) -> Vec<(Arc<str>, Arc<str>)> {
285 let db = self.snapshot_db();
286 let imports = db.file_class_imports(file);
287 imports
288 .iter()
289 .map(|(alias, fqcn)| (Arc::from(alias.as_str()), Arc::from(fqcn.as_str())))
290 .collect()
291 }
292
293 /// ingests an open buffer, it can call this and lazy-load the returned
294 /// FQCNs on a worker thread so the user's first Cmd+Click into vendor
295 /// code doesn't pay the file-read+parse cost.
296 ///
297 /// Returns an empty Vec if the file hasn't been ingested or has no
298 /// unresolved imports.
299 pub fn pending_lazy_loads(&self, file: &str) -> Vec<Arc<str>> {
300 use rustc_hash::FxHashSet;
301
302 let db = self.snapshot_db();
303 let imports = db.file_imports(file);
304 if imports.is_empty() {
305 return Vec::new();
306 }
307 let mut out = Vec::new();
308 let mut seen: FxHashSet<Arc<str>> = FxHashSet::default();
309 for fqcn in imports.values() {
310 let here = crate::db::Fqcn::interned(&db, *fqcn);
311 if crate::db::find_class_like(&db, here).is_some() {
312 continue;
313 }
314 if let Some(resolver) = &self.resolver {
315 let fqcn_arc: Arc<str> = Arc::from(fqcn.as_str());
316 if !seen.insert(fqcn_arc.clone()) {
317 continue;
318 }
319 if resolver.resolve(fqcn.as_str()).is_some() {
320 out.push(fqcn_arc);
321 }
322 }
323 }
324 out
325 }
326
327 /// Convenience: synchronously lazy-load every import of `file` that
328 /// isn't already in the codebase. Returns the number successfully loaded.
329 ///
330 /// For non-blocking prefetch, call this from a worker thread:
331 ///
332 /// ```ignore
333 /// let s = session.clone(); // AnalysisSession is wrapped in Arc by callers
334 /// std::thread::spawn(move || {
335 /// s.prefetch_imports(&file_path);
336 /// });
337 /// ```
338 ///
339 /// Uses a single shared-visited two-tier BFS across all pending imports
340 /// (see [`Self::load_classes_transitive_bounded`]) with a shallow depth so
341 /// member access on imported types type-checks without pulling in the
342 /// entire vendor tree.
343 pub fn prefetch_imports(&self, file: &str) -> usize {
344 let pending = self.pending_lazy_loads(file);
345 if pending.is_empty() {
346 return 0;
347 }
348 // Fault in each imported FQCN directly (single-file load + tier-merge).
349 // Inheritance ancestors / signature types resolve through the eagerly
350 // built workspace symbol index — no transitive walk needed here.
351 let _deferred_bumps = self.defer_revision_bumps();
352 let mut loaded = 0;
353 for fqcn in &pending {
354 if self.load_class(fqcn.as_ref()).is_loaded() {
355 loaded += 1;
356 }
357 }
358 loaded
359 }
360
361 /// All class / interface / trait / enum FQCNs currently known to the
362 /// session, each paired with the file that defines them when available.
363 ///
364 /// Use this to build workspace-wide views (outline, fuzzy search, etc.).
365 /// Consumers implement their own search/match logic on top — the analyzer
366 /// only exposes the iterator.
367 pub fn all_classes(&self) -> Vec<(Arc<str>, Option<mir_types::Location>)> {
368 let db = self.snapshot_db();
369 crate::db::workspace_classes(&db)
370 .iter()
371 .filter_map(|fqcn| {
372 let here = crate::db::Fqcn::from_str(&db, fqcn.as_str());
373 crate::db::find_class_like(&db, here)
374 .map(|class| (Arc::<str>::from(fqcn.as_str()), class.location().cloned()))
375 })
376 .collect()
377 }
378
379 /// All global function FQNs currently known to the session, each paired
380 /// with their declaration location when available.
381 pub fn all_functions(&self) -> Vec<(Arc<str>, Option<mir_types::Location>)> {
382 let db = self.snapshot_db();
383 crate::db::workspace_functions(&db)
384 .iter()
385 .filter_map(|fqn| {
386 let here = crate::db::Fqcn::from_str(&db, fqn.as_str());
387 crate::db::find_function(&db, here)
388 .map(|f| (Arc::<str>::from(fqn.as_str()), f.location.clone()))
389 })
390 .collect()
391 }
392
393 /// Every class/interface/trait/enum in the workspace whose *own* short
394 /// (unqualified) name is exactly `short_name`, each paired with its FQCN
395 /// and declaration location.
396 ///
397 /// Compatibility helper for callers that surface a bare class name without
398 /// first resolving it to a canonical FQCN.
399 pub fn classes_named(&self, short_name: &str) -> Vec<(Arc<str>, Option<mir_types::Location>)> {
400 let db = self.snapshot_db();
401 let key = mir_types::Name::new(short_name).ascii_lowercase();
402 crate::db::workspace_index(&db)
403 .class_likes_named(key)
404 .iter()
405 .copied()
406 .filter_map(|fqcn_key| {
407 let here = crate::db::Fqcn::from_str(&db, fqcn_key.as_str());
408 crate::db::find_class_like(&db, here)
409 .map(|class| (class.fqcn().clone(), class.location().cloned()))
410 })
411 .collect()
412 }
413
414 /// Every ancestor of `fqcn` — extended class, implemented interfaces,
415 /// used traits, transitively — most-derived first. Does not include
416 /// `fqcn` itself. Memoized per FQCN (shares `class_ancestors_by_fqcn`'s
417 /// tracked cache with member-resolution paths).
418 pub fn ancestors_of(&self, fqcn: &str) -> Vec<Arc<str>> {
419 let db = self.snapshot_db();
420 let here = crate::db::Fqcn::from_str(&db, fqcn.trim_start_matches('\\'));
421 crate::db::class_ancestors_by_fqcn(&db, here)
422 .iter()
423 .skip(1)
424 .cloned()
425 .collect()
426 }
427
428 /// Full signature for a global function, resolved by FQN.
429 pub fn function_signature(&self, fqn: &str) -> Option<Arc<crate::FunctionDef>> {
430 let db = self.snapshot_db();
431 let here = crate::db::Fqcn::from_str(&db, fqn);
432 crate::db::find_function(&db, here)
433 }
434
435 /// Compute `file`'s outgoing dependency edges and persist them to the
436 /// disk cache's reverse-dep graph (if configured). The in-memory graph
437 /// is no longer maintained imperatively: `dependency_graph()` derives
438 /// structural symbols from the memoized [`crate::db::file_structural_symbols`]
439 /// tracked query, then projects them through the workspace symbol index
440 /// alongside body-reference symbols.
441 pub(super) fn update_reverse_deps_for(&self, file: &str) {
442 if let Some(cache) = self.cache.as_deref() {
443 let db = self.snapshot_db();
444 let targets = file_outgoing_dependencies(&db, file, true);
445 cache.update_reverse_deps_for_file(file, &targets);
446 }
447 }
448
449 /// File dependency graph: which files depend on which other files.
450 /// Used for incremental invalidation in LSP servers and build systems.
451 ///
452 /// File dependency graph: which files depend on which other files.
453 /// Used for incremental invalidation in LSP servers and build systems.
454 ///
455 /// O(edges) — iterates symbol edges from the reference index and from
456 /// `file_structural_symbols`, then resolves each symbol to its defining
457 /// file via O(1) lookup. Total cost is O(E) where E is the number of
458 /// (file, symbol) edges.
459 pub fn dependency_graph(&self) -> crate::DependencyGraph {
460 if let Some(graph) = self.dependency_graph_cache.read().as_ref() {
461 return graph.clone();
462 }
463 let db = self.snapshot_db();
464
465 fn push_edge(
466 dependencies: &mut [Vec<u32>],
467 dependents: &mut [Vec<u32>],
468 from: u32,
469 to: u32,
470 ) {
471 if from == to {
472 return;
473 }
474 dependents[to as usize].push(from);
475 dependencies[from as usize].push(to);
476 }
477
478 fn sort_and_dedup(adjacency: &mut [Vec<u32>]) {
479 for deps in adjacency {
480 deps.sort();
481 deps.dedup();
482 }
483 }
484
485 fn symbol_defining_file_id(
486 db: &dyn crate::db::MirDatabase,
487 file_ids: &HashMap<Arc<str>, u32>,
488 symbol_key: &str,
489 ) -> Option<u32> {
490 let lookup = crate::defining_file_lookup_key(symbol_key);
491 db.symbol_defining_file(lookup)
492 .and_then(|file| file_ids.get(file.as_ref()).copied())
493 }
494
495 let mut all_files: Vec<Arc<str>> = db.source_file_paths().to_vec();
496 all_files.sort();
497 assert!(
498 u32::try_from(all_files.len()).is_ok(),
499 "dependency graph file ids require at most u32::MAX files"
500 );
501 let file_ids: HashMap<Arc<str>, u32> = all_files
502 .iter()
503 .enumerate()
504 .map(|(id, file)| (file.clone(), id as u32))
505 .collect();
506
507 let mut dependencies = vec![Vec::new(); all_files.len()];
508 let mut dependents = vec![Vec::new(); all_files.len()];
509 for (file_id, file) in all_files.iter().enumerate() {
510 let file_id = file_id as u32;
511 let mut file_deps: HashSet<u32> = HashSet::default();
512
513 // O(degree(file)) — forward reference-index lookup, no full-table scan.
514 for symbol_key in db.file_referenced_symbols(file.as_ref()) {
515 if let Some(def_id) = symbol_defining_file_id(&db, &file_ids, &symbol_key) {
516 file_deps.insert(def_id);
517 }
518 }
519
520 // Declaration-level symbol edges from Salsa. These cover imports,
521 // class hierarchy edges, and type-hint-only references that never
522 // appear in file_referenced_symbols.
523 if let Some(sf) = db.lookup_source_file(file.as_ref()) {
524 for symbol in crate::db::file_structural_symbols(&db, sf).iter() {
525 if let Some(def_id) = symbol_defining_file_id(&db, &file_ids, symbol) {
526 file_deps.insert(def_id);
527 }
528 }
529 }
530
531 for dep_id in file_deps {
532 push_edge(&mut dependencies, &mut dependents, file_id, dep_id);
533 }
534 }
535
536 sort_and_dedup(&mut dependents);
537 sort_and_dedup(&mut dependencies);
538
539 // Augment with stale dependents: files referencing symbols that were
540 // deleted from their defining file. These edges disappear from the
541 // symbol_defining_file lookup but the referencing file still needs
542 // re-analysis to surface the now-broken reference.
543 {
544 let stale = self.stale_defined_symbols.read();
545 if !stale.is_empty() {
546 for (file, deleted_syms) in stale.iter() {
547 let Some(&file_id) = file_ids.get(file.as_str()) else {
548 continue;
549 };
550 for sym in deleted_syms {
551 let lookup = crate::defining_file_lookup_key(sym);
552 // `defined_symbols()` only yields top-level FQ names
553 // (classes/interfaces/traits/enums, functions, global
554 // constants) — never knows here which kind `sym` was,
555 // so probe every prefix the reference index actually
556 // uses (see `Name::codebase_key`) rather than guessing
557 // one and silently missing referencers of the others.
558 for prefix in ["cls:", "fn:", "gcnst:"] {
559 for referencing_file in
560 db.symbol_referencers_of(&format!("{prefix}{lookup}"))
561 {
562 if let Some(&ref_id) = file_ids.get(referencing_file.as_ref()) {
563 push_edge(&mut dependencies, &mut dependents, ref_id, file_id);
564 }
565 }
566 }
567 }
568 }
569 // Re-sort and dedup since we may have added entries.
570 sort_and_dedup(&mut dependents);
571 sort_and_dedup(&mut dependencies);
572 }
573 }
574
575 let graph = crate::DependencyGraph::from_compact_parts(
576 all_files,
577 file_ids,
578 dependencies,
579 dependents,
580 );
581 *self.dependency_graph_cache.write() = Some(graph.clone());
582 graph
583 }
584}