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