1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum UpsertOutcome {
21 Inserted,
23 Updated,
25 Unchanged,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Guarantee {
32 Exact,
34 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#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum NodeState {
51 Analyzed {
53 content_hash: u64,
55 has_opaque_imports: bool,
57 language: Option<CompactString>,
59 },
60 Stub,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum EdgeTarget {
67 Node(u32),
69 External(CompactString),
71 Unresolved(UnresolvedReason),
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum EdgeTargetOwned {
78 Path(CompactString),
80 External(CompactString),
82 Unresolved(UnresolvedReason),
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct ImportEdge {
89 pub raw: RawImport,
91 pub target: EdgeTarget,
93}
94
95#[derive(Debug, Clone)]
97pub struct ModuleNode {
98 pub path: CompactString,
100 pub state: NodeState,
102 pub out: Vec<ImportEdge>,
104 pub(crate) rdeps: FxHashSet<u32>,
106 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 #[must_use]
131 pub fn resolved_generation(&self) -> Option<u64> {
132 matches!(self.state, NodeState::Analyzed { .. }).then_some(self.resolved_at)
133 }
134
135 #[must_use]
137 pub fn imports_supported(&self) -> bool {
138 self.imports_supported
139 }
140
141 #[must_use]
143 pub fn resolver_live(&self) -> bool {
144 self.resolver_live
145 }
146
147 #[must_use]
149 pub fn resolution_complete(&self) -> bool {
150 self.resolution_complete
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct DepEdge {
157 pub from: CompactString,
159 pub to: EdgeTargetOwned,
161 pub specifier: CompactString,
163 pub kind: ImportKind,
165 pub line: u32,
167 pub span: (u32, u32),
169}
170
171#[derive(Debug, Clone, Default, PartialEq, Eq)]
173pub struct Coverage {
174 pub analyzed: u64,
176 pub stubs: u64,
178 pub opaque_files: u64,
180 pub basis: Vec<(CompactString, u64)>,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct DepsResult {
187 pub edges: Vec<DepEdge>,
189 pub guarantee: Guarantee,
191 pub coverage: Coverage,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct NeighborhoodResult {
198 pub nodes: Vec<CompactString>,
200 pub edges: Vec<DepEdge>,
202 pub guarantee: Guarantee,
204 pub coverage: Coverage,
206}
207
208#[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 #[must_use]
223 pub fn new() -> Self {
224 Self::default()
225 }
226
227 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 #[must_use]
237 pub fn generation(&self) -> u64 {
238 self.generation
239 }
240
241 #[must_use]
243 pub fn resolver_generation(&self) -> u64 {
244 self.resolver_generation
245 }
246
247 #[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 #[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 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 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 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 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 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 #[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 pub fn paths(&self) -> impl Iterator<Item = &str> {
463 self.nodes.iter().flatten().map(|node| node.path.as_str())
464 }
465
466 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 #[must_use]
484 pub fn edge_count(&self) -> usize {
485 self.nodes.iter().flatten().map(|node| node.out.len()).sum()
486 }
487
488 #[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 #[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 #[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 && 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}