Skip to main content

mir_analyzer/session/
incremental.rs

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