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