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_guarantee_for(&self, path: &str) -> Option<Guarantee> {
514 self.by_path.get(path)?;
515 Some(self.rdeps_guarantee())
516 }
517
518 #[must_use]
520 pub fn rdeps_bounded(&self, path: &str, limit: usize) -> Option<DepsResult> {
521 let target = *self.by_path.get(path)?;
522 let node = self.occupied(target);
523 let mut visited = FxHashSet::default();
524 visited.insert(target);
525 let mut edges = Vec::new();
526 'sources: for &source in &node.rdeps {
527 let source_node = self.occupied(source);
528 visited.insert(source);
529 for edge in source_node
530 .out
531 .iter()
532 .filter(|edge| edge.target == EdgeTarget::Node(target))
533 {
534 if edges.len() >= limit {
535 break 'sources;
536 }
537 edges.push(self.owned_edge(source, edge));
538 }
539 }
540 sort_edges(&mut edges);
541 Some(DepsResult {
542 edges,
543 guarantee: self.rdeps_guarantee(),
544 coverage: self.coverage(&visited),
545 })
546 }
547
548 #[must_use]
550 pub fn rdeps(&self, path: &str) -> Option<DepsResult> {
551 let target = *self.by_path.get(path)?;
552 let node = self.occupied(target);
553 let mut visited = FxHashSet::default();
554 visited.insert(target);
555 let mut edges = Vec::new();
556 for &source in &node.rdeps {
557 let source_node = self.occupied(source);
558 visited.insert(source);
559 edges.extend(
560 source_node
561 .out
562 .iter()
563 .filter(|edge| edge.target == EdgeTarget::Node(target))
564 .map(|edge| self.owned_edge(source, edge)),
565 );
566 }
567 sort_edges(&mut edges);
568
569 Some(DepsResult {
570 edges,
571 guarantee: self.rdeps_guarantee(),
572 coverage: self.coverage(&visited),
573 })
574 }
575
576 #[must_use]
578 pub fn neighborhood(&self, path: &str, depth: u32) -> Option<NeighborhoodResult> {
579 let center = *self.by_path.get(path)?;
580 let mut visited = FxHashSet::default();
581 let mut queue = VecDeque::new();
582 visited.insert(center);
583 queue.push_back((center, 0_u32));
584
585 while let Some((slot, distance)) = queue.pop_front() {
586 if distance == depth {
587 continue;
588 }
589 let node = self.occupied(slot);
590 let neighbors = node
591 .out
592 .iter()
593 .filter_map(|edge| match edge.target {
594 EdgeTarget::Node(target) => Some(target),
595 EdgeTarget::External(_) | EdgeTarget::Unresolved(_) => None,
596 })
597 .chain(node.rdeps.iter().copied())
598 .collect::<Vec<_>>();
599 for neighbor in neighbors {
600 if visited.insert(neighbor) {
601 queue.push_back((neighbor, distance + 1));
602 }
603 }
604 }
605
606 let mut nodes: Vec<_> = visited
607 .iter()
608 .map(|&slot| self.occupied(slot).path.clone())
609 .collect();
610 nodes.sort_unstable();
611
612 let mut edges = Vec::new();
613 if depth != 0 {
614 for &source in &visited {
615 edges.extend(
616 self.occupied(source)
617 .out
618 .iter()
619 .filter(|edge| {
620 matches!(edge.target, EdgeTarget::Node(target) if visited.contains(&target))
621 })
622 .map(|edge| self.owned_edge(source, edge)),
623 );
624 }
625 }
626 sort_edges(&mut edges);
627
628 let guarantee = visited
629 .iter()
630 .fold(Guarantee::Exact, |guarantee, &slot| {
631 guarantee.weakest(self.deps_guarantee(slot))
632 })
633 .weakest(if depth == 0 {
634 Guarantee::Exact
635 } else {
636 self.rdeps_guarantee()
637 });
638
639 Some(NeighborhoodResult {
640 nodes,
641 edges,
642 guarantee,
643 coverage: self.coverage(&visited),
644 })
645 }
646
647 fn deps_guarantee(&self, slot: u32) -> Guarantee {
648 let node = self.occupied(slot);
649 let exact = matches!(
650 node.state,
651 NodeState::Analyzed {
652 has_opaque_imports: false,
653 ..
654 }
655 ) && node.imports_supported
656 && node.resolver_live
657 && node.resolution_complete
658 && node.resolved_at == self.resolver_generation;
661 if exact {
662 Guarantee::Exact
663 } else {
664 Guarantee::Approximate
665 }
666 }
667
668 fn rdeps_guarantee(&self) -> Guarantee {
669 let exact = self.universe_complete && self.inexact_nodes == 0;
670 if exact {
671 Guarantee::Exact
672 } else {
673 Guarantee::Approximate
674 }
675 }
676
677 fn coverage(&self, slots: &FxHashSet<u32>) -> Coverage {
678 let mut ordered: Vec<_> = slots.iter().map(|&slot| self.occupied(slot)).collect();
679 ordered.sort_unstable_by(|left, right| left.path.cmp(&right.path));
680
681 let mut coverage = Coverage::default();
682 for node in ordered {
683 match &node.state {
684 NodeState::Analyzed {
685 content_hash,
686 has_opaque_imports,
687 ..
688 } => {
689 coverage.analyzed += 1;
690 coverage.opaque_files += u64::from(*has_opaque_imports);
691 coverage.basis.push((node.path.clone(), *content_hash));
692 }
693 NodeState::Stub => coverage.stubs += 1,
694 }
695 }
696 coverage
697 }
698
699 fn materialize_resolutions(
700 &mut self,
701 resolutions: Vec<(RawImport, ResolutionOutcome)>,
702 baseline_completeness: ResolutionCompleteness,
703 ) -> (Vec<ImportEdge>, Vec<CompactString>, bool) {
704 let mut dependencies = FxHashSet::default();
705 let mut resolution_complete = baseline_completeness == ResolutionCompleteness::Complete;
706 let edges = resolutions
707 .into_iter()
708 .map(|(raw, outcome)| {
709 resolution_complete &= outcome.completeness == ResolutionCompleteness::Complete;
710 dependencies.extend(outcome.dependencies);
711 let target = match outcome.resolved {
712 Resolved::Path(path) => EdgeTarget::Node(self.ensure_stub(path)),
713 Resolved::External(package) => EdgeTarget::External(package),
714 Resolved::Unresolved(reason) => EdgeTarget::Unresolved(reason),
715 };
716 ImportEdge { raw, target }
717 })
718 .collect();
719 let mut dependencies: Vec<_> = dependencies.into_iter().collect();
720 dependencies.sort_unstable();
721 (edges, dependencies, resolution_complete)
722 }
723
724 fn ensure_stub(&mut self, path: CompactString) -> u32 {
725 self.by_path
726 .get(path.as_str())
727 .copied()
728 .unwrap_or_else(|| self.allocate(ModuleNode::stub(path)))
729 }
730
731 fn allocate(&mut self, node: ModuleNode) -> u32 {
732 let exact = rdeps_node_is_exact(&node, self.resolver_generation);
733 let path = node.path.clone();
734 let slot = if let Some(slot) = self.free.pop() {
735 debug_assert!(self.nodes[slot as usize].is_none());
736 self.nodes[slot as usize] = Some(node);
737 slot
738 } else {
739 let slot =
740 u32::try_from(self.nodes.len()).expect("module graph exhausted its u32 slot space");
741 self.nodes.push(Some(node));
742 slot
743 };
744 self.by_path.insert(path, slot);
745 self.inexact_nodes += usize::from(!exact);
746 slot
747 }
748
749 fn free_slot(&mut self, slot: u32) {
750 let node = self.nodes[slot as usize]
751 .take()
752 .expect("slot to free must be occupied");
753 if !rdeps_node_is_exact(&node, self.resolver_generation) {
754 self.inexact_nodes -= 1;
755 }
756 let removed = self.by_path.remove(node.path.as_str());
757 debug_assert_eq!(removed, Some(slot));
758 self.free.push(slot);
759 }
760
761 fn prune_orphan_stub(&mut self, slot: u32) {
762 let should_prune = self.nodes[slot as usize]
763 .as_ref()
764 .is_some_and(|node| matches!(node.state, NodeState::Stub) && node.rdeps.is_empty());
765 if should_prune {
766 self.free_slot(slot);
767 }
768 }
769
770 fn node_targets(&self, slot: u32) -> FxHashSet<u32> {
771 targets_from_edges(&self.occupied(slot).out)
772 }
773
774 fn update_rdeps(
775 &mut self,
776 source: u32,
777 old_targets: &FxHashSet<u32>,
778 new_targets: &FxHashSet<u32>,
779 ) {
780 for &target in new_targets.difference(old_targets) {
781 self.occupied_mut(target).rdeps.insert(source);
782 }
783 let removed: Vec<_> = old_targets.difference(new_targets).copied().collect();
784 for &target in &removed {
785 self.occupied_mut(target).rdeps.remove(&source);
786 }
787 for target in removed {
788 self.prune_orphan_stub(target);
789 }
790 }
791
792 fn owned_edge(&self, source: u32, edge: &ImportEdge) -> DepEdge {
793 let raw = &edge.raw;
794 let to = match &edge.target {
795 EdgeTarget::Node(target) => EdgeTargetOwned::Path(self.occupied(*target).path.clone()),
796 EdgeTarget::External(package) => EdgeTargetOwned::External(package.clone()),
797 EdgeTarget::Unresolved(reason) => EdgeTargetOwned::Unresolved(reason.clone()),
798 };
799 DepEdge {
800 from: self.occupied(source).path.clone(),
801 to,
802 specifier: raw.specifier.clone(),
803 kind: raw.kind,
804 line: raw.line,
805 span: raw.span,
806 }
807 }
808
809 fn occupied(&self, slot: u32) -> &ModuleNode {
810 self.nodes[slot as usize]
811 .as_ref()
812 .expect("graph edge must reference an occupied slot")
813 }
814
815 fn occupied_mut(&mut self, slot: u32) -> &mut ModuleNode {
816 self.nodes[slot as usize]
817 .as_mut()
818 .expect("graph edge must reference an occupied slot")
819 }
820
821 fn node_is_rdeps_exact(&self, slot: u32) -> bool {
822 rdeps_node_is_exact(self.occupied(slot), self.resolver_generation)
823 }
824
825 fn record_exactness_transition(&mut self, was_exact: bool, is_exact: bool) {
826 match (was_exact, is_exact) {
827 (false, true) => self.inexact_nodes -= 1,
828 (true, false) => self.inexact_nodes += 1,
829 (false, false) | (true, true) => {}
830 }
831 }
832
833 fn record_mutation(&mut self) {
834 self.generation += 1;
835 }
836}
837
838fn rdeps_node_is_exact(node: &ModuleNode, resolver_generation: u64) -> bool {
839 matches!(
840 node.state,
841 NodeState::Analyzed {
842 has_opaque_imports: false,
843 ..
844 }
845 ) && node.imports_supported
846 && node.resolver_live
847 && node.resolution_complete
848 && node.resolved_at == resolver_generation
849}
850
851fn resolve_imports(
852 resolvers: &ResolverSet,
853 path: &str,
854 imports: &[RawImport],
855) -> Vec<(RawImport, ResolutionOutcome)> {
856 imports
857 .iter()
858 .cloned()
859 .map(|raw| {
860 let outcome = resolvers.resolve(path, &raw);
861 (raw, outcome)
862 })
863 .collect()
864}
865
866fn resolver_is_live(
867 language: Option<&str>,
868 imports: &[RawImport],
869 resolvers: &ResolverSet,
870) -> bool {
871 match language {
872 Some("rust") => resolvers.rust.is_some(),
873 Some("typescript" | "tsx" | "javascript" | "jsx" | "vue") => resolvers.js.is_some(),
874 Some(_) if !imports.is_empty() => imports.iter().all(|raw| match raw.kind {
875 ImportKind::RustUse | ImportKind::RustMod => resolvers.rust.is_some(),
876 _ => resolvers.js.is_some(),
877 }),
878 Some(_) | None => false,
879 }
880}
881
882fn targets_from_edges(edges: &[ImportEdge]) -> FxHashSet<u32> {
883 edges
884 .iter()
885 .filter_map(|edge| match edge.target {
886 EdgeTarget::Node(target) => Some(target),
887 EdgeTarget::External(_) | EdgeTarget::Unresolved(_) => None,
888 })
889 .collect()
890}
891
892fn sort_edges(edges: &mut [DepEdge]) {
893 edges.sort_by(|left, right| {
894 left.from
895 .cmp(&right.from)
896 .then(left.line.cmp(&right.line))
897 .then(left.span.0.cmp(&right.span.0))
898 .then(left.span.1.cmp(&right.span.1))
899 .then(left.specifier.cmp(&right.specifier))
900 });
901}