1use std::collections::VecDeque;
4
5use fallow_types::discover::FileId;
6use fallow_types::extract::ExportName;
7use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
8
9use super::types::ExportSymbol;
10use crate::resolve::ResolvedModule;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
14pub enum ExportNamespace {
15 Type,
17 Value,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
23pub struct EffectiveExportBinding {
24 file_id: FileId,
25 kind: EffectiveExportBindingKind,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
29enum EffectiveExportBindingKind {
30 Declaration(usize),
31 NamespaceObject {
32 source: FileId,
33 },
34 ImplicitDefault,
35 ExternalReExport(usize),
40}
41
42impl EffectiveExportBinding {
43 #[must_use]
45 pub const fn origin_file(&self) -> FileId {
46 self.file_id
47 }
48
49 pub(in crate::graph) const fn origin_slot(self) -> Option<usize> {
50 match self.kind {
51 EffectiveExportBindingKind::Declaration(slot) => Some(slot),
52 EffectiveExportBindingKind::NamespaceObject { .. }
53 | EffectiveExportBindingKind::ImplicitDefault
54 | EffectiveExportBindingKind::ExternalReExport(_) => None,
55 }
56 }
57
58 #[must_use]
60 pub const fn namespace_source(self) -> Option<FileId> {
61 match self.kind {
62 EffectiveExportBindingKind::NamespaceObject { source } => Some(source),
63 EffectiveExportBindingKind::Declaration(_)
64 | EffectiveExportBindingKind::ImplicitDefault
65 | EffectiveExportBindingKind::ExternalReExport(_) => None,
66 }
67 }
68
69 #[must_use]
71 pub const fn is_implicit_default(self) -> bool {
72 matches!(self.kind, EffectiveExportBindingKind::ImplicitDefault)
73 }
74}
75
76#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
78pub enum EffectiveExportResolution {
79 #[default]
81 Missing,
82 Unique(EffectiveExportBinding),
84 Ambiguous,
86}
87
88impl EffectiveExportResolution {
89 fn merged_with(self, incoming: Self) -> Self {
90 match (self, incoming) {
91 (Self::Missing, resolution) | (resolution, Self::Missing) => resolution,
92 (Self::Unique(left), Self::Unique(right)) if left == right => self,
93 (Self::Ambiguous, _) | (_, Self::Ambiguous) | (Self::Unique(_), Self::Unique(_)) => {
94 Self::Ambiguous
95 }
96 }
97 }
98}
99
100#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
102pub(super) struct ExportNamespaces {
103 r#type: bool,
104 value: bool,
105}
106
107impl ExportNamespaces {
108 pub(super) const fn contains(self, namespace: ExportNamespace) -> bool {
109 match namespace {
110 ExportNamespace::Type => self.r#type,
111 ExportNamespace::Value => self.value,
112 }
113 }
114
115 pub(super) const fn insert(&mut self, namespace: ExportNamespace) -> bool {
116 let slot = match namespace {
117 ExportNamespace::Type => &mut self.r#type,
118 ExportNamespace::Value => &mut self.value,
119 };
120 let inserted = !*slot;
121 *slot = true;
122 inserted
123 }
124
125 pub(super) const fn extend(&mut self, namespaces: Self) {
126 self.r#type |= namespaces.r#type;
127 self.value |= namespaces.value;
128 }
129
130 pub(super) const fn is_empty(self) -> bool {
131 !self.r#type && !self.value
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
136struct ExportNameId(usize);
137
138impl ExportNameId {
139 const DEFAULT: Self = Self(0);
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
143struct ExportKey {
144 file_id: FileId,
145 name: ExportNameId,
146}
147
148impl ExportKey {
149 const fn new(file_id: FileId, name: ExportNameId) -> Self {
150 Self { file_id, name }
151 }
152
153 const fn with_file(self, file_id: FileId) -> Self {
154 Self { file_id, ..self }
155 }
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
173enum ResolutionLane {
174 Type,
175 TypeFallback,
176 Value,
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
180struct ExportLookup {
181 key: ExportKey,
182 lane: ResolutionLane,
183}
184
185impl ExportLookup {
186 const fn new(file_id: FileId, name: ExportNameId, lane: ResolutionLane) -> Self {
187 Self {
188 key: ExportKey::new(file_id, name),
189 lane,
190 }
191 }
192
193 const fn with_file(self, file_id: FileId) -> Self {
194 Self {
195 key: self.key.with_file(file_id),
196 ..self
197 }
198 }
199}
200
201#[derive(Debug, Clone, Copy, Default)]
209struct SeededLanes(u8);
210
211impl SeededLanes {
212 const VALUE_TYPE_RESERVATION: u8 = 1 << 6;
217
218 const fn bit(lane: ResolutionLane) -> u8 {
219 match lane {
220 ResolutionLane::Type => 1,
221 ResolutionLane::TypeFallback => 2,
222 ResolutionLane::Value => 4,
223 }
224 }
225
226 const fn direct_mask(lane: ResolutionLane) -> u8 {
227 match lane {
228 ResolutionLane::Type => Self::bit(lane) | Self::VALUE_TYPE_RESERVATION,
229 ResolutionLane::TypeFallback | ResolutionLane::Value => Self::bit(lane),
230 }
231 }
232
233 const fn is_direct(self, lane: ResolutionLane) -> bool {
234 self.0 & Self::direct_mask(lane) != 0
235 }
236
237 const fn reserve_type_for_value_declaration(&mut self) {
238 self.0 |= Self::VALUE_TYPE_RESERVATION;
239 }
240
241 const fn claim_direct(&mut self, lane: ResolutionLane) -> bool {
243 let bit = Self::bit(lane);
244 let claimed = self.0 & bit == 0;
245 self.0 |= bit;
246 claimed
247 }
248
249 const fn is_explicit(self, lane: ResolutionLane) -> bool {
250 self.0 & (Self::direct_mask(lane) | (Self::bit(lane) << 3)) != 0
251 }
252
253 const fn claim_explicit(&mut self, lane: ResolutionLane) {
254 self.0 |= Self::bit(lane) << 3;
255 }
256}
257
258#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
259struct NamespaceResolutions {
260 r#type: EffectiveExportResolution,
261 type_fallback: EffectiveExportResolution,
262 value: EffectiveExportResolution,
263 #[serde(skip)]
264 seeded: SeededLanes,
265}
266
267impl NamespaceResolutions {
268 fn merge(&mut self, lane: ResolutionLane, incoming: EffectiveExportResolution) -> bool {
270 let current = self.get(lane);
271 let next = current.merged_with(incoming);
272 if current == next {
273 return false;
274 }
275 self.set(lane, next);
276 true
277 }
278
279 const fn get(self, lane: ResolutionLane) -> EffectiveExportResolution {
280 match lane {
281 ResolutionLane::Type => self.r#type,
282 ResolutionLane::TypeFallback => self.type_fallback,
283 ResolutionLane::Value => self.value,
284 }
285 }
286
287 const fn set(&mut self, lane: ResolutionLane, resolution: EffectiveExportResolution) {
288 match lane {
289 ResolutionLane::Type => self.r#type = resolution,
290 ResolutionLane::TypeFallback => self.type_fallback = resolution,
291 ResolutionLane::Value => self.value = resolution,
292 }
293 }
294
295 const fn collides_in_type_space(self) -> bool {
304 matches!(self.r#type, EffectiveExportResolution::Ambiguous)
305 || (matches!(self.type_fallback, EffectiveExportResolution::Ambiguous)
306 && !matches!(self.value, EffectiveExportResolution::Ambiguous))
307 }
308
309 fn effective(self, namespace: ExportNamespace) -> EffectiveExportResolution {
310 match namespace {
311 ExportNamespace::Value => self.value,
312 ExportNamespace::Type => match (self.r#type, self.type_fallback) {
316 (EffectiveExportResolution::Missing, EffectiveExportResolution::Missing) => {
317 self.value
318 }
319 (EffectiveExportResolution::Missing, fallback) => fallback,
320 (resolution, _) => resolution,
321 },
322 }
323 }
324}
325
326#[derive(Clone, Copy)]
332struct ProjectExportSizes {
333 keys: usize,
335 lane_keys: usize,
337 names: usize,
339}
340
341impl ProjectExportSizes {
342 fn measure(modules: &[ResolvedModule], type_fallback_modules: &FxHashSet<FileId>) -> Self {
343 let mut keys = 0;
344 let mut lane_keys = 0;
345 let mut names = 0;
346 for module in modules {
347 let fallback_exports = if type_fallback_modules.contains(&module.file_id) {
348 module
349 .exports
350 .iter()
351 .filter(|export| !export.is_type_only)
352 .count()
353 } else {
354 0
355 };
356 keys += module.exports.len() + module.re_exports.len();
357 lane_keys += module.exports.len() + fallback_exports + module.re_exports.len() * 3;
358 names += module.exports.len() + module.re_exports.len() * 2;
359 }
360 Self {
361 keys,
362 lane_keys,
363 names,
364 }
365 }
366}
367
368struct ExportNameInterner {
369 ids: FxHashMap<Box<str>, ExportNameId>,
370 next_id: usize,
371}
372
373impl ExportNameInterner {
374 fn with_capacity(capacity: usize) -> Self {
375 let mut ids = FxHashMap::with_capacity_and_hasher(capacity + 1, FxBuildHasher);
376 ids.insert(Box::<str>::from("default"), ExportNameId::DEFAULT);
377 Self { ids, next_id: 1 }
378 }
379
380 fn intern_export_name(&mut self, name: &ExportName) -> ExportNameId {
381 match name {
382 ExportName::Default => ExportNameId::DEFAULT,
383 ExportName::Named(name) => self.intern(name),
384 }
385 }
386
387 fn intern(&mut self, name: &str) -> ExportNameId {
388 if let Some(id) = self.ids.get(name) {
389 return *id;
390 }
391 let id = ExportNameId(self.next_id);
392 self.next_id += 1;
393 self.ids.insert(Box::from(name), id);
394 id
395 }
396}
397
398#[derive(Clone, Copy)]
399struct StarObserver {
400 barrel: FileId,
401 type_only: bool,
402 type_fallback: bool,
404}
405
406enum NamedDestinations {
412 One(ExportLookup),
413 Many(Vec<ExportLookup>),
414}
415
416impl NamedDestinations {
417 fn push(&mut self, destination: ExportLookup) {
418 match self {
419 Self::One(first) => *self = Self::Many(vec![*first, destination]),
420 Self::Many(destinations) => destinations.push(destination),
421 }
422 }
423
424 fn iter(&self) -> std::slice::Iter<'_, ExportLookup> {
425 match self {
426 Self::One(destination) => std::slice::from_ref(destination).iter(),
427 Self::Many(destinations) => destinations.iter(),
428 }
429 }
430}
431
432struct PropagationObservers {
433 named: FxHashMap<ExportLookup, NamedDestinations>,
434 star: FxHashMap<FileId, Vec<StarObserver>>,
435}
436
437struct ObserverBuildState<'a> {
438 type_fallback: bool,
439 interner: &'a mut ExportNameInterner,
440 resolutions: &'a mut FxHashMap<ExportKey, NamespaceResolutions>,
441 queue: &'a mut VecDeque<ExportLookup>,
442 observers: &'a mut PropagationObservers,
443}
444
445#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
451pub(super) struct EffectiveExportIndex {
452 name_ids: FxHashMap<Box<str>, ExportNameId>,
453 resolutions: FxHashMap<ExportKey, NamespaceResolutions>,
454 names_by_file: FxHashMap<FileId, Box<[ExportNameId]>>,
457 declaration_merge_groups: DeclarationMergeGroups,
458}
459
460#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
461struct DeclarationMergeGroups {
462 groups: Vec<Box<[usize]>>,
463 group_by_slot: FxHashMap<(FileId, usize), usize>,
464}
465
466impl EffectiveExportIndex {
467 pub(super) fn build(modules: &[ResolvedModule]) -> Self {
468 let type_fallback_modules = modules_needing_type_fallback(modules);
469 let sizes = ProjectExportSizes::measure(modules, &type_fallback_modules);
470 let mut interner = ExportNameInterner::with_capacity(sizes.names);
471 let mut resolutions = FxHashMap::with_capacity_and_hasher(sizes.keys, FxBuildHasher);
472 let mut queue = VecDeque::with_capacity(sizes.lane_keys);
473 seed_direct_bindings(
474 modules,
475 &type_fallback_modules,
476 &mut interner,
477 &mut resolutions,
478 &mut queue,
479 );
480 let observers = collect_observers(
481 modules,
482 &type_fallback_modules,
483 &mut interner,
484 &mut resolutions,
485 &mut queue,
486 );
487 propagate_bindings(&mut resolutions, &mut queue, &observers);
488
489 Self {
490 name_ids: interner.ids,
491 names_by_file: index_names_by_file(&resolutions),
492 resolutions,
493 declaration_merge_groups: collect_declaration_merge_groups(modules),
494 }
495 }
496
497 pub(super) fn resolve(
498 &self,
499 file_id: FileId,
500 name: &str,
501 namespace: ExportNamespace,
502 ) -> EffectiveExportResolution {
503 let Some(name) = self.name_ids.get(name) else {
504 return EffectiveExportResolution::Missing;
505 };
506 self.resolutions
507 .get(&ExportKey::new(file_id, *name))
508 .map_or(EffectiveExportResolution::Missing, |resolutions| {
509 resolutions.effective(namespace)
510 })
511 }
512
513 pub(super) fn resolves_through(
514 &self,
515 barrel: FileId,
516 barrel_name: &str,
517 source: FileId,
518 source_name: &str,
519 namespace: ExportNamespace,
520 ) -> bool {
521 matches!(
522 (
523 self.resolve(barrel, barrel_name, namespace),
524 self.resolve(source, source_name, namespace),
525 ),
526 (
527 EffectiveExportResolution::Unique(barrel_binding),
528 EffectiveExportResolution::Unique(source_binding),
529 ) if barrel_binding == source_binding
530 )
531 }
532
533 pub(super) fn contributes_through(
534 &self,
535 barrel: FileId,
536 barrel_name: &str,
537 source: FileId,
538 source_name: &str,
539 namespace: ExportNamespace,
540 ) -> bool {
541 match (
542 self.resolve(barrel, barrel_name, namespace),
543 self.resolve(source, source_name, namespace),
544 ) {
545 (
546 EffectiveExportResolution::Unique(barrel_binding),
547 EffectiveExportResolution::Unique(source_binding),
548 ) => barrel_binding == source_binding,
549 (
550 EffectiveExportResolution::Ambiguous,
551 EffectiveExportResolution::Unique(_) | EffectiveExportResolution::Ambiguous,
552 ) => true,
553 _ => false,
554 }
555 }
556
557 pub(super) fn ambiguous_names(&self) -> Vec<(FileId, &str, ExportNamespace)> {
563 let mut ambiguous = Vec::new();
564 for (file_id, names) in &self.names_by_file {
565 self.collect_ambiguous_ids(*file_id, names, &mut ambiguous);
566 }
567 self.name_texts(ambiguous)
568 }
569
570 pub(super) fn ambiguous_names_on(
572 &self,
573 file_id: FileId,
574 ) -> Vec<(FileId, &str, ExportNamespace)> {
575 let Some(names) = self.names_by_file.get(&file_id) else {
576 return Vec::new();
577 };
578 let mut ambiguous = Vec::new();
579 self.collect_ambiguous_ids(file_id, names, &mut ambiguous);
580 self.name_texts(ambiguous)
581 }
582
583 fn collect_ambiguous_ids(
584 &self,
585 file_id: FileId,
586 names: &[ExportNameId],
587 ambiguous: &mut Vec<(FileId, ExportNameId, ExportNamespace)>,
588 ) {
589 for name in names {
590 let Some(resolutions) = self.resolutions.get(&ExportKey::new(file_id, *name)) else {
591 continue;
592 };
593 for (namespace, collides) in [
594 (ExportNamespace::Type, resolutions.collides_in_type_space()),
595 (
596 ExportNamespace::Value,
597 matches!(resolutions.value, EffectiveExportResolution::Ambiguous),
598 ),
599 ] {
600 if collides {
601 ambiguous.push((file_id, *name, namespace));
602 }
603 }
604 }
605 }
606
607 fn name_texts(
611 &self,
612 ambiguous: Vec<(FileId, ExportNameId, ExportNamespace)>,
613 ) -> Vec<(FileId, &str, ExportNamespace)> {
614 if ambiguous.is_empty() {
615 return Vec::new();
616 }
617 let table = self.name_table();
618 ambiguous
619 .into_iter()
620 .filter_map(|(file_id, name, namespace)| {
621 table
622 .get(name.0)
623 .copied()
624 .map(|text| (file_id, text, namespace))
625 })
626 .collect()
627 }
628
629 fn name_table(&self) -> Vec<&str> {
631 let mut table = vec![""; self.name_ids.len()];
632 for (name, id) in &self.name_ids {
633 if let Some(slot) = table.get_mut(id.0) {
634 *slot = name.as_ref();
635 }
636 }
637 table
638 }
639
640 pub(super) fn unique_bindings(
641 &self,
642 file_id: FileId,
643 namespace: ExportNamespace,
644 ) -> FxHashSet<EffectiveExportBinding> {
645 let Some(names) = self.names_by_file.get(&file_id) else {
646 return FxHashSet::default();
647 };
648 names
649 .iter()
650 .filter_map(|name| {
651 match self
652 .resolutions
653 .get(&ExportKey::new(file_id, *name))?
654 .effective(namespace)
655 {
656 EffectiveExportResolution::Unique(binding) => Some(binding),
657 EffectiveExportResolution::Missing | EffectiveExportResolution::Ambiguous => {
658 None
659 }
660 }
661 })
662 .collect()
663 }
664
665 pub(super) fn declaration_group_slots(&self, binding: EffectiveExportBinding) -> &[usize] {
666 binding
667 .origin_slot()
668 .and_then(|slot| {
669 self.declaration_merge_groups
670 .group_by_slot
671 .get(&(binding.origin_file(), slot))
672 })
673 .and_then(|group| self.declaration_merge_groups.groups.get(*group))
674 .map_or(&[], Box::as_ref)
675 }
676
677 pub(super) fn is_declaration_slot(
678 &self,
679 exports: &[ExportSymbol],
680 file_id: FileId,
681 name: &str,
682 namespace: ExportNamespace,
683 export_index: usize,
684 ) -> bool {
685 let Some(export) = exports.get(export_index) else {
686 return false;
687 };
688 if !export.name.matches_str(name) {
689 return false;
690 }
691 let EffectiveExportResolution::Unique(binding) = self.resolve(file_id, name, namespace)
692 else {
693 return false;
694 };
695 if binding.origin_file() != file_id {
696 return true;
697 }
698 let Some(origin_slot) = binding.origin_slot() else {
699 return true;
700 };
701 if namespace == ExportNamespace::Type {
702 let group = self.declaration_group_slots(binding);
703 if !group.is_empty() {
704 return group.contains(&export_index);
705 }
706 }
707 exports
708 .get(origin_slot)
709 .is_some_and(|origin| export.is_type_only == origin.is_type_only)
710 }
711
712 pub(super) fn declaration_slots(
713 &self,
714 exports: &[ExportSymbol],
715 candidates: &[usize],
716 file_id: FileId,
717 name: &str,
718 namespace: ExportNamespace,
719 ) -> Vec<usize> {
720 let mut slots = Vec::new();
721 self.extend_declaration_slots(
722 DeclarationSlotQuery {
723 exports,
724 candidates,
725 file_id,
726 name,
727 namespace,
728 },
729 &mut slots,
730 );
731 slots
732 }
733
734 pub(super) fn extend_declaration_slots(
736 &self,
737 query: DeclarationSlotQuery<'_>,
738 slots: &mut Vec<usize>,
739 ) {
740 let DeclarationSlotQuery {
741 exports,
742 candidates,
743 file_id,
744 name,
745 namespace,
746 } = query;
747 let EffectiveExportResolution::Unique(binding) = self.resolve(file_id, name, namespace)
748 else {
749 return;
750 };
751 if binding.origin_file() == file_id && binding.origin_slot().is_some() {
752 slots.extend(candidates.iter().copied().filter(|&index| {
753 self.is_declaration_slot(exports, file_id, name, namespace, index)
754 }));
755 return;
756 }
757
758 let exact_type_only = namespace == ExportNamespace::Type
759 && candidates.iter().any(|&index| exports[index].is_type_only);
760 slots.extend(candidates.iter().copied().filter(|&index| {
761 exports[index].name.matches_str(name) && exports[index].is_type_only == exact_type_only
762 }));
763 }
764}
765
766#[derive(Clone, Copy)]
768pub(in crate::graph) struct DeclarationSlotQuery<'a> {
769 pub(in crate::graph) exports: &'a [ExportSymbol],
770 pub(in crate::graph) candidates: &'a [usize],
771 pub(in crate::graph) file_id: FileId,
772 pub(in crate::graph) name: &'a str,
773 pub(in crate::graph) namespace: ExportNamespace,
774}
775
776fn index_names_by_file(
777 resolutions: &FxHashMap<ExportKey, NamespaceResolutions>,
778) -> FxHashMap<FileId, Box<[ExportNameId]>> {
779 let mut names_by_file: FxHashMap<FileId, Vec<ExportNameId>> = FxHashMap::default();
780 for key in resolutions.keys() {
781 names_by_file.entry(key.file_id).or_default().push(key.name);
782 }
783 names_by_file
784 .into_iter()
785 .map(|(file_id, mut names)| {
786 names.sort_unstable_by_key(|name| name.0);
787 (file_id, names.into_boxed_slice())
788 })
789 .collect()
790}
791
792fn collect_declaration_merge_groups(modules: &[ResolvedModule]) -> DeclarationMergeGroups {
793 let mut collected = DeclarationMergeGroups::default();
794 for module in modules {
795 let merge_facts: Vec<_> = module
796 .semantic_facts
797 .iter()
798 .filter_map(|fact| match fact {
799 fallow_types::extract::SemanticFact::DeclarationMerge(group) => Some(group),
800 _ => None,
801 })
802 .collect();
803 if merge_facts.is_empty() {
804 continue;
805 }
806 let slot_by_span: FxHashMap<_, _> = module
807 .exports
808 .iter()
809 .enumerate()
810 .map(|(slot, export)| ((export.span.start, export.span.end), slot))
811 .collect();
812 for group in merge_facts {
813 let mut slots: Vec<_> = group
814 .export_spans
815 .iter()
816 .filter_map(|span| slot_by_span.get(span).copied())
817 .collect();
818 slots.sort_unstable();
819 slots.dedup();
820 if slots.len() < 2 {
821 continue;
822 }
823 let group_id = collected.groups.len();
824 for &slot in &slots {
825 collected
826 .group_by_slot
827 .insert((module.file_id, slot), group_id);
828 }
829 collected.groups.push(slots.into_boxed_slice());
830 }
831 }
832 collected
833}
834
835fn modules_needing_type_fallback(modules: &[ResolvedModule]) -> FxHashSet<FileId> {
844 let mut needing = FxHashSet::default();
845 let mut pending: Vec<FileId> = modules
846 .iter()
847 .filter(|module| {
848 module
849 .re_exports
850 .iter()
851 .any(|re_export| re_export.info.is_type_only && reads_source_lanes(&re_export.info))
852 })
853 .map(|module| module.file_id)
854 .collect();
855 if pending.is_empty() {
856 return needing;
857 }
858
859 let mut edges = ReExportEdges::collect(modules);
860 let mut fed_by_type_only = FxHashSet::default();
861 while let Some(file_id) = pending.pop() {
862 if !fed_by_type_only.insert(file_id) {
863 continue;
864 }
865 pending.extend(edges.barrels_by_source.remove(&file_id).unwrap_or_default());
866 }
867
868 let mut pending: Vec<FileId> = fed_by_type_only.into_iter().collect();
869 while let Some(file_id) = pending.pop() {
870 if !needing.insert(file_id) {
871 continue;
872 }
873 if let Some(sources) = edges.sources_by_barrel.get(&file_id) {
874 pending.extend(sources.iter().copied());
875 }
876 }
877 needing
878}
879
880struct ReExportEdges {
882 sources_by_barrel: FxHashMap<FileId, Vec<FileId>>,
883 barrels_by_source: FxHashMap<FileId, Vec<FileId>>,
884}
885
886impl ReExportEdges {
887 fn collect(modules: &[ResolvedModule]) -> Self {
888 let mut edges = Self {
889 sources_by_barrel: FxHashMap::default(),
890 barrels_by_source: FxHashMap::default(),
891 };
892 for module in modules {
893 for re_export in &module.re_exports {
894 if !reads_source_lanes(&re_export.info) {
895 continue;
896 }
897 let Some(source) = re_export.target.internal_file_id() else {
898 continue;
899 };
900 edges
901 .sources_by_barrel
902 .entry(module.file_id)
903 .or_default()
904 .push(source);
905 edges
906 .barrels_by_source
907 .entry(source)
908 .or_default()
909 .push(module.file_id);
910 }
911 }
912 edges
913 }
914}
915
916fn reads_source_lanes(info: &fallow_types::extract::ReExportInfo) -> bool {
920 info.imported_name != "*" || info.exported_name == "*"
921}
922
923fn seed_direct_bindings(
924 modules: &[ResolvedModule],
925 type_fallback_modules: &FxHashSet<FileId>,
926 interner: &mut ExportNameInterner,
927 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
928 queue: &mut VecDeque<ExportLookup>,
929) {
930 for module in modules {
931 let seed_type_fallback = type_fallback_modules.contains(&module.file_id);
932 for (slot, export) in module.exports.iter().enumerate() {
933 let lane = if export.is_type_only {
934 ResolutionLane::Type
935 } else {
936 ResolutionLane::Value
937 };
938 let name = interner.intern_export_name(&export.name);
939 let key = ExportLookup::new(module.file_id, name, lane);
940 let binding = EffectiveExportResolution::Unique(EffectiveExportBinding {
941 file_id: module.file_id,
942 kind: EffectiveExportBindingKind::Declaration(slot),
943 });
944 let entry = resolutions.entry(key.key).or_default();
945 if entry.seeded.claim_direct(lane) && entry.merge(lane, binding) {
950 queue.push_back(key);
951 }
952 if lane == ResolutionLane::Value {
953 entry.seeded.reserve_type_for_value_declaration();
957 if seed_type_fallback
958 && entry.seeded.claim_direct(ResolutionLane::TypeFallback)
959 && entry.merge(ResolutionLane::TypeFallback, binding)
960 {
961 queue.push_back(ExportLookup::new(
962 module.file_id,
963 name,
964 ResolutionLane::TypeFallback,
965 ));
966 }
967 }
968 }
969 seed_implicit_sfc_default(module, interner, resolutions, queue);
970 }
971}
972
973fn seed_implicit_sfc_default(
974 module: &ResolvedModule,
975 interner: &mut ExportNameInterner,
976 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
977 queue: &mut VecDeque<ExportLookup>,
978) {
979 if !is_sfc_path(&module.path) {
980 return;
981 }
982 let name = interner.intern("default");
983 let key = ExportKey::new(module.file_id, name);
984 let entry = resolutions.entry(key).or_default();
985 if entry.seeded.is_direct(ResolutionLane::Value) {
986 return;
987 }
988 let binding = EffectiveExportResolution::Unique(EffectiveExportBinding {
989 file_id: module.file_id,
990 kind: EffectiveExportBindingKind::ImplicitDefault,
991 });
992 for lane in [ResolutionLane::Value, ResolutionLane::Type] {
993 entry.seeded.claim_direct(lane);
994 if entry.merge(lane, binding) {
995 queue.push_back(ExportLookup::new(module.file_id, name, lane));
996 }
997 }
998}
999
1000fn is_sfc_path(path: &std::path::Path) -> bool {
1001 matches!(
1002 path.extension().and_then(std::ffi::OsStr::to_str),
1003 Some("vue" | "svelte" | "astro")
1004 )
1005}
1006
1007fn collect_observers(
1008 modules: &[ResolvedModule],
1009 type_fallback_modules: &FxHashSet<FileId>,
1010 interner: &mut ExportNameInterner,
1011 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
1012 queue: &mut VecDeque<ExportLookup>,
1013) -> PropagationObservers {
1014 let mut observers = PropagationObservers {
1015 named: FxHashMap::default(),
1016 star: FxHashMap::default(),
1017 };
1018 for module in modules {
1019 let type_fallback = type_fallback_modules.contains(&module.file_id);
1020 for (re_export_index, re_export) in module.re_exports.iter().enumerate() {
1021 let Some(source) = re_export.target.internal_file_id() else {
1022 if re_export.info.exported_name != "*" {
1023 register_opaque_re_export(
1024 module.file_id,
1025 re_export_index,
1026 &re_export.info,
1027 interner,
1028 resolutions,
1029 queue,
1030 );
1031 }
1032 continue;
1033 };
1034 if re_export.info.exported_name == "*" {
1035 observers
1036 .star
1037 .entry(source)
1038 .or_default()
1039 .push(StarObserver {
1040 barrel: module.file_id,
1041 type_only: re_export.info.is_type_only,
1042 type_fallback,
1043 });
1044 continue;
1045 }
1046 register_named_observer(
1047 module.file_id,
1048 source,
1049 &re_export.info,
1050 ObserverBuildState {
1051 type_fallback,
1052 interner,
1053 resolutions,
1054 queue,
1055 observers: &mut observers,
1056 },
1057 );
1058 }
1059 }
1060 observers
1061}
1062
1063fn register_opaque_re_export(
1072 barrel: FileId,
1073 re_export_index: usize,
1074 info: &fallow_types::extract::ReExportInfo,
1075 interner: &mut ExportNameInterner,
1076 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
1077 queue: &mut VecDeque<ExportLookup>,
1078) {
1079 let exported_name = interner.intern(&info.exported_name);
1080 let binding = EffectiveExportResolution::Unique(EffectiveExportBinding {
1081 file_id: barrel,
1082 kind: EffectiveExportBindingKind::ExternalReExport(re_export_index),
1083 });
1084 let lanes: &[ResolutionLane] = if info.is_type_only {
1085 &[ResolutionLane::Type]
1086 } else {
1087 &[ResolutionLane::Type, ResolutionLane::Value]
1088 };
1089 for &lane in lanes {
1090 let destination = ExportLookup::new(barrel, exported_name, lane);
1091 let entry = resolutions.entry(destination.key).or_default();
1092 if entry.seeded.is_direct(lane) {
1093 continue;
1094 }
1095 entry.seeded.claim_explicit(lane);
1096 if entry.merge(lane, binding) {
1097 queue.push_back(destination);
1098 }
1099 }
1100}
1101
1102fn register_named_observer(
1103 barrel: FileId,
1104 source: FileId,
1105 info: &fallow_types::extract::ReExportInfo,
1106 state: ObserverBuildState<'_>,
1107) {
1108 let ObserverBuildState {
1109 type_fallback,
1110 interner,
1111 resolutions,
1112 queue,
1113 observers,
1114 } = state;
1115 let exported_name = interner.intern(&info.exported_name);
1116 if info.imported_name == "*" {
1117 let lanes: &[ResolutionLane] = if info.is_type_only {
1118 &[ResolutionLane::Type]
1119 } else {
1120 &[ResolutionLane::Type, ResolutionLane::Value]
1121 };
1122 let binding = EffectiveExportResolution::Unique(EffectiveExportBinding {
1123 file_id: barrel,
1124 kind: EffectiveExportBindingKind::NamespaceObject { source },
1125 });
1126 for &lane in lanes {
1127 let destination = ExportLookup::new(barrel, exported_name, lane);
1128 let entry = resolutions.entry(destination.key).or_default();
1129 if entry.seeded.is_direct(lane) {
1130 continue;
1131 }
1132 entry.seeded.claim_explicit(lane);
1133 if entry.merge(lane, binding) {
1134 queue.push_back(destination);
1135 }
1136 }
1137 return;
1138 }
1139
1140 let imported_name = interner.intern(&info.imported_name);
1141 let lanes: &[ResolutionLane] = match (info.is_type_only, type_fallback) {
1142 (true, _) => &[ResolutionLane::Type, ResolutionLane::TypeFallback],
1143 (false, true) => &[
1144 ResolutionLane::Type,
1145 ResolutionLane::TypeFallback,
1146 ResolutionLane::Value,
1147 ],
1148 (false, false) => &[ResolutionLane::Type, ResolutionLane::Value],
1149 };
1150 for &lane in lanes {
1151 let destination = ExportLookup::new(barrel, exported_name, lane);
1152 let entry = resolutions.entry(destination.key).or_default();
1153 if entry.seeded.is_direct(lane) {
1154 continue;
1155 }
1156 entry.seeded.claim_explicit(lane);
1157 observers
1158 .named
1159 .entry(ExportLookup::new(source, imported_name, lane))
1160 .and_modify(|destinations| destinations.push(destination))
1161 .or_insert(NamedDestinations::One(destination));
1162 }
1163}
1164
1165fn propagate_bindings(
1166 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
1167 queue: &mut VecDeque<ExportLookup>,
1168 observers: &PropagationObservers,
1169) {
1170 while let Some(source_key) = queue.pop_front() {
1171 let Some(source_resolution) = resolutions
1172 .get(&source_key.key)
1173 .map(|resolutions| resolutions.get(source_key.lane))
1174 else {
1175 continue;
1176 };
1177 if let Some(destinations) = observers.named.get(&source_key) {
1178 for destination in destinations.iter() {
1179 merge_resolution(resolutions, queue, *destination, source_resolution);
1180 }
1181 }
1182 propagate_star_binding(
1183 resolutions,
1184 queue,
1185 observers,
1186 &source_key,
1187 source_resolution,
1188 );
1189 }
1190}
1191
1192fn propagate_star_binding(
1193 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
1194 queue: &mut VecDeque<ExportLookup>,
1195 observers: &PropagationObservers,
1196 source_key: &ExportLookup,
1197 source_resolution: EffectiveExportResolution,
1198) {
1199 if source_key.key.name == ExportNameId::DEFAULT {
1200 return;
1201 }
1202 let Some(star_observers) = observers.star.get(&source_key.key.file_id) else {
1203 return;
1204 };
1205 for observer in star_observers {
1206 if observer.type_only && source_key.lane == ResolutionLane::Value {
1207 continue;
1208 }
1209 if !observer.type_fallback && source_key.lane == ResolutionLane::TypeFallback {
1210 continue;
1211 }
1212 let destination = source_key.with_file(observer.barrel);
1213 let entry = resolutions.entry(destination.key).or_default();
1214 if entry.seeded.is_explicit(destination.lane) {
1215 continue;
1216 }
1217 if entry.merge(destination.lane, source_resolution) {
1218 queue.push_back(destination);
1219 }
1220 }
1221}
1222
1223fn merge_resolution(
1224 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
1225 queue: &mut VecDeque<ExportLookup>,
1226 key: ExportLookup,
1227 incoming: EffectiveExportResolution,
1228) {
1229 if resolutions
1230 .entry(key.key)
1231 .or_default()
1232 .merge(key.lane, incoming)
1233 {
1234 queue.push_back(key);
1235 }
1236}
1237
1238#[cfg(test)]
1239mod tests {
1240 use super::*;
1241 use crate::resolve::{ResolveResult, ResolvedReExport};
1242 use fallow_types::extract::{ExportInfo, ReExportInfo, VisibilityTag};
1243
1244 fn value_export(name: &str) -> ExportInfo {
1245 ExportInfo {
1246 name: ExportName::Named(name.to_string()),
1247 local_name: Some(name.to_string()),
1248 is_type_only: false,
1249 visibility: VisibilityTag::None,
1250 expected_unused_reason: None,
1251 span: oxc_span::Span::default(),
1252 members: Vec::new(),
1253 is_side_effect_used: false,
1254 super_class: None,
1255 deprecated: false,
1256 deprecated_reason: None,
1257 }
1258 }
1259
1260 fn re_export(source: FileId, imported: &str, exported: &str) -> ResolvedReExport {
1261 ResolvedReExport {
1262 info: ReExportInfo {
1263 source: format!("./{}", source.0),
1264 imported_name: imported.to_string(),
1265 exported_name: exported.to_string(),
1266 is_type_only: false,
1267 span: oxc_span::Span::default(),
1268 statement_span: oxc_span::Span::default(),
1269 source_span: oxc_span::Span::default(),
1270 },
1271 target: ResolveResult::InternalModule(source),
1272 }
1273 }
1274
1275 fn module(
1276 file_id: u32,
1277 exports: Vec<ExportInfo>,
1278 re_exports: Vec<ResolvedReExport>,
1279 ) -> ResolvedModule {
1280 ResolvedModule {
1281 file_id: FileId(file_id),
1282 exports: exports.into(),
1283 re_exports,
1284 ..Default::default()
1285 }
1286 }
1287
1288 fn resolves_through(
1289 index: &EffectiveExportIndex,
1290 barrel: FileId,
1291 source: FileId,
1292 name: &str,
1293 ) -> bool {
1294 index.resolves_through(barrel, name, source, name, ExportNamespace::Value)
1295 }
1296
1297 fn external_re_export(imported: &str, exported: &str, type_only: bool) -> ResolvedReExport {
1298 ResolvedReExport {
1299 info: ReExportInfo {
1300 source: "node:path".to_string(),
1301 imported_name: imported.to_string(),
1302 exported_name: exported.to_string(),
1303 is_type_only: type_only,
1304 span: oxc_span::Span::default(),
1305 statement_span: oxc_span::Span::default(),
1306 source_span: oxc_span::Span::default(),
1307 },
1308 target: ResolveResult::NpmPackage("node:path".to_string()),
1309 }
1310 }
1311
1312 #[test]
1313 fn external_named_re_exports_keep_an_opaque_local_binding() {
1314 let index = EffectiveExportIndex::build(&[module(
1315 0,
1316 Vec::new(),
1317 vec![
1318 external_re_export("join", "join", false),
1319 external_re_export("Stats", "Stats", true),
1320 external_re_export("*", "path", false),
1321 ],
1322 )]);
1323 let encoded = postcard::to_allocvec(&index).expect("encode effective export index");
1324 let decoded: EffectiveExportIndex =
1325 postcard::from_bytes(&encoded).expect("decode effective export index");
1326
1327 let value = decoded.resolve(FileId(0), "join", ExportNamespace::Value);
1328 assert!(matches!(
1329 value,
1330 EffectiveExportResolution::Unique(binding)
1331 if binding.origin_file() == FileId(0) && binding.origin_slot().is_none()
1332 ));
1333 assert_eq!(
1334 value,
1335 decoded.resolve(FileId(0), "join", ExportNamespace::Type)
1336 );
1337 assert!(matches!(
1338 decoded.resolve(FileId(0), "Stats", ExportNamespace::Type),
1339 EffectiveExportResolution::Unique(_)
1340 ));
1341 assert_eq!(
1342 decoded.resolve(FileId(0), "Stats", ExportNamespace::Value),
1343 EffectiveExportResolution::Missing
1344 );
1345 assert_eq!(
1346 decoded.resolve(FileId(0), "path", ExportNamespace::Type),
1347 decoded.resolve(FileId(0), "path", ExportNamespace::Value)
1348 );
1349 }
1350
1351 #[test]
1352 fn unresolved_named_re_exports_keep_an_opaque_local_binding() {
1353 let mut unresolved = external_re_export("missing", "missing", false);
1354 unresolved.target = ResolveResult::Unresolvable("./missing".to_string());
1355 let mut unresolved_star = external_re_export("*", "*", false);
1356 unresolved_star.target = ResolveResult::Unresolvable("./missing-star".to_string());
1357 let index = EffectiveExportIndex::build(&[module(
1358 0,
1359 Vec::new(),
1360 vec![unresolved, unresolved_star],
1361 )]);
1362
1363 for namespace in [ExportNamespace::Type, ExportNamespace::Value] {
1364 assert!(matches!(
1365 index.resolve(FileId(0), "missing", namespace),
1366 EffectiveExportResolution::Unique(binding)
1367 if binding.origin_file() == FileId(0) && binding.origin_slot().is_none()
1368 ));
1369 assert_eq!(
1370 index.resolve(FileId(0), "other", namespace),
1371 EffectiveExportResolution::Missing,
1372 "an unresolved star re-export must not invent names"
1373 );
1374 }
1375 }
1376
1377 #[test]
1378 fn missing_export_is_explicit_in_the_resolution_contract() {
1379 let index = EffectiveExportIndex::build(&[module(0, Vec::new(), Vec::new())]);
1380
1381 assert_eq!(
1382 index.resolve(FileId(0), "missing", ExportNamespace::Value),
1383 EffectiveExportResolution::Missing
1384 );
1385 }
1386
1387 #[test]
1388 fn same_module_declaration_merges_keep_one_binding() {
1389 let index = EffectiveExportIndex::build(&[module(
1390 0,
1391 vec![value_export("Merged"), value_export("Merged")],
1392 Vec::new(),
1393 )]);
1394
1395 assert!(matches!(
1396 index.resolve(FileId(0), "Merged", ExportNamespace::Value),
1397 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(0)
1398 ));
1399 }
1400
1401 #[test]
1402 fn declaration_merge_groups_survive_graph_cache_roundtrip() {
1403 let mut interface = value_export("Merged");
1404 interface.is_type_only = true;
1405 interface.span = oxc_span::Span::new(0, 6);
1406 let mut namespace = value_export("Merged");
1407 namespace.span = oxc_span::Span::new(10, 16);
1408 let modules = vec![ResolvedModule {
1409 file_id: FileId(0),
1410 exports: vec![interface, namespace].into(),
1411 semantic_facts: vec![fallow_types::extract::SemanticFact::DeclarationMerge(
1412 fallow_types::extract::DeclarationMergeFact {
1413 export_spans: vec![(0, 6), (10, 16)],
1414 },
1415 )]
1416 .into(),
1417 ..Default::default()
1418 }];
1419 let index = EffectiveExportIndex::build(&modules);
1420 let encoded = postcard::to_allocvec(&index).expect("encode effective export index");
1421 let decoded: EffectiveExportIndex =
1422 postcard::from_bytes(&encoded).expect("decode effective export index");
1423 let EffectiveExportResolution::Unique(binding) =
1424 decoded.resolve(FileId(0), "Merged", ExportNamespace::Type)
1425 else {
1426 panic!("merged type binding must remain unique");
1427 };
1428
1429 assert_eq!(decoded.declaration_group_slots(binding), &[0, 1]);
1430 }
1431
1432 #[test]
1433 fn explicit_re_export_shadows_a_star_binding() {
1434 let modules = vec![
1435 module(
1436 0,
1437 Vec::new(),
1438 vec![
1439 re_export(FileId(1), "*", "*"),
1440 re_export(FileId(2), "foo", "foo"),
1441 ],
1442 ),
1443 module(1, vec![value_export("foo")], Vec::new()),
1444 module(2, vec![value_export("foo")], Vec::new()),
1445 ];
1446 let index = EffectiveExportIndex::build(&modules);
1447
1448 assert!(!resolves_through(&index, FileId(0), FileId(1), "foo"));
1449 assert!(resolves_through(&index, FileId(0), FileId(2), "foo"));
1450 assert!(!index.contributes_through(
1451 FileId(0),
1452 "foo",
1453 FileId(1),
1454 "foo",
1455 ExportNamespace::Value
1456 ));
1457 }
1458
1459 #[test]
1460 fn distinct_star_bindings_are_ambiguous() {
1461 let modules = vec![
1462 module(
1463 0,
1464 Vec::new(),
1465 vec![
1466 re_export(FileId(1), "*", "*"),
1467 re_export(FileId(2), "*", "*"),
1468 ],
1469 ),
1470 module(1, vec![value_export("foo")], Vec::new()),
1471 module(2, vec![value_export("foo")], Vec::new()),
1472 ];
1473 let index = EffectiveExportIndex::build(&modules);
1474
1475 assert!(!resolves_through(&index, FileId(0), FileId(1), "foo"));
1476 assert!(!resolves_through(&index, FileId(0), FileId(2), "foo"));
1477 assert!(index.contributes_through(
1478 FileId(0),
1479 "foo",
1480 FileId(1),
1481 "foo",
1482 ExportNamespace::Value
1483 ));
1484 assert!(index.contributes_through(
1485 FileId(0),
1486 "foo",
1487 FileId(2),
1488 "foo",
1489 ExportNamespace::Value
1490 ));
1491 }
1492
1493 #[test]
1494 fn convergent_star_paths_keep_one_binding() {
1495 let modules = vec![
1496 module(
1497 0,
1498 Vec::new(),
1499 vec![
1500 re_export(FileId(1), "*", "*"),
1501 re_export(FileId(2), "*", "*"),
1502 ],
1503 ),
1504 module(1, Vec::new(), vec![re_export(FileId(3), "*", "*")]),
1505 module(2, Vec::new(), vec![re_export(FileId(3), "*", "*")]),
1506 module(3, vec![value_export("foo")], Vec::new()),
1507 ];
1508 let index = EffectiveExportIndex::build(&modules);
1509
1510 assert!(resolves_through(&index, FileId(0), FileId(1), "foo"));
1511 assert!(resolves_through(&index, FileId(0), FileId(2), "foo"));
1512 }
1513
1514 #[test]
1515 fn a_real_type_declaration_wins_over_a_value_type_fallback() {
1516 let mut interface = value_export("User");
1517 interface.is_type_only = true;
1518 let modules = vec![
1519 module(
1520 0,
1521 Vec::new(),
1522 vec![
1523 re_export(FileId(1), "*", "*"),
1524 re_export(FileId(2), "*", "*"),
1525 ],
1526 ),
1527 module(1, vec![value_export("User")], Vec::new()),
1528 module(2, vec![interface], Vec::new()),
1529 ];
1530 let index = EffectiveExportIndex::build(&modules);
1531
1532 assert!(matches!(
1533 index.resolve(FileId(0), "User", ExportNamespace::Type),
1534 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(2)
1535 ));
1536 assert!(matches!(
1537 index.resolve(FileId(0), "User", ExportNamespace::Value),
1538 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(1)
1539 ));
1540 assert!(index.resolves_through(
1541 FileId(0),
1542 "User",
1543 FileId(2),
1544 "User",
1545 ExportNamespace::Type
1546 ));
1547 assert!(index.resolves_through(
1548 FileId(0),
1549 "User",
1550 FileId(1),
1551 "User",
1552 ExportNamespace::Value
1553 ));
1554 }
1555
1556 #[test]
1557 fn colliding_real_type_declarations_stay_ambiguous() {
1558 let type_export = |name: &str| {
1559 let mut export = value_export(name);
1560 export.is_type_only = true;
1561 export
1562 };
1563 let modules = vec![
1564 module(
1565 0,
1566 Vec::new(),
1567 vec![
1568 re_export(FileId(1), "*", "*"),
1569 re_export(FileId(2), "*", "*"),
1570 ],
1571 ),
1572 module(1, vec![type_export("User")], Vec::new()),
1573 module(2, vec![type_export("User")], Vec::new()),
1574 ];
1575 let index = EffectiveExportIndex::build(&modules);
1576
1577 assert_eq!(
1578 index.resolve(FileId(0), "User", ExportNamespace::Type),
1579 EffectiveExportResolution::Ambiguous
1580 );
1581 assert_eq!(
1582 index.ambiguous_names_on(FileId(0)),
1583 vec![(FileId(0), "User", ExportNamespace::Type)]
1584 );
1585 }
1586
1587 #[test]
1588 fn a_colliding_value_export_reports_one_collision_in_the_value_namespace() {
1589 let modules = vec![
1590 module(
1591 0,
1592 Vec::new(),
1593 vec![
1594 re_export(FileId(1), "*", "*"),
1595 re_export(FileId(2), "*", "*"),
1596 ],
1597 ),
1598 module(1, vec![value_export("foo")], Vec::new()),
1599 module(2, vec![value_export("foo")], Vec::new()),
1600 ];
1601 let index = EffectiveExportIndex::build(&modules);
1602
1603 assert_eq!(
1604 index.ambiguous_names(),
1605 vec![(FileId(0), "foo", ExportNamespace::Value)]
1606 );
1607 }
1608
1609 #[test]
1610 fn a_value_export_still_carries_its_type_meaning_through_a_barrel() {
1611 let modules = vec![
1612 module(0, Vec::new(), vec![re_export(FileId(1), "*", "*")]),
1613 module(1, vec![value_export("Widget")], Vec::new()),
1614 ];
1615 let index = EffectiveExportIndex::build(&modules);
1616
1617 assert!(index.resolves_through(
1618 FileId(0),
1619 "Widget",
1620 FileId(1),
1621 "Widget",
1622 ExportNamespace::Type
1623 ));
1624 }
1625
1626 #[test]
1627 fn a_type_only_re_export_carries_a_value_declaration_as_a_type_only() {
1628 let mut type_only = re_export(FileId(1), "Widget", "Widget");
1629 type_only.info.is_type_only = true;
1630 let index = EffectiveExportIndex::build(&[
1631 module(0, Vec::new(), vec![type_only]),
1632 module(1, vec![value_export("Widget")], Vec::new()),
1633 ]);
1634
1635 assert!(matches!(
1636 index.resolve(FileId(0), "Widget", ExportNamespace::Type),
1637 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(1)
1638 ));
1639 assert_eq!(
1640 index.resolve(FileId(0), "Widget", ExportNamespace::Value),
1641 EffectiveExportResolution::Missing
1642 );
1643 }
1644
1645 #[test]
1646 fn a_type_only_re_export_reaches_a_value_declaration_behind_a_barrel() {
1647 let mut type_only = re_export(FileId(1), "Widget", "Widget");
1648 type_only.info.is_type_only = true;
1649 let index = EffectiveExportIndex::build(&[
1650 module(0, Vec::new(), vec![type_only]),
1651 module(1, Vec::new(), vec![re_export(FileId(2), "*", "*")]),
1652 module(2, vec![value_export("Widget")], Vec::new()),
1653 ]);
1654
1655 assert!(matches!(
1656 index.resolve(FileId(0), "Widget", ExportNamespace::Type),
1657 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(2)
1658 ));
1659 }
1660
1661 #[test]
1662 fn a_type_only_re_export_shadows_a_star_binding_in_the_type_namespace() {
1663 let mut type_only = re_export(FileId(1), "foo", "foo");
1664 type_only.info.is_type_only = true;
1665 let index = EffectiveExportIndex::build(&[
1666 module(
1667 0,
1668 Vec::new(),
1669 vec![type_only, re_export(FileId(2), "*", "*")],
1670 ),
1671 module(1, vec![value_export("foo")], Vec::new()),
1672 module(2, vec![value_export("foo")], Vec::new()),
1673 ]);
1674
1675 assert!(matches!(
1676 index.resolve(FileId(0), "foo", ExportNamespace::Type),
1677 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(1)
1678 ));
1679 assert!(matches!(
1680 index.resolve(FileId(0), "foo", ExportNamespace::Value),
1681 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(2)
1682 ));
1683 }
1684
1685 #[test]
1686 fn a_type_only_barrel_still_sees_a_star_collision_as_ambiguous() {
1687 let mut type_only = re_export(FileId(1), "foo", "foo");
1688 type_only.info.is_type_only = true;
1689 let index = EffectiveExportIndex::build(&[
1690 module(0, Vec::new(), vec![type_only]),
1691 module(
1692 1,
1693 Vec::new(),
1694 vec![
1695 re_export(FileId(2), "*", "*"),
1696 re_export(FileId(3), "*", "*"),
1697 ],
1698 ),
1699 module(2, vec![value_export("foo")], Vec::new()),
1700 module(3, vec![value_export("foo")], Vec::new()),
1701 ]);
1702
1703 assert_eq!(
1704 index.resolve(FileId(0), "foo", ExportNamespace::Type),
1705 EffectiveExportResolution::Ambiguous
1706 );
1707 }
1708
1709 #[test]
1710 fn a_star_collision_stays_ambiguous_beside_an_unrelated_type_only_barrel() {
1711 let mut type_only = re_export(FileId(1), "foo", "foo");
1712 type_only.info.is_type_only = true;
1713 let index = EffectiveExportIndex::build(&[
1714 module(0, Vec::new(), vec![type_only]),
1715 module(1, Vec::new(), vec![re_export(FileId(2), "*", "*")]),
1716 module(2, vec![value_export("foo")], Vec::new()),
1717 module(
1718 3,
1719 Vec::new(),
1720 vec![
1721 re_export(FileId(2), "*", "*"),
1722 re_export(FileId(4), "*", "*"),
1723 ],
1724 ),
1725 module(4, vec![value_export("foo")], Vec::new()),
1726 ]);
1727
1728 assert_eq!(
1729 index.resolve(FileId(3), "foo", ExportNamespace::Type),
1730 EffectiveExportResolution::Ambiguous
1731 );
1732 }
1733
1734 #[test]
1735 fn star_exports_exclude_default_bindings() {
1736 let mut default = value_export("local");
1737 default.name = ExportName::Default;
1738 let index = EffectiveExportIndex::build(&[
1739 module(0, Vec::new(), vec![re_export(FileId(1), "*", "*")]),
1740 module(1, vec![default], Vec::new()),
1741 ]);
1742
1743 assert_eq!(
1744 index.resolve(FileId(0), "default", ExportNamespace::Value),
1745 EffectiveExportResolution::Missing
1746 );
1747 }
1748
1749 #[test]
1750 fn star_cycles_converge_on_the_same_binding() {
1751 let index = EffectiveExportIndex::build(&[
1752 module(0, Vec::new(), vec![re_export(FileId(1), "*", "*")]),
1753 module(
1754 1,
1755 Vec::new(),
1756 vec![
1757 re_export(FileId(0), "*", "*"),
1758 re_export(FileId(2), "*", "*"),
1759 ],
1760 ),
1761 module(2, vec![value_export("foo")], Vec::new()),
1762 ]);
1763
1764 assert!(resolves_through(&index, FileId(0), FileId(2), "foo"));
1765 assert!(resolves_through(&index, FileId(1), FileId(2), "foo"));
1766 }
1767
1768 #[test]
1769 fn type_only_namespace_re_export_resolves_only_in_type_namespace() {
1770 let mut namespace = re_export(FileId(1), "*", "Types");
1771 namespace.info.is_type_only = true;
1772 let index = EffectiveExportIndex::build(&[
1773 module(0, Vec::new(), vec![namespace]),
1774 module(1, vec![value_export("foo")], Vec::new()),
1775 ]);
1776
1777 assert!(matches!(
1778 index.resolve(FileId(0), "Types", ExportNamespace::Type),
1779 EffectiveExportResolution::Unique(_)
1780 ));
1781 assert_eq!(
1782 index.resolve(FileId(0), "Types", ExportNamespace::Value),
1783 EffectiveExportResolution::Missing
1784 );
1785 }
1786
1787 #[test]
1788 fn normal_namespace_re_export_resolves_in_both_namespaces() {
1789 let index = EffectiveExportIndex::build(&[
1790 module(0, Vec::new(), vec![re_export(FileId(1), "*", "Namespace")]),
1791 module(1, vec![value_export("foo")], Vec::new()),
1792 ]);
1793
1794 let type_binding = index.resolve(FileId(0), "Namespace", ExportNamespace::Type);
1795 let value_binding = index.resolve(FileId(0), "Namespace", ExportNamespace::Value);
1796 assert!(matches!(type_binding, EffectiveExportResolution::Unique(_)));
1797 assert_eq!(type_binding, value_binding);
1798 }
1799
1800 #[test]
1801 fn persisted_index_remains_queryable_without_reconstruction() {
1802 let index = EffectiveExportIndex::build(&[
1803 module(0, Vec::new(), vec![re_export(FileId(1), "foo", "bar")]),
1804 module(1, vec![value_export("foo")], Vec::new()),
1805 ]);
1806 let encoded = postcard::to_allocvec(&index).expect("encode effective export index");
1807 let decoded: EffectiveExportIndex =
1808 postcard::from_bytes(&encoded).expect("decode effective export index");
1809
1810 assert!(matches!(
1811 decoded.resolve(FileId(0), "bar", ExportNamespace::Value),
1812 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(1)
1813 ));
1814 assert_eq!(
1815 decoded.resolve(FileId(0), "missing", ExportNamespace::Value),
1816 EffectiveExportResolution::Missing
1817 );
1818 }
1819
1820 #[test]
1821 fn sfc_file_has_an_implicit_default_value_binding() {
1822 let mut sfc = module(0, Vec::new(), Vec::new());
1823 sfc.path = std::path::PathBuf::from("/project/Widget.vue");
1824 let index = EffectiveExportIndex::build(&[sfc]);
1825
1826 assert!(matches!(
1827 index.resolve(FileId(0), "default", ExportNamespace::Value),
1828 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(0)
1829 ));
1830 }
1831}