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