1use std::collections::{HashMap, HashSet, VecDeque};
2use std::ops::Range;
3use std::path::PathBuf;
4
5use fixedbitset::FixedBitSet;
6
7use crate::discover::{DiscoveredFile, EntryPoint, FileId};
8use crate::extract::{ExportName, ImportedName};
9use crate::resolve::{ResolveResult, ResolvedModule};
10
11#[derive(Debug)]
13pub struct ModuleGraph {
14 pub modules: Vec<ModuleNode>,
16 edges: Vec<Edge>,
18 pub package_usage: HashMap<String, Vec<FileId>>,
20 pub entry_points: HashSet<FileId>,
22 pub reverse_deps: Vec<Vec<FileId>>,
24 namespace_imported: FixedBitSet,
26}
27
28#[derive(Debug)]
30pub struct ModuleNode {
31 pub file_id: FileId,
32 pub path: PathBuf,
33 pub edge_range: Range<usize>,
35 pub exports: Vec<ExportSymbol>,
37 pub re_exports: Vec<ReExportEdge>,
39 pub is_entry_point: bool,
41 pub is_reachable: bool,
43 pub has_cjs_exports: bool,
45}
46
47#[derive(Debug)]
49pub struct ReExportEdge {
50 pub source_file: FileId,
52 pub imported_name: String,
54 pub exported_name: String,
56 pub is_type_only: bool,
58}
59
60#[derive(Debug)]
62pub struct ExportSymbol {
63 pub name: ExportName,
64 pub is_type_only: bool,
65 pub span: oxc_span::Span,
66 pub references: Vec<SymbolReference>,
68 pub members: Vec<crate::extract::MemberInfo>,
70}
71
72#[derive(Debug, Clone)]
74pub struct SymbolReference {
75 pub from_file: FileId,
76 pub kind: ReferenceKind,
77}
78
79#[derive(Debug, Clone, PartialEq)]
81pub enum ReferenceKind {
82 NamedImport,
83 DefaultImport,
84 NamespaceImport,
85 ReExport,
86 DynamicImport,
87 SideEffectImport,
88}
89
90#[derive(Debug)]
92#[allow(dead_code)]
93struct Edge {
94 source: FileId,
95 target: FileId,
96 symbols: Vec<ImportedSymbol>,
97 is_dynamic: bool,
98 is_side_effect: bool,
99}
100
101#[derive(Debug)]
103struct ImportedSymbol {
104 imported_name: ImportedName,
105 #[allow(dead_code)]
106 local_name: String,
107}
108
109impl ModuleGraph {
110 pub fn build(
112 resolved_modules: &[ResolvedModule],
113 entry_points: &[EntryPoint],
114 files: &[DiscoveredFile],
115 ) -> Self {
116 let _span = tracing::info_span!("build_graph").entered();
117
118 let module_count = files.len();
119
120 let max_file_id = files
123 .iter()
124 .map(|f| f.id.0 as usize)
125 .max()
126 .map(|m| m + 1)
127 .unwrap_or(0);
128 let total_capacity = max_file_id.max(module_count);
129
130 let path_to_id: HashMap<PathBuf, FileId> =
132 files.iter().map(|f| (f.path.clone(), f.id)).collect();
133
134 let module_by_id: HashMap<FileId, &ResolvedModule> =
136 resolved_modules.iter().map(|m| (m.file_id, m)).collect();
137
138 let mut all_edges = Vec::new();
139 let mut modules = Vec::with_capacity(module_count);
140 let mut package_usage: HashMap<String, Vec<FileId>> = HashMap::new();
141 let mut reverse_deps = vec![Vec::new(); total_capacity];
142
143 let entry_point_ids: HashSet<FileId> = entry_points
145 .iter()
146 .filter_map(|ep| {
147 path_to_id.get(&ep.path).copied().or_else(|| {
149 ep.path.canonicalize().ok().and_then(|c| {
151 path_to_id
152 .iter()
153 .find(|(p, _)| p.canonicalize().ok().as_ref() == Some(&c))
154 .map(|(_, &id)| id)
155 })
156 })
157 })
158 .collect();
159
160 let mut namespace_imported = FixedBitSet::with_capacity(total_capacity);
162
163 for file in files {
164 let edge_start = all_edges.len();
165
166 if let Some(resolved) = module_by_id.get(&file.id) {
167 let mut edges_by_target: HashMap<FileId, Vec<ImportedSymbol>> = HashMap::new();
169
170 for import in &resolved.resolved_imports {
171 match &import.target {
172 ResolveResult::InternalModule(target_id) => {
173 if matches!(import.info.imported_name, ImportedName::Namespace) {
175 let idx = target_id.0 as usize;
176 if idx < total_capacity {
177 namespace_imported.insert(idx);
178 }
179 }
180 edges_by_target
181 .entry(*target_id)
182 .or_default()
183 .push(ImportedSymbol {
184 imported_name: import.info.imported_name.clone(),
185 local_name: import.info.local_name.clone(),
186 });
187 }
188 ResolveResult::NpmPackage(name) => {
189 package_usage.entry(name.clone()).or_default().push(file.id);
190 }
191 _ => {}
192 }
193 }
194
195 for re_export in &resolved.re_exports {
197 if let ResolveResult::InternalModule(target_id) = &re_export.target {
198 let imp_name = if re_export.info.imported_name == "*" {
199 ImportedName::Namespace
200 } else {
201 ImportedName::Named(re_export.info.imported_name.clone())
202 };
203 if matches!(imp_name, ImportedName::Namespace) {
205 let idx = target_id.0 as usize;
206 if idx < total_capacity {
207 namespace_imported.insert(idx);
208 }
209 }
210 edges_by_target
211 .entry(*target_id)
212 .or_default()
213 .push(ImportedSymbol {
214 imported_name: imp_name,
215 local_name: re_export.info.exported_name.clone(),
216 });
217 } else if let ResolveResult::NpmPackage(name) = &re_export.target {
218 package_usage.entry(name.clone()).or_default().push(file.id);
219 }
220 }
221
222 for import in &resolved.resolved_dynamic_imports {
224 if let ResolveResult::InternalModule(target_id) = &import.target {
225 let idx = target_id.0 as usize;
226 if idx < total_capacity {
227 namespace_imported.insert(idx);
228 }
229 edges_by_target
230 .entry(*target_id)
231 .or_default()
232 .push(ImportedSymbol {
233 imported_name: ImportedName::Namespace,
234 local_name: String::new(),
235 });
236 }
237 }
238
239 for (_pattern, matched_ids) in &resolved.resolved_dynamic_patterns {
241 for target_id in matched_ids {
242 let idx = target_id.0 as usize;
243 if idx < total_capacity {
244 namespace_imported.insert(idx);
245 }
246 edges_by_target
247 .entry(*target_id)
248 .or_default()
249 .push(ImportedSymbol {
250 imported_name: ImportedName::Namespace,
251 local_name: String::new(),
252 });
253 }
254 }
255
256 for (target_id, symbols) in edges_by_target {
257 let is_side_effect = symbols
258 .iter()
259 .any(|s| matches!(s.imported_name, ImportedName::SideEffect));
260
261 all_edges.push(Edge {
262 source: file.id,
263 target: target_id,
264 symbols,
265 is_dynamic: false,
266 is_side_effect,
267 });
268
269 if (target_id.0 as usize) < reverse_deps.len() {
270 reverse_deps[target_id.0 as usize].push(file.id);
271 }
272 }
273 }
274
275 let edge_end = all_edges.len();
276
277 let mut exports: Vec<ExportSymbol> = module_by_id
278 .get(&file.id)
279 .map(|m| {
280 m.exports
281 .iter()
282 .map(|e| ExportSymbol {
283 name: e.name.clone(),
284 is_type_only: e.is_type_only,
285 span: e.span,
286 references: Vec::new(),
287 members: e.members.clone(),
288 })
289 .collect()
290 })
291 .unwrap_or_default();
292
293 if let Some(resolved) = module_by_id.get(&file.id) {
298 for re in &resolved.re_exports {
299 if re.info.exported_name == "*" {
303 continue;
304 }
305
306 let export_name = if re.info.exported_name == "default" {
310 ExportName::Default
311 } else {
312 ExportName::Named(re.info.exported_name.clone())
313 };
314 let already_exists = exports.iter().any(|e| e.name == export_name);
315 if already_exists {
316 continue;
317 }
318
319 exports.push(ExportSymbol {
320 name: export_name,
321 is_type_only: re.info.is_type_only,
322 span: oxc_span::Span::new(0, 0), references: Vec::new(),
324 members: Vec::new(),
325 });
326 }
327 }
328
329 let has_cjs_exports = module_by_id
330 .get(&file.id)
331 .map(|m| m.has_cjs_exports)
332 .unwrap_or(false);
333
334 let re_export_edges: Vec<ReExportEdge> = module_by_id
336 .get(&file.id)
337 .map(|m| {
338 m.re_exports
339 .iter()
340 .filter_map(|re| {
341 if let ResolveResult::InternalModule(target_id) = &re.target {
342 Some(ReExportEdge {
343 source_file: *target_id,
344 imported_name: re.info.imported_name.clone(),
345 exported_name: re.info.exported_name.clone(),
346 is_type_only: re.info.is_type_only,
347 })
348 } else {
349 None
350 }
351 })
352 .collect()
353 })
354 .unwrap_or_default();
355
356 modules.push(ModuleNode {
357 file_id: file.id,
358 path: file.path.clone(),
359 edge_range: edge_start..edge_end,
360 exports,
361 re_exports: re_export_edges,
362 is_entry_point: entry_point_ids.contains(&file.id),
363 is_reachable: false,
364 has_cjs_exports,
365 });
366 }
367
368 for edge in &all_edges {
370 let source_id = edge.source;
371 let Some(target_module) = modules.get_mut(edge.target.0 as usize) else {
372 continue;
373 };
374 for sym in &edge.symbols {
375 let ref_kind = match &sym.imported_name {
376 ImportedName::Named(_) => ReferenceKind::NamedImport,
377 ImportedName::Default => ReferenceKind::DefaultImport,
378 ImportedName::Namespace => ReferenceKind::NamespaceImport,
379 ImportedName::SideEffect => ReferenceKind::SideEffectImport,
380 };
381
382 if let Some(export) = target_module
384 .exports
385 .iter_mut()
386 .find(|e| export_matches(&e.name, &sym.imported_name))
387 {
388 export.references.push(SymbolReference {
389 from_file: source_id,
390 kind: ref_kind,
391 });
392 }
393
394 if matches!(sym.imported_name, ImportedName::Namespace)
399 && !sym.local_name.is_empty()
400 {
401 let local_name = &sym.local_name;
402 let source_mod = module_by_id.get(&source_id);
403 let accessed_members: Vec<String> = source_mod
404 .map(|m| {
405 m.member_accesses
406 .iter()
407 .filter(|ma| ma.object == *local_name)
408 .map(|ma| ma.member.clone())
409 .collect()
410 })
411 .unwrap_or_default();
412
413 let is_re_exported_from_non_entry = source_mod
417 .map(|m| {
418 m.exports
419 .iter()
420 .any(|e| e.local_name.as_deref() == Some(local_name.as_str()))
421 })
422 .unwrap_or(false)
423 && !entry_point_ids.contains(&source_id);
424
425 let is_entry_with_no_access =
429 accessed_members.is_empty() && entry_point_ids.contains(&source_id);
430
431 if !is_entry_with_no_access
432 && (accessed_members.is_empty() || is_re_exported_from_non_entry)
433 {
434 for export in &mut target_module.exports {
436 if export.references.iter().all(|r| r.from_file != source_id) {
437 export.references.push(SymbolReference {
438 from_file: source_id,
439 kind: ReferenceKind::NamespaceImport,
440 });
441 }
442 }
443 } else {
444 for export in &mut target_module.exports {
446 let name_str = export.name.to_string();
447 if accessed_members.contains(&name_str)
448 && export.references.iter().all(|r| r.from_file != source_id)
449 {
450 export.references.push(SymbolReference {
451 from_file: source_id,
452 kind: ReferenceKind::NamespaceImport,
453 });
454 }
455 }
456 }
457 } else if matches!(sym.imported_name, ImportedName::Namespace) {
458 for export in &mut target_module.exports {
460 if export.references.iter().all(|r| r.from_file != source_id) {
461 export.references.push(SymbolReference {
462 from_file: source_id,
463 kind: ReferenceKind::NamespaceImport,
464 });
465 }
466 }
467 }
468 }
469 }
470
471 let mut visited = FixedBitSet::with_capacity(total_capacity);
473 let mut queue = VecDeque::new();
474
475 for &ep_id in &entry_point_ids {
476 if (ep_id.0 as usize) < total_capacity {
477 visited.insert(ep_id.0 as usize);
478 queue.push_back(ep_id);
479 }
480 }
481
482 while let Some(file_id) = queue.pop_front() {
483 if (file_id.0 as usize) >= modules.len() {
484 continue;
485 }
486 let module = &modules[file_id.0 as usize];
487 for edge in &all_edges[module.edge_range.clone()] {
488 let target_idx = edge.target.0 as usize;
489 if target_idx < total_capacity && !visited.contains(target_idx) {
490 visited.insert(target_idx);
491 queue.push_back(edge.target);
492 }
493 }
494 }
495
496 for (idx, module) in modules.iter_mut().enumerate() {
497 module.is_reachable = visited.contains(idx);
498 }
499
500 let mut graph = Self {
501 modules,
502 edges: all_edges,
503 package_usage,
504 entry_points: entry_point_ids,
505 reverse_deps,
506 namespace_imported,
507 };
508
509 graph.resolve_re_export_chains();
511
512 graph
513 }
514
515 fn resolve_re_export_chains(&mut self) {
519 let re_export_info: Vec<(FileId, FileId, String, String)> = self
521 .modules
522 .iter()
523 .flat_map(|m| {
524 m.re_exports.iter().map(move |re| {
525 (
526 m.file_id,
527 re.source_file,
528 re.imported_name.clone(),
529 re.exported_name.clone(),
530 )
531 })
532 })
533 .collect();
534
535 if re_export_info.is_empty() {
536 return;
537 }
538
539 let mut changed = true;
543 let max_iterations = 20; let mut iteration = 0;
545 let mut existing_refs: HashSet<FileId> = HashSet::new();
549
550 while changed && iteration < max_iterations {
551 changed = false;
552 iteration += 1;
553
554 for &(barrel_id, source_id, ref imported_name, ref exported_name) in &re_export_info {
555 let barrel_idx = barrel_id.0 as usize;
556 let source_idx = source_id.0 as usize;
557
558 if barrel_idx >= self.modules.len() || source_idx >= self.modules.len() {
559 continue;
560 }
561
562 let refs_on_barrel: Vec<SymbolReference> = {
564 let barrel = &self.modules[barrel_idx];
565 barrel
566 .exports
567 .iter()
568 .filter(|e| e.name.to_string() == *exported_name)
569 .flat_map(|e| e.references.clone())
570 .collect()
571 };
572
573 if refs_on_barrel.is_empty() {
574 continue;
575 }
576
577 let source = &mut self.modules[source_idx];
579 let target_exports: Vec<usize> = if imported_name == "*" {
580 (0..source.exports.len()).collect()
582 } else {
583 source
584 .exports
585 .iter()
586 .enumerate()
587 .filter(|(_, e)| e.name.to_string() == *imported_name)
588 .map(|(i, _)| i)
589 .collect()
590 };
591
592 for export_idx in target_exports {
593 existing_refs.clear();
594 existing_refs.extend(
595 source.exports[export_idx]
596 .references
597 .iter()
598 .map(|r| r.from_file),
599 );
600 for ref_item in &refs_on_barrel {
601 if !existing_refs.contains(&ref_item.from_file) {
602 source.exports[export_idx].references.push(ref_item.clone());
603 changed = true;
604 }
605 }
606 }
607 }
608 }
609
610 if iteration >= max_iterations {
611 tracing::warn!(
612 iterations = max_iterations,
613 "Re-export chain resolution hit iteration limit, some chains may be incomplete"
614 );
615 }
616 }
617
618 pub fn module_count(&self) -> usize {
620 self.modules.len()
621 }
622
623 pub fn edge_count(&self) -> usize {
625 self.edges.len()
626 }
627
628 pub fn has_namespace_import(&self, file_id: FileId) -> bool {
631 let idx = file_id.0 as usize;
632 if idx >= self.namespace_imported.len() {
633 return false;
634 }
635 self.namespace_imported.contains(idx)
636 }
637}
638
639fn export_matches(export: &ExportName, import: &ImportedName) -> bool {
641 match (export, import) {
642 (ExportName::Named(e), ImportedName::Named(i)) => e == i,
643 (ExportName::Default, ImportedName::Default) => true,
644 _ => false,
645 }
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
652 use crate::extract::{ExportName, ImportInfo, ImportedName};
653 use crate::resolve::{ResolveResult, ResolvedImport, ResolvedModule, ResolvedReExport};
654 use std::path::PathBuf;
655
656 #[test]
657 fn export_matches_named_same() {
658 assert!(export_matches(
659 &ExportName::Named("foo".to_string()),
660 &ImportedName::Named("foo".to_string())
661 ));
662 }
663
664 #[test]
665 fn export_matches_named_different() {
666 assert!(!export_matches(
667 &ExportName::Named("foo".to_string()),
668 &ImportedName::Named("bar".to_string())
669 ));
670 }
671
672 #[test]
673 fn export_matches_default() {
674 assert!(export_matches(&ExportName::Default, &ImportedName::Default));
675 }
676
677 #[test]
678 fn export_matches_named_vs_default() {
679 assert!(!export_matches(
680 &ExportName::Named("foo".to_string()),
681 &ImportedName::Default
682 ));
683 }
684
685 #[test]
686 fn export_matches_default_vs_named() {
687 assert!(!export_matches(
688 &ExportName::Default,
689 &ImportedName::Named("foo".to_string())
690 ));
691 }
692
693 #[test]
694 fn export_matches_namespace_no_match() {
695 assert!(!export_matches(
696 &ExportName::Named("foo".to_string()),
697 &ImportedName::Namespace
698 ));
699 assert!(!export_matches(
700 &ExportName::Default,
701 &ImportedName::Namespace
702 ));
703 }
704
705 #[test]
706 fn export_matches_side_effect_no_match() {
707 assert!(!export_matches(
708 &ExportName::Named("foo".to_string()),
709 &ImportedName::SideEffect
710 ));
711 }
712
713 fn build_simple_graph() -> ModuleGraph {
715 let files = vec![
717 DiscoveredFile {
718 id: FileId(0),
719 path: PathBuf::from("/project/src/entry.ts"),
720 size_bytes: 100,
721 },
722 DiscoveredFile {
723 id: FileId(1),
724 path: PathBuf::from("/project/src/utils.ts"),
725 size_bytes: 50,
726 },
727 ];
728
729 let entry_points = vec![EntryPoint {
730 path: PathBuf::from("/project/src/entry.ts"),
731 source: EntryPointSource::PackageJsonMain,
732 }];
733
734 let resolved_modules = vec![
735 ResolvedModule {
736 file_id: FileId(0),
737 path: PathBuf::from("/project/src/entry.ts"),
738 exports: vec![],
739 re_exports: vec![],
740 resolved_imports: vec![ResolvedImport {
741 info: ImportInfo {
742 source: "./utils".to_string(),
743 imported_name: ImportedName::Named("foo".to_string()),
744 local_name: "foo".to_string(),
745 is_type_only: false,
746 span: oxc_span::Span::new(0, 10),
747 },
748 target: ResolveResult::InternalModule(FileId(1)),
749 }],
750 resolved_dynamic_imports: vec![],
751 resolved_dynamic_patterns: vec![],
752 member_accesses: vec![],
753 whole_object_uses: vec![],
754 has_cjs_exports: false,
755 },
756 ResolvedModule {
757 file_id: FileId(1),
758 path: PathBuf::from("/project/src/utils.ts"),
759 exports: vec![
760 crate::extract::ExportInfo {
761 name: ExportName::Named("foo".to_string()),
762 local_name: Some("foo".to_string()),
763 is_type_only: false,
764 span: oxc_span::Span::new(0, 20),
765 members: vec![],
766 },
767 crate::extract::ExportInfo {
768 name: ExportName::Named("bar".to_string()),
769 local_name: Some("bar".to_string()),
770 is_type_only: false,
771 span: oxc_span::Span::new(25, 45),
772 members: vec![],
773 },
774 ],
775 re_exports: vec![],
776 resolved_imports: vec![],
777 resolved_dynamic_imports: vec![],
778 resolved_dynamic_patterns: vec![],
779 member_accesses: vec![],
780 whole_object_uses: vec![],
781 has_cjs_exports: false,
782 },
783 ];
784
785 ModuleGraph::build(&resolved_modules, &entry_points, &files)
786 }
787
788 #[test]
789 fn graph_module_count() {
790 let graph = build_simple_graph();
791 assert_eq!(graph.module_count(), 2);
792 }
793
794 #[test]
795 fn graph_edge_count() {
796 let graph = build_simple_graph();
797 assert_eq!(graph.edge_count(), 1);
798 }
799
800 #[test]
801 fn graph_entry_point_is_reachable() {
802 let graph = build_simple_graph();
803 assert!(graph.modules[0].is_entry_point);
804 assert!(graph.modules[0].is_reachable);
805 }
806
807 #[test]
808 fn graph_imported_module_is_reachable() {
809 let graph = build_simple_graph();
810 assert!(!graph.modules[1].is_entry_point);
811 assert!(graph.modules[1].is_reachable);
812 }
813
814 #[test]
815 fn graph_export_has_reference() {
816 let graph = build_simple_graph();
817 let utils = &graph.modules[1];
818 let foo_export = utils
819 .exports
820 .iter()
821 .find(|e| e.name.to_string() == "foo")
822 .unwrap();
823 assert!(
824 !foo_export.references.is_empty(),
825 "foo should have references"
826 );
827 }
828
829 #[test]
830 fn graph_unused_export_no_reference() {
831 let graph = build_simple_graph();
832 let utils = &graph.modules[1];
833 let bar_export = utils
834 .exports
835 .iter()
836 .find(|e| e.name.to_string() == "bar")
837 .unwrap();
838 assert!(
839 bar_export.references.is_empty(),
840 "bar should have no references"
841 );
842 }
843
844 #[test]
845 fn graph_no_namespace_import() {
846 let graph = build_simple_graph();
847 assert!(!graph.has_namespace_import(FileId(0)));
848 assert!(!graph.has_namespace_import(FileId(1)));
849 }
850
851 #[test]
852 fn graph_has_namespace_import() {
853 let files = vec![
854 DiscoveredFile {
855 id: FileId(0),
856 path: PathBuf::from("/project/entry.ts"),
857 size_bytes: 100,
858 },
859 DiscoveredFile {
860 id: FileId(1),
861 path: PathBuf::from("/project/utils.ts"),
862 size_bytes: 50,
863 },
864 ];
865
866 let entry_points = vec![EntryPoint {
867 path: PathBuf::from("/project/entry.ts"),
868 source: EntryPointSource::PackageJsonMain,
869 }];
870
871 let resolved_modules = vec![
872 ResolvedModule {
873 file_id: FileId(0),
874 path: PathBuf::from("/project/entry.ts"),
875 exports: vec![],
876 re_exports: vec![],
877 resolved_imports: vec![ResolvedImport {
878 info: ImportInfo {
879 source: "./utils".to_string(),
880 imported_name: ImportedName::Namespace,
881 local_name: "utils".to_string(),
882 is_type_only: false,
883 span: oxc_span::Span::new(0, 10),
884 },
885 target: ResolveResult::InternalModule(FileId(1)),
886 }],
887 resolved_dynamic_imports: vec![],
888 resolved_dynamic_patterns: vec![],
889 member_accesses: vec![],
890 whole_object_uses: vec![],
891 has_cjs_exports: false,
892 },
893 ResolvedModule {
894 file_id: FileId(1),
895 path: PathBuf::from("/project/utils.ts"),
896 exports: vec![crate::extract::ExportInfo {
897 name: ExportName::Named("foo".to_string()),
898 local_name: Some("foo".to_string()),
899 is_type_only: false,
900 span: oxc_span::Span::new(0, 20),
901 members: vec![],
902 }],
903 re_exports: vec![],
904 resolved_imports: vec![],
905 resolved_dynamic_imports: vec![],
906 resolved_dynamic_patterns: vec![],
907 member_accesses: vec![],
908 whole_object_uses: vec![],
909 has_cjs_exports: false,
910 },
911 ];
912
913 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
914 assert!(
915 graph.has_namespace_import(FileId(1)),
916 "utils should have namespace import"
917 );
918 }
919
920 #[test]
921 fn graph_has_namespace_import_out_of_bounds() {
922 let graph = build_simple_graph();
923 assert!(!graph.has_namespace_import(FileId(999)));
924 }
925
926 #[test]
927 fn graph_unreachable_module() {
928 let files = vec![
930 DiscoveredFile {
931 id: FileId(0),
932 path: PathBuf::from("/project/entry.ts"),
933 size_bytes: 100,
934 },
935 DiscoveredFile {
936 id: FileId(1),
937 path: PathBuf::from("/project/utils.ts"),
938 size_bytes: 50,
939 },
940 DiscoveredFile {
941 id: FileId(2),
942 path: PathBuf::from("/project/orphan.ts"),
943 size_bytes: 30,
944 },
945 ];
946
947 let entry_points = vec![EntryPoint {
948 path: PathBuf::from("/project/entry.ts"),
949 source: EntryPointSource::PackageJsonMain,
950 }];
951
952 let resolved_modules = vec![
953 ResolvedModule {
954 file_id: FileId(0),
955 path: PathBuf::from("/project/entry.ts"),
956 exports: vec![],
957 re_exports: vec![],
958 resolved_imports: vec![ResolvedImport {
959 info: ImportInfo {
960 source: "./utils".to_string(),
961 imported_name: ImportedName::Named("foo".to_string()),
962 local_name: "foo".to_string(),
963 is_type_only: false,
964 span: oxc_span::Span::new(0, 10),
965 },
966 target: ResolveResult::InternalModule(FileId(1)),
967 }],
968 resolved_dynamic_imports: vec![],
969 resolved_dynamic_patterns: vec![],
970 member_accesses: vec![],
971 whole_object_uses: vec![],
972 has_cjs_exports: false,
973 },
974 ResolvedModule {
975 file_id: FileId(1),
976 path: PathBuf::from("/project/utils.ts"),
977 exports: vec![crate::extract::ExportInfo {
978 name: ExportName::Named("foo".to_string()),
979 local_name: Some("foo".to_string()),
980 is_type_only: false,
981 span: oxc_span::Span::new(0, 20),
982 members: vec![],
983 }],
984 re_exports: vec![],
985 resolved_imports: vec![],
986 resolved_dynamic_imports: vec![],
987 resolved_dynamic_patterns: vec![],
988 member_accesses: vec![],
989 whole_object_uses: vec![],
990 has_cjs_exports: false,
991 },
992 ResolvedModule {
993 file_id: FileId(2),
994 path: PathBuf::from("/project/orphan.ts"),
995 exports: vec![crate::extract::ExportInfo {
996 name: ExportName::Named("orphan".to_string()),
997 local_name: Some("orphan".to_string()),
998 is_type_only: false,
999 span: oxc_span::Span::new(0, 20),
1000 members: vec![],
1001 }],
1002 re_exports: vec![],
1003 resolved_imports: vec![],
1004 resolved_dynamic_imports: vec![],
1005 resolved_dynamic_patterns: vec![],
1006 member_accesses: vec![],
1007 whole_object_uses: vec![],
1008 has_cjs_exports: false,
1009 },
1010 ];
1011
1012 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1013
1014 assert!(graph.modules[0].is_reachable, "entry should be reachable");
1015 assert!(graph.modules[1].is_reachable, "utils should be reachable");
1016 assert!(
1017 !graph.modules[2].is_reachable,
1018 "orphan should NOT be reachable"
1019 );
1020 }
1021
1022 #[test]
1023 fn graph_package_usage_tracked() {
1024 let files = vec![DiscoveredFile {
1025 id: FileId(0),
1026 path: PathBuf::from("/project/entry.ts"),
1027 size_bytes: 100,
1028 }];
1029
1030 let entry_points = vec![EntryPoint {
1031 path: PathBuf::from("/project/entry.ts"),
1032 source: EntryPointSource::PackageJsonMain,
1033 }];
1034
1035 let resolved_modules = vec![ResolvedModule {
1036 file_id: FileId(0),
1037 path: PathBuf::from("/project/entry.ts"),
1038 exports: vec![],
1039 re_exports: vec![],
1040 resolved_imports: vec![
1041 ResolvedImport {
1042 info: ImportInfo {
1043 source: "react".to_string(),
1044 imported_name: ImportedName::Default,
1045 local_name: "React".to_string(),
1046 is_type_only: false,
1047 span: oxc_span::Span::new(0, 10),
1048 },
1049 target: ResolveResult::NpmPackage("react".to_string()),
1050 },
1051 ResolvedImport {
1052 info: ImportInfo {
1053 source: "lodash".to_string(),
1054 imported_name: ImportedName::Named("merge".to_string()),
1055 local_name: "merge".to_string(),
1056 is_type_only: false,
1057 span: oxc_span::Span::new(15, 30),
1058 },
1059 target: ResolveResult::NpmPackage("lodash".to_string()),
1060 },
1061 ],
1062 resolved_dynamic_imports: vec![],
1063 resolved_dynamic_patterns: vec![],
1064 member_accesses: vec![],
1065 whole_object_uses: vec![],
1066 has_cjs_exports: false,
1067 }];
1068
1069 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1070 assert!(graph.package_usage.contains_key("react"));
1071 assert!(graph.package_usage.contains_key("lodash"));
1072 assert!(!graph.package_usage.contains_key("express"));
1073 }
1074
1075 #[test]
1076 fn graph_re_export_chain_propagates_references() {
1077 let files = vec![
1079 DiscoveredFile {
1080 id: FileId(0),
1081 path: PathBuf::from("/project/entry.ts"),
1082 size_bytes: 100,
1083 },
1084 DiscoveredFile {
1085 id: FileId(1),
1086 path: PathBuf::from("/project/barrel.ts"),
1087 size_bytes: 50,
1088 },
1089 DiscoveredFile {
1090 id: FileId(2),
1091 path: PathBuf::from("/project/source.ts"),
1092 size_bytes: 50,
1093 },
1094 ];
1095
1096 let entry_points = vec![EntryPoint {
1097 path: PathBuf::from("/project/entry.ts"),
1098 source: EntryPointSource::PackageJsonMain,
1099 }];
1100
1101 let resolved_modules = vec![
1102 ResolvedModule {
1104 file_id: FileId(0),
1105 path: PathBuf::from("/project/entry.ts"),
1106 exports: vec![],
1107 re_exports: vec![],
1108 resolved_imports: vec![ResolvedImport {
1109 info: ImportInfo {
1110 source: "./barrel".to_string(),
1111 imported_name: ImportedName::Named("foo".to_string()),
1112 local_name: "foo".to_string(),
1113 is_type_only: false,
1114 span: oxc_span::Span::new(0, 10),
1115 },
1116 target: ResolveResult::InternalModule(FileId(1)),
1117 }],
1118 resolved_dynamic_imports: vec![],
1119 resolved_dynamic_patterns: vec![],
1120 member_accesses: vec![],
1121 whole_object_uses: vec![],
1122 has_cjs_exports: false,
1123 },
1124 ResolvedModule {
1126 file_id: FileId(1),
1127 path: PathBuf::from("/project/barrel.ts"),
1128 exports: vec![crate::extract::ExportInfo {
1129 name: ExportName::Named("foo".to_string()),
1130 local_name: Some("foo".to_string()),
1131 is_type_only: false,
1132 span: oxc_span::Span::new(0, 20),
1133 members: vec![],
1134 }],
1135 re_exports: vec![ResolvedReExport {
1136 info: crate::extract::ReExportInfo {
1137 source: "./source".to_string(),
1138 imported_name: "foo".to_string(),
1139 exported_name: "foo".to_string(),
1140 is_type_only: false,
1141 },
1142 target: ResolveResult::InternalModule(FileId(2)),
1143 }],
1144 resolved_imports: vec![],
1145 resolved_dynamic_imports: vec![],
1146 resolved_dynamic_patterns: vec![],
1147 member_accesses: vec![],
1148 whole_object_uses: vec![],
1149 has_cjs_exports: false,
1150 },
1151 ResolvedModule {
1153 file_id: FileId(2),
1154 path: PathBuf::from("/project/source.ts"),
1155 exports: vec![crate::extract::ExportInfo {
1156 name: ExportName::Named("foo".to_string()),
1157 local_name: Some("foo".to_string()),
1158 is_type_only: false,
1159 span: oxc_span::Span::new(0, 20),
1160 members: vec![],
1161 }],
1162 re_exports: vec![],
1163 resolved_imports: vec![],
1164 resolved_dynamic_imports: vec![],
1165 resolved_dynamic_patterns: vec![],
1166 member_accesses: vec![],
1167 whole_object_uses: vec![],
1168 has_cjs_exports: false,
1169 },
1170 ];
1171
1172 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1173
1174 let source_module = &graph.modules[2];
1176 let foo_export = source_module
1177 .exports
1178 .iter()
1179 .find(|e| e.name.to_string() == "foo")
1180 .unwrap();
1181 assert!(
1182 !foo_export.references.is_empty(),
1183 "source foo should have propagated references through barrel re-export chain"
1184 );
1185 }
1186
1187 #[test]
1188 fn graph_empty() {
1189 let graph = ModuleGraph::build(&[], &[], &[]);
1190 assert_eq!(graph.module_count(), 0);
1191 assert_eq!(graph.edge_count(), 0);
1192 }
1193
1194 #[test]
1195 fn graph_cjs_exports_tracked() {
1196 let files = vec![DiscoveredFile {
1197 id: FileId(0),
1198 path: PathBuf::from("/project/entry.ts"),
1199 size_bytes: 100,
1200 }];
1201
1202 let entry_points = vec![EntryPoint {
1203 path: PathBuf::from("/project/entry.ts"),
1204 source: EntryPointSource::PackageJsonMain,
1205 }];
1206
1207 let resolved_modules = vec![ResolvedModule {
1208 file_id: FileId(0),
1209 path: PathBuf::from("/project/entry.ts"),
1210 exports: vec![],
1211 re_exports: vec![],
1212 resolved_imports: vec![],
1213 resolved_dynamic_imports: vec![],
1214 resolved_dynamic_patterns: vec![],
1215 member_accesses: vec![],
1216 whole_object_uses: vec![],
1217 has_cjs_exports: true,
1218 }];
1219
1220 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1221 assert!(graph.modules[0].has_cjs_exports);
1222 }
1223
1224 #[test]
1225 fn barrel_re_export_creates_export_symbol() {
1226 let files = vec![
1229 DiscoveredFile {
1230 id: FileId(0),
1231 path: PathBuf::from("/project/entry.ts"),
1232 size_bytes: 100,
1233 },
1234 DiscoveredFile {
1235 id: FileId(1),
1236 path: PathBuf::from("/project/barrel.ts"),
1237 size_bytes: 50,
1238 },
1239 DiscoveredFile {
1240 id: FileId(2),
1241 path: PathBuf::from("/project/source.ts"),
1242 size_bytes: 50,
1243 },
1244 ];
1245
1246 let entry_points = vec![EntryPoint {
1247 path: PathBuf::from("/project/entry.ts"),
1248 source: EntryPointSource::PackageJsonMain,
1249 }];
1250
1251 let resolved_modules = vec![
1252 ResolvedModule {
1253 file_id: FileId(0),
1254 path: PathBuf::from("/project/entry.ts"),
1255 exports: vec![],
1256 re_exports: vec![],
1257 resolved_imports: vec![ResolvedImport {
1258 info: ImportInfo {
1259 source: "./barrel".to_string(),
1260 imported_name: ImportedName::Named("foo".to_string()),
1261 local_name: "foo".to_string(),
1262 is_type_only: false,
1263 span: oxc_span::Span::new(0, 10),
1264 },
1265 target: ResolveResult::InternalModule(FileId(1)),
1266 }],
1267 resolved_dynamic_imports: vec![],
1268 resolved_dynamic_patterns: vec![],
1269 member_accesses: vec![],
1270 whole_object_uses: vec![],
1271 has_cjs_exports: false,
1272 },
1273 ResolvedModule {
1275 file_id: FileId(1),
1276 path: PathBuf::from("/project/barrel.ts"),
1277 exports: vec![], re_exports: vec![ResolvedReExport {
1279 info: crate::extract::ReExportInfo {
1280 source: "./source".to_string(),
1281 imported_name: "foo".to_string(),
1282 exported_name: "foo".to_string(),
1283 is_type_only: false,
1284 },
1285 target: ResolveResult::InternalModule(FileId(2)),
1286 }],
1287 resolved_imports: vec![],
1288 resolved_dynamic_imports: vec![],
1289 resolved_dynamic_patterns: vec![],
1290 member_accesses: vec![],
1291 whole_object_uses: vec![],
1292 has_cjs_exports: false,
1293 },
1294 ResolvedModule {
1295 file_id: FileId(2),
1296 path: PathBuf::from("/project/source.ts"),
1297 exports: vec![crate::extract::ExportInfo {
1298 name: ExportName::Named("foo".to_string()),
1299 local_name: Some("foo".to_string()),
1300 is_type_only: false,
1301 span: oxc_span::Span::new(0, 20),
1302 members: vec![],
1303 }],
1304 re_exports: vec![],
1305 resolved_imports: vec![],
1306 resolved_dynamic_imports: vec![],
1307 resolved_dynamic_patterns: vec![],
1308 member_accesses: vec![],
1309 whole_object_uses: vec![],
1310 has_cjs_exports: false,
1311 },
1312 ];
1313
1314 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1315
1316 let barrel = &graph.modules[1];
1318 let foo_export = barrel.exports.iter().find(|e| e.name.to_string() == "foo");
1319 assert!(
1320 foo_export.is_some(),
1321 "barrel should have ExportSymbol for re-exported 'foo'"
1322 );
1323
1324 let foo = foo_export.unwrap();
1326 assert!(
1327 !foo.references.is_empty(),
1328 "barrel's foo should have a reference from entry.ts"
1329 );
1330
1331 let source = &graph.modules[2];
1333 let source_foo = source
1334 .exports
1335 .iter()
1336 .find(|e| e.name.to_string() == "foo")
1337 .unwrap();
1338 assert!(
1339 !source_foo.references.is_empty(),
1340 "source foo should have propagated references through barrel"
1341 );
1342 }
1343
1344 #[test]
1345 fn barrel_unused_re_export_has_no_references() {
1346 let files = vec![
1349 DiscoveredFile {
1350 id: FileId(0),
1351 path: PathBuf::from("/project/entry.ts"),
1352 size_bytes: 100,
1353 },
1354 DiscoveredFile {
1355 id: FileId(1),
1356 path: PathBuf::from("/project/barrel.ts"),
1357 size_bytes: 50,
1358 },
1359 DiscoveredFile {
1360 id: FileId(2),
1361 path: PathBuf::from("/project/source.ts"),
1362 size_bytes: 50,
1363 },
1364 ];
1365
1366 let entry_points = vec![EntryPoint {
1367 path: PathBuf::from("/project/entry.ts"),
1368 source: EntryPointSource::PackageJsonMain,
1369 }];
1370
1371 let resolved_modules = vec![
1372 ResolvedModule {
1373 file_id: FileId(0),
1374 path: PathBuf::from("/project/entry.ts"),
1375 exports: vec![],
1376 re_exports: vec![],
1377 resolved_imports: vec![ResolvedImport {
1378 info: ImportInfo {
1379 source: "./barrel".to_string(),
1380 imported_name: ImportedName::Named("foo".to_string()),
1381 local_name: "foo".to_string(),
1382 is_type_only: false,
1383 span: oxc_span::Span::new(0, 10),
1384 },
1385 target: ResolveResult::InternalModule(FileId(1)),
1386 }],
1387 resolved_dynamic_imports: vec![],
1388 resolved_dynamic_patterns: vec![],
1389 member_accesses: vec![],
1390 whole_object_uses: vec![],
1391 has_cjs_exports: false,
1392 },
1393 ResolvedModule {
1394 file_id: FileId(1),
1395 path: PathBuf::from("/project/barrel.ts"),
1396 exports: vec![],
1397 re_exports: vec![
1398 ResolvedReExport {
1399 info: crate::extract::ReExportInfo {
1400 source: "./source".to_string(),
1401 imported_name: "foo".to_string(),
1402 exported_name: "foo".to_string(),
1403 is_type_only: false,
1404 },
1405 target: ResolveResult::InternalModule(FileId(2)),
1406 },
1407 ResolvedReExport {
1408 info: crate::extract::ReExportInfo {
1409 source: "./source".to_string(),
1410 imported_name: "bar".to_string(),
1411 exported_name: "bar".to_string(),
1412 is_type_only: false,
1413 },
1414 target: ResolveResult::InternalModule(FileId(2)),
1415 },
1416 ],
1417 resolved_imports: vec![],
1418 resolved_dynamic_imports: vec![],
1419 resolved_dynamic_patterns: vec![],
1420 member_accesses: vec![],
1421 whole_object_uses: vec![],
1422 has_cjs_exports: false,
1423 },
1424 ResolvedModule {
1425 file_id: FileId(2),
1426 path: PathBuf::from("/project/source.ts"),
1427 exports: vec![
1428 crate::extract::ExportInfo {
1429 name: ExportName::Named("foo".to_string()),
1430 local_name: Some("foo".to_string()),
1431 is_type_only: false,
1432 span: oxc_span::Span::new(0, 20),
1433 members: vec![],
1434 },
1435 crate::extract::ExportInfo {
1436 name: ExportName::Named("bar".to_string()),
1437 local_name: Some("bar".to_string()),
1438 is_type_only: false,
1439 span: oxc_span::Span::new(25, 45),
1440 members: vec![],
1441 },
1442 ],
1443 re_exports: vec![],
1444 resolved_imports: vec![],
1445 resolved_dynamic_imports: vec![],
1446 resolved_dynamic_patterns: vec![],
1447 member_accesses: vec![],
1448 whole_object_uses: vec![],
1449 has_cjs_exports: false,
1450 },
1451 ];
1452
1453 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1454
1455 let barrel = &graph.modules[1];
1456 let foo = barrel
1458 .exports
1459 .iter()
1460 .find(|e| e.name.to_string() == "foo")
1461 .unwrap();
1462 assert!(!foo.references.is_empty(), "barrel's foo should be used");
1463
1464 let bar = barrel
1465 .exports
1466 .iter()
1467 .find(|e| e.name.to_string() == "bar")
1468 .unwrap();
1469 assert!(
1470 bar.references.is_empty(),
1471 "barrel's bar should be unused (no consumer imports it)"
1472 );
1473 }
1474
1475 #[test]
1476 fn type_only_re_export_creates_type_only_export_symbol() {
1477 let files = vec![
1479 DiscoveredFile {
1480 id: FileId(0),
1481 path: PathBuf::from("/project/entry.ts"),
1482 size_bytes: 100,
1483 },
1484 DiscoveredFile {
1485 id: FileId(1),
1486 path: PathBuf::from("/project/barrel.ts"),
1487 size_bytes: 50,
1488 },
1489 DiscoveredFile {
1490 id: FileId(2),
1491 path: PathBuf::from("/project/source.ts"),
1492 size_bytes: 50,
1493 },
1494 ];
1495
1496 let entry_points = vec![EntryPoint {
1497 path: PathBuf::from("/project/entry.ts"),
1498 source: EntryPointSource::PackageJsonMain,
1499 }];
1500
1501 let resolved_modules = vec![
1502 ResolvedModule {
1503 file_id: FileId(0),
1504 path: PathBuf::from("/project/entry.ts"),
1505 exports: vec![],
1506 re_exports: vec![],
1507 resolved_imports: vec![ResolvedImport {
1508 info: ImportInfo {
1509 source: "./barrel".to_string(),
1510 imported_name: ImportedName::Named("UsedType".to_string()),
1511 local_name: "UsedType".to_string(),
1512 is_type_only: true,
1513 span: oxc_span::Span::new(0, 10),
1514 },
1515 target: ResolveResult::InternalModule(FileId(1)),
1516 }],
1517 resolved_dynamic_imports: vec![],
1518 resolved_dynamic_patterns: vec![],
1519 member_accesses: vec![],
1520 whole_object_uses: vec![],
1521 has_cjs_exports: false,
1522 },
1523 ResolvedModule {
1524 file_id: FileId(1),
1525 path: PathBuf::from("/project/barrel.ts"),
1526 exports: vec![],
1527 re_exports: vec![
1528 ResolvedReExport {
1529 info: crate::extract::ReExportInfo {
1530 source: "./source".to_string(),
1531 imported_name: "UsedType".to_string(),
1532 exported_name: "UsedType".to_string(),
1533 is_type_only: true,
1534 },
1535 target: ResolveResult::InternalModule(FileId(2)),
1536 },
1537 ResolvedReExport {
1538 info: crate::extract::ReExportInfo {
1539 source: "./source".to_string(),
1540 imported_name: "UnusedType".to_string(),
1541 exported_name: "UnusedType".to_string(),
1542 is_type_only: true,
1543 },
1544 target: ResolveResult::InternalModule(FileId(2)),
1545 },
1546 ],
1547 resolved_imports: vec![],
1548 resolved_dynamic_imports: vec![],
1549 resolved_dynamic_patterns: vec![],
1550 member_accesses: vec![],
1551 whole_object_uses: vec![],
1552 has_cjs_exports: false,
1553 },
1554 ResolvedModule {
1555 file_id: FileId(2),
1556 path: PathBuf::from("/project/source.ts"),
1557 exports: vec![
1558 crate::extract::ExportInfo {
1559 name: ExportName::Named("UsedType".to_string()),
1560 local_name: Some("UsedType".to_string()),
1561 is_type_only: true,
1562 span: oxc_span::Span::new(0, 20),
1563 members: vec![],
1564 },
1565 crate::extract::ExportInfo {
1566 name: ExportName::Named("UnusedType".to_string()),
1567 local_name: Some("UnusedType".to_string()),
1568 is_type_only: true,
1569 span: oxc_span::Span::new(25, 45),
1570 members: vec![],
1571 },
1572 ],
1573 re_exports: vec![],
1574 resolved_imports: vec![],
1575 resolved_dynamic_imports: vec![],
1576 resolved_dynamic_patterns: vec![],
1577 member_accesses: vec![],
1578 whole_object_uses: vec![],
1579 has_cjs_exports: false,
1580 },
1581 ];
1582
1583 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1584
1585 let barrel = &graph.modules[1];
1586
1587 let used_type = barrel
1589 .exports
1590 .iter()
1591 .find(|e| e.name.to_string() == "UsedType")
1592 .expect("barrel should have ExportSymbol for UsedType");
1593 assert!(used_type.is_type_only, "UsedType should be type-only");
1594 assert!(
1595 !used_type.references.is_empty(),
1596 "UsedType should have references"
1597 );
1598
1599 let unused_type = barrel
1600 .exports
1601 .iter()
1602 .find(|e| e.name.to_string() == "UnusedType")
1603 .expect("barrel should have ExportSymbol for UnusedType");
1604 assert!(unused_type.is_type_only, "UnusedType should be type-only");
1605 assert!(
1606 unused_type.references.is_empty(),
1607 "UnusedType should have no references"
1608 );
1609 }
1610
1611 #[test]
1612 fn default_re_export_creates_default_export_symbol() {
1613 let files = vec![
1615 DiscoveredFile {
1616 id: FileId(0),
1617 path: PathBuf::from("/project/entry.ts"),
1618 size_bytes: 100,
1619 },
1620 DiscoveredFile {
1621 id: FileId(1),
1622 path: PathBuf::from("/project/barrel.ts"),
1623 size_bytes: 50,
1624 },
1625 DiscoveredFile {
1626 id: FileId(2),
1627 path: PathBuf::from("/project/source.ts"),
1628 size_bytes: 50,
1629 },
1630 ];
1631
1632 let entry_points = vec![EntryPoint {
1633 path: PathBuf::from("/project/entry.ts"),
1634 source: EntryPointSource::PackageJsonMain,
1635 }];
1636
1637 let resolved_modules = vec![
1638 ResolvedModule {
1639 file_id: FileId(0),
1640 path: PathBuf::from("/project/entry.ts"),
1641 exports: vec![],
1642 re_exports: vec![],
1643 resolved_imports: vec![ResolvedImport {
1644 info: ImportInfo {
1645 source: "./barrel".to_string(),
1646 imported_name: ImportedName::Named("Accordion".to_string()),
1647 local_name: "Accordion".to_string(),
1648 is_type_only: false,
1649 span: oxc_span::Span::new(0, 10),
1650 },
1651 target: ResolveResult::InternalModule(FileId(1)),
1652 }],
1653 resolved_dynamic_imports: vec![],
1654 resolved_dynamic_patterns: vec![],
1655 member_accesses: vec![],
1656 whole_object_uses: vec![],
1657 has_cjs_exports: false,
1658 },
1659 ResolvedModule {
1660 file_id: FileId(1),
1661 path: PathBuf::from("/project/barrel.ts"),
1662 exports: vec![],
1663 re_exports: vec![ResolvedReExport {
1664 info: crate::extract::ReExportInfo {
1665 source: "./source".to_string(),
1666 imported_name: "default".to_string(),
1667 exported_name: "Accordion".to_string(),
1668 is_type_only: false,
1669 },
1670 target: ResolveResult::InternalModule(FileId(2)),
1671 }],
1672 resolved_imports: vec![],
1673 resolved_dynamic_imports: vec![],
1674 resolved_dynamic_patterns: vec![],
1675 member_accesses: vec![],
1676 whole_object_uses: vec![],
1677 has_cjs_exports: false,
1678 },
1679 ResolvedModule {
1680 file_id: FileId(2),
1681 path: PathBuf::from("/project/source.ts"),
1682 exports: vec![crate::extract::ExportInfo {
1683 name: ExportName::Default,
1684 local_name: None,
1685 is_type_only: false,
1686 span: oxc_span::Span::new(0, 20),
1687 members: vec![],
1688 }],
1689 re_exports: vec![],
1690 resolved_imports: vec![],
1691 resolved_dynamic_imports: vec![],
1692 resolved_dynamic_patterns: vec![],
1693 member_accesses: vec![],
1694 whole_object_uses: vec![],
1695 has_cjs_exports: false,
1696 },
1697 ];
1698
1699 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1700
1701 let barrel = &graph.modules[1];
1703 let accordion = barrel
1704 .exports
1705 .iter()
1706 .find(|e| e.name.to_string() == "Accordion")
1707 .expect("barrel should have ExportSymbol for Accordion");
1708 assert!(
1709 !accordion.references.is_empty(),
1710 "Accordion should have reference from entry.ts"
1711 );
1712
1713 let source = &graph.modules[2];
1715 let default_export = source
1716 .exports
1717 .iter()
1718 .find(|e| matches!(e.name, ExportName::Default))
1719 .unwrap();
1720 assert!(
1721 !default_export.references.is_empty(),
1722 "source default export should have propagated references"
1723 );
1724 }
1725
1726 #[test]
1727 fn multi_level_re_export_chain_propagation() {
1728 let files = vec![
1730 DiscoveredFile {
1731 id: FileId(0),
1732 path: PathBuf::from("/project/entry.ts"),
1733 size_bytes: 100,
1734 },
1735 DiscoveredFile {
1736 id: FileId(1),
1737 path: PathBuf::from("/project/barrel1.ts"),
1738 size_bytes: 50,
1739 },
1740 DiscoveredFile {
1741 id: FileId(2),
1742 path: PathBuf::from("/project/barrel2.ts"),
1743 size_bytes: 50,
1744 },
1745 DiscoveredFile {
1746 id: FileId(3),
1747 path: PathBuf::from("/project/source.ts"),
1748 size_bytes: 50,
1749 },
1750 ];
1751
1752 let entry_points = vec![EntryPoint {
1753 path: PathBuf::from("/project/entry.ts"),
1754 source: EntryPointSource::PackageJsonMain,
1755 }];
1756
1757 let resolved_modules = vec![
1758 ResolvedModule {
1759 file_id: FileId(0),
1760 path: PathBuf::from("/project/entry.ts"),
1761 exports: vec![],
1762 re_exports: vec![],
1763 resolved_imports: vec![ResolvedImport {
1764 info: ImportInfo {
1765 source: "./barrel1".to_string(),
1766 imported_name: ImportedName::Named("foo".to_string()),
1767 local_name: "foo".to_string(),
1768 is_type_only: false,
1769 span: oxc_span::Span::new(0, 10),
1770 },
1771 target: ResolveResult::InternalModule(FileId(1)),
1772 }],
1773 resolved_dynamic_imports: vec![],
1774 resolved_dynamic_patterns: vec![],
1775 member_accesses: vec![],
1776 whole_object_uses: vec![],
1777 has_cjs_exports: false,
1778 },
1779 ResolvedModule {
1781 file_id: FileId(1),
1782 path: PathBuf::from("/project/barrel1.ts"),
1783 exports: vec![],
1784 re_exports: vec![ResolvedReExport {
1785 info: crate::extract::ReExportInfo {
1786 source: "./barrel2".to_string(),
1787 imported_name: "foo".to_string(),
1788 exported_name: "foo".to_string(),
1789 is_type_only: false,
1790 },
1791 target: ResolveResult::InternalModule(FileId(2)),
1792 }],
1793 resolved_imports: vec![],
1794 resolved_dynamic_imports: vec![],
1795 resolved_dynamic_patterns: vec![],
1796 member_accesses: vec![],
1797 whole_object_uses: vec![],
1798 has_cjs_exports: false,
1799 },
1800 ResolvedModule {
1802 file_id: FileId(2),
1803 path: PathBuf::from("/project/barrel2.ts"),
1804 exports: vec![],
1805 re_exports: vec![ResolvedReExport {
1806 info: crate::extract::ReExportInfo {
1807 source: "./source".to_string(),
1808 imported_name: "foo".to_string(),
1809 exported_name: "foo".to_string(),
1810 is_type_only: false,
1811 },
1812 target: ResolveResult::InternalModule(FileId(3)),
1813 }],
1814 resolved_imports: vec![],
1815 resolved_dynamic_imports: vec![],
1816 resolved_dynamic_patterns: vec![],
1817 member_accesses: vec![],
1818 whole_object_uses: vec![],
1819 has_cjs_exports: false,
1820 },
1821 ResolvedModule {
1822 file_id: FileId(3),
1823 path: PathBuf::from("/project/source.ts"),
1824 exports: vec![crate::extract::ExportInfo {
1825 name: ExportName::Named("foo".to_string()),
1826 local_name: Some("foo".to_string()),
1827 is_type_only: false,
1828 span: oxc_span::Span::new(0, 20),
1829 members: vec![],
1830 }],
1831 re_exports: vec![],
1832 resolved_imports: vec![],
1833 resolved_dynamic_imports: vec![],
1834 resolved_dynamic_patterns: vec![],
1835 member_accesses: vec![],
1836 whole_object_uses: vec![],
1837 has_cjs_exports: false,
1838 },
1839 ];
1840
1841 let graph = ModuleGraph::build(&resolved_modules, &entry_points, &files);
1842
1843 let barrel1 = &graph.modules[1];
1845 let b1_foo = barrel1
1846 .exports
1847 .iter()
1848 .find(|e| e.name.to_string() == "foo")
1849 .unwrap();
1850 assert!(
1851 !b1_foo.references.is_empty(),
1852 "barrel1's foo should be referenced"
1853 );
1854
1855 let barrel2 = &graph.modules[2];
1856 let b2_foo = barrel2
1857 .exports
1858 .iter()
1859 .find(|e| e.name.to_string() == "foo")
1860 .unwrap();
1861 assert!(
1862 !b2_foo.references.is_empty(),
1863 "barrel2's foo should be referenced (propagated through chain)"
1864 );
1865
1866 let source = &graph.modules[3];
1867 let src_foo = source
1868 .exports
1869 .iter()
1870 .find(|e| e.name.to_string() == "foo")
1871 .unwrap();
1872 assert!(
1873 !src_foo.references.is_empty(),
1874 "source's foo should be referenced (propagated through 2-level chain)"
1875 );
1876 }
1877}