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