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))
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.reanalyze_file_set(files.to_vec(), cancel)
83 }
84
85 /// Shared body of [`Self::reanalyze_dependents_cancellable`] and
86 /// [`Self::reanalyze_files_cancellable`]: warm up, analyze in parallel,
87 /// commit reference locations.
88 fn reanalyze_file_set(
89 &self,
90 files: Vec<Arc<str>>,
91 cancel: &crate::IndexCancel,
92 ) -> Vec<(Arc<str>, crate::FileAnalysis)> {
93 use rayon::prelude::*;
94
95 let dependents = files;
96
97 // Phase 2a: fault in each dependent's direct class references if the
98 // background indexer hasn't reached them yet (mirrors the FileAnalyzer
99 // warm-up behavior, avoiding transient false `UndefinedClass` during
100 // index warm-up).
101 //
102 // This runs SERIALLY and *before* the parallel analyze loop below:
103 // `prepare_ast_for_analysis` resolves and loads classes, and loading
104 // mutates the shared session salsa storage (`load_class` →
105 // `ingest_file` sets salsa inputs). Salsa input mutation cancels and
106 // blocks until every other database handle is released, so it must run
107 // with NO live snapshot in scope:
108 //
109 // - in parallel (the v0.37.0 regression), sibling rayon workers held
110 // live snapshot clones mid-`analyze_file`, so the first warm-up
111 // write blocked on them forever — under high dependent fan-out this
112 // deadlocked the whole runtime; and
113 // - even serially, a snapshot held across the loop (e.g. one taken to
114 // parse the dependents) blocks the very first write.
115 //
116 // `prepare_file_for_analysis` takes a *scoped* snapshot to fetch the
117 // parsed AST, drops it (the `Arc<ParseResult>` is owned), and only
118 // then warms up. Files already prepared against their current text
119 // skip the parse + AST walk entirely — hosts on the
120 // `ingest_file_prepared` write path pre-pay this per edit, making the
121 // whole loop a map-lookup sweep.
122 for file in &dependents {
123 if cancel.is_cancelled() {
124 return Vec::new();
125 }
126 self.prepare_file_for_analysis(file);
127 }
128
129 // Phase 2b: drive each dependent through the `analyze_file` tracked
130 // query in parallel. Salsa's memo validation does the real work
131 // here: after a body-only edit, a dependent whose tracked inputs are
132 // structurally unchanged (`FileDefinitions` backdating) returns its
133 // cached output without re-running body analysis — re-analysis cost
134 // scales with what actually changed, not with dependent count.
135 //
136 // The snapshot is taken AFTER the warm-up above so each worker observes
137 // the freshly-loaded classes. This loop is read-only on salsa: no
138 // worker mutates inputs, so the snapshots never contend on a write.
139 //
140 // Dependents' `FileAnalysis::symbols` are empty on this path:
141 // per-expression symbols are intentionally not memoized (a typical
142 // file resolves thousands; caching them balloons memory), and
143 // diagnostics consumers don't read them. Hover / go-to-definition
144 // flows analyze the open file directly via [`crate::FileAnalyzer`].
145 //
146 // Each worker short-circuits when cancellation has been requested.
147 // Generation before the snapshot: a file add racing the sweep leaves
148 // the commits stale (self-healing), never wrongly fresh.
149 let commit_gen = self.index_generation();
150 let db_main = self.snapshot_db();
151 type Analyzed = (
152 Arc<str>,
153 Arc<str>,
154 std::sync::Arc<crate::db::AnalyzeOutput>,
155 Vec<crate::db::SubtypeEntry>,
156 );
157 let results: Vec<Analyzed> = dependents
158 .into_par_iter()
159 .map_with(db_main, |db, file| {
160 if cancel.is_cancelled() {
161 return None;
162 }
163 let sf = db.lookup_source_file(file.as_ref())?;
164 // Capture the text the analysis ran against: the freshness
165 // marks below must record exactly this Arc, so a text write
166 // racing the sweep leaves the file dirty rather than
167 // wrongly marked fresh.
168 let text = sf.text(&*db as &dyn crate::db::MirDatabase);
169 let out = crate::db::analyze_file(&*db as &dyn crate::db::MirDatabase, sf);
170 let defs =
171 crate::db::collect_file_definitions(&*db as &dyn crate::db::MirDatabase, sf);
172 let entries = crate::db::subtype_index::entries_from_slice(&defs.slice);
173 Some((file, text, out, entries))
174 })
175 .flatten()
176 .collect();
177
178 // Serial commit: each dependent's output is its complete reference
179 // set, so replace rather than append. Both inverted indexes and their
180 // freshness marks update here — this is what keeps read queries
181 // lookup-shaped instead of re-validating every candidate memo.
182 // Unchanged files (same text, same memoized output) skip the rebuild
183 // entirely, so a no-op re-sweep is a pointer compare per file.
184 {
185 let guard = self.db.salsa.read();
186 for (file, text, out, entries) in &results {
187 // Pointer-identical memo ⇒ identical postings: skip the
188 // index rewrite. The mark is re-stamped unconditionally so a
189 // no-op sweep still advances the commit's generation.
190 if !self.ref_commit_is_current(file.as_ref(), text, out) {
191 guard.set_file_reference_locations(file.as_ref(), out.ref_locs.to_vec());
192 }
193 self.mark_ref_committed(
194 file,
195 text,
196 Some(out),
197 commit_gen,
198 !out.has_unresolved_names(),
199 );
200 if !self.is_defs_committed(file.as_ref(), text) {
201 guard.set_file_class_edges(file, entries.clone());
202 self.mark_defs_committed(file, text);
203 }
204 }
205 }
206
207 results
208 .into_iter()
209 .map(|(file, _, out, _)| {
210 (
211 file,
212 crate::FileAnalysis {
213 issues: out.issues.to_vec(),
214 symbols: Vec::new(),
215 },
216 )
217 })
218 .collect()
219 }
220
221 /// FQCNs that `file` imports via `use` statements but that aren't yet
222 /// loaded in the session.
223 ///
224 /// Designed as the input to background prefetching: after the LSP server
225 /// Return the `use`-import alias map for a file: a list of `(alias, fqcn)`
226 /// pairs where `alias` is the local name (e.g. `"Str"`) and `fqcn` is the
227 /// fully-qualified name (e.g. `"Illuminate\\Support\\Str"`).
228 ///
229 /// Completion handlers can use this to expand a short class name written
230 /// before `::` into its FQN before looking up static members, mirroring the
231 /// same alias expansion that go-to-definition already performs via
232 /// `symbol_at` + `definition_of`.
233 ///
234 /// Returns an empty Vec if the file has not been ingested or has no use
235 /// imports.
236 pub fn class_imports(&self, file: &str) -> Vec<(Arc<str>, Arc<str>)> {
237 let db = self.snapshot_db();
238 let imports = db.file_class_imports(file);
239 imports
240 .iter()
241 .map(|(alias, fqcn)| (Arc::from(alias.as_str()), Arc::from(fqcn.as_str())))
242 .collect()
243 }
244
245 /// ingests an open buffer, it can call this and lazy-load the returned
246 /// FQCNs on a worker thread so the user's first Cmd+Click into vendor
247 /// code doesn't pay the file-read+parse cost.
248 ///
249 /// Returns an empty Vec if the file hasn't been ingested or has no
250 /// unresolved imports.
251 pub fn pending_lazy_loads(&self, file: &str) -> Vec<Arc<str>> {
252 let db = self.snapshot_db();
253 let imports = db.file_imports(file);
254 if imports.is_empty() {
255 return Vec::new();
256 }
257 let mut out = Vec::new();
258 for fqcn in imports.values() {
259 let here = crate::db::Fqcn::new(&db, *fqcn);
260 if crate::db::find_class_like(&db, here).is_some() {
261 continue;
262 }
263 if let Some(resolver) = &self.resolver {
264 if resolver.resolve(fqcn.as_str()).is_some() {
265 out.push(Arc::from(fqcn.as_str()));
266 }
267 }
268 }
269 out
270 }
271
272 /// Convenience: synchronously lazy-load every import of `file` that
273 /// isn't already in the codebase. Returns the number successfully loaded.
274 ///
275 /// For non-blocking prefetch, call this from a worker thread:
276 ///
277 /// ```ignore
278 /// let s = session.clone(); // AnalysisSession is wrapped in Arc by callers
279 /// std::thread::spawn(move || {
280 /// s.prefetch_imports(&file_path);
281 /// });
282 /// ```
283 ///
284 /// Uses a single shared-visited two-tier BFS across all pending imports
285 /// (see [`Self::load_classes_transitive_bounded`]) with a shallow depth so
286 /// member access on imported types type-checks without pulling in the
287 /// entire vendor tree.
288 pub fn prefetch_imports(&self, file: &str) -> usize {
289 let pending = self.pending_lazy_loads(file);
290 if pending.is_empty() {
291 return 0;
292 }
293 // Fault in each imported FQCN directly (single-file load + tier-merge).
294 // Inheritance ancestors / signature types resolve through the eagerly
295 // built workspace symbol index — no transitive walk needed here.
296 let mut loaded = 0;
297 for fqcn in &pending {
298 if self.load_class(fqcn.as_ref()).is_loaded() {
299 loaded += 1;
300 }
301 }
302 loaded
303 }
304
305 /// All class / interface / trait / enum FQCNs currently known to the
306 /// session, each paired with the file that defines them when available.
307 ///
308 /// Use this to build workspace-wide views (outline, fuzzy search, etc.).
309 /// Consumers implement their own search/match logic on top — the analyzer
310 /// only exposes the iterator.
311 pub fn all_classes(&self) -> Vec<(Arc<str>, Option<mir_types::Location>)> {
312 let db = self.snapshot_db();
313 crate::db::workspace_classes(&db)
314 .iter()
315 .filter_map(|fqcn| {
316 let here = crate::db::Fqcn::from_str(&db, fqcn.as_ref());
317 crate::db::find_class_like(&db, here)
318 .map(|class| (fqcn.clone(), class.location().cloned()))
319 })
320 .collect()
321 }
322
323 /// All global function FQNs currently known to the session, each paired
324 /// with their declaration location when available.
325 pub fn all_functions(&self) -> Vec<(Arc<str>, Option<mir_types::Location>)> {
326 let db = self.snapshot_db();
327 crate::db::workspace_functions(&db)
328 .iter()
329 .filter_map(|fqn| {
330 let here = crate::db::Fqcn::from_str(&db, fqn.as_ref());
331 crate::db::find_function(&db, here).map(|f| (fqn.clone(), f.location.clone()))
332 })
333 .collect()
334 }
335
336 /// Compute `file`'s outgoing dependency edges and persist them to the
337 /// disk cache's reverse-dep graph (if configured). The in-memory graph
338 /// is no longer maintained imperatively: `dependency_graph()` derives
339 /// structural edges from the memoized [`crate::db::file_structural_deps`]
340 /// tracked query, so there is no second copy to drift out of sync.
341 pub(super) fn update_reverse_deps_for(&self, file: &str) {
342 if let Some(cache) = self.cache.as_deref() {
343 let db = self.snapshot_db();
344 let targets = file_outgoing_dependencies(&db, file, true);
345 cache.update_reverse_deps_for_file(file, &targets);
346 }
347 }
348
349 /// File dependency graph: which files depend on which other files.
350 /// Used for incremental invalidation in LSP servers and build systems.
351 ///
352 /// File dependency graph: which files depend on which other files.
353 /// Used for incremental invalidation in LSP servers and build systems.
354 ///
355 /// O(edges) — iterates the `file_references` forward index (file → symbol
356 /// keys it references) which is always current, then resolves each symbol
357 /// to its defining file via O(1) lookup. Total cost is O(E) where E is the
358 /// number of (file, symbol) reference edges, vs. the old O(F × S × R) scan.
359 pub fn dependency_graph(&self) -> crate::DependencyGraph {
360 let db = self.snapshot_db();
361
362 let all_files: Vec<String> = db
363 .source_file_paths()
364 .iter()
365 .map(|f| f.as_ref().to_string())
366 .collect();
367
368 let mut dependencies: HashMap<String, Vec<String>> = HashMap::default();
369 let mut dependents: HashMap<String, Vec<String>> = HashMap::default();
370
371 for file in &all_files {
372 // O(degree(file)) — forward index lookup, no full-table scan.
373 let symbol_keys = db.file_referenced_symbols(file);
374 let mut file_deps: HashSet<String> = HashSet::default();
375 for symbol_key in &symbol_keys {
376 let lookup = crate::defining_file_lookup_key(symbol_key);
377 if let Some(def_file) = db.symbol_defining_file(lookup) {
378 let def = def_file.as_ref().to_string();
379 if &def != file {
380 file_deps.insert(def);
381 }
382 }
383 }
384 for dep in &file_deps {
385 dependents
386 .entry(dep.clone())
387 .or_default()
388 .push(file.clone());
389 dependencies
390 .entry(file.clone())
391 .or_default()
392 .push(dep.clone());
393 }
394 }
395
396 // Merge structural deps derived from definition collection. The
397 // forward pass above only captures bare-FQN references recorded
398 // during body analysis; `file_structural_deps` covers imports, class
399 // hierarchy (extends/implements/use), and type-hint-only references
400 // that never appear in file_referenced_symbols. The query is salsa-
401 // memoized, so the warm rebuild costs one map lookup per file rather
402 // than a definition walk — and there is no imperatively-maintained
403 // reverse map to drift out of sync with the definitions.
404 for file in &all_files {
405 let Some(sf) = db.lookup_source_file(file) else {
406 continue;
407 };
408 for target in crate::db::file_structural_deps(&db, sf).iter() {
409 let target = target.as_ref().to_string();
410 if &target != file {
411 dependents
412 .entry(target.clone())
413 .or_default()
414 .push(file.clone());
415 dependencies.entry(file.clone()).or_default().push(target);
416 }
417 }
418 }
419
420 for deps in dependents.values_mut() {
421 deps.sort();
422 deps.dedup();
423 }
424 for deps in dependencies.values_mut() {
425 deps.sort();
426 deps.dedup();
427 }
428
429 // Augment with stale dependents: files referencing symbols that were
430 // deleted from their defining file. These edges disappear from the
431 // symbol_defining_file lookup but the referencing file still needs
432 // re-analysis to surface the now-broken reference.
433 {
434 let stale = self.stale_defined_symbols.read();
435 if !stale.is_empty() {
436 for (file, deleted_syms) in stale.iter() {
437 for sym in deleted_syms {
438 let lookup = crate::defining_file_lookup_key(sym);
439 // `defined_symbols()` only yields top-level FQ names
440 // (classes/interfaces/traits/enums, functions, global
441 // constants) — never knows here which kind `sym` was,
442 // so probe every prefix the reference index actually
443 // uses (see `Name::codebase_key`) rather than guessing
444 // one and silently missing referencers of the others.
445 for prefix in ["cls:", "fn:", "gcnst:"] {
446 for referencing_file in
447 db.symbol_referencers_of(&format!("{prefix}{lookup}"))
448 {
449 let ref_file = referencing_file.as_ref().to_string();
450 if &ref_file != file {
451 dependents
452 .entry(file.clone())
453 .or_default()
454 .push(ref_file.clone());
455 dependencies.entry(ref_file).or_default().push(file.clone());
456 }
457 }
458 }
459 }
460 }
461 // Re-sort and dedup since we may have added entries.
462 for deps in dependents.values_mut() {
463 deps.sort();
464 deps.dedup();
465 }
466 for deps in dependencies.values_mut() {
467 deps.sort();
468 deps.dedup();
469 }
470 }
471 }
472
473 crate::DependencyGraph {
474 dependencies,
475 dependents,
476 }
477 }
478}