Skip to main content

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            for (file, text, out, entries, put, mentions) in results.iter_mut() {
223                // Pointer-identical memo ⇒ identical postings: skip the
224                // index rewrite. The mark is re-stamped unconditionally so a
225                // no-op sweep still advances the commit's generation.
226                if !self.ref_commit_is_current(file.as_ref(), text, out) {
227                    guard.set_file_reference_locations(file.as_ref(), out.ref_locs.to_vec());
228                }
229                if let (Some(s), Some(m)) = (&mention_scanner, mentions.take()) {
230                    guard.set_file_class_mentions(file, text, s.epoch(), m);
231                }
232                if let Some(put) = put.take() {
233                    self.apply_ref_cache_put(file.as_ref(), out, put);
234                }
235                self.mark_ref_committed(
236                    file,
237                    text,
238                    Some(out),
239                    commit_gen,
240                    !out.has_unresolved_names(),
241                );
242                if !self.is_defs_committed(file.as_ref(), text) {
243                    guard.set_file_class_edges(file, entries.clone());
244                    self.mark_defs_committed(file, text);
245                }
246            }
247        }
248
249        results
250            .into_iter()
251            .map(|(file, _, out, _, _, _)| {
252                (
253                    file,
254                    crate::FileAnalysis {
255                        issues: out.issues.to_vec(),
256                        symbols: Vec::new(),
257                    },
258                )
259            })
260            .collect()
261    }
262
263    /// FQCNs that `file` imports via `use` statements but that aren't yet
264    /// loaded in the session.
265    ///
266    /// Designed as the input to background prefetching: after the LSP server
267    /// Return the `use`-import alias map for a file: a list of `(alias, fqcn)`
268    /// pairs where `alias` is the local name (e.g. `"Str"`) and `fqcn` is the
269    /// fully-qualified name (e.g. `"Illuminate\\Support\\Str"`).
270    ///
271    /// Completion handlers can use this to expand a short class name written
272    /// before `::` into its FQN before looking up static members, mirroring the
273    /// same alias expansion that go-to-definition already performs via
274    /// `symbol_at` + `definition_of`.
275    ///
276    /// Returns an empty Vec if the file has not been ingested or has no use
277    /// imports.
278    pub fn class_imports(&self, file: &str) -> Vec<(Arc<str>, Arc<str>)> {
279        let db = self.snapshot_db();
280        let imports = db.file_class_imports(file);
281        imports
282            .iter()
283            .map(|(alias, fqcn)| (Arc::from(alias.as_str()), Arc::from(fqcn.as_str())))
284            .collect()
285    }
286
287    /// ingests an open buffer, it can call this and lazy-load the returned
288    /// FQCNs on a worker thread so the user's first Cmd+Click into vendor
289    /// code doesn't pay the file-read+parse cost.
290    ///
291    /// Returns an empty Vec if the file hasn't been ingested or has no
292    /// unresolved imports.
293    pub fn pending_lazy_loads(&self, file: &str) -> Vec<Arc<str>> {
294        let db = self.snapshot_db();
295        let imports = db.file_imports(file);
296        if imports.is_empty() {
297            return Vec::new();
298        }
299        let mut out = Vec::new();
300        for fqcn in imports.values() {
301            let here = crate::db::Fqcn::new(&db, *fqcn);
302            if crate::db::find_class_like(&db, here).is_some() {
303                continue;
304            }
305            if let Some(resolver) = &self.resolver {
306                if resolver.resolve(fqcn.as_str()).is_some() {
307                    out.push(Arc::from(fqcn.as_str()));
308                }
309            }
310        }
311        out
312    }
313
314    /// Convenience: synchronously lazy-load every import of `file` that
315    /// isn't already in the codebase. Returns the number successfully loaded.
316    ///
317    /// For non-blocking prefetch, call this from a worker thread:
318    ///
319    /// ```ignore
320    /// let s = session.clone();  // AnalysisSession is wrapped in Arc by callers
321    /// std::thread::spawn(move || {
322    ///     s.prefetch_imports(&file_path);
323    /// });
324    /// ```
325    ///
326    /// Uses a single shared-visited two-tier BFS across all pending imports
327    /// (see [`Self::load_classes_transitive_bounded`]) with a shallow depth so
328    /// member access on imported types type-checks without pulling in the
329    /// entire vendor tree.
330    pub fn prefetch_imports(&self, file: &str) -> usize {
331        let pending = self.pending_lazy_loads(file);
332        if pending.is_empty() {
333            return 0;
334        }
335        // Fault in each imported FQCN directly (single-file load + tier-merge).
336        // Inheritance ancestors / signature types resolve through the eagerly
337        // built workspace symbol index — no transitive walk needed here.
338        let mut loaded = 0;
339        for fqcn in &pending {
340            if self.load_class(fqcn.as_ref()).is_loaded() {
341                loaded += 1;
342            }
343        }
344        loaded
345    }
346
347    /// All class / interface / trait / enum FQCNs currently known to the
348    /// session, each paired with the file that defines them when available.
349    ///
350    /// Use this to build workspace-wide views (outline, fuzzy search, etc.).
351    /// Consumers implement their own search/match logic on top — the analyzer
352    /// only exposes the iterator.
353    pub fn all_classes(&self) -> Vec<(Arc<str>, Option<mir_types::Location>)> {
354        let db = self.snapshot_db();
355        crate::db::workspace_classes(&db)
356            .iter()
357            .filter_map(|fqcn| {
358                let here = crate::db::Fqcn::from_str(&db, fqcn.as_ref());
359                crate::db::find_class_like(&db, here)
360                    .map(|class| (fqcn.clone(), class.location().cloned()))
361            })
362            .collect()
363    }
364
365    /// All global function FQNs currently known to the session, each paired
366    /// with their declaration location when available.
367    pub fn all_functions(&self) -> Vec<(Arc<str>, Option<mir_types::Location>)> {
368        let db = self.snapshot_db();
369        crate::db::workspace_functions(&db)
370            .iter()
371            .filter_map(|fqn| {
372                let here = crate::db::Fqcn::from_str(&db, fqn.as_ref());
373                crate::db::find_function(&db, here).map(|f| (fqn.clone(), f.location.clone()))
374            })
375            .collect()
376    }
377
378    /// Every class/interface/trait/enum in the workspace whose *own* short
379    /// (unqualified) name is exactly `short_name`, each paired with its FQCN
380    /// and declaration location.
381    ///
382    /// Backed by `WorkspaceSymbolIndex::class_like_by_short_name` — an O(1)
383    /// bucket lookup, incrementally maintained in lockstep with the
384    /// FQCN-keyed `class_like` map (no rebuild-from-scratch, no per-call
385    /// scan). Use this only for a name a host cannot otherwise resolve to an
386    /// FQCN (no `use`-import match, no same-namespace match) — FQCN
387    /// resolution via [`Self::definition_of`]/[`Self::definition_of_cached`]
388    /// is the standard, unambiguous path and should always be tried first;
389    /// this exists for the residual case of a bare name with no resolution
390    /// context, where multiple unrelated classes across the workspace may
391    /// share it (e.g. Laravel's many `Factory` classes) and the caller must
392    /// disambiguate itself.
393    pub fn classes_named(&self, short_name: &str) -> Vec<(Arc<str>, Option<mir_types::Location>)> {
394        let db = self.snapshot_db();
395        let key = mir_types::Name::new(short_name).ascii_lowercase();
396        crate::db::workspace_index(&db)
397            .class_like_by_short_name
398            .get(&key)
399            .into_iter()
400            .flatten()
401            .filter_map(|fqcn_key| {
402                let here = crate::db::Fqcn::from_str(&db, fqcn_key.as_str());
403                crate::db::find_class_like(&db, here)
404                    .map(|class| (class.fqcn().clone(), class.location().cloned()))
405            })
406            .collect()
407    }
408
409    /// Every ancestor of `fqcn` — extended class, implemented interfaces,
410    /// used traits, transitively — most-derived first. Does not include
411    /// `fqcn` itself. Memoized per FQCN (shares `class_ancestors_by_fqcn`'s
412    /// tracked cache with member-resolution paths).
413    pub fn ancestors_of(&self, fqcn: &str) -> Vec<Arc<str>> {
414        let db = self.snapshot_db();
415        let here = crate::db::Fqcn::from_str(&db, fqcn.trim_start_matches('\\'));
416        crate::db::class_ancestors_by_fqcn(&db, here)
417            .iter()
418            .skip(1)
419            .cloned()
420            .collect()
421    }
422
423    /// Full signature for a global function, resolved by FQN.
424    pub fn function_signature(&self, fqn: &str) -> Option<Arc<crate::FunctionDef>> {
425        let db = self.snapshot_db();
426        let here = crate::db::Fqcn::from_str(&db, fqn);
427        crate::db::find_function(&db, here)
428    }
429
430    /// Compute `file`'s outgoing dependency edges and persist them to the
431    /// disk cache's reverse-dep graph (if configured). The in-memory graph
432    /// is no longer maintained imperatively: `dependency_graph()` derives
433    /// structural edges from the memoized [`crate::db::file_structural_deps`]
434    /// tracked query, so there is no second copy to drift out of sync.
435    pub(super) fn update_reverse_deps_for(&self, file: &str) {
436        if let Some(cache) = self.cache.as_deref() {
437            let db = self.snapshot_db();
438            let targets = file_outgoing_dependencies(&db, file, true);
439            cache.update_reverse_deps_for_file(file, &targets);
440        }
441    }
442
443    /// File dependency graph: which files depend on which other files.
444    /// Used for incremental invalidation in LSP servers and build systems.
445    ///
446    /// File dependency graph: which files depend on which other files.
447    /// Used for incremental invalidation in LSP servers and build systems.
448    ///
449    /// O(edges) — iterates the `file_references` forward index (file → symbol
450    /// keys it references) which is always current, then resolves each symbol
451    /// to its defining file via O(1) lookup.  Total cost is O(E) where E is the
452    /// number of (file, symbol) reference edges, vs. the old O(F × S × R) scan.
453    pub fn dependency_graph(&self) -> crate::DependencyGraph {
454        let db = self.snapshot_db();
455
456        let all_files: Vec<String> = db
457            .source_file_paths()
458            .iter()
459            .map(|f| f.as_ref().to_string())
460            .collect();
461
462        let mut dependencies: HashMap<String, Vec<String>> = HashMap::default();
463        let mut dependents: HashMap<String, Vec<String>> = HashMap::default();
464
465        for file in &all_files {
466            // O(degree(file)) — forward index lookup, no full-table scan.
467            let symbol_keys = db.file_referenced_symbols(file);
468            let mut file_deps: HashSet<String> = HashSet::default();
469            for symbol_key in &symbol_keys {
470                let lookup = crate::defining_file_lookup_key(symbol_key);
471                if let Some(def_file) = db.symbol_defining_file(lookup) {
472                    let def = def_file.as_ref().to_string();
473                    if &def != file {
474                        file_deps.insert(def);
475                    }
476                }
477            }
478            for dep in &file_deps {
479                dependents
480                    .entry(dep.clone())
481                    .or_default()
482                    .push(file.clone());
483                dependencies
484                    .entry(file.clone())
485                    .or_default()
486                    .push(dep.clone());
487            }
488        }
489
490        // Merge structural deps derived from definition collection. The
491        // forward pass above only captures bare-FQN references recorded
492        // during body analysis; `file_structural_deps` covers imports, class
493        // hierarchy (extends/implements/use), and type-hint-only references
494        // that never appear in file_referenced_symbols. The query is salsa-
495        // memoized, so the warm rebuild costs one map lookup per file rather
496        // than a definition walk — and there is no imperatively-maintained
497        // reverse map to drift out of sync with the definitions.
498        for file in &all_files {
499            let Some(sf) = db.lookup_source_file(file) else {
500                continue;
501            };
502            for target in crate::db::file_structural_deps(&db, sf).iter() {
503                let target = target.as_ref().to_string();
504                if &target != file {
505                    dependents
506                        .entry(target.clone())
507                        .or_default()
508                        .push(file.clone());
509                    dependencies.entry(file.clone()).or_default().push(target);
510                }
511            }
512        }
513
514        for deps in dependents.values_mut() {
515            deps.sort();
516            deps.dedup();
517        }
518        for deps in dependencies.values_mut() {
519            deps.sort();
520            deps.dedup();
521        }
522
523        // Augment with stale dependents: files referencing symbols that were
524        // deleted from their defining file. These edges disappear from the
525        // symbol_defining_file lookup but the referencing file still needs
526        // re-analysis to surface the now-broken reference.
527        {
528            let stale = self.stale_defined_symbols.read();
529            if !stale.is_empty() {
530                for (file, deleted_syms) in stale.iter() {
531                    for sym in deleted_syms {
532                        let lookup = crate::defining_file_lookup_key(sym);
533                        // `defined_symbols()` only yields top-level FQ names
534                        // (classes/interfaces/traits/enums, functions, global
535                        // constants) — never knows here which kind `sym` was,
536                        // so probe every prefix the reference index actually
537                        // uses (see `Name::codebase_key`) rather than guessing
538                        // one and silently missing referencers of the others.
539                        for prefix in ["cls:", "fn:", "gcnst:"] {
540                            for referencing_file in
541                                db.symbol_referencers_of(&format!("{prefix}{lookup}"))
542                            {
543                                let ref_file = referencing_file.as_ref().to_string();
544                                if &ref_file != file {
545                                    dependents
546                                        .entry(file.clone())
547                                        .or_default()
548                                        .push(ref_file.clone());
549                                    dependencies.entry(ref_file).or_default().push(file.clone());
550                                }
551                            }
552                        }
553                    }
554                }
555                // Re-sort and dedup since we may have added entries.
556                for deps in dependents.values_mut() {
557                    deps.sort();
558                    deps.dedup();
559                }
560                for deps in dependencies.values_mut() {
561                    deps.sort();
562                    deps.dedup();
563                }
564            }
565        }
566
567        crate::DependencyGraph {
568            dependencies,
569            dependents,
570        }
571    }
572}