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}