Skip to main content

tsz_core/
exports.rs

1//! Export Tracking and Analysis
2//!
3//! This module provides data structures and utilities for tracking exports
4//! in TypeScript/JavaScript source files, including:
5//! - Named exports
6//! - Default exports
7//! - Re-exports (including `export * from`)
8//! - CommonJS module.exports
9
10use crate::binder::SymbolId;
11use crate::parser::NodeIndex;
12use rustc_hash::{FxHashMap, FxHashSet};
13
14/// Kind of export statement
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum ExportKind {
17    /// Named export: `export { foo }`
18    Named,
19    /// Default export: `export default foo`
20    Default,
21    /// Named export from declaration: `export function foo() {}`
22    Declaration,
23    /// Re-export: `export { foo } from 'mod'`
24    ReExport,
25    /// Namespace re-export: `export * from 'mod'`
26    NamespaceReExport,
27    /// Namespace re-export with alias: `export * as ns from 'mod'`
28    NamespaceReExportAs,
29    /// Type-only export: `export type { Foo }`
30    TypeOnly,
31    /// CommonJS: `module.exports = ...`
32    CommonJSDefault,
33    /// CommonJS: `exports.foo = ...`
34    CommonJSNamed,
35}
36
37/// Information about a single exported binding
38#[derive(Debug, Clone)]
39pub struct ExportedBinding {
40    /// Exported name (name visible to importers)
41    pub exported_name: String,
42    /// Local name in this file (may differ from `exported_name`)
43    pub local_name: String,
44    /// Kind of export
45    pub kind: ExportKind,
46    /// AST node index of the export declaration
47    pub declaration_node: NodeIndex,
48    /// AST node index of the local declaration (if any)
49    pub local_declaration_node: NodeIndex,
50    /// Symbol ID for the exported binding
51    pub symbol_id: SymbolId,
52    /// Whether this is a type-only export
53    pub is_type_only: bool,
54    /// For re-exports, the source module specifier
55    pub source_module: Option<String>,
56    /// For re-exports, the original name in the source module
57    pub original_name: Option<String>,
58}
59
60/// Information about an export declaration
61#[derive(Debug, Clone)]
62pub struct ExportDeclaration {
63    /// AST node index
64    pub node: NodeIndex,
65    /// Individual bindings from this export
66    pub bindings: Vec<ExportedBinding>,
67    /// Whether the entire export is type-only
68    pub is_type_only: bool,
69    /// For re-exports, the source module
70    pub from_module: Option<String>,
71    /// Source position (start)
72    pub start: u32,
73    /// Source position (end)
74    pub end: u32,
75}
76
77/// Namespace re-export: `export * from 'module'` or `export * as ns from 'module'`
78#[derive(Debug, Clone)]
79pub struct NamespaceReExport {
80    /// Source module specifier
81    pub module_specifier: String,
82    /// Alias name (for `export * as ns from 'mod'`)
83    pub alias: Option<String>,
84    /// AST node index
85    pub node: NodeIndex,
86    /// Source position
87    pub start: u32,
88    pub end: u32,
89}
90
91/// Tracks all exports in a source file
92#[derive(Debug, Default)]
93pub struct ExportTracker {
94    /// All export declarations in this file
95    pub declarations: Vec<ExportDeclaration>,
96    /// Map from exported name to binding
97    pub bindings_by_name: FxHashMap<String, ExportedBinding>,
98    /// Default export (if any)
99    pub default_export: Option<ExportedBinding>,
100    /// Type-only exports by name
101    pub type_only_exports: FxHashSet<String>,
102    /// Namespace re-exports (`export * from`)
103    pub namespace_reexports: Vec<NamespaceReExport>,
104    /// Named re-exports by exported name
105    pub reexports: FxHashMap<String, ExportedBinding>,
106    /// All modules this file re-exports from
107    pub reexport_sources: FxHashSet<String>,
108    /// CommonJS exports
109    pub commonjs_exports: Vec<CommonJSExport>,
110    /// Whether this file has a default export
111    pub has_default_export: bool,
112    /// Whether this file uses CommonJS exports
113    pub has_commonjs_exports: bool,
114}
115
116/// CommonJS export information
117#[derive(Debug, Clone)]
118pub struct CommonJSExport {
119    /// Export kind (default or named)
120    pub kind: ExportKind,
121    /// Property name (for `exports.foo = ...`)
122    pub property_name: Option<String>,
123    /// AST node index
124    pub node: NodeIndex,
125    /// Source position
126    pub start: u32,
127    pub end: u32,
128}
129
130impl ExportTracker {
131    /// Create a new export tracker
132    pub fn new() -> Self {
133        Self::default()
134    }
135
136    /// Add an export declaration
137    pub fn add_declaration(&mut self, decl: ExportDeclaration) {
138        // Track re-export sources
139        if let Some(ref from) = decl.from_module {
140            self.reexport_sources.insert(from.clone());
141        }
142
143        // Process bindings
144        for binding in &decl.bindings {
145            match binding.kind {
146                ExportKind::Default => {
147                    self.default_export = Some(binding.clone());
148                    self.has_default_export = true;
149                }
150                ExportKind::ReExport | ExportKind::NamespaceReExportAs => {
151                    self.reexports
152                        .insert(binding.exported_name.clone(), binding.clone());
153                }
154                _ => {
155                    self.bindings_by_name
156                        .insert(binding.exported_name.clone(), binding.clone());
157                }
158            }
159
160            if binding.is_type_only {
161                self.type_only_exports.insert(binding.exported_name.clone());
162            }
163        }
164
165        self.declarations.push(decl);
166    }
167
168    /// Add a namespace re-export
169    pub fn add_namespace_reexport(&mut self, reexport: NamespaceReExport) {
170        self.reexport_sources
171            .insert(reexport.module_specifier.clone());
172
173        if let Some(ref alias) = reexport.alias {
174            // `export * as ns from 'mod'` creates a named export
175            let binding = ExportedBinding {
176                exported_name: alias.clone(),
177                local_name: alias.clone(),
178                kind: ExportKind::NamespaceReExportAs,
179                declaration_node: reexport.node,
180                local_declaration_node: NodeIndex::NONE,
181                symbol_id: SymbolId::NONE,
182                is_type_only: false,
183                source_module: Some(reexport.module_specifier.clone()),
184                original_name: None,
185            };
186            self.bindings_by_name.insert(alias.clone(), binding);
187        }
188
189        self.namespace_reexports.push(reexport);
190    }
191
192    /// Add a CommonJS export
193    pub fn add_commonjs_export(&mut self, export: CommonJSExport) {
194        self.has_commonjs_exports = true;
195
196        if matches!(export.kind, ExportKind::CommonJSDefault) {
197            self.has_default_export = true;
198        }
199
200        self.commonjs_exports.push(export);
201    }
202
203    /// Get an exported binding by name
204    pub fn get_export(&self, name: &str) -> Option<&ExportedBinding> {
205        if name == "default" {
206            return self.default_export.as_ref();
207        }
208
209        self.bindings_by_name
210            .get(name)
211            .or_else(|| self.reexports.get(name))
212    }
213
214    /// Check if a name is exported
215    pub fn is_exported(&self, name: &str) -> bool {
216        if name == "default" {
217            return self.has_default_export;
218        }
219
220        self.bindings_by_name.contains_key(name) || self.reexports.contains_key(name)
221    }
222
223    /// Check if a name is a type-only export
224    pub fn is_type_only_export(&self, name: &str) -> bool {
225        self.type_only_exports.contains(name)
226    }
227
228    /// Get all exported names
229    pub fn get_exported_names(&self) -> impl Iterator<Item = &String> {
230        let named = self.bindings_by_name.keys();
231        let reexported = self.reexports.keys();
232        named.chain(reexported)
233    }
234
235    /// Get all direct exports (excluding re-exports)
236    pub fn get_direct_exports(&self) -> impl Iterator<Item = &ExportedBinding> {
237        self.bindings_by_name.values()
238    }
239
240    /// Get all re-exports
241    pub fn get_reexports(&self) -> impl Iterator<Item = &ExportedBinding> {
242        self.reexports.values()
243    }
244
245    /// Check if this file re-exports from a module
246    pub fn reexports_from(&self, specifier: &str) -> bool {
247        self.reexport_sources.contains(specifier)
248    }
249
250    /// Get statistics about exports
251    pub fn stats(&self) -> ExportStats {
252        let mut stats = ExportStats::default();
253
254        for binding in self.bindings_by_name.values() {
255            match binding.kind {
256                ExportKind::Named | ExportKind::Declaration => stats.named_exports += 1,
257                ExportKind::Default => stats.default_exports += 1,
258                ExportKind::TypeOnly => stats.type_only_exports += 1,
259                _ => {}
260            }
261        }
262
263        if self.has_default_export {
264            stats.default_exports = 1;
265        }
266
267        stats.reexports = self.reexports.len();
268        stats.namespace_reexports = self.namespace_reexports.len();
269        stats.commonjs_exports = self.commonjs_exports.len();
270        stats.reexport_sources = self.reexport_sources.len();
271
272        stats
273    }
274
275    /// Clear all tracked exports
276    pub fn clear(&mut self) {
277        self.declarations.clear();
278        self.bindings_by_name.clear();
279        self.default_export = None;
280        self.type_only_exports.clear();
281        self.namespace_reexports.clear();
282        self.reexports.clear();
283        self.reexport_sources.clear();
284        self.commonjs_exports.clear();
285        self.has_default_export = false;
286        self.has_commonjs_exports = false;
287    }
288
289    /// Resolve an export considering re-exports
290    ///
291    /// This follows re-export chains to find the original export.
292    /// Returns the module specifier and export name to look up.
293    pub fn resolve_export(&self, name: &str) -> ExportResolution {
294        // Check direct exports first
295        if let Some(binding) = self.bindings_by_name.get(name)
296            && binding.source_module.is_none()
297        {
298            return ExportResolution::Direct(binding.clone());
299        }
300
301        // Check named re-exports
302        if let Some(binding) = self.reexports.get(name)
303            && let Some(ref source) = binding.source_module
304        {
305            let original = binding.original_name.as_deref().unwrap_or(name);
306            return ExportResolution::ReExport {
307                source_module: source.clone(),
308                original_name: original.to_string(),
309            };
310        }
311
312        // Check namespace re-exports (export * from)
313        if !self.namespace_reexports.is_empty() {
314            return ExportResolution::PossibleNamespaceReExport {
315                sources: self
316                    .namespace_reexports
317                    .iter()
318                    .filter(|r| r.alias.is_none())
319                    .map(|r| r.module_specifier.clone())
320                    .collect(),
321                name: name.to_string(),
322            };
323        }
324
325        ExportResolution::NotFound
326    }
327}
328
329/// Result of resolving an export
330#[derive(Debug, Clone)]
331pub enum ExportResolution {
332    /// Export is defined directly in this file
333    Direct(ExportedBinding),
334    /// Export is re-exported from another module
335    ReExport {
336        source_module: String,
337        original_name: String,
338    },
339    /// Export might come from a namespace re-export
340    PossibleNamespaceReExport { sources: Vec<String>, name: String },
341    /// Export not found
342    NotFound,
343}
344
345/// Statistics about exports in a file
346#[derive(Debug, Clone, Default)]
347pub struct ExportStats {
348    pub named_exports: usize,
349    pub default_exports: usize,
350    pub type_only_exports: usize,
351    pub reexports: usize,
352    pub namespace_reexports: usize,
353    pub commonjs_exports: usize,
354    pub reexport_sources: usize,
355}
356
357/// Builder for creating `ExportedBinding`
358pub struct ExportedBindingBuilder {
359    exported_name: String,
360    local_name: String,
361    kind: ExportKind,
362    declaration_node: NodeIndex,
363    local_declaration_node: NodeIndex,
364    symbol_id: SymbolId,
365    is_type_only: bool,
366    source_module: Option<String>,
367    original_name: Option<String>,
368}
369
370impl ExportedBindingBuilder {
371    pub fn new(exported_name: impl Into<String>) -> Self {
372        let name = exported_name.into();
373        Self {
374            local_name: name.clone(),
375            exported_name: name,
376            kind: ExportKind::Named,
377            declaration_node: NodeIndex::NONE,
378            local_declaration_node: NodeIndex::NONE,
379            symbol_id: SymbolId::NONE,
380            is_type_only: false,
381            source_module: None,
382            original_name: None,
383        }
384    }
385
386    pub fn local_name(mut self, name: impl Into<String>) -> Self {
387        self.local_name = name.into();
388        self
389    }
390
391    pub const fn kind(mut self, kind: ExportKind) -> Self {
392        self.kind = kind;
393        self
394    }
395
396    pub const fn declaration_node(mut self, node: NodeIndex) -> Self {
397        self.declaration_node = node;
398        self
399    }
400
401    pub const fn local_declaration_node(mut self, node: NodeIndex) -> Self {
402        self.local_declaration_node = node;
403        self
404    }
405
406    pub const fn symbol_id(mut self, id: SymbolId) -> Self {
407        self.symbol_id = id;
408        self
409    }
410
411    pub const fn type_only(mut self, is_type_only: bool) -> Self {
412        self.is_type_only = is_type_only;
413        self
414    }
415
416    pub fn source_module(mut self, module: impl Into<String>) -> Self {
417        self.source_module = Some(module.into());
418        self
419    }
420
421    pub fn original_name(mut self, name: impl Into<String>) -> Self {
422        self.original_name = Some(name.into());
423        self
424    }
425
426    pub fn build(self) -> ExportedBinding {
427        ExportedBinding {
428            exported_name: self.exported_name,
429            local_name: self.local_name,
430            kind: self.kind,
431            declaration_node: self.declaration_node,
432            local_declaration_node: self.local_declaration_node,
433            symbol_id: self.symbol_id,
434            is_type_only: self.is_type_only,
435            source_module: self.source_module,
436            original_name: self.original_name,
437        }
438    }
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444
445    #[test]
446    fn test_export_tracker_basic() {
447        let mut tracker = ExportTracker::new();
448
449        let binding = ExportedBindingBuilder::new("useState")
450            .kind(ExportKind::Named)
451            .build();
452
453        let decl = ExportDeclaration {
454            node: NodeIndex::NONE,
455            bindings: vec![binding],
456            is_type_only: false,
457            from_module: None,
458            start: 0,
459            end: 25,
460        };
461
462        tracker.add_declaration(decl);
463
464        assert!(tracker.is_exported("useState"));
465        assert!(!tracker.is_exported("useEffect"));
466    }
467
468    #[test]
469    fn test_export_tracker_default() {
470        let mut tracker = ExportTracker::new();
471
472        let binding = ExportedBindingBuilder::new("default")
473            .local_name("MyComponent")
474            .kind(ExportKind::Default)
475            .build();
476
477        let decl = ExportDeclaration {
478            node: NodeIndex::NONE,
479            bindings: vec![binding],
480            is_type_only: false,
481            from_module: None,
482            start: 0,
483            end: 30,
484        };
485
486        tracker.add_declaration(decl);
487
488        assert!(tracker.has_default_export);
489        assert!(tracker.is_exported("default"));
490    }
491
492    #[test]
493    fn test_export_tracker_reexport() {
494        let mut tracker = ExportTracker::new();
495
496        let binding = ExportedBindingBuilder::new("map")
497            .kind(ExportKind::ReExport)
498            .source_module("lodash")
499            .build();
500
501        let decl = ExportDeclaration {
502            node: NodeIndex::NONE,
503            bindings: vec![binding],
504            is_type_only: false,
505            from_module: Some("lodash".to_string()),
506            start: 0,
507            end: 30,
508        };
509
510        tracker.add_declaration(decl);
511
512        assert!(tracker.reexports_from("lodash"));
513        assert!(tracker.is_exported("map"));
514    }
515
516    #[test]
517    fn test_namespace_reexport() {
518        let mut tracker = ExportTracker::new();
519
520        tracker.add_namespace_reexport(NamespaceReExport {
521            module_specifier: "./utils".to_string(),
522            alias: None,
523            node: NodeIndex::NONE,
524            start: 0,
525            end: 25,
526        });
527
528        assert!(tracker.reexports_from("./utils"));
529        assert_eq!(tracker.namespace_reexports.len(), 1);
530    }
531
532    #[test]
533    fn test_resolve_export() {
534        let mut tracker = ExportTracker::new();
535
536        // Direct export
537        tracker.add_declaration(ExportDeclaration {
538            node: NodeIndex::NONE,
539            bindings: vec![
540                ExportedBindingBuilder::new("foo")
541                    .kind(ExportKind::Named)
542                    .build(),
543            ],
544            is_type_only: false,
545            from_module: None,
546            start: 0,
547            end: 0,
548        });
549
550        // Re-export
551        tracker.add_declaration(ExportDeclaration {
552            node: NodeIndex::NONE,
553            bindings: vec![
554                ExportedBindingBuilder::new("bar")
555                    .kind(ExportKind::ReExport)
556                    .source_module("./other")
557                    .build(),
558            ],
559            is_type_only: false,
560            from_module: Some("./other".to_string()),
561            start: 0,
562            end: 0,
563        });
564
565        match tracker.resolve_export("foo") {
566            ExportResolution::Direct(b) => assert_eq!(b.exported_name, "foo"),
567            _ => panic!("Expected direct export"),
568        }
569
570        match tracker.resolve_export("bar") {
571            ExportResolution::ReExport { source_module, .. } => {
572                assert_eq!(source_module, "./other")
573            }
574            _ => panic!("Expected re-export"),
575        }
576    }
577
578    #[test]
579    fn test_export_stats() {
580        let mut tracker = ExportTracker::new();
581
582        tracker.add_declaration(ExportDeclaration {
583            node: NodeIndex::NONE,
584            bindings: vec![
585                ExportedBindingBuilder::new("a")
586                    .kind(ExportKind::Named)
587                    .build(),
588                ExportedBindingBuilder::new("b")
589                    .kind(ExportKind::Named)
590                    .build(),
591            ],
592            is_type_only: false,
593            from_module: None,
594            start: 0,
595            end: 0,
596        });
597
598        tracker.add_namespace_reexport(NamespaceReExport {
599            module_specifier: "./other".to_string(),
600            alias: None,
601            node: NodeIndex::NONE,
602            start: 0,
603            end: 0,
604        });
605
606        let stats = tracker.stats();
607        assert_eq!(stats.named_exports, 2);
608        assert_eq!(stats.namespace_reexports, 1);
609    }
610}