Skip to main content

fallow_graph/graph/
effective_exports.rs

1//! Canonical effective export bindings for direct and transitive re-exports.
2
3use 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/// The namespace in which an exported name is resolved.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
14pub enum ExportNamespace {
15    /// Type declarations and the type side of dual-space declarations.
16    Type,
17    /// Runtime value declarations.
18    Value,
19}
20
21/// One canonical declaration that an exported name resolves to.
22#[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    /// A known export surface whose declaration lives outside this graph.
36    /// The re-export slot keeps opaque bindings distinct without pretending
37    /// that Fallow can inspect or canonicalize the external declaration.
38    ExternalReExport(usize),
39}
40
41impl EffectiveExportBinding {
42    /// Module that owns the resolved declaration or opaque external surface.
43    #[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    /// Source module represented by a namespace-object export.
58    #[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    /// Whether this binding is the implicit default export of an SFC file.
69    #[must_use]
70    pub const fn is_implicit_default(self) -> bool {
71        matches!(self.kind, EffectiveExportBindingKind::ImplicitDefault)
72    }
73}
74
75/// Effective resolution for one module/name/namespace tuple.
76#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
77pub enum EffectiveExportResolution {
78    /// The module does not export this name in the requested namespace.
79    #[default]
80    Missing,
81    /// Exactly one canonical declaration supplies the exported name.
82    Unique(EffectiveExportBinding),
83    /// Multiple distinct star-exported declarations supply the same name.
84    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/// Bitset over the two export namespaces, for consumer-side namespace demand.
100#[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/// Propagation lane for one exported name.
158///
159/// The type namespace is resolved in two lanes. `Type` holds declarations that
160/// really occupy TypeScript's type space (`interface`, `type`, `export type`).
161/// `TypeFallback` holds the type meaning synthesized for plain value exports so
162/// dual-space declarations (`class`, `enum`) keep resolving as types. A real
163/// type declaration therefore wins over a synthesized one instead of colliding
164/// with it, while two real declarations still collide into `Ambiguous`.
165#[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/// Which lanes of one exported name are claimed by the module itself.
195///
196/// Seeding used to carry this in two `(file, name, lane)` hash sets alongside
197/// the resolution table. The bits live on the resolution entry instead, so a
198/// seed or observer registration hashes the name once instead of three times.
199/// Build-time only: the flags are rebuilt with the index and stay out of the
200/// serialized cache.
201#[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    /// Claim `lane` as locally declared, reporting whether it was unclaimed.
218    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    /// Merge `incoming` into `lane`, reporting whether the lane advanced.
245    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/// Upfront element counts for the propagation tables.
283///
284/// The tables are keyed by interned name, so they grow into the thousands on
285/// real projects and re-hash repeatedly when they start empty. Counting is a
286/// linear scan over already-resident slices and buys exact capacity.
287#[derive(Clone, Copy)]
288struct ProjectExportSizes {
289    /// Distinct `(file, name)` pairs, an upper bound on the resolution table.
290    keys: usize,
291    /// Distinct `(file, name, lane)` triples, an upper bound on seeded lanes.
292    lane_keys: usize,
293    /// Interned name upper bound: every export plus every re-export alias.
294    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            // Value exports also reserve the type lane and seed the fallback lane.
310            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
357/// Destinations fed by one source lane.
358///
359/// A re-exported name almost always has a single barrel destination, so the
360/// common case stays inline instead of allocating a one-element vector per
361/// observed lane.
362enum 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/// Immutable effective export bindings for one resolved project.
396///
397/// Resolution is a finite monotone propagation: each key advances at most from
398/// missing to one binding and then to ambiguous. Cycles therefore terminate,
399/// while multiple paths to the same binding remain unique.
400#[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 resolved on each module, so [`EffectiveExportIndex::unique_bindings`]
405    /// reads one module instead of scanning every key in the project.
406    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    /// Append the declaration slots for one name into `slots`.
589    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/// One name's candidate export slots on a module.
621#[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            // Same-name declarations inside one module form one local export
711            // entry. This covers legal TypeScript declaration merging (for
712            // example class/function plus namespace) without weakening the
713            // ambiguity rule for distinct bindings arriving through stars.
714            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        // A locally declared name owns its module's type surface even when the
739        // declaration only occupies value space, so the real type lane stays
740        // reserved here and star sources cannot propagate into it.
741        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
851/// Seed an opaque barrel-owned binding for a named re-export of an external
852/// declaration, so consumers importing through the barrel keep crediting the
853/// export instead of losing it to a missing resolution. Unresolvable targets
854/// stay `Missing`: an unknown surface must not manufacture credit.
855fn 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}