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 let Some(results) = self.query_snapshot(|snap| snap.warm_pass(&dependents, cancel)) else {
131 return Vec::new();
132 };
133
134 results
135 .into_iter()
136 .map(|a| {
137 (
138 a.file,
139 crate::FileAnalysis {
140 issues: a.out.issues.to_vec(),
141 symbols: Vec::new(),
142 },
143 )
144 })
145 .collect()
146 }
147
148 /// Return the `use`-import alias map for a file: a list of `(alias, fqcn)`
149 /// pairs where `alias` is the local name (e.g. `"Str"`) and `fqcn` is the
150 /// fully-qualified name (e.g. `"Illuminate\\Support\\Str"`).
151 ///
152 /// Completion handlers can use this to expand a short class name written
153 /// before `::` into its FQN before looking up static members, mirroring the
154 /// same alias expansion that go-to-definition already performs via
155 /// `symbol_at` + `definition_of`.
156 ///
157 /// Returns an empty Vec if the file has not been ingested or has no use
158 /// imports.
159 pub fn class_imports(&self, file: &str) -> Vec<(Arc<str>, Arc<str>)> {
160 let db = self.snapshot_db();
161 let imports = db.file_class_imports(file);
162 imports
163 .iter()
164 .map(|(alias, fqcn)| (Arc::from(alias.as_str()), Arc::from(fqcn.as_str())))
165 .collect()
166 }
167
168 /// FQCNs that `file` imports via `use` statements but that aren't yet
169 /// in the session's symbol index.
170 ///
171 /// Designed as the input to background prefetching: after the LSP server
172 /// ingests an open buffer, it can call this and lazy-load the returned
173 /// FQCNs on a worker thread so the user's first Cmd+Click into vendor
174 /// code doesn't pay the file-read+parse cost.
175 ///
176 /// Returns an empty Vec if the file hasn't been ingested or has no
177 /// unresolved imports.
178 pub fn pending_lazy_loads(&self, file: &str) -> Vec<Arc<str>> {
179 use rustc_hash::FxHashSet;
180
181 let db = self.snapshot_db();
182 let imports = db.file_imports(file);
183 if imports.is_empty() {
184 return Vec::new();
185 }
186 let mut out = Vec::new();
187 let mut seen: FxHashSet<Arc<str>> = FxHashSet::default();
188 for fqcn in imports.values() {
189 let here = crate::db::Fqcn::interned(&db, *fqcn);
190 if crate::db::class_like_indexed(&db, here) {
191 continue;
192 }
193 if let Some(resolver) = &self.resolver {
194 let fqcn_arc: Arc<str> = Arc::from(fqcn.as_str());
195 if !seen.insert(fqcn_arc.clone()) {
196 continue;
197 }
198 if resolver.resolve(fqcn.as_str()).is_some() {
199 out.push(fqcn_arc);
200 }
201 }
202 }
203 out
204 }
205
206 /// Convenience: synchronously lazy-load every import of `file` that
207 /// isn't already in the symbol index. Returns the number successfully loaded.
208 ///
209 /// Loads only the imported files themselves; their ancestors and
210 /// signature types resolve through the symbol index or on demand.
211 pub fn prefetch_imports(&mut self, file: &str) -> usize {
212 let pending = self.pending_lazy_loads(file);
213 if pending.is_empty() {
214 return 0;
215 }
216 let mut session = self.defer_revision_bumps();
217 let mut loaded = 0;
218 for fqcn in &pending {
219 if session.load_class(fqcn.as_ref()).is_loaded() {
220 loaded += 1;
221 }
222 }
223 loaded
224 }
225
226 /// All class / interface / trait / enum FQCNs currently known to the
227 /// session, each paired with the file that defines them when available.
228 ///
229 /// Use this to build workspace-wide views (outline, fuzzy search, etc.).
230 /// Consumers implement their own search/match logic on top — the analyzer
231 /// only exposes the iterator.
232 pub fn all_classes(&self) -> Vec<(Arc<str>, Option<mir_types::Location>)> {
233 let db = self.snapshot_db();
234 crate::db::workspace_classes(&db)
235 .iter()
236 .filter_map(|fqcn| {
237 let here = crate::db::Fqcn::from_str(&db, fqcn.as_str());
238 crate::db::find_class_like(&db, here)
239 .map(|class| (Arc::<str>::from(fqcn.as_str()), class.location().cloned()))
240 })
241 .collect()
242 }
243
244 /// All global function FQNs currently known to the session, each paired
245 /// with their declaration location when available.
246 pub fn all_functions(&self) -> Vec<(Arc<str>, Option<mir_types::Location>)> {
247 let db = self.snapshot_db();
248 crate::db::workspace_functions(&db)
249 .iter()
250 .filter_map(|fqn| {
251 let here = crate::db::Fqcn::from_str(&db, fqn.as_str());
252 crate::db::find_function(&db, here)
253 .map(|f| (Arc::<str>::from(fqn.as_str()), f.location.clone()))
254 })
255 .collect()
256 }
257
258 /// Every ancestor of `fqcn` — extended class, implemented interfaces,
259 /// used traits, transitively — most-derived first. Does not include
260 /// `fqcn` itself. Memoized per FQCN (shares `class_ancestors_by_fqcn`'s
261 /// tracked cache with member-resolution paths).
262 pub fn ancestors_of(&self, fqcn: &str) -> Vec<Arc<str>> {
263 let db = self.snapshot_db();
264 let here = crate::db::Fqcn::from_str(&db, fqcn.trim_start_matches('\\'));
265 crate::db::class_ancestors_by_fqcn(&db, here)
266 .iter()
267 .skip(1)
268 .cloned()
269 .collect()
270 }
271
272 /// Full signature for a global function, resolved by FQN.
273 pub fn function_signature(&self, fqn: &str) -> Option<Arc<crate::FunctionDef>> {
274 let db = self.snapshot_db();
275 let here = crate::db::Fqcn::from_str(&db, fqn);
276 crate::db::find_function(&db, here)
277 }
278
279 /// Persist `file`'s outgoing dependency edges to the disk cache's
280 /// reverse-dep graph, if one is attached.
281 pub(super) fn update_reverse_deps_for(&self, file: &str) {
282 if let Some(cache) = self.cache.as_deref() {
283 let db = self.snapshot_db();
284 let targets = file_outgoing_dependencies(&db, file, true);
285 cache.update_reverse_deps_for_file(file, &targets);
286 }
287 }
288
289 /// File dependency graph: which files depend on which other files.
290 /// Used for incremental invalidation in LSP servers and build systems.
291 ///
292 /// O(E) in the (file, symbol) edges from the reference index and
293 /// `file_structural_symbols`, each resolved to its defining
294 /// file by index lookup. Cached until the file set or an edge changes.
295 pub fn dependency_graph(&self) -> crate::DependencyGraph {
296 let stamp = self.index.dependency_graph_stamp(&self.db.salsa);
297 if let Some(graph) = self.index.cached_dependency_graph(stamp) {
298 return graph;
299 }
300 let db = self.snapshot_db();
301
302 fn push_edge(
303 dependencies: &mut [Vec<u32>],
304 dependents: &mut [Vec<u32>],
305 from: u32,
306 to: u32,
307 ) {
308 if from == to {
309 return;
310 }
311 dependents[to as usize].push(from);
312 dependencies[from as usize].push(to);
313 }
314
315 fn sort_and_dedup(adjacency: &mut [Vec<u32>]) {
316 for deps in adjacency {
317 deps.sort();
318 deps.dedup();
319 }
320 }
321
322 // Every file a symbol resolves to joins the graph, including ones
323 // loaded on demand while resolving, so their own edges are walked too.
324 let mut known: HashSet<Arc<str>> = db.known_file_paths().into_iter().collect();
325 let mut pending: Vec<Arc<str>> = known.iter().cloned().collect();
326 let mut outgoing: Vec<(Arc<str>, HashSet<Arc<str>>)> = Vec::with_capacity(pending.len());
327 while let Some(file) = pending.pop() {
328 let mut targets: HashSet<Arc<str>> = HashSet::default();
329 let mut resolve = |symbol_key: &str| {
330 let lookup = crate::defining_file_lookup_key(symbol_key);
331 if let Some(defining_file) = db.symbol_defining_file(lookup) {
332 targets.insert(defining_file);
333 }
334 };
335 // O(degree(file)) — forward reference-index lookup, no full-table scan.
336 for symbol_key in db.file_referenced_symbols(file.as_ref()) {
337 resolve(&symbol_key);
338 }
339 // Declaration-level symbol edges from Salsa. These cover imports,
340 // class hierarchy edges, and type-hint-only references that never
341 // appear in file_referenced_symbols.
342 if let Some(sf) = db.lookup_source_file(file.as_ref()) {
343 for symbol in crate::db::file_structural_symbols(&db, sf).iter() {
344 resolve(symbol);
345 }
346 }
347 for target in &targets {
348 if known.insert(target.clone()) {
349 pending.push(target.clone());
350 }
351 }
352 outgoing.push((file, targets));
353 }
354
355 let mut all_files: Vec<Arc<str>> = known.into_iter().collect();
356 all_files.sort();
357 assert!(
358 u32::try_from(all_files.len()).is_ok(),
359 "dependency graph file ids require at most u32::MAX files"
360 );
361 let file_ids: HashMap<Arc<str>, u32> = all_files
362 .iter()
363 .enumerate()
364 .map(|(id, file)| (file.clone(), id as u32))
365 .collect();
366
367 let mut dependencies = vec![Vec::new(); all_files.len()];
368 let mut dependents = vec![Vec::new(); all_files.len()];
369 for (file, targets) in outgoing {
370 let file_id = file_ids[&file];
371 for target in targets {
372 push_edge(
373 &mut dependencies,
374 &mut dependents,
375 file_id,
376 file_ids[&target],
377 );
378 }
379 }
380
381 sort_and_dedup(&mut dependents);
382 sort_and_dedup(&mut dependencies);
383
384 // Augment with stale dependents: files referencing symbols that were
385 // deleted from their defining file. These edges disappear from the
386 // symbol_defining_file lookup but the referencing file still needs
387 // re-analysis to surface the now-broken reference.
388 {
389 let stale = &self.stale_defined_symbols;
390 if !stale.is_empty() {
391 for (file, deleted_syms) in stale.iter() {
392 let Some(&file_id) = file_ids.get(file.as_str()) else {
393 continue;
394 };
395 for sym in deleted_syms {
396 let lookup = crate::defining_file_lookup_key(sym);
397 // `defined_symbols()` only yields top-level FQ names
398 // (classes/interfaces/traits/enums, functions, global
399 // constants) — never knows here which kind `sym` was,
400 // so probe every prefix the reference index actually
401 // uses (see `Name::codebase_key`) rather than guessing
402 // one and silently missing referencers of the others.
403 for prefix in ["cls:", "fn:", "gcnst:"] {
404 for referencing_file in
405 db.symbol_referencers_of(&format!("{prefix}{lookup}"))
406 {
407 if let Some(&ref_id) = file_ids.get(referencing_file.as_ref()) {
408 push_edge(&mut dependencies, &mut dependents, ref_id, file_id);
409 }
410 }
411 }
412 }
413 }
414 // Re-sort and dedup since we may have added entries.
415 sort_and_dedup(&mut dependents);
416 sort_and_dedup(&mut dependencies);
417 }
418 }
419
420 let graph = crate::DependencyGraph::from_compact_parts(
421 all_files,
422 file_ids,
423 dependencies,
424 dependents,
425 );
426 self.index.store_dependency_graph(stamp, graph.clone());
427 graph
428 }
429}