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. Returns the per-file analysis results so the LSP
19    /// server can publish updated diagnostics in one batch.
20    ///
21    /// Source text for dependents is retrieved from the session's salsa
22    /// inputs (set by previous `ingest_file` calls) — the caller doesn't
23    /// need to track or re-read files. Files for which the session has no
24    /// source are silently skipped (returns the analyzable subset).
25    ///
26    /// Cross-file inferred return types are resolved on demand via salsa.
27    pub fn reanalyze_dependents(&mut self, file: &str) -> Vec<(Arc<str>, crate::FileAnalysis)> {
28        self.reanalyze_dependents_cancellable(file, &crate::IndexCancel::new())
29    }
30
31    /// Cancellable variant of [`Self::reanalyze_dependents`].
32    ///
33    /// The consumer flips `cancel` (typically because a newer edit arrived) to
34    /// abandon the re-analysis; the flag is checked at each file boundary. Salsa
35    /// cannot unwind the plain-Rust body-analysis walk mid-flight, so a file
36    /// already in progress finishes, but no further files are started. Files
37    /// skipped due to cancellation are simply absent from the returned vec —
38    /// the consumer should drop a stale flag and start fresh work on each edit.
39    pub fn reanalyze_dependents_cancellable(
40        &mut self,
41        file: &str,
42        cancel: &crate::IndexCancel,
43    ) -> Vec<(Arc<str>, crate::FileAnalysis)> {
44        if cancel.is_cancelled() {
45            return Vec::new();
46        }
47
48        // Phase 1: compute dependents outside the analysis loop.
49        let dependents = self.dependency_graph().transitive_dependents(file);
50        if dependents.is_empty() {
51            return Vec::new();
52        }
53        let dependents: Vec<Arc<str>> = dependents
54            .into_iter()
55            .map(|path| Arc::from(path.as_str()))
56            .collect();
57        self.reanalyze_file_set(dependents, cancel)
58    }
59
60    /// Re-analyze an explicit file set — typically the editor's currently
61    /// open files — after an edit elsewhere in the workspace.
62    ///
63    /// The caller passes the files it publishes diagnostics for instead of
64    /// computing the edited file's transitive dependents; salsa memoization
65    /// makes unaffected files ~free. Per-edit cost is O(open files).
66    ///
67    /// Files the session has no source for are silently skipped. Cancellation
68    /// semantics match [`Self::reanalyze_dependents_cancellable`].
69    pub fn reanalyze_files_cancellable(
70        &mut self,
71        files: &[Arc<str>],
72        cancel: &crate::IndexCancel,
73    ) -> Vec<(Arc<str>, crate::FileAnalysis)> {
74        if cancel.is_cancelled() || files.is_empty() {
75            return Vec::new();
76        }
77        if !self.settle_workspace_index_cancellable(&|| cancel.is_cancelled()) {
78            return Vec::new();
79        }
80        self.reanalyze_file_set(files.to_vec(), cancel)
81    }
82
83    /// Shared body of [`Self::reanalyze_dependents_cancellable`] and
84    /// [`Self::reanalyze_files_cancellable`]: warm up, analyze in parallel,
85    /// commit reference locations.
86    fn reanalyze_file_set(
87        &mut self,
88        files: Vec<Arc<str>>,
89        cancel: &crate::IndexCancel,
90    ) -> Vec<(Arc<str>, crate::FileAnalysis)> {
91        let dependents = files;
92
93        // Phase 2a: index each dependent's direct class references the
94        // background indexer hasn't reached yet, as `FileAnalyzer` does.
95        // Loading writes salsa inputs, and a write waits for every live db
96        // handle, so this runs serially before the pass takes its snapshot.
97        //
98        // Files already prepared against their current text skip the parse +
99        // AST walk entirely — hosts on the `ingest_file_prepared` write path
100        // pre-pay this per edit, making the whole loop a map-lookup sweep.
101        {
102            // Closed before the pass view is taken, so commits carry the
103            // post-load generation.
104            let mut session = self.defer_revision_bumps();
105            for file in &dependents {
106                if cancel.is_cancelled() {
107                    return Vec::new();
108                }
109                if !session.prepare_file_for_analysis_cancellable(file, &|| cancel.is_cancelled()) {
110                    return Vec::new();
111                }
112            }
113        }
114
115        // Phase 2b: drive each dependent through the `analyze_file` tracked
116        // query. Salsa's memo validation does the real work here: after a
117        // body-only edit, a dependent whose tracked inputs are structurally
118        // unchanged (`FileDefinitions` backdating) returns its cached output
119        // without re-running body analysis — re-analysis cost scales with
120        // what actually changed, not with dependent count.
121        //
122        // The pass runs serially on the owner's thread, checking `cancel`
123        // between files.
124        //
125        // Dependents' `FileAnalysis::symbols` are empty on this path:
126        // per-expression symbols are intentionally not memoized (a typical
127        // file resolves thousands; caching them balloons memory), and
128        // diagnostics consumers don't read them. Cursor navigation resolves
129        // the open file on demand via `symbol_at`/`name_at`.
130        self.query_snapshot(|snap| snap.reanalyze_files(&dependents, cancel))
131            .unwrap_or_default()
132    }
133
134    /// Return the `use`-import alias map for a file: a list of `(alias, fqcn)`
135    /// pairs where `alias` is the local name (e.g. `"Str"`) and `fqcn` is the
136    /// fully-qualified name (e.g. `"Illuminate\\Support\\Str"`).
137    ///
138    /// Completion handlers can use this to expand a short class name written
139    /// before `::` into its FQN before looking up static members, mirroring the
140    /// same alias expansion that go-to-definition already performs via
141    /// `symbol_at` + `definition_of`.
142    ///
143    /// Returns an empty Vec if the file has not been ingested or has no use
144    /// imports.
145    pub fn class_imports(&self, file: &str) -> Vec<(Arc<str>, Arc<str>)> {
146        let db = self.snapshot_db();
147        let imports = db.file_class_imports(file);
148        imports
149            .iter()
150            .map(|(alias, fqcn)| (Arc::from(alias.as_str()), Arc::from(fqcn.as_str())))
151            .collect()
152    }
153
154    /// FQCNs that `file` imports via `use` statements but that aren't yet
155    /// in the session's symbol index.
156    ///
157    /// Designed as the input to background prefetching: after the LSP server
158    /// ingests an open buffer, it can call this and lazy-load the returned
159    /// FQCNs on a worker thread so the user's first Cmd+Click into vendor
160    /// code doesn't pay the file-read+parse cost.
161    ///
162    /// Returns an empty Vec if the file hasn't been ingested or has no
163    /// unresolved imports.
164    pub fn pending_lazy_loads(&self, file: &str) -> Vec<Arc<str>> {
165        use rustc_hash::FxHashSet;
166
167        let db = self.snapshot_db();
168        let imports = db.file_imports(file);
169        if imports.is_empty() {
170            return Vec::new();
171        }
172        let mut out = Vec::new();
173        let mut seen: FxHashSet<Arc<str>> = FxHashSet::default();
174        for fqcn in imports.values() {
175            let here = crate::db::Fqcn::interned(&db, *fqcn);
176            if crate::db::class_like_indexed(&db, here) {
177                continue;
178            }
179            if let Some(resolver) = &self.resolver {
180                let fqcn_arc: Arc<str> = Arc::from(fqcn.as_str());
181                if !seen.insert(fqcn_arc.clone()) {
182                    continue;
183                }
184                if resolver.resolve(fqcn.as_str()).is_some() {
185                    out.push(fqcn_arc);
186                }
187            }
188        }
189        out
190    }
191
192    /// Convenience: synchronously lazy-load every import of `file` that
193    /// isn't already in the symbol index. Returns the number successfully loaded.
194    ///
195    /// Loads only the imported files themselves; their ancestors and
196    /// signature types resolve through the symbol index or on demand.
197    pub fn prefetch_imports(&mut self, file: &str) -> usize {
198        let pending = self.pending_lazy_loads(file);
199        if pending.is_empty() {
200            return 0;
201        }
202        let mut session = self.defer_revision_bumps();
203        let mut loaded = 0;
204        for fqcn in &pending {
205            if session.load_class(fqcn.as_ref()).is_loaded() {
206                loaded += 1;
207            }
208        }
209        loaded
210    }
211
212    /// All class / interface / trait / enum FQCNs currently known to the
213    /// session, each paired with the file that defines them when available.
214    ///
215    /// Use this to build workspace-wide views (outline, fuzzy search, etc.).
216    /// Consumers implement their own search/match logic on top — the analyzer
217    /// only exposes the iterator.
218    pub fn all_classes(&self) -> Vec<(Arc<str>, Option<mir_types::Location>)> {
219        let db = self.snapshot_db();
220        crate::db::workspace_classes(&db)
221            .iter()
222            .filter_map(|fqcn| {
223                let here = crate::db::Fqcn::from_str(&db, fqcn.as_str());
224                crate::db::find_class_like(&db, here)
225                    .map(|class| (Arc::<str>::from(fqcn.as_str()), class.location().cloned()))
226            })
227            .collect()
228    }
229
230    /// All global function FQNs currently known to the session, each paired
231    /// with their declaration location when available.
232    pub fn all_functions(&self) -> Vec<(Arc<str>, Option<mir_types::Location>)> {
233        let db = self.snapshot_db();
234        crate::db::workspace_functions(&db)
235            .iter()
236            .filter_map(|fqn| {
237                let here = crate::db::Fqcn::from_str(&db, fqn.as_str());
238                crate::db::find_function(&db, here)
239                    .map(|f| (Arc::<str>::from(fqn.as_str()), f.location.clone()))
240            })
241            .collect()
242    }
243
244    /// Every ancestor of `fqcn` — extended class, implemented interfaces,
245    /// used traits, transitively — most-derived first. Does not include
246    /// `fqcn` itself. Memoized per FQCN (shares `class_ancestors_by_fqcn`'s
247    /// tracked cache with member-resolution paths).
248    pub fn ancestors_of(&self, fqcn: &str) -> Vec<Arc<str>> {
249        let db = self.snapshot_db();
250        let here = crate::db::Fqcn::from_str(&db, fqcn.trim_start_matches('\\'));
251        crate::db::class_ancestors_by_fqcn(&db, here)
252            .iter()
253            .skip(1)
254            .cloned()
255            .collect()
256    }
257
258    /// Full signature for a global function, resolved by FQN.
259    pub fn function_signature(&self, fqn: &str) -> Option<Arc<crate::FunctionDef>> {
260        let db = self.snapshot_db();
261        let here = crate::db::Fqcn::from_str(&db, fqn);
262        crate::db::find_function(&db, here)
263    }
264
265    /// Persist `file`'s outgoing dependency edges to the disk cache's
266    /// reverse-dep graph, if one is attached.
267    pub(super) fn update_reverse_deps_for(&self, file: &str) {
268        if let Some(cache) = self.cache.as_deref() {
269            let db = self.snapshot_db();
270            let targets = file_outgoing_dependencies(&db, file, true);
271            cache.update_reverse_deps_for_file(file, &targets);
272        }
273    }
274
275    /// File dependency graph: which files depend on which other files.
276    /// Used for incremental invalidation in LSP servers and build systems.
277    ///
278    /// O(E) in the (file, symbol) edges from the reference index and
279    /// `file_structural_symbols`, each resolved to its defining
280    /// file by index lookup. Cached until the file set or an edge changes.
281    pub fn dependency_graph(&self) -> crate::DependencyGraph {
282        let stamp = self.index.dependency_graph_stamp(&self.db.salsa);
283        if let Some(graph) = self.index.cached_dependency_graph(stamp) {
284            return graph;
285        }
286        let db = self.snapshot_db();
287
288        fn push_edge(
289            dependencies: &mut [Vec<u32>],
290            dependents: &mut [Vec<u32>],
291            from: u32,
292            to: u32,
293        ) {
294            if from == to {
295                return;
296            }
297            dependents[to as usize].push(from);
298            dependencies[from as usize].push(to);
299        }
300
301        fn sort_and_dedup(adjacency: &mut [Vec<u32>]) {
302            for deps in adjacency {
303                deps.sort();
304                deps.dedup();
305            }
306        }
307
308        // Every file a symbol resolves to joins the graph, including ones
309        // loaded on demand while resolving, so their own edges are walked too.
310        let mut known: HashSet<Arc<str>> = db.known_file_paths().into_iter().collect();
311        let mut pending: Vec<Arc<str>> = known.iter().cloned().collect();
312        let mut outgoing: Vec<(Arc<str>, HashSet<Arc<str>>)> = Vec::with_capacity(pending.len());
313        while let Some(file) = pending.pop() {
314            let mut targets: HashSet<Arc<str>> = HashSet::default();
315            let mut resolve = |symbol_key: &str| {
316                let lookup = crate::defining_file_lookup_key(symbol_key);
317                if let Some(defining_file) = db.symbol_defining_file(lookup) {
318                    targets.insert(defining_file);
319                }
320            };
321            // O(degree(file)) — forward reference-index lookup, no full-table scan.
322            for symbol_key in db.file_referenced_symbols(file.as_ref()) {
323                resolve(&symbol_key);
324            }
325            // Declaration-level symbol edges from Salsa. These cover imports,
326            // class hierarchy edges, and type-hint-only references that never
327            // appear in file_referenced_symbols.
328            if let Some(sf) = db.lookup_source_file(file.as_ref()) {
329                for symbol in crate::db::file_structural_symbols(&db, sf).iter() {
330                    resolve(symbol);
331                }
332            }
333            for target in &targets {
334                if known.insert(target.clone()) {
335                    pending.push(target.clone());
336                }
337            }
338            outgoing.push((file, targets));
339        }
340
341        let mut all_files: Vec<Arc<str>> = known.into_iter().collect();
342        all_files.sort();
343        assert!(
344            u32::try_from(all_files.len()).is_ok(),
345            "dependency graph file ids require at most u32::MAX files"
346        );
347        let file_ids: HashMap<Arc<str>, u32> = all_files
348            .iter()
349            .enumerate()
350            .map(|(id, file)| (file.clone(), id as u32))
351            .collect();
352
353        let mut dependencies = vec![Vec::new(); all_files.len()];
354        let mut dependents = vec![Vec::new(); all_files.len()];
355        for (file, targets) in outgoing {
356            let file_id = file_ids[&file];
357            for target in targets {
358                push_edge(
359                    &mut dependencies,
360                    &mut dependents,
361                    file_id,
362                    file_ids[&target],
363                );
364            }
365        }
366
367        sort_and_dedup(&mut dependents);
368        sort_and_dedup(&mut dependencies);
369
370        // Augment with stale dependents: files referencing symbols that were
371        // deleted from their defining file. These edges disappear from the
372        // symbol_defining_file lookup but the referencing file still needs
373        // re-analysis to surface the now-broken reference.
374        {
375            let stale = &self.stale_defined_symbols;
376            if !stale.is_empty() {
377                for (file, deleted_syms) in stale.iter() {
378                    let Some(&file_id) = file_ids.get(file.as_str()) else {
379                        continue;
380                    };
381                    for sym in deleted_syms {
382                        let lookup = crate::defining_file_lookup_key(sym);
383                        // `defined_symbols()` only yields top-level FQ names
384                        // (classes/interfaces/traits/enums, functions, global
385                        // constants) — never knows here which kind `sym` was,
386                        // so probe every prefix the reference index actually
387                        // uses (see `Name::codebase_key`) rather than guessing
388                        // one and silently missing referencers of the others.
389                        for prefix in ["cls:", "fn:", "gcnst:"] {
390                            for referencing_file in
391                                db.symbol_referencers_of(&format!("{prefix}{lookup}"))
392                            {
393                                if let Some(&ref_id) = file_ids.get(referencing_file.as_ref()) {
394                                    push_edge(&mut dependencies, &mut dependents, ref_id, file_id);
395                                }
396                            }
397                        }
398                    }
399                }
400                // Re-sort and dedup since we may have added entries.
401                sort_and_dedup(&mut dependents);
402                sort_and_dedup(&mut dependencies);
403            }
404        }
405
406        let graph = crate::DependencyGraph::from_compact_parts(
407            all_files,
408            file_ids,
409            dependencies,
410            dependents,
411        );
412        self.index.store_dependency_graph(stamp, graph.clone());
413        graph
414    }
415}