Skip to main content

fallow_graph/graph/
effective_exports.rs

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