fob_graph/memory/
exports.rs

1//! Export analysis methods for ModuleGraph.
2
3use std::sync::Arc;
4
5use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
6
7use super::super::import::{ImportKind, ImportSpecifier};
8use super::super::{ExportKind, Module, ModuleId};
9use super::graph::{GraphInner, ModuleGraph};
10use crate::{Error, Result};
11
12impl ModuleGraph {
13    /// Discover unused exports, respecting framework markers and namespace imports.
14    pub fn unused_exports(&self) -> Result<Vec<super::super::UnusedExport>> {
15        let inner = self.inner.read();
16        let mut unused = Vec::new();
17
18        for module in inner.modules.values() {
19            if module.is_entry {
20                continue;
21            }
22
23            for export in module.exports.iter() {
24                if export.is_framework_used {
25                    continue;
26                }
27
28                if !Self::is_export_used_inner(&inner, &module.id, &export.name)? {
29                    unused.push(super::super::UnusedExport {
30                        module_id: module.id.clone(),
31                        export: export.clone(),
32                    });
33                }
34            }
35        }
36
37        Ok(unused)
38    }
39
40    pub(super) fn is_export_used_inner(
41        inner: &GraphInner,
42        module_id: &ModuleId,
43        export_name: &str,
44    ) -> Result<bool> {
45        let dependents = inner.dependents.get(module_id).cloned().unwrap_or_default();
46
47        for importer_id in dependents {
48            if let Some(importer) = inner.modules.get(&importer_id) {
49                for import_record in importer.imports.iter() {
50                    if import_record.resolved_to.as_ref() != Some(module_id) {
51                        continue;
52                    }
53
54                    if import_record.specifiers.is_empty() {
55                        // Side-effect import does not use exports.
56                        continue;
57                    }
58
59                    let is_used =
60                        import_record
61                            .specifiers
62                            .iter()
63                            .any(|specifier| match specifier {
64                                ImportSpecifier::Named(name) => name == export_name,
65                                ImportSpecifier::Default => export_name == "default",
66                                ImportSpecifier::Namespace(_) => {
67                                    // True namespace imports (import * as X) use ALL exports
68                                    // But star re-exports (export * from) only forward, not use
69                                    !matches!(import_record.kind, ImportKind::ReExport)
70                                }
71                            });
72
73                    if is_used {
74                        return Ok(true);
75                    }
76                }
77            }
78        }
79
80        // Check if this export is re-exported by other modules and used transitively
81        // This handles cases like: validators.ts exports validateEmail -> helpers.ts does
82        // export * from validators.ts -> demo.tsx imports { validateEmail } from helpers.ts
83
84        // Get the source module's path for comparison (re_exported_from uses path, not module ID)
85        let source_module = inner.modules.get(module_id).ok_or_else(|| {
86            Error::InvalidConfig(format!("Module {} not found in graph", module_id))
87        })?;
88        let source_path = source_module.path.to_string_lossy();
89
90        for (re_exporter_id, re_exporter_module) in &inner.modules {
91            for export in re_exporter_module.exports.iter() {
92                match export.kind {
93                    ExportKind::StarReExport => {
94                        // Star re-export: check if it's from our module
95                        if let Some(ref re_exported_from) = export.re_exported_from {
96                            if re_exported_from == source_path.as_ref() {
97                                // This module re-exports all exports from our module
98                                // Recursively check if this re-exporting module's export is used
99                                if Self::is_export_used_inner(inner, re_exporter_id, export_name)? {
100                                    return Ok(true);
101                                }
102                            }
103                        }
104                    }
105                    ExportKind::ReExport => {
106                        // Named re-export: check if it matches our export
107                        if export.name == export_name {
108                            if let Some(ref re_exported_from) = export.re_exported_from {
109                                if re_exported_from == source_path.as_ref() {
110                                    // This is a named re-export of our specific export
111                                    // Recursively check if THIS re-export is used
112                                    if Self::is_export_used_inner(
113                                        inner,
114                                        re_exporter_id,
115                                        &export.name,
116                                    )? {
117                                        return Ok(true);
118                                    }
119                                }
120                            }
121                        }
122                    }
123                    _ => {
124                        // Named, Default, TypeOnly - not re-exports, skip
125                    }
126                }
127            }
128        }
129
130        Ok(false)
131    }
132
133    /// Computes and sets usage counts for all exports in the module graph.
134    ///
135    /// For each export in each module, this counts how many times it's imported
136    /// across all dependent modules and updates the `usage_count` field.
137    ///
138    /// Usage counts are determined by:
139    /// - Named imports: Each `import { foo }` increments the count for export "foo"
140    /// - Default imports: Each `import foo` increments the count for export "default"
141    /// - Namespace imports: Each `import * as ns` increments the count for ALL exports by 1
142    ///   (except star re-exports which only forward, not consume)
143    /// - Re-exports: Counted separately as they create new import paths
144    ///
145    /// After calling this method, each Export will have `usage_count` set to:
146    /// - `Some(0)` if the export is unused
147    /// - `Some(n)` where n > 0 for the number of import sites
148    pub fn compute_export_usage_counts(&self) -> Result<()> {
149        // 1. Snapshot only IDs (not full HashMap)
150        let module_ids: Vec<ModuleId> = {
151            let inner = self.inner.read();
152            inner.modules.keys().cloned().collect()
153        };
154
155        // 2. Process each module with brief read locks
156        let mut updates = HashMap::default();
157        for module_id in module_ids {
158            let (module, dependents) = {
159                let inner = self.inner.read();
160                // Skip modules that were removed concurrently
161                let Some(module_arc) = inner.modules.get(&module_id) else {
162                    continue;
163                };
164                let module = (**module_arc).clone();
165                let dependents = inner
166                    .dependents
167                    .get(&module_id)
168                    .cloned()
169                    .unwrap_or_default();
170                (module, dependents)
171            }; // Lock released here
172
173            let mut updated_module = module;
174            // Use Arc::make_mut to get mutable access to exports
175            let exports = std::sync::Arc::make_mut(&mut updated_module.exports);
176            for export in exports.iter_mut() {
177                let count = {
178                    // Get importer modules for counting
179                    let inner = self.inner.read();
180                    Self::count_export_usage_standalone(
181                        &inner.modules,
182                        &module_id,
183                        &export.name,
184                        &dependents,
185                    )?
186                };
187                export.set_usage_count(count);
188            }
189
190            updates.insert(module_id, std::sync::Arc::new(updated_module));
191        }
192
193        // 3. Apply updates with single write lock
194        {
195            let mut inner = self.inner.write();
196            for (id, module) in updates {
197                inner.modules.insert(id, module);
198            }
199        }
200
201        Ok(())
202    }
203
204    /// Standalone helper to count export usage.
205    ///
206    /// This works with a read lock on modules HashMap, counting how many times
207    /// an export is imported by dependent modules.
208    fn count_export_usage_standalone(
209        modules: &HashMap<ModuleId, Arc<Module>>,
210        module_id: &ModuleId,
211        export_name: &str,
212        dependents: &HashSet<ModuleId>,
213    ) -> Result<usize> {
214        let mut count = 0;
215
216        for importer_id in dependents {
217            if let Some(importer) = modules.get(importer_id) {
218                for import_record in importer.imports.iter() {
219                    if import_record.resolved_to.as_ref() != Some(module_id) {
220                        continue;
221                    }
222
223                    if import_record.specifiers.is_empty() {
224                        // Side-effect import does not use exports.
225                        continue;
226                    }
227
228                    // Count matching specifiers
229                    for specifier in &import_record.specifiers {
230                        let matches = match specifier {
231                            ImportSpecifier::Named(name) => name == export_name,
232                            ImportSpecifier::Default => export_name == "default",
233                            ImportSpecifier::Namespace(_) => {
234                                // Namespace imports (import * as X) use ALL exports once
235                                // But star re-exports (export * from) only forward, not use
236                                !matches!(import_record.kind, ImportKind::ReExport)
237                            }
238                        };
239
240                        if matches {
241                            count += 1;
242                            // For namespace imports, we only count once per import statement
243                            // not once per export, so we break here
244                            if matches!(specifier, ImportSpecifier::Namespace(_)) {
245                                break;
246                            }
247                        }
248                    }
249                }
250            }
251        }
252
253        Ok(count)
254    }
255}