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)]
136pub(in crate::graph) struct 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(in crate::graph) fn name_id(&self, name: &str) -> Option<ExportNameId> {
515 self.name_ids.get(name).copied()
516 }
517
518 pub(in crate::graph) fn names_on(&self, file_id: FileId) -> &[ExportNameId] {
520 self.names_by_file.get(&file_id).map_or(&[], AsRef::as_ref)
521 }
522
523 pub(in crate::graph) fn has_name(&self, file_id: FileId, name: ExportNameId) -> bool {
526 self.resolutions
527 .contains_key(&ExportKey::new(file_id, name))
528 }
529
530 pub(super) fn resolves_through(
531 &self,
532 barrel: FileId,
533 barrel_name: &str,
534 source: FileId,
535 source_name: &str,
536 namespace: ExportNamespace,
537 ) -> bool {
538 matches!(
539 (
540 self.resolve(barrel, barrel_name, namespace),
541 self.resolve(source, source_name, namespace),
542 ),
543 (
544 EffectiveExportResolution::Unique(barrel_binding),
545 EffectiveExportResolution::Unique(source_binding),
546 ) if barrel_binding == source_binding
547 )
548 }
549
550 pub(super) fn contributes_through(
551 &self,
552 barrel: FileId,
553 barrel_name: &str,
554 source: FileId,
555 source_name: &str,
556 namespace: ExportNamespace,
557 ) -> bool {
558 match (
559 self.resolve(barrel, barrel_name, namespace),
560 self.resolve(source, source_name, namespace),
561 ) {
562 (
563 EffectiveExportResolution::Unique(barrel_binding),
564 EffectiveExportResolution::Unique(source_binding),
565 ) => barrel_binding == source_binding,
566 (
567 EffectiveExportResolution::Ambiguous,
568 EffectiveExportResolution::Unique(_) | EffectiveExportResolution::Ambiguous,
569 ) => true,
570 _ => false,
571 }
572 }
573
574 pub(super) fn ambiguous_names(&self) -> Vec<(FileId, &str, ExportNamespace)> {
580 let mut ambiguous = Vec::new();
581 for (file_id, names) in &self.names_by_file {
582 self.collect_ambiguous_ids(*file_id, names, &mut ambiguous);
583 }
584 self.name_texts(ambiguous)
585 }
586
587 pub(super) fn ambiguous_names_on(
589 &self,
590 file_id: FileId,
591 ) -> Vec<(FileId, &str, ExportNamespace)> {
592 let Some(names) = self.names_by_file.get(&file_id) else {
593 return Vec::new();
594 };
595 let mut ambiguous = Vec::new();
596 self.collect_ambiguous_ids(file_id, names, &mut ambiguous);
597 self.name_texts(ambiguous)
598 }
599
600 fn collect_ambiguous_ids(
601 &self,
602 file_id: FileId,
603 names: &[ExportNameId],
604 ambiguous: &mut Vec<(FileId, ExportNameId, ExportNamespace)>,
605 ) {
606 for name in names {
607 let Some(resolutions) = self.resolutions.get(&ExportKey::new(file_id, *name)) else {
608 continue;
609 };
610 for (namespace, collides) in [
611 (ExportNamespace::Type, resolutions.collides_in_type_space()),
612 (
613 ExportNamespace::Value,
614 matches!(resolutions.value, EffectiveExportResolution::Ambiguous),
615 ),
616 ] {
617 if collides {
618 ambiguous.push((file_id, *name, namespace));
619 }
620 }
621 }
622 }
623
624 fn name_texts(
628 &self,
629 ambiguous: Vec<(FileId, ExportNameId, ExportNamespace)>,
630 ) -> Vec<(FileId, &str, ExportNamespace)> {
631 if ambiguous.is_empty() {
632 return Vec::new();
633 }
634 let table = self.name_table();
635 ambiguous
636 .into_iter()
637 .filter_map(|(file_id, name, namespace)| {
638 table
639 .get(name.0)
640 .copied()
641 .map(|text| (file_id, text, namespace))
642 })
643 .collect()
644 }
645
646 fn name_table(&self) -> Vec<&str> {
648 let mut table = vec![""; self.name_ids.len()];
649 for (name, id) in &self.name_ids {
650 if let Some(slot) = table.get_mut(id.0) {
651 *slot = name.as_ref();
652 }
653 }
654 table
655 }
656
657 pub(super) fn unique_bindings(
658 &self,
659 file_id: FileId,
660 namespace: ExportNamespace,
661 ) -> FxHashSet<EffectiveExportBinding> {
662 let Some(names) = self.names_by_file.get(&file_id) else {
663 return FxHashSet::default();
664 };
665 names
666 .iter()
667 .filter_map(|name| {
668 match self
669 .resolutions
670 .get(&ExportKey::new(file_id, *name))?
671 .effective(namespace)
672 {
673 EffectiveExportResolution::Unique(binding) => Some(binding),
674 EffectiveExportResolution::Missing | EffectiveExportResolution::Ambiguous => {
675 None
676 }
677 }
678 })
679 .collect()
680 }
681
682 pub(super) fn declaration_group_slots(&self, binding: EffectiveExportBinding) -> &[usize] {
683 binding
684 .origin_slot()
685 .and_then(|slot| {
686 self.declaration_merge_groups
687 .group_by_slot
688 .get(&(binding.origin_file(), slot))
689 })
690 .and_then(|group| self.declaration_merge_groups.groups.get(*group))
691 .map_or(&[], Box::as_ref)
692 }
693
694 pub(super) fn is_declaration_slot(
695 &self,
696 exports: &[ExportSymbol],
697 file_id: FileId,
698 name: &str,
699 namespace: ExportNamespace,
700 export_index: usize,
701 ) -> bool {
702 let Some(export) = exports.get(export_index) else {
703 return false;
704 };
705 if !export.name.matches_str(name) {
706 return false;
707 }
708 let EffectiveExportResolution::Unique(binding) = self.resolve(file_id, name, namespace)
709 else {
710 return false;
711 };
712 if binding.origin_file() != file_id {
713 return true;
714 }
715 let Some(origin_slot) = binding.origin_slot() else {
716 return true;
717 };
718 if namespace == ExportNamespace::Type {
719 let group = self.declaration_group_slots(binding);
720 if !group.is_empty() {
721 return group.contains(&export_index);
722 }
723 }
724 exports
725 .get(origin_slot)
726 .is_some_and(|origin| export.is_type_only == origin.is_type_only)
727 }
728
729 pub(super) fn declaration_slots(
730 &self,
731 exports: &[ExportSymbol],
732 candidates: &[usize],
733 file_id: FileId,
734 name: &str,
735 namespace: ExportNamespace,
736 ) -> Vec<usize> {
737 let mut slots = Vec::new();
738 self.extend_declaration_slots(
739 DeclarationSlotQuery {
740 exports,
741 candidates,
742 file_id,
743 name,
744 namespace,
745 },
746 &mut slots,
747 );
748 slots
749 }
750
751 pub(super) fn extend_declaration_slots(
753 &self,
754 query: DeclarationSlotQuery<'_>,
755 slots: &mut Vec<usize>,
756 ) {
757 let DeclarationSlotQuery {
758 exports,
759 candidates,
760 file_id,
761 name,
762 namespace,
763 } = query;
764 let EffectiveExportResolution::Unique(binding) = self.resolve(file_id, name, namespace)
765 else {
766 return;
767 };
768 if binding.origin_file() == file_id && binding.origin_slot().is_some() {
769 slots.extend(candidates.iter().copied().filter(|&index| {
770 self.is_declaration_slot(exports, file_id, name, namespace, index)
771 }));
772 return;
773 }
774
775 let exact_type_only = namespace == ExportNamespace::Type
776 && candidates.iter().any(|&index| exports[index].is_type_only);
777 slots.extend(candidates.iter().copied().filter(|&index| {
778 exports[index].name.matches_str(name) && exports[index].is_type_only == exact_type_only
779 }));
780 }
781}
782
783#[derive(Clone, Copy)]
785pub(in crate::graph) struct DeclarationSlotQuery<'a> {
786 pub(in crate::graph) exports: &'a [ExportSymbol],
787 pub(in crate::graph) candidates: &'a [usize],
788 pub(in crate::graph) file_id: FileId,
789 pub(in crate::graph) name: &'a str,
790 pub(in crate::graph) namespace: ExportNamespace,
791}
792
793fn index_names_by_file(
794 resolutions: &FxHashMap<ExportKey, NamespaceResolutions>,
795) -> FxHashMap<FileId, Box<[ExportNameId]>> {
796 let mut names_by_file: FxHashMap<FileId, Vec<ExportNameId>> = FxHashMap::default();
797 for key in resolutions.keys() {
798 names_by_file.entry(key.file_id).or_default().push(key.name);
799 }
800 names_by_file
801 .into_iter()
802 .map(|(file_id, mut names)| {
803 names.sort_unstable_by_key(|name| name.0);
804 (file_id, names.into_boxed_slice())
805 })
806 .collect()
807}
808
809fn collect_declaration_merge_groups(modules: &[ResolvedModule]) -> DeclarationMergeGroups {
810 let mut collected = DeclarationMergeGroups::default();
811 for module in modules {
812 let merge_facts: Vec<_> = module
813 .semantic_facts
814 .iter()
815 .filter_map(|fact| match fact {
816 fallow_types::extract::SemanticFact::DeclarationMerge(group) => Some(group),
817 _ => None,
818 })
819 .collect();
820 if merge_facts.is_empty() {
821 continue;
822 }
823 let slot_by_span: FxHashMap<_, _> = module
824 .exports
825 .iter()
826 .enumerate()
827 .map(|(slot, export)| ((export.span.start, export.span.end), slot))
828 .collect();
829 for group in merge_facts {
830 let mut slots: Vec<_> = group
831 .export_spans
832 .iter()
833 .filter_map(|span| slot_by_span.get(span).copied())
834 .collect();
835 slots.sort_unstable();
836 slots.dedup();
837 if slots.len() < 2 {
838 continue;
839 }
840 let group_id = collected.groups.len();
841 for &slot in &slots {
842 collected
843 .group_by_slot
844 .insert((module.file_id, slot), group_id);
845 }
846 collected.groups.push(slots.into_boxed_slice());
847 }
848 }
849 collected
850}
851
852fn modules_needing_type_fallback(modules: &[ResolvedModule]) -> FxHashSet<FileId> {
861 let mut needing = FxHashSet::default();
862 let mut pending: Vec<FileId> = modules
863 .iter()
864 .filter(|module| {
865 module
866 .re_exports
867 .iter()
868 .any(|re_export| re_export.info.is_type_only && reads_source_lanes(&re_export.info))
869 })
870 .map(|module| module.file_id)
871 .collect();
872 if pending.is_empty() {
873 return needing;
874 }
875
876 let mut edges = ReExportEdges::collect(modules);
877 let mut fed_by_type_only = FxHashSet::default();
878 while let Some(file_id) = pending.pop() {
879 if !fed_by_type_only.insert(file_id) {
880 continue;
881 }
882 pending.extend(edges.barrels_by_source.remove(&file_id).unwrap_or_default());
883 }
884
885 let mut pending: Vec<FileId> = fed_by_type_only.into_iter().collect();
886 while let Some(file_id) = pending.pop() {
887 if !needing.insert(file_id) {
888 continue;
889 }
890 if let Some(sources) = edges.sources_by_barrel.get(&file_id) {
891 pending.extend(sources.iter().copied());
892 }
893 }
894 needing
895}
896
897struct ReExportEdges {
899 sources_by_barrel: FxHashMap<FileId, Vec<FileId>>,
900 barrels_by_source: FxHashMap<FileId, Vec<FileId>>,
901}
902
903impl ReExportEdges {
904 fn collect(modules: &[ResolvedModule]) -> Self {
905 let mut edges = Self {
906 sources_by_barrel: FxHashMap::default(),
907 barrels_by_source: FxHashMap::default(),
908 };
909 for module in modules {
910 for re_export in &module.re_exports {
911 if !reads_source_lanes(&re_export.info) {
912 continue;
913 }
914 let Some(source) = re_export.target.internal_file_id() else {
915 continue;
916 };
917 edges
918 .sources_by_barrel
919 .entry(module.file_id)
920 .or_default()
921 .push(source);
922 edges
923 .barrels_by_source
924 .entry(source)
925 .or_default()
926 .push(module.file_id);
927 }
928 }
929 edges
930 }
931}
932
933fn reads_source_lanes(info: &fallow_types::extract::ReExportInfo) -> bool {
937 info.imported_name != "*" || info.exported_name == "*"
938}
939
940fn seed_direct_bindings(
941 modules: &[ResolvedModule],
942 type_fallback_modules: &FxHashSet<FileId>,
943 interner: &mut ExportNameInterner,
944 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
945 queue: &mut VecDeque<ExportLookup>,
946) {
947 for module in modules {
948 let seed_type_fallback = type_fallback_modules.contains(&module.file_id);
949 for (slot, export) in module.exports.iter().enumerate() {
950 let lane = if export.is_type_only {
951 ResolutionLane::Type
952 } else {
953 ResolutionLane::Value
954 };
955 let name = interner.intern_export_name(&export.name);
956 let key = ExportLookup::new(module.file_id, name, lane);
957 let binding = EffectiveExportResolution::Unique(EffectiveExportBinding {
958 file_id: module.file_id,
959 kind: EffectiveExportBindingKind::Declaration(slot),
960 });
961 let entry = resolutions.entry(key.key).or_default();
962 if entry.seeded.claim_direct(lane) && entry.merge(lane, binding) {
967 queue.push_back(key);
968 }
969 if lane == ResolutionLane::Value {
970 entry.seeded.reserve_type_for_value_declaration();
974 if seed_type_fallback
975 && entry.seeded.claim_direct(ResolutionLane::TypeFallback)
976 && entry.merge(ResolutionLane::TypeFallback, binding)
977 {
978 queue.push_back(ExportLookup::new(
979 module.file_id,
980 name,
981 ResolutionLane::TypeFallback,
982 ));
983 }
984 }
985 }
986 seed_implicit_sfc_default(module, interner, resolutions, queue);
987 }
988}
989
990fn seed_implicit_sfc_default(
991 module: &ResolvedModule,
992 interner: &mut ExportNameInterner,
993 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
994 queue: &mut VecDeque<ExportLookup>,
995) {
996 if !is_sfc_path(&module.path) {
997 return;
998 }
999 let name = interner.intern("default");
1000 let key = ExportKey::new(module.file_id, name);
1001 let entry = resolutions.entry(key).or_default();
1002 if entry.seeded.is_direct(ResolutionLane::Value) {
1003 return;
1004 }
1005 let binding = EffectiveExportResolution::Unique(EffectiveExportBinding {
1006 file_id: module.file_id,
1007 kind: EffectiveExportBindingKind::ImplicitDefault,
1008 });
1009 for lane in [ResolutionLane::Value, ResolutionLane::Type] {
1010 entry.seeded.claim_direct(lane);
1011 if entry.merge(lane, binding) {
1012 queue.push_back(ExportLookup::new(module.file_id, name, lane));
1013 }
1014 }
1015}
1016
1017fn is_sfc_path(path: &std::path::Path) -> bool {
1018 matches!(
1019 path.extension().and_then(std::ffi::OsStr::to_str),
1020 Some("vue" | "svelte" | "astro")
1021 )
1022}
1023
1024fn collect_observers(
1025 modules: &[ResolvedModule],
1026 type_fallback_modules: &FxHashSet<FileId>,
1027 interner: &mut ExportNameInterner,
1028 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
1029 queue: &mut VecDeque<ExportLookup>,
1030) -> PropagationObservers {
1031 let mut observers = PropagationObservers {
1032 named: FxHashMap::default(),
1033 star: FxHashMap::default(),
1034 };
1035 for module in modules {
1036 let type_fallback = type_fallback_modules.contains(&module.file_id);
1037 for (re_export_index, re_export) in module.re_exports.iter().enumerate() {
1038 let Some(source) = re_export.target.internal_file_id() else {
1039 if re_export.info.exported_name != "*" {
1040 register_opaque_re_export(
1041 module.file_id,
1042 re_export_index,
1043 &re_export.info,
1044 interner,
1045 resolutions,
1046 queue,
1047 );
1048 }
1049 continue;
1050 };
1051 if re_export.info.exported_name == "*" {
1052 observers
1053 .star
1054 .entry(source)
1055 .or_default()
1056 .push(StarObserver {
1057 barrel: module.file_id,
1058 type_only: re_export.info.is_type_only,
1059 type_fallback,
1060 });
1061 continue;
1062 }
1063 register_named_observer(
1064 module.file_id,
1065 source,
1066 &re_export.info,
1067 ObserverBuildState {
1068 type_fallback,
1069 interner,
1070 resolutions,
1071 queue,
1072 observers: &mut observers,
1073 },
1074 );
1075 }
1076 }
1077 observers
1078}
1079
1080fn register_opaque_re_export(
1089 barrel: FileId,
1090 re_export_index: usize,
1091 info: &fallow_types::extract::ReExportInfo,
1092 interner: &mut ExportNameInterner,
1093 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
1094 queue: &mut VecDeque<ExportLookup>,
1095) {
1096 let exported_name = interner.intern(&info.exported_name);
1097 let binding = EffectiveExportResolution::Unique(EffectiveExportBinding {
1098 file_id: barrel,
1099 kind: EffectiveExportBindingKind::ExternalReExport(re_export_index),
1100 });
1101 let lanes: &[ResolutionLane] = if info.is_type_only {
1102 &[ResolutionLane::Type]
1103 } else {
1104 &[ResolutionLane::Type, ResolutionLane::Value]
1105 };
1106 for &lane in lanes {
1107 let destination = ExportLookup::new(barrel, exported_name, lane);
1108 let entry = resolutions.entry(destination.key).or_default();
1109 if entry.seeded.is_direct(lane) {
1110 continue;
1111 }
1112 entry.seeded.claim_explicit(lane);
1113 if entry.merge(lane, binding) {
1114 queue.push_back(destination);
1115 }
1116 }
1117}
1118
1119fn register_named_observer(
1120 barrel: FileId,
1121 source: FileId,
1122 info: &fallow_types::extract::ReExportInfo,
1123 state: ObserverBuildState<'_>,
1124) {
1125 let ObserverBuildState {
1126 type_fallback,
1127 interner,
1128 resolutions,
1129 queue,
1130 observers,
1131 } = state;
1132 let exported_name = interner.intern(&info.exported_name);
1133 if info.imported_name == "*" {
1134 let lanes: &[ResolutionLane] = if info.is_type_only {
1135 &[ResolutionLane::Type]
1136 } else {
1137 &[ResolutionLane::Type, ResolutionLane::Value]
1138 };
1139 let binding = EffectiveExportResolution::Unique(EffectiveExportBinding {
1140 file_id: barrel,
1141 kind: EffectiveExportBindingKind::NamespaceObject { source },
1142 });
1143 for &lane in lanes {
1144 let destination = ExportLookup::new(barrel, exported_name, lane);
1145 let entry = resolutions.entry(destination.key).or_default();
1146 if entry.seeded.is_direct(lane) {
1147 continue;
1148 }
1149 entry.seeded.claim_explicit(lane);
1150 if entry.merge(lane, binding) {
1151 queue.push_back(destination);
1152 }
1153 }
1154 return;
1155 }
1156
1157 let imported_name = interner.intern(&info.imported_name);
1158 let lanes: &[ResolutionLane] = match (info.is_type_only, type_fallback) {
1159 (true, _) => &[ResolutionLane::Type, ResolutionLane::TypeFallback],
1160 (false, true) => &[
1161 ResolutionLane::Type,
1162 ResolutionLane::TypeFallback,
1163 ResolutionLane::Value,
1164 ],
1165 (false, false) => &[ResolutionLane::Type, ResolutionLane::Value],
1166 };
1167 for &lane in lanes {
1168 let destination = ExportLookup::new(barrel, exported_name, lane);
1169 let entry = resolutions.entry(destination.key).or_default();
1170 if entry.seeded.is_direct(lane) {
1171 continue;
1172 }
1173 entry.seeded.claim_explicit(lane);
1174 observers
1175 .named
1176 .entry(ExportLookup::new(source, imported_name, lane))
1177 .and_modify(|destinations| destinations.push(destination))
1178 .or_insert(NamedDestinations::One(destination));
1179 }
1180}
1181
1182fn propagate_bindings(
1183 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
1184 queue: &mut VecDeque<ExportLookup>,
1185 observers: &PropagationObservers,
1186) {
1187 while let Some(source_key) = queue.pop_front() {
1188 let Some(source_resolution) = resolutions
1189 .get(&source_key.key)
1190 .map(|resolutions| resolutions.get(source_key.lane))
1191 else {
1192 continue;
1193 };
1194 if let Some(destinations) = observers.named.get(&source_key) {
1195 for destination in destinations.iter() {
1196 merge_resolution(resolutions, queue, *destination, source_resolution);
1197 }
1198 }
1199 propagate_star_binding(
1200 resolutions,
1201 queue,
1202 observers,
1203 &source_key,
1204 source_resolution,
1205 );
1206 }
1207}
1208
1209fn propagate_star_binding(
1210 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
1211 queue: &mut VecDeque<ExportLookup>,
1212 observers: &PropagationObservers,
1213 source_key: &ExportLookup,
1214 source_resolution: EffectiveExportResolution,
1215) {
1216 if source_key.key.name == ExportNameId::DEFAULT {
1217 return;
1218 }
1219 let Some(star_observers) = observers.star.get(&source_key.key.file_id) else {
1220 return;
1221 };
1222 for observer in star_observers {
1223 if observer.type_only && source_key.lane == ResolutionLane::Value {
1224 continue;
1225 }
1226 if !observer.type_fallback && source_key.lane == ResolutionLane::TypeFallback {
1227 continue;
1228 }
1229 let destination = source_key.with_file(observer.barrel);
1230 let entry = resolutions.entry(destination.key).or_default();
1231 if entry.seeded.is_explicit(destination.lane) {
1232 continue;
1233 }
1234 if entry.merge(destination.lane, source_resolution) {
1235 queue.push_back(destination);
1236 }
1237 }
1238}
1239
1240fn merge_resolution(
1241 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
1242 queue: &mut VecDeque<ExportLookup>,
1243 key: ExportLookup,
1244 incoming: EffectiveExportResolution,
1245) {
1246 if resolutions
1247 .entry(key.key)
1248 .or_default()
1249 .merge(key.lane, incoming)
1250 {
1251 queue.push_back(key);
1252 }
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257 use super::*;
1258 use crate::resolve::{ResolveResult, ResolvedReExport};
1259 use fallow_types::extract::{ExportInfo, ReExportInfo, VisibilityTag};
1260
1261 fn value_export(name: &str) -> ExportInfo {
1262 ExportInfo {
1263 name: ExportName::Named(name.to_string()),
1264 local_name: Some(name.to_string()),
1265 is_type_only: false,
1266 visibility: VisibilityTag::None,
1267 expected_unused_reason: None,
1268 span: oxc_span::Span::default(),
1269 members: Vec::new(),
1270 is_side_effect_used: false,
1271 super_class: None,
1272 deprecated: false,
1273 deprecated_reason: None,
1274 }
1275 }
1276
1277 fn re_export(source: FileId, imported: &str, exported: &str) -> ResolvedReExport {
1278 ResolvedReExport {
1279 info: ReExportInfo {
1280 source: format!("./{}", source.0),
1281 imported_name: imported.to_string(),
1282 exported_name: exported.to_string(),
1283 is_type_only: false,
1284 span: oxc_span::Span::default(),
1285 statement_span: oxc_span::Span::default(),
1286 source_span: oxc_span::Span::default(),
1287 },
1288 target: ResolveResult::InternalModule(source),
1289 }
1290 }
1291
1292 fn module(
1293 file_id: u32,
1294 exports: Vec<ExportInfo>,
1295 re_exports: Vec<ResolvedReExport>,
1296 ) -> ResolvedModule {
1297 ResolvedModule {
1298 file_id: FileId(file_id),
1299 exports: exports.into(),
1300 re_exports,
1301 ..Default::default()
1302 }
1303 }
1304
1305 fn resolves_through(
1306 index: &EffectiveExportIndex,
1307 barrel: FileId,
1308 source: FileId,
1309 name: &str,
1310 ) -> bool {
1311 index.resolves_through(barrel, name, source, name, ExportNamespace::Value)
1312 }
1313
1314 fn external_re_export(imported: &str, exported: &str, type_only: bool) -> ResolvedReExport {
1315 ResolvedReExport {
1316 info: ReExportInfo {
1317 source: "node:path".to_string(),
1318 imported_name: imported.to_string(),
1319 exported_name: exported.to_string(),
1320 is_type_only: type_only,
1321 span: oxc_span::Span::default(),
1322 statement_span: oxc_span::Span::default(),
1323 source_span: oxc_span::Span::default(),
1324 },
1325 target: ResolveResult::NpmPackage("node:path".to_string()),
1326 }
1327 }
1328
1329 #[test]
1330 fn external_named_re_exports_keep_an_opaque_local_binding() {
1331 let index = EffectiveExportIndex::build(&[module(
1332 0,
1333 Vec::new(),
1334 vec![
1335 external_re_export("join", "join", false),
1336 external_re_export("Stats", "Stats", true),
1337 external_re_export("*", "path", false),
1338 ],
1339 )]);
1340 let encoded = postcard::to_allocvec(&index).expect("encode effective export index");
1341 let decoded: EffectiveExportIndex =
1342 postcard::from_bytes(&encoded).expect("decode effective export index");
1343
1344 let value = decoded.resolve(FileId(0), "join", ExportNamespace::Value);
1345 assert!(matches!(
1346 value,
1347 EffectiveExportResolution::Unique(binding)
1348 if binding.origin_file() == FileId(0) && binding.origin_slot().is_none()
1349 ));
1350 assert_eq!(
1351 value,
1352 decoded.resolve(FileId(0), "join", ExportNamespace::Type)
1353 );
1354 assert!(matches!(
1355 decoded.resolve(FileId(0), "Stats", ExportNamespace::Type),
1356 EffectiveExportResolution::Unique(_)
1357 ));
1358 assert_eq!(
1359 decoded.resolve(FileId(0), "Stats", ExportNamespace::Value),
1360 EffectiveExportResolution::Missing
1361 );
1362 assert_eq!(
1363 decoded.resolve(FileId(0), "path", ExportNamespace::Type),
1364 decoded.resolve(FileId(0), "path", ExportNamespace::Value)
1365 );
1366 }
1367
1368 #[test]
1369 fn unresolved_named_re_exports_keep_an_opaque_local_binding() {
1370 let mut unresolved = external_re_export("missing", "missing", false);
1371 unresolved.target = ResolveResult::Unresolvable("./missing".to_string());
1372 let mut unresolved_star = external_re_export("*", "*", false);
1373 unresolved_star.target = ResolveResult::Unresolvable("./missing-star".to_string());
1374 let index = EffectiveExportIndex::build(&[module(
1375 0,
1376 Vec::new(),
1377 vec![unresolved, unresolved_star],
1378 )]);
1379
1380 for namespace in [ExportNamespace::Type, ExportNamespace::Value] {
1381 assert!(matches!(
1382 index.resolve(FileId(0), "missing", namespace),
1383 EffectiveExportResolution::Unique(binding)
1384 if binding.origin_file() == FileId(0) && binding.origin_slot().is_none()
1385 ));
1386 assert_eq!(
1387 index.resolve(FileId(0), "other", namespace),
1388 EffectiveExportResolution::Missing,
1389 "an unresolved star re-export must not invent names"
1390 );
1391 }
1392 }
1393
1394 #[test]
1395 fn missing_export_is_explicit_in_the_resolution_contract() {
1396 let index = EffectiveExportIndex::build(&[module(0, Vec::new(), Vec::new())]);
1397
1398 assert_eq!(
1399 index.resolve(FileId(0), "missing", ExportNamespace::Value),
1400 EffectiveExportResolution::Missing
1401 );
1402 }
1403
1404 #[test]
1405 fn same_module_declaration_merges_keep_one_binding() {
1406 let index = EffectiveExportIndex::build(&[module(
1407 0,
1408 vec![value_export("Merged"), value_export("Merged")],
1409 Vec::new(),
1410 )]);
1411
1412 assert!(matches!(
1413 index.resolve(FileId(0), "Merged", ExportNamespace::Value),
1414 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(0)
1415 ));
1416 }
1417
1418 #[test]
1419 fn declaration_merge_groups_survive_graph_cache_roundtrip() {
1420 let mut interface = value_export("Merged");
1421 interface.is_type_only = true;
1422 interface.span = oxc_span::Span::new(0, 6);
1423 let mut namespace = value_export("Merged");
1424 namespace.span = oxc_span::Span::new(10, 16);
1425 let modules = vec![ResolvedModule {
1426 file_id: FileId(0),
1427 exports: vec![interface, namespace].into(),
1428 semantic_facts: vec![fallow_types::extract::SemanticFact::DeclarationMerge(
1429 fallow_types::extract::DeclarationMergeFact {
1430 export_spans: vec![(0, 6), (10, 16)],
1431 },
1432 )]
1433 .into(),
1434 ..Default::default()
1435 }];
1436 let index = EffectiveExportIndex::build(&modules);
1437 let encoded = postcard::to_allocvec(&index).expect("encode effective export index");
1438 let decoded: EffectiveExportIndex =
1439 postcard::from_bytes(&encoded).expect("decode effective export index");
1440 let EffectiveExportResolution::Unique(binding) =
1441 decoded.resolve(FileId(0), "Merged", ExportNamespace::Type)
1442 else {
1443 panic!("merged type binding must remain unique");
1444 };
1445
1446 assert_eq!(decoded.declaration_group_slots(binding), &[0, 1]);
1447 }
1448
1449 #[test]
1450 fn explicit_re_export_shadows_a_star_binding() {
1451 let modules = vec![
1452 module(
1453 0,
1454 Vec::new(),
1455 vec![
1456 re_export(FileId(1), "*", "*"),
1457 re_export(FileId(2), "foo", "foo"),
1458 ],
1459 ),
1460 module(1, vec![value_export("foo")], Vec::new()),
1461 module(2, vec![value_export("foo")], Vec::new()),
1462 ];
1463 let index = EffectiveExportIndex::build(&modules);
1464
1465 assert!(!resolves_through(&index, FileId(0), FileId(1), "foo"));
1466 assert!(resolves_through(&index, FileId(0), FileId(2), "foo"));
1467 assert!(!index.contributes_through(
1468 FileId(0),
1469 "foo",
1470 FileId(1),
1471 "foo",
1472 ExportNamespace::Value
1473 ));
1474 }
1475
1476 #[test]
1477 fn distinct_star_bindings_are_ambiguous() {
1478 let modules = vec![
1479 module(
1480 0,
1481 Vec::new(),
1482 vec![
1483 re_export(FileId(1), "*", "*"),
1484 re_export(FileId(2), "*", "*"),
1485 ],
1486 ),
1487 module(1, vec![value_export("foo")], Vec::new()),
1488 module(2, vec![value_export("foo")], Vec::new()),
1489 ];
1490 let index = EffectiveExportIndex::build(&modules);
1491
1492 assert!(!resolves_through(&index, FileId(0), FileId(1), "foo"));
1493 assert!(!resolves_through(&index, FileId(0), FileId(2), "foo"));
1494 assert!(index.contributes_through(
1495 FileId(0),
1496 "foo",
1497 FileId(1),
1498 "foo",
1499 ExportNamespace::Value
1500 ));
1501 assert!(index.contributes_through(
1502 FileId(0),
1503 "foo",
1504 FileId(2),
1505 "foo",
1506 ExportNamespace::Value
1507 ));
1508 }
1509
1510 #[test]
1511 fn convergent_star_paths_keep_one_binding() {
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::new(), vec![re_export(FileId(3), "*", "*")]),
1522 module(2, Vec::new(), vec![re_export(FileId(3), "*", "*")]),
1523 module(3, vec![value_export("foo")], Vec::new()),
1524 ];
1525 let index = EffectiveExportIndex::build(&modules);
1526
1527 assert!(resolves_through(&index, FileId(0), FileId(1), "foo"));
1528 assert!(resolves_through(&index, FileId(0), FileId(2), "foo"));
1529 }
1530
1531 #[test]
1532 fn a_real_type_declaration_wins_over_a_value_type_fallback() {
1533 let mut interface = value_export("User");
1534 interface.is_type_only = true;
1535 let modules = vec![
1536 module(
1537 0,
1538 Vec::new(),
1539 vec![
1540 re_export(FileId(1), "*", "*"),
1541 re_export(FileId(2), "*", "*"),
1542 ],
1543 ),
1544 module(1, vec![value_export("User")], Vec::new()),
1545 module(2, vec![interface], Vec::new()),
1546 ];
1547 let index = EffectiveExportIndex::build(&modules);
1548
1549 assert!(matches!(
1550 index.resolve(FileId(0), "User", ExportNamespace::Type),
1551 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(2)
1552 ));
1553 assert!(matches!(
1554 index.resolve(FileId(0), "User", ExportNamespace::Value),
1555 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(1)
1556 ));
1557 assert!(index.resolves_through(
1558 FileId(0),
1559 "User",
1560 FileId(2),
1561 "User",
1562 ExportNamespace::Type
1563 ));
1564 assert!(index.resolves_through(
1565 FileId(0),
1566 "User",
1567 FileId(1),
1568 "User",
1569 ExportNamespace::Value
1570 ));
1571 }
1572
1573 #[test]
1574 fn colliding_real_type_declarations_stay_ambiguous() {
1575 let type_export = |name: &str| {
1576 let mut export = value_export(name);
1577 export.is_type_only = true;
1578 export
1579 };
1580 let modules = vec![
1581 module(
1582 0,
1583 Vec::new(),
1584 vec![
1585 re_export(FileId(1), "*", "*"),
1586 re_export(FileId(2), "*", "*"),
1587 ],
1588 ),
1589 module(1, vec![type_export("User")], Vec::new()),
1590 module(2, vec![type_export("User")], Vec::new()),
1591 ];
1592 let index = EffectiveExportIndex::build(&modules);
1593
1594 assert_eq!(
1595 index.resolve(FileId(0), "User", ExportNamespace::Type),
1596 EffectiveExportResolution::Ambiguous
1597 );
1598 assert_eq!(
1599 index.ambiguous_names_on(FileId(0)),
1600 vec![(FileId(0), "User", ExportNamespace::Type)]
1601 );
1602 }
1603
1604 #[test]
1605 fn a_colliding_value_export_reports_one_collision_in_the_value_namespace() {
1606 let modules = vec![
1607 module(
1608 0,
1609 Vec::new(),
1610 vec![
1611 re_export(FileId(1), "*", "*"),
1612 re_export(FileId(2), "*", "*"),
1613 ],
1614 ),
1615 module(1, vec![value_export("foo")], Vec::new()),
1616 module(2, vec![value_export("foo")], Vec::new()),
1617 ];
1618 let index = EffectiveExportIndex::build(&modules);
1619
1620 assert_eq!(
1621 index.ambiguous_names(),
1622 vec![(FileId(0), "foo", ExportNamespace::Value)]
1623 );
1624 }
1625
1626 #[test]
1627 fn a_value_export_still_carries_its_type_meaning_through_a_barrel() {
1628 let modules = vec![
1629 module(0, Vec::new(), vec![re_export(FileId(1), "*", "*")]),
1630 module(1, vec![value_export("Widget")], Vec::new()),
1631 ];
1632 let index = EffectiveExportIndex::build(&modules);
1633
1634 assert!(index.resolves_through(
1635 FileId(0),
1636 "Widget",
1637 FileId(1),
1638 "Widget",
1639 ExportNamespace::Type
1640 ));
1641 }
1642
1643 #[test]
1644 fn a_type_only_re_export_carries_a_value_declaration_as_a_type_only() {
1645 let mut type_only = re_export(FileId(1), "Widget", "Widget");
1646 type_only.info.is_type_only = true;
1647 let index = EffectiveExportIndex::build(&[
1648 module(0, Vec::new(), vec![type_only]),
1649 module(1, vec![value_export("Widget")], Vec::new()),
1650 ]);
1651
1652 assert!(matches!(
1653 index.resolve(FileId(0), "Widget", ExportNamespace::Type),
1654 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(1)
1655 ));
1656 assert_eq!(
1657 index.resolve(FileId(0), "Widget", ExportNamespace::Value),
1658 EffectiveExportResolution::Missing
1659 );
1660 }
1661
1662 #[test]
1663 fn a_type_only_re_export_reaches_a_value_declaration_behind_a_barrel() {
1664 let mut type_only = re_export(FileId(1), "Widget", "Widget");
1665 type_only.info.is_type_only = true;
1666 let index = EffectiveExportIndex::build(&[
1667 module(0, Vec::new(), vec![type_only]),
1668 module(1, Vec::new(), vec![re_export(FileId(2), "*", "*")]),
1669 module(2, vec![value_export("Widget")], Vec::new()),
1670 ]);
1671
1672 assert!(matches!(
1673 index.resolve(FileId(0), "Widget", ExportNamespace::Type),
1674 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(2)
1675 ));
1676 }
1677
1678 #[test]
1679 fn a_type_only_re_export_shadows_a_star_binding_in_the_type_namespace() {
1680 let mut type_only = re_export(FileId(1), "foo", "foo");
1681 type_only.info.is_type_only = true;
1682 let index = EffectiveExportIndex::build(&[
1683 module(
1684 0,
1685 Vec::new(),
1686 vec![type_only, re_export(FileId(2), "*", "*")],
1687 ),
1688 module(1, vec![value_export("foo")], Vec::new()),
1689 module(2, vec![value_export("foo")], Vec::new()),
1690 ]);
1691
1692 assert!(matches!(
1693 index.resolve(FileId(0), "foo", ExportNamespace::Type),
1694 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(1)
1695 ));
1696 assert!(matches!(
1697 index.resolve(FileId(0), "foo", ExportNamespace::Value),
1698 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(2)
1699 ));
1700 }
1701
1702 #[test]
1703 fn a_type_only_barrel_still_sees_a_star_collision_as_ambiguous() {
1704 let mut type_only = re_export(FileId(1), "foo", "foo");
1705 type_only.info.is_type_only = true;
1706 let index = EffectiveExportIndex::build(&[
1707 module(0, Vec::new(), vec![type_only]),
1708 module(
1709 1,
1710 Vec::new(),
1711 vec![
1712 re_export(FileId(2), "*", "*"),
1713 re_export(FileId(3), "*", "*"),
1714 ],
1715 ),
1716 module(2, vec![value_export("foo")], Vec::new()),
1717 module(3, vec![value_export("foo")], Vec::new()),
1718 ]);
1719
1720 assert_eq!(
1721 index.resolve(FileId(0), "foo", ExportNamespace::Type),
1722 EffectiveExportResolution::Ambiguous
1723 );
1724 }
1725
1726 #[test]
1727 fn a_star_collision_stays_ambiguous_beside_an_unrelated_type_only_barrel() {
1728 let mut type_only = re_export(FileId(1), "foo", "foo");
1729 type_only.info.is_type_only = true;
1730 let index = EffectiveExportIndex::build(&[
1731 module(0, Vec::new(), vec![type_only]),
1732 module(1, Vec::new(), vec![re_export(FileId(2), "*", "*")]),
1733 module(2, vec![value_export("foo")], Vec::new()),
1734 module(
1735 3,
1736 Vec::new(),
1737 vec![
1738 re_export(FileId(2), "*", "*"),
1739 re_export(FileId(4), "*", "*"),
1740 ],
1741 ),
1742 module(4, vec![value_export("foo")], Vec::new()),
1743 ]);
1744
1745 assert_eq!(
1746 index.resolve(FileId(3), "foo", ExportNamespace::Type),
1747 EffectiveExportResolution::Ambiguous
1748 );
1749 }
1750
1751 #[test]
1752 fn star_exports_exclude_default_bindings() {
1753 let mut default = value_export("local");
1754 default.name = ExportName::Default;
1755 let index = EffectiveExportIndex::build(&[
1756 module(0, Vec::new(), vec![re_export(FileId(1), "*", "*")]),
1757 module(1, vec![default], Vec::new()),
1758 ]);
1759
1760 assert_eq!(
1761 index.resolve(FileId(0), "default", ExportNamespace::Value),
1762 EffectiveExportResolution::Missing
1763 );
1764 }
1765
1766 #[test]
1767 fn star_cycles_converge_on_the_same_binding() {
1768 let index = EffectiveExportIndex::build(&[
1769 module(0, Vec::new(), vec![re_export(FileId(1), "*", "*")]),
1770 module(
1771 1,
1772 Vec::new(),
1773 vec![
1774 re_export(FileId(0), "*", "*"),
1775 re_export(FileId(2), "*", "*"),
1776 ],
1777 ),
1778 module(2, vec![value_export("foo")], Vec::new()),
1779 ]);
1780
1781 assert!(resolves_through(&index, FileId(0), FileId(2), "foo"));
1782 assert!(resolves_through(&index, FileId(1), FileId(2), "foo"));
1783 }
1784
1785 #[test]
1786 fn type_only_namespace_re_export_resolves_only_in_type_namespace() {
1787 let mut namespace = re_export(FileId(1), "*", "Types");
1788 namespace.info.is_type_only = true;
1789 let index = EffectiveExportIndex::build(&[
1790 module(0, Vec::new(), vec![namespace]),
1791 module(1, vec![value_export("foo")], Vec::new()),
1792 ]);
1793
1794 assert!(matches!(
1795 index.resolve(FileId(0), "Types", ExportNamespace::Type),
1796 EffectiveExportResolution::Unique(_)
1797 ));
1798 assert_eq!(
1799 index.resolve(FileId(0), "Types", ExportNamespace::Value),
1800 EffectiveExportResolution::Missing
1801 );
1802 }
1803
1804 #[test]
1805 fn normal_namespace_re_export_resolves_in_both_namespaces() {
1806 let index = EffectiveExportIndex::build(&[
1807 module(0, Vec::new(), vec![re_export(FileId(1), "*", "Namespace")]),
1808 module(1, vec![value_export("foo")], Vec::new()),
1809 ]);
1810
1811 let type_binding = index.resolve(FileId(0), "Namespace", ExportNamespace::Type);
1812 let value_binding = index.resolve(FileId(0), "Namespace", ExportNamespace::Value);
1813 assert!(matches!(type_binding, EffectiveExportResolution::Unique(_)));
1814 assert_eq!(type_binding, value_binding);
1815 }
1816
1817 #[test]
1818 fn persisted_index_remains_queryable_without_reconstruction() {
1819 let index = EffectiveExportIndex::build(&[
1820 module(0, Vec::new(), vec![re_export(FileId(1), "foo", "bar")]),
1821 module(1, vec![value_export("foo")], Vec::new()),
1822 ]);
1823 let encoded = postcard::to_allocvec(&index).expect("encode effective export index");
1824 let decoded: EffectiveExportIndex =
1825 postcard::from_bytes(&encoded).expect("decode effective export index");
1826
1827 assert!(matches!(
1828 decoded.resolve(FileId(0), "bar", ExportNamespace::Value),
1829 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(1)
1830 ));
1831 assert_eq!(
1832 decoded.resolve(FileId(0), "missing", ExportNamespace::Value),
1833 EffectiveExportResolution::Missing
1834 );
1835 }
1836
1837 #[test]
1838 fn sfc_file_has_an_implicit_default_value_binding() {
1839 let mut sfc = module(0, Vec::new(), Vec::new());
1840 sfc.path = std::path::PathBuf::from("/project/Widget.vue");
1841 let index = EffectiveExportIndex::build(&[sfc]);
1842
1843 assert!(matches!(
1844 index.resolve(FileId(0), "default", ExportNamespace::Value),
1845 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(0)
1846 ));
1847 }
1848}