Skip to main content

turbopack_core/module_graph/
binding_usage_info.rs

1use std::{cmp::Reverse, collections::hash_map::Entry};
2
3use anyhow::{Context, Result, bail};
4use auto_hash_map::AutoSet;
5use rustc_hash::{FxHashMap, FxHashSet};
6use tracing::Instrument;
7use turbo_rcstr::RcStr;
8use turbo_tasks::{OperationVc, ResolvedVc, Vc};
9
10use crate::{
11    chunk::chunking_context::UnusedReferences,
12    module::Module,
13    module_graph::{
14        GraphEdgeIndex, GraphTraversalAction, ModuleGraph,
15        side_effect_module_info::compute_side_effect_free_module_info,
16    },
17    resolve::{ExportUsage, ImportUsage},
18};
19
20#[turbo_tasks::value(transparent, cell = "keyed")]
21pub struct UsedExportsMap(FxHashMap<ResolvedVc<Box<dyn Module>>, ModuleExportUsageInfo>);
22
23#[turbo_tasks::value(transparent, cell = "keyed")]
24pub struct ExportCircuitBreakers(FxHashSet<ResolvedVc<Box<dyn Module>>>);
25
26/// Modules that are read through a *partial namespace object* — an
27/// [`ExportUsage::PartialNamespaceObject`] edge points at them.
28///
29/// The used export names are still known individually (that is why the usage stays
30/// [`ModuleExportUsageInfo::Exports`]), but the reads went through a namespace value: a namespace
31/// binding's member reads or destructuring, or the object a dynamic `import()` resolves to. Some of
32/// those reads are lowered to direct named accesses and some are not, and this set does not
33/// distinguish them — so a consumer that wants to rename the module's export keys has to assume the
34/// original names may still be read somewhere, and leave them alone.
35#[turbo_tasks::value(transparent, cell = "keyed")]
36pub struct PartialNamespaceModules(FxHashSet<ResolvedVc<Box<dyn Module>>>);
37
38/// The module graph edges of references that are unused and should be skipped during traversals.
39#[turbo_tasks::value(transparent, cell = "keyed")]
40#[derive(Debug)]
41pub struct UnusedReferenceEdges(FxHashSet<GraphEdgeIndex>);
42
43#[turbo_tasks::value]
44#[derive(Clone, Default, Debug)]
45pub struct BindingUsageInfo {
46    unused_references: ResolvedVc<UnusedReferences>,
47    unused_references_edges: ResolvedVc<UnusedReferenceEdges>,
48
49    used_exports: ResolvedVc<UsedExportsMap>,
50    export_circuit_breakers: ResolvedVc<ExportCircuitBreakers>,
51    partial_namespace_modules: ResolvedVc<PartialNamespaceModules>,
52}
53
54#[turbo_tasks::value(transparent)]
55pub struct OptionBindingUsageInfo(Option<ResolvedVc<BindingUsageInfo>>);
56
57#[turbo_tasks::value]
58pub struct ModuleExportUsage {
59    pub export_usage: ResolvedVc<ModuleExportUsageInfo>,
60    /// Whether this module must expose exports before imports. This is conservative when export
61    /// usage analysis did not run, because a cycle cannot be ruled out.
62    pub is_circuit_breaker: bool,
63    /// Whether this module is read through a namespace value somewhere, which means one of those
64    /// reads may still use an original export name. See [`PartialNamespaceModules`].
65    pub namespace_object_may_escape: bool,
66}
67#[turbo_tasks::value_impl]
68impl ModuleExportUsage {
69    #[turbo_tasks::function]
70    pub async fn unknown() -> Result<Vc<Self>> {
71        Ok(Self {
72            export_usage: ModuleExportUsageInfo::all().to_resolved().await?,
73            is_circuit_breaker: true,
74            namespace_object_may_escape: true,
75        }
76        .cell())
77    }
78}
79
80impl BindingUsageInfo {
81    pub async fn used_exports(
82        &self,
83        module: ResolvedVc<Box<dyn Module>>,
84    ) -> Result<Vc<ModuleExportUsage>> {
85        let is_circuit_breaker = self.export_circuit_breakers.contains_key(&module).await?;
86        let Some(exports) = self.used_exports.get(&module).await? else {
87            bail!(
88                "export usage not found for module: {:?}",
89                module.ident_string().await?
90            );
91        };
92        let namespace_object_may_escape =
93            self.partial_namespace_modules.contains_key(&module).await?;
94        Ok(ModuleExportUsage {
95            export_usage: (*exports).clone().resolved_cell(),
96            is_circuit_breaker,
97            namespace_object_may_escape,
98        }
99        .cell())
100    }
101}
102
103#[turbo_tasks::value_impl]
104impl BindingUsageInfo {
105    #[turbo_tasks::function]
106    pub fn unused_references(&self) -> Vc<UnusedReferences> {
107        *self.unused_references
108    }
109
110    #[turbo_tasks::function]
111    pub fn unused_references_edges(&self) -> Vc<UnusedReferenceEdges> {
112        *self.unused_references_edges
113    }
114}
115
116#[turbo_tasks::function(operation)]
117pub async fn compute_binding_usage_info(
118    graph: OperationVc<ModuleGraph>,
119    remove_unused_imports: bool,
120) -> Result<Vc<BindingUsageInfo>> {
121    let span_outer = tracing::info_span!(
122        "compute binding usage info",
123        visit_count = tracing::field::Empty,
124        unused_reference_count = tracing::field::Empty
125    );
126    let span = span_outer.clone();
127
128    async move {
129        let mut used_exports = FxHashMap::<_, ModuleExportUsageInfo>::default();
130        let mut partial_namespace_modules = FxHashSet::default();
131        #[cfg(debug_assertions)]
132        let mut debug_unused_references_name = FxHashSet::<(
133            ResolvedVc<Box<dyn Module>>,
134            ExportUsage,
135            ResolvedVc<Box<dyn Module>>,
136        )>::default();
137        let mut unused_references_edges = FxHashSet::default();
138        let mut unused_references =
139            FxHashMap::<_, FxHashSet<ResolvedVc<Box<dyn Module>>>>::default();
140
141        let graph = graph.connect();
142        let graph_ref = graph.await?;
143        if graph_ref.unused_references_edges.is_some() {
144            // Reject graphs that already have binding usage info. Recomputing it on such a graph
145            // is unfortunately easy to do with
146            // ```
147            // fn get_module_graph(){
148            //   let graph = ....;
149            //   let graph = graph.without_unused_references(compute_binding_usage_info(graph));
150            //   return graph
151            // }
152            //
153            // compute_binding_usage_info(get_module_graph())
154            // ```
155            panic!(
156                "don't run compute_binding_usage_info on a graph after calling \
157                 without_unused_references"
158            );
159        }
160        let side_effect_free_modules = if remove_unused_imports {
161            let side_effect_free_modules = compute_side_effect_free_module_info(graph).await?;
162            span.record("side_effect_free_modules", side_effect_free_modules.len());
163            Some(side_effect_free_modules)
164        } else {
165            None
166        };
167
168        // Visit modules in reverse postorder. In an acyclic graph, this processes each module only
169        // after all modules importing it, i.e. once, with its final usage. Cycles still rely on the
170        // fixed point and may revisit modules in or after them. This avoids order-dependent
171        // revisits in DAG-shaped graphs, at the cost of an additional traversal to compute ranks.
172        let module_rank =
173            graph_ref.reverse_postorder_ranks(graph_ref.all_chunk_group_entry_modules())?;
174
175        let visit_count = graph_ref.traverse_edges_fixed_point_with_priority(
176            graph_ref
177                .all_chunk_group_entry_modules()
178                .map(|m| {
179                    Ok((
180                        m,
181                        Reverse(*module_rank.get(&m).context("Module rank not found")?),
182                    ))
183                })
184                .collect::<Result<Vec<_>>>()?,
185            &mut (),
186            |parent, target, _, _| {
187                // Entries are always used
188                let Some((parent, ref_data, edge)) = parent else {
189                    used_exports.insert(target, ModuleExportUsageInfo::All);
190                    return Ok(GraphTraversalAction::Continue);
191                };
192
193                if remove_unused_imports {
194                    // If this is an evaluation reference and the target has no side effects
195                    // then we can drop it. NOTE: many `imports` create parallel Evaluation
196                    // and Named/All references
197                    if matches!(&ref_data.binding_usage.export, ExportUsage::Evaluation)
198                        && side_effect_free_modules
199                            .as_ref()
200                            .expect("this must be present if `remove_unused_imports` is true")
201                            .contains(&target)
202                    {
203                        #[cfg(debug_assertions)]
204                        debug_unused_references_name.insert((
205                            parent,
206                            ref_data.binding_usage.export.clone(),
207                            target,
208                        ));
209                        unused_references_edges.insert(edge);
210                        unused_references
211                            .entry(ref_data.reference)
212                            .or_default()
213                            .insert(target);
214                        return Ok(GraphTraversalAction::Skip);
215                    }
216                    // If the current edge is an unused import, skip it
217                    match &ref_data.binding_usage.import {
218                        ImportUsage::Exports(exports) => {
219                            let source_used_exports = used_exports
220                                .get(&parent)
221                                .context("parent module must have usage info")?;
222                            if exports
223                                .iter()
224                                .all(|e| !source_used_exports.is_export_used(e))
225                            {
226                                // all exports are unused
227                                #[cfg(debug_assertions)]
228                                debug_unused_references_name.insert((
229                                    parent,
230                                    ref_data.binding_usage.export.clone(),
231                                    target,
232                                ));
233                                unused_references_edges.insert(edge);
234                                unused_references
235                                    .entry(ref_data.reference)
236                                    .or_default()
237                                    .insert(target);
238
239                                return Ok(GraphTraversalAction::Skip);
240                            } else {
241                                #[cfg(debug_assertions)]
242                                debug_unused_references_name.remove(&(
243                                    parent,
244                                    ref_data.binding_usage.export.clone(),
245                                    target,
246                                ));
247                                unused_references_edges.remove(&edge);
248                                if let Entry::Occupied(mut e) =
249                                    unused_references.entry(ref_data.reference)
250                                {
251                                    e.get_mut().remove(&target);
252                                    if e.get().is_empty() {
253                                        e.remove();
254                                    }
255                                }
256                                // Continue, add export
257                            }
258                        }
259                        ImportUsage::TopLevel => {
260                            #[cfg(debug_assertions)]
261                            debug_unused_references_name.remove(&(
262                                parent,
263                                ref_data.binding_usage.export.clone(),
264                                target,
265                            ));
266                            unused_references_edges.remove(&edge);
267                            if let Entry::Occupied(mut e) =
268                                unused_references.entry(ref_data.reference)
269                            {
270                                e.get_mut().remove(&target);
271                                if e.get().is_empty() {
272                                    e.remove();
273                                }
274                            }
275                            // Continue, has to always be included
276                        }
277                    }
278                }
279
280                let is_first_visit = !used_exports.contains_key(&target);
281                let changed = match &ref_data.binding_usage.export {
282                    ExportUsage::Passthrough {
283                        namespace_object_may_escape,
284                    } => {
285                        // Passthrough edges always carry namespace provenance that already reached
286                        // their parent. Some edges (for example a forwarded CommonJS namespace)
287                        // additionally expose the target's original property names themselves.
288                        let namespace_changed = if *namespace_object_may_escape
289                            || partial_namespace_modules.contains(&parent)
290                        {
291                            partial_namespace_modules.insert(target)
292                        } else {
293                            false
294                        };
295                        let usage_changed = if parent == target {
296                            // Merging the usage of a module into itself never changes it (and
297                            // `get_disjoint_mut` would panic on the duplicate key).
298                            false
299                        } else {
300                            used_exports.entry(target).or_default();
301                            let [Some(parent_usage), Some(target_usage)] =
302                                used_exports.get_disjoint_mut([&parent, &target])
303                            else {
304                                bail!("parent module must have usage info");
305                            };
306                            target_usage.add_usage_info(parent_usage)
307                        };
308                        namespace_changed || usage_changed
309                    }
310                    export_usage => {
311                        // A partial namespace edge means `target` is read through a namespace
312                        // value. We know which names are used, but not that every read was
313                        // lowered to a direct named access.
314                        //
315                        // The provenance can be new even when the used names are not (the target
316                        // already uses `All` or these names), and it still has to be forwarded
317                        // over the target's passthrough edges, so it counts as a change.
318                        let namespace_changed =
319                            matches!(export_usage, ExportUsage::PartialNamespaceObject(_))
320                                && partial_namespace_modules.insert(target);
321                        let usage_changed =
322                            used_exports.entry(target).or_default().add(export_usage);
323                        namespace_changed || usage_changed
324                    }
325                };
326                if changed || is_first_visit {
327                    // First visit, or the used exports/namespace provenance changed. Either can
328                    // cause more imports to become used downstream.
329                    Ok(GraphTraversalAction::Continue)
330                } else {
331                    Ok(GraphTraversalAction::Skip)
332                }
333            },
334            |successor, _| {
335                Ok(Reverse(
336                    *module_rank
337                        .get(&successor)
338                        .context("Module rank not found")?,
339                ))
340            },
341        )?;
342
343        // Compute cycles and select modules to be 'circuit breakers'
344        //
345        // To break cycles we need to ensure that no importing module can observe a
346        // partially populated exports object.
347        //
348        // A circuit breaker module will need to eagerly export lazy getters for its exports to
349        // break an evaluation cycle all other modules can export values after defining them
350        //
351        // In particular, this is also needed with scope hoisting and self-imports, as in
352        // that case `__turbopack_esm__` is what initializes the exports object. (Without
353        // scope hoisting, the exports object is already populated before any executing the
354        // module factory.)
355        let mut export_circuit_breakers = FxHashSet::default();
356
357        graph_ref.traverse_cycles(
358            // No need to traverse edges that are unused.
359            |e| e.chunking_type.is_parallel() && !unused_references.contains_key(&e.reference),
360            |cycle| {
361                // We could compute this based on the module graph via a DFS from each entry point
362                // to the cycle.  Whatever node is hit first is an entry point to the cycle.
363                // (scope hoisting does something similar) and then we would only need to
364                // mark 'entry' modules (basically the targets of back edges in the export graph) as
365                // circuit breakers.  For now we just mark everything on the theory that cycles are
366                // rare.  For vercel-site on 8/22/2025 there were 106 cycles covering 800 modules
367                // (or 1.2% of all modules).  So with this analysis we could potentially drop 80% of
368                // the cycle breaker modules.
369                export_circuit_breakers.extend(cycle.iter().map(|n| **n));
370                Ok(())
371            },
372        )?;
373
374        span.record("visit_count", visit_count);
375        span.record("unused_reference_count", unused_references.len());
376
377        #[cfg(debug_assertions)]
378        {
379            use std::sync::LazyLock;
380            static PRINT_UNUSED_REFERENCES: LazyLock<bool> = LazyLock::new(|| {
381                std::env::var_os("TURBOPACK_PRINT_UNUSED_REFERENCES")
382                    .is_some_and(|v| v == "1" || v == "true")
383            });
384            if *PRINT_UNUSED_REFERENCES {
385                use turbo_tasks::TryJoinIterExt;
386                println!(
387                    "unused references: {:#?}",
388                    debug_unused_references_name
389                        .iter()
390                        .map(async |(s, e, t)| Ok((
391                            s.ident_string().await?,
392                            e,
393                            t.ident_string().await?,
394                        )))
395                        .try_join()
396                        .await?
397                );
398            }
399
400            static PRINT_USED_EXPORTS: LazyLock<bool> = LazyLock::new(|| {
401                std::env::var_os("TURBOPACK_PRINT_USED_EXPORTS")
402                    .is_some_and(|v| v == "1" || v == "true")
403            });
404            if *PRINT_USED_EXPORTS {
405                use turbo_tasks::TryJoinIterExt;
406                println!(
407                    "used exports: {:#?}",
408                    used_exports
409                        .iter()
410                        .map(async |(m, v)| Ok((m.ident_string().await?, v,)))
411                        .try_join()
412                        .await?
413                );
414            }
415        }
416
417        Ok(BindingUsageInfo {
418            unused_references: ResolvedVc::cell(unused_references),
419            unused_references_edges: ResolvedVc::cell(unused_references_edges),
420            used_exports: ResolvedVc::cell(used_exports),
421            export_circuit_breakers: ResolvedVc::cell(export_circuit_breakers),
422            partial_namespace_modules: ResolvedVc::cell(partial_namespace_modules),
423        }
424        .cell())
425    }
426    .instrument(span_outer)
427    .await
428}
429
430#[turbo_tasks::value]
431#[derive(Default, Clone, Debug)]
432pub enum ModuleExportUsageInfo {
433    /// Only the side effects are needed, no exports is used.
434    #[default]
435    Evaluation,
436    Exports(AutoSet<RcStr>),
437    All,
438}
439
440#[turbo_tasks::value_impl]
441impl ModuleExportUsageInfo {
442    #[turbo_tasks::function]
443    pub fn all() -> Vc<Self> {
444        ModuleExportUsageInfo::All.cell()
445    }
446}
447
448impl ModuleExportUsageInfo {
449    /// Merge the given usage into self. Returns true if Self changed.
450    pub fn add(&mut self, usage: &ExportUsage) -> bool {
451        match (&mut *self, usage) {
452            (Self::All, _) => false,
453            (_, ExportUsage::All) => {
454                *self = Self::All;
455                true
456            }
457            (Self::Evaluation, ExportUsage::Named(name)) => {
458                // Promote evaluation to something more specific
459                *self = Self::Exports(AutoSet::from_iter([name.clone()]));
460                true
461            }
462            (Self::Evaluation, ExportUsage::PartialNamespaceObject(names)) => {
463                *self = Self::Exports(AutoSet::from_iter(names.iter().cloned()));
464                true
465            }
466            (Self::Exports(l), ExportUsage::Named(r)) => {
467                // Merge exports
468                l.insert(r.clone())
469            }
470            (Self::Exports(l), ExportUsage::PartialNamespaceObject(names)) => {
471                let mut changed = false;
472                for name in names {
473                    changed |= l.insert(name.clone());
474                }
475                changed
476            }
477            (_, ExportUsage::Evaluation) => false,
478            (_, ExportUsage::Passthrough { .. }) => {
479                // Passthrough is normally resolved before `add`. If it reaches this fallback,
480                // preserve correctness by widening rather than panicking during graph analysis.
481                *self = Self::All;
482                true
483            }
484        }
485    }
486
487    /// Merge another module's resolved export usage into this one. Returns true if self changed.
488    fn add_usage_info(&mut self, usage: &Self) -> bool {
489        match (&mut *self, usage) {
490            (Self::All, _) | (_, Self::Evaluation) => false,
491            (_, Self::All) => {
492                *self = Self::All;
493                true
494            }
495            (Self::Evaluation, Self::Exports(exports)) => {
496                *self = Self::Exports(exports.clone());
497                true
498            }
499            (Self::Exports(left), Self::Exports(right)) => {
500                let mut changed = false;
501                for export in right {
502                    changed |= left.insert(export.clone());
503                }
504                changed
505            }
506        }
507    }
508
509    pub fn is_export_used(&self, export: &RcStr) -> bool {
510        match self {
511            Self::All => true,
512            Self::Evaluation => false,
513            Self::Exports(exports) => exports.contains(export),
514        }
515    }
516}
517
518#[cfg(test)]
519mod tests {
520    use turbo_rcstr::rcstr;
521
522    use super::ModuleExportUsageInfo;
523    use crate::resolve::ExportUsage;
524
525    #[test]
526    fn resolved_usage_join_is_monotonic() {
527        let mut usage = ModuleExportUsageInfo::Evaluation;
528        let first = ModuleExportUsageInfo::Exports([rcstr!("first")].into_iter().collect());
529        let second = ModuleExportUsageInfo::Exports([rcstr!("second")].into_iter().collect());
530
531        assert!(!usage.add_usage_info(&ModuleExportUsageInfo::Evaluation));
532        assert!(usage.add_usage_info(&first));
533        assert!(usage.is_export_used(&rcstr!("first")));
534        assert!(!usage.add_usage_info(&first));
535        assert!(usage.add_usage_info(&second));
536        assert!(usage.is_export_used(&rcstr!("second")));
537        assert!(!usage.add_usage_info(&ModuleExportUsageInfo::Evaluation));
538        assert!(usage.add_usage_info(&ModuleExportUsageInfo::All));
539        assert!(!usage.add_usage_info(&second));
540        assert!(matches!(usage, ModuleExportUsageInfo::All));
541    }
542
543    #[test]
544    fn unresolved_passthrough_falls_back_to_all() {
545        let mut usage = ModuleExportUsageInfo::Evaluation;
546
547        assert!(usage.add(&ExportUsage::Passthrough {
548            namespace_object_may_escape: false,
549        }));
550        assert!(matches!(usage, ModuleExportUsageInfo::All));
551    }
552}
553
554#[cfg(test)]
555mod graph_tests {
556    use anyhow::Result;
557    use rustc_hash::FxHashMap;
558    use turbo_rcstr::{RcStr, rcstr};
559    use turbo_tasks::{ResolvedVc, TryJoinIterExt, Vc};
560    use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
561    use turbo_tasks_fs::{FileSystem, FileSystemPath, VirtualFileSystem};
562
563    use super::compute_binding_usage_info;
564    use crate::{
565        asset::{Asset, AssetContent},
566        ident::AssetIdent,
567        module::{Module, ModuleSideEffects},
568        module_graph::{
569            GraphEntries, ModuleGraph, SingleModuleGraph,
570            chunk_group_info::{ChunkGroupEntry, EntryHeuristics},
571        },
572        reference::{ModuleReference, ModuleReferences, SingleChunkableModuleReference},
573        resolve::ExportUsage,
574    };
575
576    /// Module name -> outgoing edges `(target name, export usage of the edge)`, in reference order.
577    type Edges = Vec<(RcStr, Vec<(RcStr, ExportUsage)>)>;
578
579    #[turbo_tasks::value(shared)]
580    struct UsageTestRepo {
581        edges: FxHashMap<RcStr, Vec<(RcStr, ExportUsage)>>,
582    }
583
584    #[turbo_tasks::value]
585    struct UsageTestModule {
586        root: FileSystemPath,
587        name: RcStr,
588        repo: ResolvedVc<UsageTestRepo>,
589    }
590
591    #[turbo_tasks::value_impl]
592    impl UsageTestModule {
593        /// Interned through a task so that every reference to the same name yields the same module.
594        #[turbo_tasks::function]
595        fn new(root: FileSystemPath, name: RcStr, repo: ResolvedVc<UsageTestRepo>) -> Vc<Self> {
596            Self { root, name, repo }.cell()
597        }
598    }
599
600    #[turbo_tasks::value_impl]
601    impl Asset for UsageTestModule {
602        #[turbo_tasks::function]
603        fn content(&self) -> Vc<AssetContent> {
604            panic!("UsageTestModule::content shouldn't be called")
605        }
606    }
607
608    #[turbo_tasks::value_impl]
609    impl Module for UsageTestModule {
610        #[turbo_tasks::function]
611        fn ident(&self) -> Result<Vc<AssetIdent>> {
612            Ok(AssetIdent::from_path(self.root.join(&self.name)?).into_vc())
613        }
614
615        #[turbo_tasks::function]
616        fn source(&self) -> Vc<crate::source::OptionSource> {
617            Vc::cell(None)
618        }
619
620        #[turbo_tasks::function]
621        async fn references(&self) -> Result<Vc<ModuleReferences>> {
622            let repo = self.repo.await?;
623            let references = repo
624                .edges
625                .get(&self.name)
626                .into_iter()
627                .flatten()
628                .map(async |(target, export)| {
629                    let target =
630                        UsageTestModule::new(self.root.clone(), target.clone(), *self.repo);
631                    Ok(ResolvedVc::upcast::<Box<dyn ModuleReference>>(
632                        SingleChunkableModuleReference::new(
633                            Vc::upcast(target),
634                            rcstr!("test-dep"),
635                            export_usage_vc(export),
636                        )
637                        .to_resolved()
638                        .await?,
639                    ))
640                })
641                .try_join()
642                .await?;
643            Ok(Vc::cell(references))
644        }
645
646        #[turbo_tasks::function]
647        fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects> {
648            ModuleSideEffects::SideEffectful.cell()
649        }
650    }
651
652    /// `ExportUsage` cells are private to `resolve`, so go through its public constructors.
653    fn export_usage_vc(export: &ExportUsage) -> Vc<ExportUsage> {
654        match export {
655            ExportUsage::All => ExportUsage::all(),
656            ExportUsage::Named(name) => ExportUsage::named(name.clone()),
657            ExportUsage::PartialNamespaceObject(names) => {
658                ExportUsage::partial_namespace_object(names.to_vec())
659            }
660            ExportUsage::Evaluation => ExportUsage::evaluation(),
661            ExportUsage::Passthrough {
662                namespace_object_may_escape,
663            } => ExportUsage::passthrough(*namespace_object_may_escape),
664        }
665    }
666
667    /// `(module name, namespace_object_may_escape)` for every module in the graph, sorted by name.
668    #[turbo_tasks::value(transparent)]
669    struct NamespaceEscapes(Vec<(RcStr, bool)>);
670
671    #[turbo_tasks::function(operation, root)]
672    async fn namespace_escapes_operation(
673        namespace_path_is_longer: bool,
674        plain_usage_is_all: bool,
675    ) -> Result<Vc<NamespaceEscapes>> {
676        let plain_usage = if plain_usage_is_all {
677            ExportUsage::All
678        } else {
679            ExportUsage::Named(rcstr!("a"))
680        };
681        let edges = namespace_graph(namespace_path_is_longer, plain_usage);
682        let root = VirtualFileSystem::new_with_name(rcstr!("test"))
683            .root()
684            .owned()
685            .await?;
686        let names = edges
687            .iter()
688            .flat_map(|(from, to)| {
689                std::iter::once(from.clone()).chain(to.iter().map(|(t, _)| t.clone()))
690            })
691            .collect::<std::collections::BTreeSet<_>>();
692        let repo = UsageTestRepo {
693            edges: edges.into_iter().collect(),
694        }
695        .resolved_cell();
696        let module = async |name: &RcStr| -> Result<ResolvedVc<Box<dyn Module>>> {
697            Ok(ResolvedVc::upcast(
698                UsageTestModule::new(root.clone(), name.clone(), *repo)
699                    .to_resolved()
700                    .await?,
701            ))
702        };
703
704        let single_graph = SingleModuleGraph::new_with_entries(
705            GraphEntries::resolved_cell(GraphEntries::new(
706                vec![ChunkGroupEntry::Entry {
707                    modules: vec![module(&rcstr!("entry")).await?],
708                    heuristics: EntryHeuristics::default(),
709                }],
710                vec![],
711            )),
712            false,
713            true,
714        );
715        let graph = ModuleGraph::from_graphs(vec![single_graph], None);
716        let binding_usage = compute_binding_usage_info(graph, false).connect().await?;
717
718        let escapes = names
719            .iter()
720            .map(async |name| {
721                let usage = binding_usage
722                    .used_exports(module(name).await?)
723                    .await?
724                    .await?;
725                Ok((name.clone(), usage.namespace_object_may_escape))
726            })
727            .try_join()
728            .await?;
729        Ok(Vc::cell(escapes))
730    }
731
732    async fn namespace_escapes(
733        namespace_path_is_longer: bool,
734        plain_usage_is_all: bool,
735    ) -> Vec<(RcStr, bool)> {
736        let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
737            BackendOptions::default(),
738            noop_backing_storage(),
739        ));
740        tt.run_once(async move {
741            let escapes = namespace_escapes_operation(namespace_path_is_longer, plain_usage_is_all)
742                .read_strongly_consistent()
743                .await?;
744            Ok((*escapes).clone())
745        })
746        .await
747        .unwrap()
748    }
749
750    fn namespace_a() -> ExportUsage {
751        ExportUsage::PartialNamespaceObject([rcstr!("a")].into_iter().collect())
752    }
753
754    fn reexport_all() -> ExportUsage {
755        // `export * from "y"`
756        ExportUsage::Passthrough {
757            namespace_object_may_escape: false,
758        }
759    }
760
761    /// `x` is reached directly from `entry` and through `entry -> m1 -> m2 -> x`; `x -> y` is an
762    /// `export *` passthrough edge.
763    ///
764    /// When `namespace_path_is_longer` is true, the plain edge reaches `x` first and its
765    /// passthrough is processed before the namespace edge. In that order, the namespace usage does
766    /// not add any export to `x`. Otherwise namespace provenance reaches `x` first.
767    fn namespace_graph(namespace_path_is_longer: bool, plain_usage: ExportUsage) -> Edges {
768        let (direct_usage, late_usage) = if namespace_path_is_longer {
769            (plain_usage, namespace_a())
770        } else {
771            (namespace_a(), plain_usage)
772        };
773        vec![
774            (
775                rcstr!("entry"),
776                vec![
777                    (rcstr!("x"), direct_usage),
778                    (rcstr!("m1"), ExportUsage::All),
779                ],
780            ),
781            (rcstr!("m1"), vec![(rcstr!("m2"), ExportUsage::All)]),
782            (rcstr!("m2"), vec![(rcstr!("x"), late_usage)]),
783            (rcstr!("x"), vec![(rcstr!("y"), reexport_all())]),
784        ]
785    }
786
787    fn expected_escapes() -> Vec<(RcStr, bool)> {
788        vec![
789            (rcstr!("entry"), false),
790            (rcstr!("m1"), false),
791            (rcstr!("m2"), false),
792            (rcstr!("x"), true),
793            (rcstr!("y"), true),
794        ]
795    }
796
797    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
798    async fn late_namespace_usage_of_all_module_reaches_passthrough_target() {
799        assert_eq!(namespace_escapes(true, true).await, expected_escapes());
800    }
801
802    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
803    async fn late_namespace_usage_of_already_used_name_reaches_passthrough_target() {
804        assert_eq!(namespace_escapes(true, false).await, expected_escapes());
805    }
806
807    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
808    async fn early_namespace_usage_of_all_module_reaches_passthrough_target() {
809        assert_eq!(namespace_escapes(false, true).await, expected_escapes());
810    }
811
812    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
813    async fn early_namespace_usage_of_already_used_name_reaches_passthrough_target() {
814        assert_eq!(namespace_escapes(false, false).await, expected_escapes());
815    }
816}