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}