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))
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        let db_main = self.snapshot_db();
148        let results: Vec<(Arc<str>, std::sync::Arc<crate::db::AnalyzeOutput>)> = dependents
149            .into_par_iter()
150            .map_with(db_main, |db, file| {
151                if cancel.is_cancelled() {
152                    return None;
153                }
154                let sf = db.lookup_source_file(file.as_ref())?;
155                let out = crate::db::analyze_file(&*db as &dyn crate::db::MirDatabase, sf);
156                Some((file, out))
157            })
158            .flatten()
159            .collect();
160
161        // Serial commit: each dependent's output is its complete reference
162        // set, so replace rather than append.
163        if self.maintain_ref_index {
164            let guard = self.db.salsa.read();
165            for (file, out) in &results {
166                guard.set_file_reference_locations(file.as_ref(), out.ref_locs.to_vec());
167            }
168        }
169
170        results
171            .into_iter()
172            .map(|(file, out)| {
173                (
174                    file,
175                    crate::FileAnalysis {
176                        issues: out.issues.to_vec(),
177                        symbols: Vec::new(),
178                    },
179                )
180            })
181            .collect()
182    }
183
184    /// FQCNs that `file` imports via `use` statements but that aren't yet
185    /// loaded in the session.
186    ///
187    /// Designed as the input to background prefetching: after the LSP server
188    /// Return the `use`-import alias map for a file: a list of `(alias, fqcn)`
189    /// pairs where `alias` is the local name (e.g. `"Str"`) and `fqcn` is the
190    /// fully-qualified name (e.g. `"Illuminate\\Support\\Str"`).
191    ///
192    /// Completion handlers can use this to expand a short class name written
193    /// before `::` into its FQN before looking up static members, mirroring the
194    /// same alias expansion that go-to-definition already performs via
195    /// `symbol_at` + `definition_of`.
196    ///
197    /// Returns an empty Vec if the file has not been ingested or has no use
198    /// imports.
199    pub fn class_imports(&self, file: &str) -> Vec<(Arc<str>, Arc<str>)> {
200        let db = self.snapshot_db();
201        let imports = db.file_class_imports(file);
202        imports
203            .iter()
204            .map(|(alias, fqcn)| (Arc::from(alias.as_str()), Arc::from(fqcn.as_str())))
205            .collect()
206    }
207
208    /// ingests an open buffer, it can call this and lazy-load the returned
209    /// FQCNs on a worker thread so the user's first Cmd+Click into vendor
210    /// code doesn't pay the file-read+parse cost.
211    ///
212    /// Returns an empty Vec if the file hasn't been ingested or has no
213    /// unresolved imports.
214    pub fn pending_lazy_loads(&self, file: &str) -> Vec<Arc<str>> {
215        let db = self.snapshot_db();
216        let imports = db.file_imports(file);
217        if imports.is_empty() {
218            return Vec::new();
219        }
220        let mut out = Vec::new();
221        for fqcn in imports.values() {
222            let here = crate::db::Fqcn::new(&db, *fqcn);
223            if crate::db::find_class_like(&db, here).is_some() {
224                continue;
225            }
226            if let Some(resolver) = &self.resolver {
227                if resolver.resolve(fqcn.as_str()).is_some() {
228                    out.push(Arc::from(fqcn.as_str()));
229                }
230            }
231        }
232        out
233    }
234
235    /// Convenience: synchronously lazy-load every import of `file` that
236    /// isn't already in the codebase. Returns the number successfully loaded.
237    ///
238    /// For non-blocking prefetch, call this from a worker thread:
239    ///
240    /// ```ignore
241    /// let s = session.clone();  // AnalysisSession is wrapped in Arc by callers
242    /// std::thread::spawn(move || {
243    ///     s.prefetch_imports(&file_path);
244    /// });
245    /// ```
246    ///
247    /// Uses a single shared-visited two-tier BFS across all pending imports
248    /// (see [`Self::load_classes_transitive_bounded`]) with a shallow depth so
249    /// member access on imported types type-checks without pulling in the
250    /// entire vendor tree.
251    pub fn prefetch_imports(&self, file: &str) -> usize {
252        let pending = self.pending_lazy_loads(file);
253        if pending.is_empty() {
254            return 0;
255        }
256        // Fault in each imported FQCN directly (single-file load + tier-merge).
257        // Inheritance ancestors / signature types resolve through the eagerly
258        // built workspace symbol index — no transitive walk needed here.
259        let mut loaded = 0;
260        for fqcn in &pending {
261            if self.load_class(fqcn.as_ref()).is_loaded() {
262                loaded += 1;
263            }
264        }
265        loaded
266    }
267
268    /// All class / interface / trait / enum FQCNs currently known to the
269    /// session, each paired with the file that defines them when available.
270    ///
271    /// Use this to build workspace-wide views (outline, fuzzy search, etc.).
272    /// Consumers implement their own search/match logic on top — the analyzer
273    /// only exposes the iterator.
274    pub fn all_classes(&self) -> Vec<(Arc<str>, Option<mir_types::Location>)> {
275        let db = self.snapshot_db();
276        crate::db::workspace_classes(&db)
277            .iter()
278            .filter_map(|fqcn| {
279                let here = crate::db::Fqcn::from_str(&db, fqcn.as_ref());
280                crate::db::find_class_like(&db, here)
281                    .map(|class| (fqcn.clone(), class.location().cloned()))
282            })
283            .collect()
284    }
285
286    /// All global function FQNs currently known to the session, each paired
287    /// with their declaration location when available.
288    pub fn all_functions(&self) -> Vec<(Arc<str>, Option<mir_types::Location>)> {
289        let db = self.snapshot_db();
290        crate::db::workspace_functions(&db)
291            .iter()
292            .filter_map(|fqn| {
293                let here = crate::db::Fqcn::from_str(&db, fqn.as_ref());
294                crate::db::find_function(&db, here).map(|f| (fqn.clone(), f.location.clone()))
295            })
296            .collect()
297    }
298
299    /// Compute `file`'s outgoing dependency edges and persist them to the
300    /// disk cache's reverse-dep graph (if configured). The in-memory graph
301    /// is no longer maintained imperatively: `dependency_graph()` derives
302    /// structural edges from the memoized [`crate::db::file_structural_deps`]
303    /// tracked query, so there is no second copy to drift out of sync.
304    pub(super) fn update_reverse_deps_for(&self, file: &str) {
305        if let Some(cache) = self.cache.as_deref() {
306            let db = self.snapshot_db();
307            // Body-level bare-FQN edges live in the RefIndex; a session that
308            // opted out of index maintenance never commits them, so reading
309            // the (empty) index would only take the lock it promised not to.
310            let targets = file_outgoing_dependencies(&db, file, self.maintain_ref_index);
311            cache.update_reverse_deps_for_file(file, &targets);
312        }
313    }
314
315    /// File dependency graph: which files depend on which other files.
316    /// Used for incremental invalidation in LSP servers and build systems.
317    ///
318    /// File dependency graph: which files depend on which other files.
319    /// Used for incremental invalidation in LSP servers and build systems.
320    ///
321    /// O(edges) — iterates the `file_references` forward index (file → symbol
322    /// keys it references) which is always current, then resolves each symbol
323    /// to its defining file via O(1) lookup.  Total cost is O(E) where E is the
324    /// number of (file, symbol) reference edges, vs. the old O(F × S × R) scan.
325    pub fn dependency_graph(&self) -> crate::DependencyGraph {
326        let db = self.snapshot_db();
327
328        let all_files: Vec<String> = db
329            .source_file_paths()
330            .iter()
331            .map(|f| f.as_ref().to_string())
332            .collect();
333
334        let mut dependencies: HashMap<String, Vec<String>> = HashMap::default();
335        let mut dependents: HashMap<String, Vec<String>> = HashMap::default();
336
337        for file in &all_files {
338            // O(degree(file)) — forward index lookup, no full-table scan.
339            let symbol_keys = db.file_referenced_symbols(file);
340            let mut file_deps: HashSet<String> = HashSet::default();
341            for symbol_key in &symbol_keys {
342                let lookup = crate::defining_file_lookup_key(symbol_key);
343                if let Some(def_file) = db.symbol_defining_file(lookup) {
344                    let def = def_file.as_ref().to_string();
345                    if &def != file {
346                        file_deps.insert(def);
347                    }
348                }
349            }
350            for dep in &file_deps {
351                dependents
352                    .entry(dep.clone())
353                    .or_default()
354                    .push(file.clone());
355                dependencies
356                    .entry(file.clone())
357                    .or_default()
358                    .push(dep.clone());
359            }
360        }
361
362        // Merge structural deps derived from definition collection. The
363        // forward pass above only captures bare-FQN references recorded
364        // during body analysis; `file_structural_deps` covers imports, class
365        // hierarchy (extends/implements/use), and type-hint-only references
366        // that never appear in file_referenced_symbols. The query is salsa-
367        // memoized, so the warm rebuild costs one map lookup per file rather
368        // than a definition walk — and there is no imperatively-maintained
369        // reverse map to drift out of sync with the definitions.
370        for file in &all_files {
371            let Some(sf) = db.lookup_source_file(file) else {
372                continue;
373            };
374            for target in crate::db::file_structural_deps(&db, sf).iter() {
375                let target = target.as_ref().to_string();
376                if &target != file {
377                    dependents
378                        .entry(target.clone())
379                        .or_default()
380                        .push(file.clone());
381                    dependencies.entry(file.clone()).or_default().push(target);
382                }
383            }
384        }
385
386        for deps in dependents.values_mut() {
387            deps.sort();
388            deps.dedup();
389        }
390        for deps in dependencies.values_mut() {
391            deps.sort();
392            deps.dedup();
393        }
394
395        // Augment with stale dependents: files referencing symbols that were
396        // deleted from their defining file. These edges disappear from the
397        // symbol_defining_file lookup but the referencing file still needs
398        // re-analysis to surface the now-broken reference.
399        {
400            let stale = self.stale_defined_symbols.read();
401            if !stale.is_empty() {
402                for (file, deleted_syms) in stale.iter() {
403                    for sym in deleted_syms {
404                        let lookup = crate::defining_file_lookup_key(sym);
405                        // `defined_symbols()` only yields top-level FQ names
406                        // (classes/interfaces/traits/enums, functions, global
407                        // constants) — never knows here which kind `sym` was,
408                        // so probe every prefix the reference index actually
409                        // uses (see `Name::codebase_key`) rather than guessing
410                        // one and silently missing referencers of the others.
411                        for prefix in ["cls:", "fn:", "gcnst:"] {
412                            for referencing_file in
413                                db.symbol_referencers_of(&format!("{prefix}{lookup}"))
414                            {
415                                let ref_file = referencing_file.as_ref().to_string();
416                                if &ref_file != file {
417                                    dependents
418                                        .entry(file.clone())
419                                        .or_default()
420                                        .push(ref_file.clone());
421                                    dependencies.entry(ref_file).or_default().push(file.clone());
422                                }
423                            }
424                        }
425                    }
426                }
427                // Re-sort and dedup since we may have added entries.
428                for deps in dependents.values_mut() {
429                    deps.sort();
430                    deps.dedup();
431                }
432                for deps in dependencies.values_mut() {
433                    deps.sort();
434                    deps.dedup();
435                }
436            }
437        }
438
439        crate::DependencyGraph {
440            dependencies,
441            dependents,
442        }
443    }
444}