1use crate::binder::SymbolId;
11use crate::parser::NodeIndex;
12use rustc_hash::{FxHashMap, FxHashSet};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum ExportKind {
17 Named,
19 Default,
21 Declaration,
23 ReExport,
25 NamespaceReExport,
27 NamespaceReExportAs,
29 TypeOnly,
31 CommonJSDefault,
33 CommonJSNamed,
35}
36
37#[derive(Debug, Clone)]
39pub struct ExportedBinding {
40 pub exported_name: String,
42 pub local_name: String,
44 pub kind: ExportKind,
46 pub declaration_node: NodeIndex,
48 pub local_declaration_node: NodeIndex,
50 pub symbol_id: SymbolId,
52 pub is_type_only: bool,
54 pub source_module: Option<String>,
56 pub original_name: Option<String>,
58}
59
60#[derive(Debug, Clone)]
62pub struct ExportDeclaration {
63 pub node: NodeIndex,
65 pub bindings: Vec<ExportedBinding>,
67 pub is_type_only: bool,
69 pub from_module: Option<String>,
71 pub start: u32,
73 pub end: u32,
75}
76
77#[derive(Debug, Clone)]
79pub struct NamespaceReExport {
80 pub module_specifier: String,
82 pub alias: Option<String>,
84 pub node: NodeIndex,
86 pub start: u32,
88 pub end: u32,
89}
90
91#[derive(Debug, Default)]
93pub struct ExportTracker {
94 pub declarations: Vec<ExportDeclaration>,
96 pub bindings_by_name: FxHashMap<String, ExportedBinding>,
98 pub default_export: Option<ExportedBinding>,
100 pub type_only_exports: FxHashSet<String>,
102 pub namespace_reexports: Vec<NamespaceReExport>,
104 pub reexports: FxHashMap<String, ExportedBinding>,
106 pub reexport_sources: FxHashSet<String>,
108 pub commonjs_exports: Vec<CommonJSExport>,
110 pub has_default_export: bool,
112 pub has_commonjs_exports: bool,
114}
115
116#[derive(Debug, Clone)]
118pub struct CommonJSExport {
119 pub kind: ExportKind,
121 pub property_name: Option<String>,
123 pub node: NodeIndex,
125 pub start: u32,
127 pub end: u32,
128}
129
130impl ExportTracker {
131 pub fn new() -> Self {
133 Self::default()
134 }
135
136 pub fn add_declaration(&mut self, decl: ExportDeclaration) {
138 if let Some(ref from) = decl.from_module {
140 self.reexport_sources.insert(from.clone());
141 }
142
143 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 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 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 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 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 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 pub fn is_type_only_export(&self, name: &str) -> bool {
225 self.type_only_exports.contains(name)
226 }
227
228 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 pub fn get_direct_exports(&self) -> impl Iterator<Item = &ExportedBinding> {
237 self.bindings_by_name.values()
238 }
239
240 pub fn get_reexports(&self) -> impl Iterator<Item = &ExportedBinding> {
242 self.reexports.values()
243 }
244
245 pub fn reexports_from(&self, specifier: &str) -> bool {
247 self.reexport_sources.contains(specifier)
248 }
249
250 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 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 pub fn resolve_export(&self, name: &str) -> ExportResolution {
294 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 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 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#[derive(Debug, Clone)]
331pub enum ExportResolution {
332 Direct(ExportedBinding),
334 ReExport {
336 source_module: String,
337 original_name: String,
338 },
339 PossibleNamespaceReExport { sources: Vec<String>, name: String },
341 NotFound,
343}
344
345#[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
357pub 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 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 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}