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