Skip to main content

hearth_graph/graph/
mod.rs

1//! Incremental module graph storage and deterministic dependency queries.
2//!
3//! Import-extraction support is supplied explicitly to [`ModuleGraph::upsert_file`].
4//! Callers should pass [`LanguageRegistry::supports_imports`][crate::LanguageRegistry::supports_imports]
5//! for the analyzed path. Resolver liveness is sampled from [`ResolverSet`] at
6//! resolution time and stored with the node, so queries need only `&self`.
7
8use std::collections::VecDeque;
9
10use compact_str::CompactString;
11use rustc_hash::{FxHashMap, FxHashSet};
12
13use crate::{
14    FileAnalysis, ImportKind, RawImport, ResolutionCompleteness, ResolutionOutcome, Resolved,
15    ResolverSet, UnresolvedReason,
16};
17
18/// The result of inserting or refreshing one analyzed file.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum UpsertOutcome {
21    /// The path did not previously exist in the graph.
22    Inserted,
23    /// An analyzed node changed or a stub was promoted.
24    Updated,
25    /// The content and resolver generation were already current.
26    Unchanged,
27}
28
29/// Accuracy attached to a dependency query.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Guarantee {
32    /// The structural completeness conditions for the query are satisfied.
33    Exact,
34    /// The graph may omit dependencies.
35    Approximate,
36}
37
38impl Guarantee {
39    fn weakest(self, other: Self) -> Self {
40        if self == Self::Exact && other == Self::Exact {
41            Self::Exact
42        } else {
43            Self::Approximate
44        }
45    }
46}
47
48/// State of one module-graph node.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum NodeState {
51    /// Source for this node was analyzed.
52    Analyzed {
53        /// Caller-supplied content hash.
54        content_hash: u64,
55        /// Whether non-literal imports prevented complete extraction.
56        has_opaque_imports: bool,
57        /// Registered language name, when recognized.
58        language: Option<CompactString>,
59    },
60    /// A resolved path that has not been analyzed yet.
61    Stub,
62}
63
64/// Target stored on an import edge.
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum EdgeTarget {
67    /// Slot of another module node.
68    Node(u32),
69    /// Package or other dependency outside the module graph.
70    External(CompactString),
71    /// Import that could not be resolved.
72    Unresolved(UnresolvedReason),
73}
74
75/// Owned target returned by graph queries.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum EdgeTargetOwned {
78    /// Path of another module node.
79    Path(CompactString),
80    /// Package or other dependency outside the module graph.
81    External(CompactString),
82    /// Import that could not be resolved.
83    Unresolved(UnresolvedReason),
84}
85
86/// One resolved import retained by the module graph.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct ImportEdge {
89    /// Import syntax and source location exactly as extracted.
90    pub raw: RawImport,
91    /// Resolution target.
92    pub target: EdgeTarget,
93}
94
95/// One occupied module-graph slot.
96#[derive(Debug, Clone)]
97pub struct ModuleNode {
98    /// Canonical path used as the graph key.
99    pub path: CompactString,
100    /// Whether this path is analyzed or only a resolution stub.
101    pub state: NodeState,
102    /// Outgoing imports in extraction order.
103    pub out: Vec<ImportEdge>,
104    /// Slots with at least one edge targeting this node.
105    pub(crate) rdeps: FxHashSet<u32>,
106    /// Deduplicated resolver configuration paths consulted by outgoing edges.
107    pub config_dependencies: Vec<CompactString>,
108    imports_supported: bool,
109    resolver_live: bool,
110    resolution_complete: bool,
111    resolved_at: u64,
112}
113
114impl ModuleNode {
115    fn stub(path: CompactString) -> Self {
116        Self {
117            path,
118            state: NodeState::Stub,
119            out: Vec::new(),
120            rdeps: FxHashSet::default(),
121            config_dependencies: Vec::new(),
122            imports_supported: false,
123            resolver_live: false,
124            resolution_complete: false,
125            resolved_at: 0,
126        }
127    }
128
129    /// Resolver generation at which this analyzed node's edges were produced.
130    #[must_use]
131    pub fn resolved_generation(&self) -> Option<u64> {
132        matches!(self.state, NodeState::Analyzed { .. }).then_some(self.resolved_at)
133    }
134
135    /// Whether import extraction was available when this node was upserted.
136    #[must_use]
137    pub fn imports_supported(&self) -> bool {
138        self.imports_supported
139    }
140
141    /// Whether the matching resolver was live when this node was resolved.
142    #[must_use]
143    pub fn resolver_live(&self) -> bool {
144        self.resolver_live
145    }
146
147    /// Whether every outgoing edge was produced by a complete resolution.
148    #[must_use]
149    pub fn resolution_complete(&self) -> bool {
150        self.resolution_complete
151    }
152}
153
154/// One dependency edge materialized with paths instead of graph slots.
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct DepEdge {
157    /// Importing file.
158    pub from: CompactString,
159    /// Owned resolution target.
160    pub to: EdgeTargetOwned,
161    /// Specifier exactly as written.
162    pub specifier: CompactString,
163    /// Syntactic import kind.
164    pub kind: ImportKind,
165    /// 1-based source line.
166    pub line: u32,
167    /// Byte range of the specifier.
168    pub span: (u32, u32),
169}
170
171/// Files used to produce a query answer.
172#[derive(Debug, Clone, Default, PartialEq, Eq)]
173pub struct Coverage {
174    /// Number of traversed analyzed nodes.
175    pub analyzed: u64,
176    /// Number of traversed stub nodes.
177    pub stubs: u64,
178    /// Number of traversed analyzed nodes with opaque imports.
179    pub opaque_files: u64,
180    /// Traversed analyzed path/hash pairs, sorted by path.
181    pub basis: Vec<(CompactString, u64)>,
182}
183
184/// Result of a forward- or reverse-dependency query.
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct DepsResult {
187    /// Matching dependency edges.
188    pub edges: Vec<DepEdge>,
189    /// Structural accuracy of this answer.
190    pub guarantee: Guarantee,
191    /// Nodes traversed to produce this answer.
192    pub coverage: Coverage,
193}
194
195/// Result of a bidirectional breadth-first traversal.
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct NeighborhoodResult {
198    /// Reached module paths, sorted lexicographically.
199    pub nodes: Vec<CompactString>,
200    /// Node-to-node edges induced by the reached paths.
201    pub edges: Vec<DepEdge>,
202    /// Weakest guarantee used by the traversal.
203    pub guarantee: Guarantee,
204    /// Reached nodes and their analyzed hashes.
205    pub coverage: Coverage,
206}
207
208/// Slotted, incrementally maintained repository module graph.
209#[derive(Debug, Default)]
210pub struct ModuleGraph {
211    nodes: Vec<Option<ModuleNode>>,
212    by_path: FxHashMap<CompactString, u32>,
213    free: Vec<u32>,
214    inexact_nodes: usize,
215    generation: u64,
216    universe_complete: bool,
217    resolver_generation: u64,
218}
219
220impl ModuleGraph {
221    /// Creates an empty graph.
222    #[must_use]
223    pub fn new() -> Self {
224        Self::default()
225    }
226
227    /// Records whether the caller has supplied a complete source universe.
228    pub fn set_universe_complete(&mut self, complete: bool) {
229        if self.universe_complete != complete {
230            self.universe_complete = complete;
231            self.record_mutation();
232        }
233    }
234
235    /// Returns the graph mutation generation.
236    #[must_use]
237    pub fn generation(&self) -> u64 {
238        self.generation
239    }
240
241    /// Returns the resolver configuration generation.
242    #[must_use]
243    pub fn resolver_generation(&self) -> u64 {
244        self.resolver_generation
245    }
246
247    /// Returns whether an analyzed node has the supplied content hash.
248    #[must_use]
249    pub fn contains(&self, path: &str, hash: u64) -> bool {
250        self.node(path).is_some_and(|node| {
251            matches!(
252                node.state,
253                NodeState::Analyzed {
254                    content_hash,
255                    ..
256                } if content_hash == hash
257            )
258        })
259    }
260
261    /// Returns an occupied node by path.
262    #[must_use]
263    pub fn node(&self, path: &str) -> Option<&ModuleNode> {
264        let slot = *self.by_path.get(path)?;
265        self.nodes.get(slot as usize)?.as_ref()
266    }
267
268    /// Sorted importer paths of `path` — a slot-history-independent view of
269    /// the reverse-dependency set.
270    pub fn rdeps_paths(&self, path: &str) -> Option<Vec<CompactString>> {
271        let slot = *self.by_path.get(path)?;
272        let node = self.occupied(slot);
273        let mut paths: Vec<CompactString> = node
274            .rdeps
275            .iter()
276            .map(|&importer| self.occupied(importer).path.clone())
277            .collect();
278        paths.sort_unstable();
279        Some(paths)
280    }
281
282    /// Inserts or refreshes one file analysis.
283    ///
284    /// `imports_supported` should be computed from the same language registry
285    /// that produced `analysis`.
286    pub fn upsert_file(
287        &mut self,
288        analysis: &FileAnalysis,
289        resolvers: &ResolverSet,
290        imports_supported: bool,
291    ) -> UpsertOutcome {
292        let existing = self.by_path.get(analysis.path.as_str()).copied();
293        if existing.is_some_and(|slot| {
294            let node = self.occupied(slot);
295            matches!(
296                node.state,
297                NodeState::Analyzed {
298                    content_hash,
299                    ..
300                } if content_hash == analysis.content_hash
301            ) && node.resolved_at == self.resolver_generation
302        }) {
303            return UpsertOutcome::Unchanged;
304        }
305
306        let resolutions = resolve_imports(resolvers, &analysis.path, &analysis.imports);
307        let resolver_live =
308            resolver_is_live(analysis.language.as_deref(), &analysis.imports, resolvers);
309        let outcome = if existing.is_some() {
310            UpsertOutcome::Updated
311        } else {
312            UpsertOutcome::Inserted
313        };
314        let slot =
315            existing.unwrap_or_else(|| self.allocate(ModuleNode::stub(analysis.path.clone())));
316        let was_exact = self.node_is_rdeps_exact(slot);
317        let old_targets = self.node_targets(slot);
318        let baseline_completeness = analysis
319            .language
320            .as_deref()
321            .map_or(ResolutionCompleteness::Complete, |language| {
322                resolvers.baseline_completeness(language)
323            });
324        let (edges, config_dependencies, resolution_complete) =
325            self.materialize_resolutions(resolutions, baseline_completeness);
326        let new_targets = targets_from_edges(&edges);
327
328        self.update_rdeps(slot, &old_targets, &new_targets);
329        let resolved_at = self.resolver_generation;
330        let node = self.occupied_mut(slot);
331        node.state = NodeState::Analyzed {
332            content_hash: analysis.content_hash,
333            has_opaque_imports: analysis.has_opaque_imports,
334            language: analysis.language.clone(),
335        };
336        node.out = edges;
337        node.config_dependencies = config_dependencies;
338        node.imports_supported = imports_supported;
339        node.resolver_live = resolver_live;
340        node.resolution_complete = resolution_complete;
341        node.resolved_at = resolved_at;
342        let is_exact = self.node_is_rdeps_exact(slot);
343        self.record_exactness_transition(was_exact, is_exact);
344        self.record_mutation();
345        outcome
346    }
347
348    /// Removes an analyzed file.
349    ///
350    /// A referenced node is demoted to a stub in the same slot. Unreferenced
351    /// nodes are freed, and outgoing reverse-dependency memberships are
352    /// removed incrementally.
353    pub fn remove_file(&mut self, path: &str) -> bool {
354        let Some(&slot) = self.by_path.get(path) else {
355            return false;
356        };
357        if matches!(self.occupied(slot).state, NodeState::Stub)
358            && !self.occupied(slot).rdeps.is_empty()
359        {
360            return true;
361        }
362
363        let was_exact = self.node_is_rdeps_exact(slot);
364        let old_targets = self.node_targets(slot);
365        self.update_rdeps(slot, &old_targets, &FxHashSet::default());
366
367        if self.occupied(slot).rdeps.is_empty() {
368            self.free_slot(slot);
369        } else {
370            let node = self.occupied_mut(slot);
371            node.state = NodeState::Stub;
372            node.out.clear();
373            node.config_dependencies.clear();
374            node.imports_supported = false;
375            node.resolver_live = false;
376            node.resolution_complete = false;
377            node.resolved_at = 0;
378            self.record_exactness_transition(was_exact, false);
379        }
380        self.record_mutation();
381        true
382    }
383
384    /// Re-resolves every analyzed node against a new resolver generation.
385    ///
386    /// Reverse dependencies are adjusted by target-set diffs for each node;
387    /// they are never rebuilt globally.
388    pub fn reresolve_all(&mut self, resolvers: &ResolverSet) {
389        self.resolver_generation += 1;
390        self.inexact_nodes = self.by_path.len();
391        let current_generation = self.resolver_generation;
392        let jobs: Vec<_> = self
393            .nodes
394            .iter()
395            .enumerate()
396            .filter_map(|(slot, node)| {
397                let node = node.as_ref()?;
398                let NodeState::Analyzed { language, .. } = &node.state else {
399                    return None;
400                };
401                Some((
402                    slot as u32,
403                    node.path.clone(),
404                    node.out
405                        .iter()
406                        .map(|edge| edge.raw.clone())
407                        .collect::<Vec<_>>(),
408                    language.clone(),
409                ))
410            })
411            .collect();
412
413        for (slot, path, imports, language) in jobs {
414            let resolutions = resolve_imports(resolvers, &path, &imports);
415            let resolver_live = resolver_is_live(language.as_deref(), &imports, resolvers);
416            let old_targets = self.node_targets(slot);
417            let baseline_completeness = language
418                .as_deref()
419                .map_or(ResolutionCompleteness::Complete, |language| {
420                    resolvers.baseline_completeness(language)
421                });
422            let (edges, config_dependencies, resolution_complete) =
423                self.materialize_resolutions(resolutions, baseline_completeness);
424            let new_targets = targets_from_edges(&edges);
425            self.update_rdeps(slot, &old_targets, &new_targets);
426
427            let node = self.occupied_mut(slot);
428            node.out = edges;
429            node.config_dependencies = config_dependencies;
430            node.resolver_live = resolver_live;
431            node.resolution_complete = resolution_complete;
432            node.resolved_at = current_generation;
433            let is_exact = self.node_is_rdeps_exact(slot);
434            self.record_exactness_transition(false, is_exact);
435        }
436        self.record_mutation();
437    }
438
439    /// Advances only the resolver generation, leaving current edges stale.
440    pub fn bump_resolver_generation(&mut self) {
441        self.resolver_generation += 1;
442        self.inexact_nodes = self.by_path.len();
443        self.record_mutation();
444    }
445
446    /// Returns the deduplicated union of resolver configuration dependencies.
447    #[must_use]
448    pub fn config_dependencies(&self) -> Vec<CompactString> {
449        let mut dependencies: Vec<_> = self
450            .nodes
451            .iter()
452            .flatten()
453            .flat_map(|node| node.config_dependencies.iter().cloned())
454            .collect::<FxHashSet<_>>()
455            .into_iter()
456            .collect();
457        dependencies.sort_unstable();
458        dependencies
459    }
460
461    /// Iterates over occupied node paths in slot order.
462    pub fn paths(&self) -> impl Iterator<Item = &str> {
463        self.nodes.iter().flatten().map(|node| node.path.as_str())
464    }
465
466    /// Iterates over every stored edge with graph slots materialized as paths.
467    pub fn edges(&self) -> impl Iterator<Item = DepEdge> + '_ {
468        self.nodes
469            .iter()
470            .enumerate()
471            .flat_map(move |(source, node)| {
472                let source =
473                    u32::try_from(source).expect("module graph slot must fit in its u32 key");
474                node.iter().flat_map(move |node| {
475                    node.out
476                        .iter()
477                        .map(move |edge| self.owned_edge(source, edge))
478                })
479            })
480    }
481
482    /// Returns the total number of resolved, external, and unresolved edges.
483    #[must_use]
484    pub fn edge_count(&self) -> usize {
485        self.nodes.iter().flatten().map(|node| node.out.len()).sum()
486    }
487
488    /// Returns direct outgoing dependencies for `path`.
489    #[must_use]
490    pub fn deps(&self, path: &str) -> Option<DepsResult> {
491        let slot = *self.by_path.get(path)?;
492        let node = self.occupied(slot);
493        let mut visited = FxHashSet::default();
494        visited.insert(slot);
495        let mut edges = Vec::with_capacity(node.out.len());
496        for edge in &node.out {
497            if let EdgeTarget::Node(target) = edge.target {
498                visited.insert(target);
499            }
500            edges.push(self.owned_edge(slot, edge));
501        }
502        sort_edges(&mut edges);
503
504        Some(DepsResult {
505            edges,
506            guarantee: self.deps_guarantee(slot),
507            coverage: self.coverage(&visited),
508        })
509    }
510
511    /// Returns all stored edges that directly target `path`.
512    #[must_use]
513    pub fn rdeps(&self, path: &str) -> Option<DepsResult> {
514        let target = *self.by_path.get(path)?;
515        let node = self.occupied(target);
516        let mut visited = FxHashSet::default();
517        visited.insert(target);
518        let mut edges = Vec::new();
519        for &source in &node.rdeps {
520            let source_node = self.occupied(source);
521            visited.insert(source);
522            edges.extend(
523                source_node
524                    .out
525                    .iter()
526                    .filter(|edge| edge.target == EdgeTarget::Node(target))
527                    .map(|edge| self.owned_edge(source, edge)),
528            );
529        }
530        sort_edges(&mut edges);
531
532        Some(DepsResult {
533            edges,
534            guarantee: self.rdeps_guarantee(),
535            coverage: self.coverage(&visited),
536        })
537    }
538
539    /// Traverses both dependency directions to `depth` edges from `path`.
540    #[must_use]
541    pub fn neighborhood(&self, path: &str, depth: u32) -> Option<NeighborhoodResult> {
542        let center = *self.by_path.get(path)?;
543        let mut visited = FxHashSet::default();
544        let mut queue = VecDeque::new();
545        visited.insert(center);
546        queue.push_back((center, 0_u32));
547
548        while let Some((slot, distance)) = queue.pop_front() {
549            if distance == depth {
550                continue;
551            }
552            let node = self.occupied(slot);
553            let neighbors = node
554                .out
555                .iter()
556                .filter_map(|edge| match edge.target {
557                    EdgeTarget::Node(target) => Some(target),
558                    EdgeTarget::External(_) | EdgeTarget::Unresolved(_) => None,
559                })
560                .chain(node.rdeps.iter().copied())
561                .collect::<Vec<_>>();
562            for neighbor in neighbors {
563                if visited.insert(neighbor) {
564                    queue.push_back((neighbor, distance + 1));
565                }
566            }
567        }
568
569        let mut nodes: Vec<_> = visited
570            .iter()
571            .map(|&slot| self.occupied(slot).path.clone())
572            .collect();
573        nodes.sort_unstable();
574
575        let mut edges = Vec::new();
576        if depth != 0 {
577            for &source in &visited {
578                edges.extend(
579                    self.occupied(source)
580                        .out
581                        .iter()
582                        .filter(|edge| {
583                            matches!(edge.target, EdgeTarget::Node(target) if visited.contains(&target))
584                        })
585                        .map(|edge| self.owned_edge(source, edge)),
586                );
587            }
588        }
589        sort_edges(&mut edges);
590
591        let guarantee = visited
592            .iter()
593            .fold(Guarantee::Exact, |guarantee, &slot| {
594                guarantee.weakest(self.deps_guarantee(slot))
595            })
596            .weakest(if depth == 0 {
597                Guarantee::Exact
598            } else {
599                self.rdeps_guarantee()
600            });
601
602        Some(NeighborhoodResult {
603            nodes,
604            edges,
605            guarantee,
606            coverage: self.coverage(&visited),
607        })
608    }
609
610    fn deps_guarantee(&self, slot: u32) -> Guarantee {
611        let node = self.occupied(slot);
612        let exact = matches!(
613            node.state,
614            NodeState::Analyzed {
615                has_opaque_imports: false,
616                ..
617            }
618        ) && node.imports_supported
619            && node.resolver_live
620            && node.resolution_complete
621            // Edges resolved under an older resolver configuration may point
622            // at the wrong targets until reresolve_all catches up.
623            && node.resolved_at == self.resolver_generation;
624        if exact {
625            Guarantee::Exact
626        } else {
627            Guarantee::Approximate
628        }
629    }
630
631    fn rdeps_guarantee(&self) -> Guarantee {
632        let exact = self.universe_complete && self.inexact_nodes == 0;
633        if exact {
634            Guarantee::Exact
635        } else {
636            Guarantee::Approximate
637        }
638    }
639
640    fn coverage(&self, slots: &FxHashSet<u32>) -> Coverage {
641        let mut ordered: Vec<_> = slots.iter().map(|&slot| self.occupied(slot)).collect();
642        ordered.sort_unstable_by(|left, right| left.path.cmp(&right.path));
643
644        let mut coverage = Coverage::default();
645        for node in ordered {
646            match &node.state {
647                NodeState::Analyzed {
648                    content_hash,
649                    has_opaque_imports,
650                    ..
651                } => {
652                    coverage.analyzed += 1;
653                    coverage.opaque_files += u64::from(*has_opaque_imports);
654                    coverage.basis.push((node.path.clone(), *content_hash));
655                }
656                NodeState::Stub => coverage.stubs += 1,
657            }
658        }
659        coverage
660    }
661
662    fn materialize_resolutions(
663        &mut self,
664        resolutions: Vec<(RawImport, ResolutionOutcome)>,
665        baseline_completeness: ResolutionCompleteness,
666    ) -> (Vec<ImportEdge>, Vec<CompactString>, bool) {
667        let mut dependencies = FxHashSet::default();
668        let mut resolution_complete = baseline_completeness == ResolutionCompleteness::Complete;
669        let edges = resolutions
670            .into_iter()
671            .map(|(raw, outcome)| {
672                resolution_complete &= outcome.completeness == ResolutionCompleteness::Complete;
673                dependencies.extend(outcome.dependencies);
674                let target = match outcome.resolved {
675                    Resolved::Path(path) => EdgeTarget::Node(self.ensure_stub(path)),
676                    Resolved::External(package) => EdgeTarget::External(package),
677                    Resolved::Unresolved(reason) => EdgeTarget::Unresolved(reason),
678                };
679                ImportEdge { raw, target }
680            })
681            .collect();
682        let mut dependencies: Vec<_> = dependencies.into_iter().collect();
683        dependencies.sort_unstable();
684        (edges, dependencies, resolution_complete)
685    }
686
687    fn ensure_stub(&mut self, path: CompactString) -> u32 {
688        self.by_path
689            .get(path.as_str())
690            .copied()
691            .unwrap_or_else(|| self.allocate(ModuleNode::stub(path)))
692    }
693
694    fn allocate(&mut self, node: ModuleNode) -> u32 {
695        let exact = rdeps_node_is_exact(&node, self.resolver_generation);
696        let path = node.path.clone();
697        let slot = if let Some(slot) = self.free.pop() {
698            debug_assert!(self.nodes[slot as usize].is_none());
699            self.nodes[slot as usize] = Some(node);
700            slot
701        } else {
702            let slot =
703                u32::try_from(self.nodes.len()).expect("module graph exhausted its u32 slot space");
704            self.nodes.push(Some(node));
705            slot
706        };
707        self.by_path.insert(path, slot);
708        self.inexact_nodes += usize::from(!exact);
709        slot
710    }
711
712    fn free_slot(&mut self, slot: u32) {
713        let node = self.nodes[slot as usize]
714            .take()
715            .expect("slot to free must be occupied");
716        if !rdeps_node_is_exact(&node, self.resolver_generation) {
717            self.inexact_nodes -= 1;
718        }
719        let removed = self.by_path.remove(node.path.as_str());
720        debug_assert_eq!(removed, Some(slot));
721        self.free.push(slot);
722    }
723
724    fn prune_orphan_stub(&mut self, slot: u32) {
725        let should_prune = self.nodes[slot as usize]
726            .as_ref()
727            .is_some_and(|node| matches!(node.state, NodeState::Stub) && node.rdeps.is_empty());
728        if should_prune {
729            self.free_slot(slot);
730        }
731    }
732
733    fn node_targets(&self, slot: u32) -> FxHashSet<u32> {
734        targets_from_edges(&self.occupied(slot).out)
735    }
736
737    fn update_rdeps(
738        &mut self,
739        source: u32,
740        old_targets: &FxHashSet<u32>,
741        new_targets: &FxHashSet<u32>,
742    ) {
743        for &target in new_targets.difference(old_targets) {
744            self.occupied_mut(target).rdeps.insert(source);
745        }
746        let removed: Vec<_> = old_targets.difference(new_targets).copied().collect();
747        for &target in &removed {
748            self.occupied_mut(target).rdeps.remove(&source);
749        }
750        for target in removed {
751            self.prune_orphan_stub(target);
752        }
753    }
754
755    fn owned_edge(&self, source: u32, edge: &ImportEdge) -> DepEdge {
756        let raw = &edge.raw;
757        let to = match &edge.target {
758            EdgeTarget::Node(target) => EdgeTargetOwned::Path(self.occupied(*target).path.clone()),
759            EdgeTarget::External(package) => EdgeTargetOwned::External(package.clone()),
760            EdgeTarget::Unresolved(reason) => EdgeTargetOwned::Unresolved(reason.clone()),
761        };
762        DepEdge {
763            from: self.occupied(source).path.clone(),
764            to,
765            specifier: raw.specifier.clone(),
766            kind: raw.kind,
767            line: raw.line,
768            span: raw.span,
769        }
770    }
771
772    fn occupied(&self, slot: u32) -> &ModuleNode {
773        self.nodes[slot as usize]
774            .as_ref()
775            .expect("graph edge must reference an occupied slot")
776    }
777
778    fn occupied_mut(&mut self, slot: u32) -> &mut ModuleNode {
779        self.nodes[slot as usize]
780            .as_mut()
781            .expect("graph edge must reference an occupied slot")
782    }
783
784    fn node_is_rdeps_exact(&self, slot: u32) -> bool {
785        rdeps_node_is_exact(self.occupied(slot), self.resolver_generation)
786    }
787
788    fn record_exactness_transition(&mut self, was_exact: bool, is_exact: bool) {
789        match (was_exact, is_exact) {
790            (false, true) => self.inexact_nodes -= 1,
791            (true, false) => self.inexact_nodes += 1,
792            (false, false) | (true, true) => {}
793        }
794    }
795
796    fn record_mutation(&mut self) {
797        self.generation += 1;
798    }
799}
800
801fn rdeps_node_is_exact(node: &ModuleNode, resolver_generation: u64) -> bool {
802    matches!(
803        node.state,
804        NodeState::Analyzed {
805            has_opaque_imports: false,
806            ..
807        }
808    ) && node.imports_supported
809        && node.resolver_live
810        && node.resolution_complete
811        && node.resolved_at == resolver_generation
812}
813
814fn resolve_imports(
815    resolvers: &ResolverSet,
816    path: &str,
817    imports: &[RawImport],
818) -> Vec<(RawImport, ResolutionOutcome)> {
819    imports
820        .iter()
821        .cloned()
822        .map(|raw| {
823            let outcome = resolvers.resolve(path, &raw);
824            (raw, outcome)
825        })
826        .collect()
827}
828
829fn resolver_is_live(
830    language: Option<&str>,
831    imports: &[RawImport],
832    resolvers: &ResolverSet,
833) -> bool {
834    match language {
835        Some("rust") => resolvers.rust.is_some(),
836        Some("typescript" | "tsx" | "javascript" | "jsx") => resolvers.js.is_some(),
837        Some(_) if !imports.is_empty() => imports.iter().all(|raw| match raw.kind {
838            ImportKind::RustUse | ImportKind::RustMod => resolvers.rust.is_some(),
839            _ => resolvers.js.is_some(),
840        }),
841        Some(_) | None => false,
842    }
843}
844
845fn targets_from_edges(edges: &[ImportEdge]) -> FxHashSet<u32> {
846    edges
847        .iter()
848        .filter_map(|edge| match edge.target {
849            EdgeTarget::Node(target) => Some(target),
850            EdgeTarget::External(_) | EdgeTarget::Unresolved(_) => None,
851        })
852        .collect()
853}
854
855fn sort_edges(edges: &mut [DepEdge]) {
856    edges.sort_by(|left, right| {
857        left.from
858            .cmp(&right.from)
859            .then(left.line.cmp(&right.line))
860            .then(left.span.0.cmp(&right.span.0))
861            .then(left.span.1.cmp(&right.span.1))
862            .then(left.specifier.cmp(&right.specifier))
863    });
864}