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)]
166enum ResolutionLane {
167 Type,
168 TypeFallback,
169 Value,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
173struct ExportLookup {
174 key: ExportKey,
175 lane: ResolutionLane,
176}
177
178impl ExportLookup {
179 const fn new(file_id: FileId, name: ExportNameId, lane: ResolutionLane) -> Self {
180 Self {
181 key: ExportKey::new(file_id, name),
182 lane,
183 }
184 }
185
186 const fn with_file(self, file_id: FileId) -> Self {
187 Self {
188 key: self.key.with_file(file_id),
189 ..self
190 }
191 }
192}
193
194#[derive(Debug, Clone, Copy, Default)]
202struct SeededLanes(u8);
203
204impl SeededLanes {
205 const fn bit(lane: ResolutionLane) -> u8 {
206 match lane {
207 ResolutionLane::Type => 1,
208 ResolutionLane::TypeFallback => 2,
209 ResolutionLane::Value => 4,
210 }
211 }
212
213 const fn is_direct(self, lane: ResolutionLane) -> bool {
214 self.0 & Self::bit(lane) != 0
215 }
216
217 const fn claim_direct(&mut self, lane: ResolutionLane) -> bool {
219 let bit = Self::bit(lane);
220 let claimed = self.0 & bit == 0;
221 self.0 |= bit;
222 claimed
223 }
224
225 const fn is_explicit(self, lane: ResolutionLane) -> bool {
226 self.0 & (Self::bit(lane) | (Self::bit(lane) << 3)) != 0
227 }
228
229 const fn claim_explicit(&mut self, lane: ResolutionLane) {
230 self.0 |= Self::bit(lane) << 3;
231 }
232}
233
234#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
235struct NamespaceResolutions {
236 r#type: EffectiveExportResolution,
237 type_fallback: EffectiveExportResolution,
238 value: EffectiveExportResolution,
239 #[serde(skip)]
240 seeded: SeededLanes,
241}
242
243impl NamespaceResolutions {
244 fn merge(&mut self, lane: ResolutionLane, incoming: EffectiveExportResolution) -> bool {
246 let current = self.get(lane);
247 let next = current.merged_with(incoming);
248 if current == next {
249 return false;
250 }
251 self.set(lane, next);
252 true
253 }
254
255 const fn get(self, lane: ResolutionLane) -> EffectiveExportResolution {
256 match lane {
257 ResolutionLane::Type => self.r#type,
258 ResolutionLane::TypeFallback => self.type_fallback,
259 ResolutionLane::Value => self.value,
260 }
261 }
262
263 const fn set(&mut self, lane: ResolutionLane, resolution: EffectiveExportResolution) {
264 match lane {
265 ResolutionLane::Type => self.r#type = resolution,
266 ResolutionLane::TypeFallback => self.type_fallback = resolution,
267 ResolutionLane::Value => self.value = resolution,
268 }
269 }
270
271 const fn effective(self, namespace: ExportNamespace) -> EffectiveExportResolution {
272 match namespace {
273 ExportNamespace::Value => self.value,
274 ExportNamespace::Type => match self.r#type {
275 EffectiveExportResolution::Missing => self.type_fallback,
276 resolution => resolution,
277 },
278 }
279 }
280}
281
282#[derive(Clone, Copy)]
288struct ProjectExportSizes {
289 keys: usize,
291 lane_keys: usize,
293 names: usize,
295}
296
297impl ProjectExportSizes {
298 fn measure(modules: &[ResolvedModule]) -> Self {
299 let mut keys = 0;
300 let mut lane_keys = 0;
301 let mut names = 0;
302 for module in modules {
303 let value_exports = module
304 .exports
305 .iter()
306 .filter(|export| !export.is_type_only)
307 .count();
308 keys += module.exports.len() + module.re_exports.len();
309 lane_keys += module.exports.len() + value_exports * 2 + module.re_exports.len() * 3;
311 names += module.exports.len() + module.re_exports.len() * 2;
312 }
313 Self {
314 keys,
315 lane_keys,
316 names,
317 }
318 }
319}
320
321struct ExportNameInterner {
322 ids: FxHashMap<Box<str>, ExportNameId>,
323 next_id: usize,
324}
325
326impl ExportNameInterner {
327 fn with_capacity(capacity: usize) -> Self {
328 let mut ids = FxHashMap::with_capacity_and_hasher(capacity + 1, FxBuildHasher);
329 ids.insert(Box::<str>::from("default"), ExportNameId::DEFAULT);
330 Self { ids, next_id: 1 }
331 }
332
333 fn intern_export_name(&mut self, name: &ExportName) -> ExportNameId {
334 match name {
335 ExportName::Default => ExportNameId::DEFAULT,
336 ExportName::Named(name) => self.intern(name),
337 }
338 }
339
340 fn intern(&mut self, name: &str) -> ExportNameId {
341 if let Some(id) = self.ids.get(name) {
342 return *id;
343 }
344 let id = ExportNameId(self.next_id);
345 self.next_id += 1;
346 self.ids.insert(Box::from(name), id);
347 id
348 }
349}
350
351#[derive(Clone, Copy)]
352struct StarObserver {
353 barrel: FileId,
354 type_only: bool,
355}
356
357enum NamedDestinations {
363 One(ExportLookup),
364 Many(Vec<ExportLookup>),
365}
366
367impl NamedDestinations {
368 fn push(&mut self, destination: ExportLookup) {
369 match self {
370 Self::One(first) => *self = Self::Many(vec![*first, destination]),
371 Self::Many(destinations) => destinations.push(destination),
372 }
373 }
374
375 fn iter(&self) -> std::slice::Iter<'_, ExportLookup> {
376 match self {
377 Self::One(destination) => std::slice::from_ref(destination).iter(),
378 Self::Many(destinations) => destinations.iter(),
379 }
380 }
381}
382
383struct PropagationObservers {
384 named: FxHashMap<ExportLookup, NamedDestinations>,
385 star: FxHashMap<FileId, Vec<StarObserver>>,
386}
387
388struct ObserverBuildState<'a> {
389 interner: &'a mut ExportNameInterner,
390 resolutions: &'a mut FxHashMap<ExportKey, NamespaceResolutions>,
391 queue: &'a mut VecDeque<ExportLookup>,
392 observers: &'a mut PropagationObservers,
393}
394
395#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
401pub(super) struct EffectiveExportIndex {
402 name_ids: FxHashMap<Box<str>, ExportNameId>,
403 resolutions: FxHashMap<ExportKey, NamespaceResolutions>,
404 names_by_file: FxHashMap<FileId, Box<[ExportNameId]>>,
407 declaration_merge_groups: DeclarationMergeGroups,
408}
409
410#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
411struct DeclarationMergeGroups {
412 groups: Vec<Box<[usize]>>,
413 group_by_slot: FxHashMap<(FileId, usize), usize>,
414}
415
416impl EffectiveExportIndex {
417 pub(super) fn build(modules: &[ResolvedModule]) -> Self {
418 let sizes = ProjectExportSizes::measure(modules);
419 let mut interner = ExportNameInterner::with_capacity(sizes.names);
420 let mut resolutions = FxHashMap::with_capacity_and_hasher(sizes.keys, FxBuildHasher);
421 let mut queue = VecDeque::with_capacity(sizes.lane_keys);
422 seed_direct_bindings(modules, &mut interner, &mut resolutions, &mut queue);
423 let observers = collect_observers(modules, &mut interner, &mut resolutions, &mut queue);
424 propagate_bindings(&mut resolutions, &mut queue, &observers);
425
426 Self {
427 name_ids: interner.ids,
428 names_by_file: index_names_by_file(&resolutions),
429 resolutions,
430 declaration_merge_groups: collect_declaration_merge_groups(modules),
431 }
432 }
433
434 pub(super) fn resolve(
435 &self,
436 file_id: FileId,
437 name: &str,
438 namespace: ExportNamespace,
439 ) -> EffectiveExportResolution {
440 let Some(name) = self.name_ids.get(name) else {
441 return EffectiveExportResolution::Missing;
442 };
443 self.resolutions
444 .get(&ExportKey::new(file_id, *name))
445 .map_or(EffectiveExportResolution::Missing, |resolutions| {
446 resolutions.effective(namespace)
447 })
448 }
449
450 pub(super) fn resolves_through(
451 &self,
452 barrel: FileId,
453 barrel_name: &str,
454 source: FileId,
455 source_name: &str,
456 namespace: ExportNamespace,
457 ) -> bool {
458 matches!(
459 (
460 self.resolve(barrel, barrel_name, namespace),
461 self.resolve(source, source_name, namespace),
462 ),
463 (
464 EffectiveExportResolution::Unique(barrel_binding),
465 EffectiveExportResolution::Unique(source_binding),
466 ) if barrel_binding == source_binding
467 )
468 }
469
470 pub(super) fn contributes_through(
471 &self,
472 barrel: FileId,
473 barrel_name: &str,
474 source: FileId,
475 source_name: &str,
476 namespace: ExportNamespace,
477 ) -> bool {
478 match (
479 self.resolve(barrel, barrel_name, namespace),
480 self.resolve(source, source_name, namespace),
481 ) {
482 (
483 EffectiveExportResolution::Unique(barrel_binding),
484 EffectiveExportResolution::Unique(source_binding),
485 ) => barrel_binding == source_binding,
486 (
487 EffectiveExportResolution::Ambiguous,
488 EffectiveExportResolution::Unique(_) | EffectiveExportResolution::Ambiguous,
489 ) => true,
490 _ => false,
491 }
492 }
493
494 pub(super) fn unique_bindings(
495 &self,
496 file_id: FileId,
497 namespace: ExportNamespace,
498 ) -> FxHashSet<EffectiveExportBinding> {
499 let Some(names) = self.names_by_file.get(&file_id) else {
500 return FxHashSet::default();
501 };
502 names
503 .iter()
504 .filter_map(|name| {
505 match self
506 .resolutions
507 .get(&ExportKey::new(file_id, *name))?
508 .effective(namespace)
509 {
510 EffectiveExportResolution::Unique(binding) => Some(binding),
511 EffectiveExportResolution::Missing | EffectiveExportResolution::Ambiguous => {
512 None
513 }
514 }
515 })
516 .collect()
517 }
518
519 pub(super) fn declaration_group_slots(&self, binding: EffectiveExportBinding) -> &[usize] {
520 binding
521 .origin_slot()
522 .and_then(|slot| {
523 self.declaration_merge_groups
524 .group_by_slot
525 .get(&(binding.origin_file(), slot))
526 })
527 .and_then(|group| self.declaration_merge_groups.groups.get(*group))
528 .map_or(&[], Box::as_ref)
529 }
530
531 pub(super) fn is_declaration_slot(
532 &self,
533 exports: &[ExportSymbol],
534 file_id: FileId,
535 name: &str,
536 namespace: ExportNamespace,
537 export_index: usize,
538 ) -> bool {
539 let Some(export) = exports.get(export_index) else {
540 return false;
541 };
542 if !export.name.matches_str(name) {
543 return false;
544 }
545 let EffectiveExportResolution::Unique(binding) = self.resolve(file_id, name, namespace)
546 else {
547 return false;
548 };
549 if binding.origin_file() != file_id {
550 return true;
551 }
552 let Some(origin_slot) = binding.origin_slot() else {
553 return true;
554 };
555 if namespace == ExportNamespace::Type {
556 let group = self.declaration_group_slots(binding);
557 if !group.is_empty() {
558 return group.contains(&export_index);
559 }
560 }
561 exports
562 .get(origin_slot)
563 .is_some_and(|origin| export.is_type_only == origin.is_type_only)
564 }
565
566 pub(super) fn declaration_slots(
567 &self,
568 exports: &[ExportSymbol],
569 candidates: &[usize],
570 file_id: FileId,
571 name: &str,
572 namespace: ExportNamespace,
573 ) -> Vec<usize> {
574 let mut slots = Vec::new();
575 self.extend_declaration_slots(
576 DeclarationSlotQuery {
577 exports,
578 candidates,
579 file_id,
580 name,
581 namespace,
582 },
583 &mut slots,
584 );
585 slots
586 }
587
588 pub(super) fn extend_declaration_slots(
590 &self,
591 query: DeclarationSlotQuery<'_>,
592 slots: &mut Vec<usize>,
593 ) {
594 let DeclarationSlotQuery {
595 exports,
596 candidates,
597 file_id,
598 name,
599 namespace,
600 } = query;
601 let EffectiveExportResolution::Unique(binding) = self.resolve(file_id, name, namespace)
602 else {
603 return;
604 };
605 if binding.origin_file() == file_id && binding.origin_slot().is_some() {
606 slots.extend(candidates.iter().copied().filter(|&index| {
607 self.is_declaration_slot(exports, file_id, name, namespace, index)
608 }));
609 return;
610 }
611
612 let exact_type_only = namespace == ExportNamespace::Type
613 && candidates.iter().any(|&index| exports[index].is_type_only);
614 slots.extend(candidates.iter().copied().filter(|&index| {
615 exports[index].name.matches_str(name) && exports[index].is_type_only == exact_type_only
616 }));
617 }
618}
619
620#[derive(Clone, Copy)]
622pub(in crate::graph) struct DeclarationSlotQuery<'a> {
623 pub(in crate::graph) exports: &'a [ExportSymbol],
624 pub(in crate::graph) candidates: &'a [usize],
625 pub(in crate::graph) file_id: FileId,
626 pub(in crate::graph) name: &'a str,
627 pub(in crate::graph) namespace: ExportNamespace,
628}
629
630fn index_names_by_file(
631 resolutions: &FxHashMap<ExportKey, NamespaceResolutions>,
632) -> FxHashMap<FileId, Box<[ExportNameId]>> {
633 let mut names_by_file: FxHashMap<FileId, Vec<ExportNameId>> = FxHashMap::default();
634 for key in resolutions.keys() {
635 names_by_file.entry(key.file_id).or_default().push(key.name);
636 }
637 names_by_file
638 .into_iter()
639 .map(|(file_id, mut names)| {
640 names.sort_unstable_by_key(|name| name.0);
641 (file_id, names.into_boxed_slice())
642 })
643 .collect()
644}
645
646fn collect_declaration_merge_groups(modules: &[ResolvedModule]) -> DeclarationMergeGroups {
647 let mut collected = DeclarationMergeGroups::default();
648 for module in modules {
649 let merge_facts: Vec<_> = module
650 .semantic_facts
651 .iter()
652 .filter_map(|fact| match fact {
653 fallow_types::extract::SemanticFact::DeclarationMerge(group) => Some(group),
654 _ => None,
655 })
656 .collect();
657 if merge_facts.is_empty() {
658 continue;
659 }
660 let slot_by_span: FxHashMap<_, _> = module
661 .exports
662 .iter()
663 .enumerate()
664 .map(|(slot, export)| ((export.span.start, export.span.end), slot))
665 .collect();
666 for group in merge_facts {
667 let mut slots: Vec<_> = group
668 .export_spans
669 .iter()
670 .filter_map(|span| slot_by_span.get(span).copied())
671 .collect();
672 slots.sort_unstable();
673 slots.dedup();
674 if slots.len() < 2 {
675 continue;
676 }
677 let group_id = collected.groups.len();
678 for &slot in &slots {
679 collected
680 .group_by_slot
681 .insert((module.file_id, slot), group_id);
682 }
683 collected.groups.push(slots.into_boxed_slice());
684 }
685 }
686 collected
687}
688
689fn seed_direct_bindings(
690 modules: &[ResolvedModule],
691 interner: &mut ExportNameInterner,
692 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
693 queue: &mut VecDeque<ExportLookup>,
694) {
695 let mut value_type_fallbacks = Vec::new();
696 for module in modules {
697 for (slot, export) in module.exports.iter().enumerate() {
698 let lane = if export.is_type_only {
699 ResolutionLane::Type
700 } else {
701 ResolutionLane::Value
702 };
703 let name = interner.intern_export_name(&export.name);
704 let key = ExportLookup::new(module.file_id, name, lane);
705 let binding = EffectiveExportResolution::Unique(EffectiveExportBinding {
706 file_id: module.file_id,
707 kind: EffectiveExportBindingKind::Declaration(slot),
708 });
709 let entry = resolutions.entry(key.key).or_default();
710 if entry.seeded.claim_direct(lane) && entry.merge(lane, binding) {
715 queue.push_back(key);
716 }
717 if lane == ResolutionLane::Value {
718 value_type_fallbacks.push((module.file_id, name, slot));
719 }
720 }
721 seed_implicit_sfc_default(module, interner, resolutions, queue);
722 }
723 seed_value_type_fallbacks(value_type_fallbacks, resolutions, queue);
724}
725
726fn seed_value_type_fallbacks(
727 fallbacks: Vec<(FileId, ExportNameId, usize)>,
728 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
729 queue: &mut VecDeque<ExportLookup>,
730) {
731 for (file_id, name, slot) in fallbacks {
732 let key = ExportKey::new(file_id, name);
733 let binding = EffectiveExportResolution::Unique(EffectiveExportBinding {
734 file_id,
735 kind: EffectiveExportBindingKind::Declaration(slot),
736 });
737 let entry = resolutions.entry(key).or_default();
738 entry.seeded.claim_direct(ResolutionLane::Type);
742 if entry.seeded.claim_direct(ResolutionLane::TypeFallback)
743 && entry.merge(ResolutionLane::TypeFallback, binding)
744 {
745 queue.push_back(ExportLookup::new(
746 file_id,
747 name,
748 ResolutionLane::TypeFallback,
749 ));
750 }
751 }
752}
753
754fn seed_implicit_sfc_default(
755 module: &ResolvedModule,
756 interner: &mut ExportNameInterner,
757 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
758 queue: &mut VecDeque<ExportLookup>,
759) {
760 if !is_sfc_path(&module.path) {
761 return;
762 }
763 let name = interner.intern("default");
764 let key = ExportKey::new(module.file_id, name);
765 let entry = resolutions.entry(key).or_default();
766 if entry.seeded.is_direct(ResolutionLane::Value) {
767 return;
768 }
769 let binding = EffectiveExportResolution::Unique(EffectiveExportBinding {
770 file_id: module.file_id,
771 kind: EffectiveExportBindingKind::ImplicitDefault,
772 });
773 for lane in [ResolutionLane::Value, ResolutionLane::Type] {
774 entry.seeded.claim_direct(lane);
775 if entry.merge(lane, binding) {
776 queue.push_back(ExportLookup::new(module.file_id, name, lane));
777 }
778 }
779}
780
781fn is_sfc_path(path: &std::path::Path) -> bool {
782 matches!(
783 path.extension().and_then(std::ffi::OsStr::to_str),
784 Some("vue" | "svelte" | "astro")
785 )
786}
787
788fn collect_observers(
789 modules: &[ResolvedModule],
790 interner: &mut ExportNameInterner,
791 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
792 queue: &mut VecDeque<ExportLookup>,
793) -> PropagationObservers {
794 let mut observers = PropagationObservers {
795 named: FxHashMap::default(),
796 star: FxHashMap::default(),
797 };
798 for module in modules {
799 for (re_export_index, re_export) in module.re_exports.iter().enumerate() {
800 let Some(source) = re_export.target.internal_file_id() else {
801 if re_export.info.exported_name != "*"
802 && is_external_re_export_target(&re_export.target)
803 {
804 register_external_re_export(
805 module.file_id,
806 re_export_index,
807 &re_export.info,
808 interner,
809 resolutions,
810 queue,
811 );
812 }
813 continue;
814 };
815 if re_export.info.exported_name == "*" {
816 observers
817 .star
818 .entry(source)
819 .or_default()
820 .push(StarObserver {
821 barrel: module.file_id,
822 type_only: re_export.info.is_type_only,
823 });
824 continue;
825 }
826 register_named_observer(
827 module.file_id,
828 source,
829 &re_export.info,
830 ObserverBuildState {
831 interner,
832 resolutions,
833 queue,
834 observers: &mut observers,
835 },
836 );
837 }
838 }
839 observers
840}
841
842fn is_external_re_export_target(target: &crate::resolve::ResolveResult) -> bool {
843 matches!(
844 target,
845 crate::resolve::ResolveResult::ExternalFile(_)
846 | crate::resolve::ResolveResult::NpmPackage(_)
847 | crate::resolve::ResolveResult::CommonJsNpmPackage(_)
848 )
849}
850
851fn register_external_re_export(
856 barrel: FileId,
857 re_export_index: usize,
858 info: &fallow_types::extract::ReExportInfo,
859 interner: &mut ExportNameInterner,
860 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
861 queue: &mut VecDeque<ExportLookup>,
862) {
863 let exported_name = interner.intern(&info.exported_name);
864 let binding = EffectiveExportResolution::Unique(EffectiveExportBinding {
865 file_id: barrel,
866 kind: EffectiveExportBindingKind::ExternalReExport(re_export_index),
867 });
868 let lanes: &[ResolutionLane] = if info.is_type_only {
869 &[ResolutionLane::Type]
870 } else {
871 &[ResolutionLane::Type, ResolutionLane::Value]
872 };
873 for &lane in lanes {
874 let destination = ExportLookup::new(barrel, exported_name, lane);
875 let entry = resolutions.entry(destination.key).or_default();
876 if entry.seeded.is_direct(lane) {
877 continue;
878 }
879 entry.seeded.claim_explicit(lane);
880 if entry.merge(lane, binding) {
881 queue.push_back(destination);
882 }
883 }
884}
885
886fn register_named_observer(
887 barrel: FileId,
888 source: FileId,
889 info: &fallow_types::extract::ReExportInfo,
890 state: ObserverBuildState<'_>,
891) {
892 let ObserverBuildState {
893 interner,
894 resolutions,
895 queue,
896 observers,
897 } = state;
898 let exported_name = interner.intern(&info.exported_name);
899 if info.imported_name == "*" {
900 let lanes: &[ResolutionLane] = if info.is_type_only {
901 &[ResolutionLane::Type]
902 } else {
903 &[ResolutionLane::Type, ResolutionLane::Value]
904 };
905 let binding = EffectiveExportResolution::Unique(EffectiveExportBinding {
906 file_id: barrel,
907 kind: EffectiveExportBindingKind::NamespaceObject { source },
908 });
909 for &lane in lanes {
910 let destination = ExportLookup::new(barrel, exported_name, lane);
911 let entry = resolutions.entry(destination.key).or_default();
912 if entry.seeded.is_direct(lane) {
913 continue;
914 }
915 entry.seeded.claim_explicit(lane);
916 if entry.merge(lane, binding) {
917 queue.push_back(destination);
918 }
919 }
920 return;
921 }
922
923 let imported_name = interner.intern(&info.imported_name);
924 let lanes: &[ResolutionLane] = if info.is_type_only {
925 &[ResolutionLane::Type, ResolutionLane::TypeFallback]
926 } else {
927 &[
928 ResolutionLane::Type,
929 ResolutionLane::TypeFallback,
930 ResolutionLane::Value,
931 ]
932 };
933 for &lane in lanes {
934 let destination = ExportLookup::new(barrel, exported_name, lane);
935 let entry = resolutions.entry(destination.key).or_default();
936 if entry.seeded.is_direct(lane) {
937 continue;
938 }
939 entry.seeded.claim_explicit(lane);
940 observers
941 .named
942 .entry(ExportLookup::new(source, imported_name, lane))
943 .and_modify(|destinations| destinations.push(destination))
944 .or_insert(NamedDestinations::One(destination));
945 }
946}
947
948fn propagate_bindings(
949 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
950 queue: &mut VecDeque<ExportLookup>,
951 observers: &PropagationObservers,
952) {
953 while let Some(source_key) = queue.pop_front() {
954 let Some(source_resolution) = resolutions
955 .get(&source_key.key)
956 .map(|resolutions| resolutions.get(source_key.lane))
957 else {
958 continue;
959 };
960 if let Some(destinations) = observers.named.get(&source_key) {
961 for destination in destinations.iter() {
962 merge_resolution(resolutions, queue, *destination, source_resolution);
963 }
964 }
965 propagate_star_binding(
966 resolutions,
967 queue,
968 observers,
969 &source_key,
970 source_resolution,
971 );
972 }
973}
974
975fn propagate_star_binding(
976 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
977 queue: &mut VecDeque<ExportLookup>,
978 observers: &PropagationObservers,
979 source_key: &ExportLookup,
980 source_resolution: EffectiveExportResolution,
981) {
982 if source_key.key.name == ExportNameId::DEFAULT {
983 return;
984 }
985 let Some(star_observers) = observers.star.get(&source_key.key.file_id) else {
986 return;
987 };
988 for observer in star_observers {
989 if observer.type_only && source_key.lane == ResolutionLane::Value {
990 continue;
991 }
992 let destination = source_key.with_file(observer.barrel);
993 let entry = resolutions.entry(destination.key).or_default();
994 if entry.seeded.is_explicit(destination.lane) {
995 continue;
996 }
997 if entry.merge(destination.lane, source_resolution) {
998 queue.push_back(destination);
999 }
1000 }
1001}
1002
1003fn merge_resolution(
1004 resolutions: &mut FxHashMap<ExportKey, NamespaceResolutions>,
1005 queue: &mut VecDeque<ExportLookup>,
1006 key: ExportLookup,
1007 incoming: EffectiveExportResolution,
1008) {
1009 if resolutions
1010 .entry(key.key)
1011 .or_default()
1012 .merge(key.lane, incoming)
1013 {
1014 queue.push_back(key);
1015 }
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020 use super::*;
1021 use crate::resolve::{ResolveResult, ResolvedReExport};
1022 use fallow_types::extract::{ExportInfo, ReExportInfo, VisibilityTag};
1023
1024 fn value_export(name: &str) -> ExportInfo {
1025 ExportInfo {
1026 name: ExportName::Named(name.to_string()),
1027 local_name: Some(name.to_string()),
1028 is_type_only: false,
1029 visibility: VisibilityTag::None,
1030 expected_unused_reason: None,
1031 span: oxc_span::Span::default(),
1032 members: Vec::new(),
1033 is_side_effect_used: false,
1034 super_class: None,
1035 }
1036 }
1037
1038 fn re_export(source: FileId, imported: &str, exported: &str) -> ResolvedReExport {
1039 ResolvedReExport {
1040 info: ReExportInfo {
1041 source: format!("./{}", source.0),
1042 imported_name: imported.to_string(),
1043 exported_name: exported.to_string(),
1044 is_type_only: false,
1045 span: oxc_span::Span::default(),
1046 statement_span: oxc_span::Span::default(),
1047 source_span: oxc_span::Span::default(),
1048 },
1049 target: ResolveResult::InternalModule(source),
1050 }
1051 }
1052
1053 fn module(
1054 file_id: u32,
1055 exports: Vec<ExportInfo>,
1056 re_exports: Vec<ResolvedReExport>,
1057 ) -> ResolvedModule {
1058 ResolvedModule {
1059 file_id: FileId(file_id),
1060 exports: exports.into(),
1061 re_exports,
1062 ..Default::default()
1063 }
1064 }
1065
1066 fn resolves_through(
1067 index: &EffectiveExportIndex,
1068 barrel: FileId,
1069 source: FileId,
1070 name: &str,
1071 ) -> bool {
1072 index.resolves_through(barrel, name, source, name, ExportNamespace::Value)
1073 }
1074
1075 fn external_re_export(imported: &str, exported: &str, type_only: bool) -> ResolvedReExport {
1076 ResolvedReExport {
1077 info: ReExportInfo {
1078 source: "node:path".to_string(),
1079 imported_name: imported.to_string(),
1080 exported_name: exported.to_string(),
1081 is_type_only: type_only,
1082 span: oxc_span::Span::default(),
1083 statement_span: oxc_span::Span::default(),
1084 source_span: oxc_span::Span::default(),
1085 },
1086 target: ResolveResult::NpmPackage("node:path".to_string()),
1087 }
1088 }
1089
1090 #[test]
1091 fn external_named_re_exports_keep_an_opaque_local_binding() {
1092 let index = EffectiveExportIndex::build(&[module(
1093 0,
1094 Vec::new(),
1095 vec![
1096 external_re_export("join", "join", false),
1097 external_re_export("Stats", "Stats", true),
1098 external_re_export("*", "path", false),
1099 ],
1100 )]);
1101 let encoded = postcard::to_allocvec(&index).expect("encode effective export index");
1102 let decoded: EffectiveExportIndex =
1103 postcard::from_bytes(&encoded).expect("decode effective export index");
1104
1105 let value = decoded.resolve(FileId(0), "join", ExportNamespace::Value);
1106 assert!(matches!(
1107 value,
1108 EffectiveExportResolution::Unique(binding)
1109 if binding.origin_file() == FileId(0) && binding.origin_slot().is_none()
1110 ));
1111 assert_eq!(
1112 value,
1113 decoded.resolve(FileId(0), "join", ExportNamespace::Type)
1114 );
1115 assert!(matches!(
1116 decoded.resolve(FileId(0), "Stats", ExportNamespace::Type),
1117 EffectiveExportResolution::Unique(_)
1118 ));
1119 assert_eq!(
1120 decoded.resolve(FileId(0), "Stats", ExportNamespace::Value),
1121 EffectiveExportResolution::Missing
1122 );
1123 assert_eq!(
1124 decoded.resolve(FileId(0), "path", ExportNamespace::Type),
1125 decoded.resolve(FileId(0), "path", ExportNamespace::Value)
1126 );
1127 }
1128
1129 #[test]
1130 fn unresolved_named_re_exports_do_not_gain_an_external_binding() {
1131 let mut unresolved = external_re_export("missing", "missing", false);
1132 unresolved.target = ResolveResult::Unresolvable("./missing".to_string());
1133 let index = EffectiveExportIndex::build(&[module(0, Vec::new(), vec![unresolved])]);
1134
1135 assert_eq!(
1136 index.resolve(FileId(0), "missing", ExportNamespace::Type),
1137 EffectiveExportResolution::Missing
1138 );
1139 assert_eq!(
1140 index.resolve(FileId(0), "missing", ExportNamespace::Value),
1141 EffectiveExportResolution::Missing
1142 );
1143 }
1144
1145 #[test]
1146 fn missing_export_is_explicit_in_the_resolution_contract() {
1147 let index = EffectiveExportIndex::build(&[module(0, Vec::new(), Vec::new())]);
1148
1149 assert_eq!(
1150 index.resolve(FileId(0), "missing", ExportNamespace::Value),
1151 EffectiveExportResolution::Missing
1152 );
1153 }
1154
1155 #[test]
1156 fn same_module_declaration_merges_keep_one_binding() {
1157 let index = EffectiveExportIndex::build(&[module(
1158 0,
1159 vec![value_export("Merged"), value_export("Merged")],
1160 Vec::new(),
1161 )]);
1162
1163 assert!(matches!(
1164 index.resolve(FileId(0), "Merged", ExportNamespace::Value),
1165 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(0)
1166 ));
1167 }
1168
1169 #[test]
1170 fn declaration_merge_groups_survive_graph_cache_roundtrip() {
1171 let mut interface = value_export("Merged");
1172 interface.is_type_only = true;
1173 interface.span = oxc_span::Span::new(0, 6);
1174 let mut namespace = value_export("Merged");
1175 namespace.span = oxc_span::Span::new(10, 16);
1176 let modules = vec![ResolvedModule {
1177 file_id: FileId(0),
1178 exports: vec![interface, namespace].into(),
1179 semantic_facts: vec![fallow_types::extract::SemanticFact::DeclarationMerge(
1180 fallow_types::extract::DeclarationMergeFact {
1181 export_spans: vec![(0, 6), (10, 16)],
1182 },
1183 )]
1184 .into(),
1185 ..Default::default()
1186 }];
1187 let index = EffectiveExportIndex::build(&modules);
1188 let encoded = postcard::to_allocvec(&index).expect("encode effective export index");
1189 let decoded: EffectiveExportIndex =
1190 postcard::from_bytes(&encoded).expect("decode effective export index");
1191 let EffectiveExportResolution::Unique(binding) =
1192 decoded.resolve(FileId(0), "Merged", ExportNamespace::Type)
1193 else {
1194 panic!("merged type binding must remain unique");
1195 };
1196
1197 assert_eq!(decoded.declaration_group_slots(binding), &[0, 1]);
1198 }
1199
1200 #[test]
1201 fn explicit_re_export_shadows_a_star_binding() {
1202 let modules = vec![
1203 module(
1204 0,
1205 Vec::new(),
1206 vec![
1207 re_export(FileId(1), "*", "*"),
1208 re_export(FileId(2), "foo", "foo"),
1209 ],
1210 ),
1211 module(1, vec![value_export("foo")], Vec::new()),
1212 module(2, vec![value_export("foo")], Vec::new()),
1213 ];
1214 let index = EffectiveExportIndex::build(&modules);
1215
1216 assert!(!resolves_through(&index, FileId(0), FileId(1), "foo"));
1217 assert!(resolves_through(&index, FileId(0), FileId(2), "foo"));
1218 assert!(!index.contributes_through(
1219 FileId(0),
1220 "foo",
1221 FileId(1),
1222 "foo",
1223 ExportNamespace::Value
1224 ));
1225 }
1226
1227 #[test]
1228 fn distinct_star_bindings_are_ambiguous() {
1229 let modules = vec![
1230 module(
1231 0,
1232 Vec::new(),
1233 vec![
1234 re_export(FileId(1), "*", "*"),
1235 re_export(FileId(2), "*", "*"),
1236 ],
1237 ),
1238 module(1, vec![value_export("foo")], Vec::new()),
1239 module(2, vec![value_export("foo")], Vec::new()),
1240 ];
1241 let index = EffectiveExportIndex::build(&modules);
1242
1243 assert!(!resolves_through(&index, FileId(0), FileId(1), "foo"));
1244 assert!(!resolves_through(&index, FileId(0), FileId(2), "foo"));
1245 assert!(index.contributes_through(
1246 FileId(0),
1247 "foo",
1248 FileId(1),
1249 "foo",
1250 ExportNamespace::Value
1251 ));
1252 assert!(index.contributes_through(
1253 FileId(0),
1254 "foo",
1255 FileId(2),
1256 "foo",
1257 ExportNamespace::Value
1258 ));
1259 }
1260
1261 #[test]
1262 fn convergent_star_paths_keep_one_binding() {
1263 let modules = vec![
1264 module(
1265 0,
1266 Vec::new(),
1267 vec![
1268 re_export(FileId(1), "*", "*"),
1269 re_export(FileId(2), "*", "*"),
1270 ],
1271 ),
1272 module(1, Vec::new(), vec![re_export(FileId(3), "*", "*")]),
1273 module(2, Vec::new(), vec![re_export(FileId(3), "*", "*")]),
1274 module(3, vec![value_export("foo")], Vec::new()),
1275 ];
1276 let index = EffectiveExportIndex::build(&modules);
1277
1278 assert!(resolves_through(&index, FileId(0), FileId(1), "foo"));
1279 assert!(resolves_through(&index, FileId(0), FileId(2), "foo"));
1280 }
1281
1282 #[test]
1283 fn a_real_type_declaration_wins_over_a_value_type_fallback() {
1284 let mut interface = value_export("User");
1285 interface.is_type_only = true;
1286 let modules = vec![
1287 module(
1288 0,
1289 Vec::new(),
1290 vec![
1291 re_export(FileId(1), "*", "*"),
1292 re_export(FileId(2), "*", "*"),
1293 ],
1294 ),
1295 module(1, vec![value_export("User")], Vec::new()),
1296 module(2, vec![interface], Vec::new()),
1297 ];
1298 let index = EffectiveExportIndex::build(&modules);
1299
1300 assert!(matches!(
1301 index.resolve(FileId(0), "User", ExportNamespace::Type),
1302 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(2)
1303 ));
1304 assert!(matches!(
1305 index.resolve(FileId(0), "User", ExportNamespace::Value),
1306 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(1)
1307 ));
1308 assert!(index.resolves_through(
1309 FileId(0),
1310 "User",
1311 FileId(2),
1312 "User",
1313 ExportNamespace::Type
1314 ));
1315 assert!(index.resolves_through(
1316 FileId(0),
1317 "User",
1318 FileId(1),
1319 "User",
1320 ExportNamespace::Value
1321 ));
1322 }
1323
1324 #[test]
1325 fn colliding_real_type_declarations_stay_ambiguous() {
1326 let type_export = |name: &str| {
1327 let mut export = value_export(name);
1328 export.is_type_only = true;
1329 export
1330 };
1331 let modules = vec![
1332 module(
1333 0,
1334 Vec::new(),
1335 vec![
1336 re_export(FileId(1), "*", "*"),
1337 re_export(FileId(2), "*", "*"),
1338 ],
1339 ),
1340 module(1, vec![type_export("User")], Vec::new()),
1341 module(2, vec![type_export("User")], Vec::new()),
1342 ];
1343 let index = EffectiveExportIndex::build(&modules);
1344
1345 assert_eq!(
1346 index.resolve(FileId(0), "User", ExportNamespace::Type),
1347 EffectiveExportResolution::Ambiguous
1348 );
1349 }
1350
1351 #[test]
1352 fn a_value_export_still_carries_its_type_meaning_through_a_barrel() {
1353 let modules = vec![
1354 module(0, Vec::new(), vec![re_export(FileId(1), "*", "*")]),
1355 module(1, vec![value_export("Widget")], Vec::new()),
1356 ];
1357 let index = EffectiveExportIndex::build(&modules);
1358
1359 assert!(index.resolves_through(
1360 FileId(0),
1361 "Widget",
1362 FileId(1),
1363 "Widget",
1364 ExportNamespace::Type
1365 ));
1366 }
1367
1368 #[test]
1369 fn star_exports_exclude_default_bindings() {
1370 let mut default = value_export("local");
1371 default.name = ExportName::Default;
1372 let index = EffectiveExportIndex::build(&[
1373 module(0, Vec::new(), vec![re_export(FileId(1), "*", "*")]),
1374 module(1, vec![default], Vec::new()),
1375 ]);
1376
1377 assert_eq!(
1378 index.resolve(FileId(0), "default", ExportNamespace::Value),
1379 EffectiveExportResolution::Missing
1380 );
1381 }
1382
1383 #[test]
1384 fn star_cycles_converge_on_the_same_binding() {
1385 let index = EffectiveExportIndex::build(&[
1386 module(0, Vec::new(), vec![re_export(FileId(1), "*", "*")]),
1387 module(
1388 1,
1389 Vec::new(),
1390 vec![
1391 re_export(FileId(0), "*", "*"),
1392 re_export(FileId(2), "*", "*"),
1393 ],
1394 ),
1395 module(2, vec![value_export("foo")], Vec::new()),
1396 ]);
1397
1398 assert!(resolves_through(&index, FileId(0), FileId(2), "foo"));
1399 assert!(resolves_through(&index, FileId(1), FileId(2), "foo"));
1400 }
1401
1402 #[test]
1403 fn type_only_namespace_re_export_resolves_only_in_type_namespace() {
1404 let mut namespace = re_export(FileId(1), "*", "Types");
1405 namespace.info.is_type_only = true;
1406 let index = EffectiveExportIndex::build(&[
1407 module(0, Vec::new(), vec![namespace]),
1408 module(1, vec![value_export("foo")], Vec::new()),
1409 ]);
1410
1411 assert!(matches!(
1412 index.resolve(FileId(0), "Types", ExportNamespace::Type),
1413 EffectiveExportResolution::Unique(_)
1414 ));
1415 assert_eq!(
1416 index.resolve(FileId(0), "Types", ExportNamespace::Value),
1417 EffectiveExportResolution::Missing
1418 );
1419 }
1420
1421 #[test]
1422 fn normal_namespace_re_export_resolves_in_both_namespaces() {
1423 let index = EffectiveExportIndex::build(&[
1424 module(0, Vec::new(), vec![re_export(FileId(1), "*", "Namespace")]),
1425 module(1, vec![value_export("foo")], Vec::new()),
1426 ]);
1427
1428 let type_binding = index.resolve(FileId(0), "Namespace", ExportNamespace::Type);
1429 let value_binding = index.resolve(FileId(0), "Namespace", ExportNamespace::Value);
1430 assert!(matches!(type_binding, EffectiveExportResolution::Unique(_)));
1431 assert_eq!(type_binding, value_binding);
1432 }
1433
1434 #[test]
1435 fn persisted_index_remains_queryable_without_reconstruction() {
1436 let index = EffectiveExportIndex::build(&[
1437 module(0, Vec::new(), vec![re_export(FileId(1), "foo", "bar")]),
1438 module(1, vec![value_export("foo")], Vec::new()),
1439 ]);
1440 let encoded = postcard::to_allocvec(&index).expect("encode effective export index");
1441 let decoded: EffectiveExportIndex =
1442 postcard::from_bytes(&encoded).expect("decode effective export index");
1443
1444 assert!(matches!(
1445 decoded.resolve(FileId(0), "bar", ExportNamespace::Value),
1446 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(1)
1447 ));
1448 assert_eq!(
1449 decoded.resolve(FileId(0), "missing", ExportNamespace::Value),
1450 EffectiveExportResolution::Missing
1451 );
1452 }
1453
1454 #[test]
1455 fn sfc_file_has_an_implicit_default_value_binding() {
1456 let mut sfc = module(0, Vec::new(), Vec::new());
1457 sfc.path = std::path::PathBuf::from("/project/Widget.vue");
1458 let index = EffectiveExportIndex::build(&[sfc]);
1459
1460 assert!(matches!(
1461 index.resolve(FileId(0), "default", ExportNamespace::Value),
1462 EffectiveExportResolution::Unique(binding) if binding.origin_file() == FileId(0)
1463 ));
1464 }
1465}