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