diff --git a/Cargo.lock b/Cargo.lock index 72bf935f9..4f2714a3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -843,6 +843,7 @@ dependencies = [ "num-bigint", "ordered-float", "oxc_allocator", + "rustc-hash", "ryu-js", "serde", "serde_json", @@ -983,8 +984,10 @@ version = "0.0.0" dependencies = [ "bitflags", "indextree", + "oxc_allocator", "oxc_ast", "oxc_diagnostics", + "oxc_parser", "rustc-hash", ] diff --git a/crates/oxc_ast/Cargo.toml b/crates/oxc_ast/Cargo.toml index e6249e77f..b0588535c 100644 --- a/crates/oxc_ast/Cargo.toml +++ b/crates/oxc_ast/Cargo.toml @@ -18,6 +18,7 @@ thiserror = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, optional = true } miette = { workspace = true } +rustc-hash = { workspace = true } num-bigint = "0.4.3" ryu-js = "0.2.2" diff --git a/crates/oxc_ast/src/lib.rs b/crates/oxc_ast/src/lib.rs index 9ea76ae95..c8363c814 100644 --- a/crates/oxc_ast/src/lib.rs +++ b/crates/oxc_ast/src/lib.rs @@ -10,6 +10,7 @@ pub mod ast; pub mod ast_builder; pub mod ast_kind; pub mod context; +pub mod module_record; pub mod source_type; pub mod span; pub mod syntax_directed_operations; diff --git a/crates/oxc_ast/src/module_record.rs b/crates/oxc_ast/src/module_record.rs new file mode 100644 index 000000000..765411b92 --- /dev/null +++ b/crates/oxc_ast/src/module_record.rs @@ -0,0 +1,197 @@ +//! Module Record +//! `https://tc39.es/ecma262/#sec-abstract-module-records` + +use rustc_hash::FxHashMap; + +use crate::{Atom, Span}; + +/// Source Text Module Record +/// +#[derive(Debug, Default)] +pub struct ModuleRecord { + /// https://tc39.es/ecma262/#sec-static-semantics-modulerequests + /// Module requests from: + /// import ImportClause FromClause + /// import ModuleSpecifier + /// export ExportFromClause FromClause + /// Keyed by FromClause, valued by all node occurrences + pub module_requests: FxHashMap>, + + /// A List of ImportEntry records derived from the code of this module + pub import_entries: Vec, + + /// A List of ExportEntry records derived from the code of this module + /// that correspond to declarations that occur within the module + pub local_export_entries: Vec, + + /// A List of ExportEntry records derived from the code of this module + /// that correspond to reexported imports that occur within the module + /// or exports from export * as namespace declarations. + pub indirect_export_entries: Vec, + + /// A List of ExportEntry records derived from the code of this module + /// that correspond to export * declarations that occur within the module, + /// not including export * as namespace declarations. + pub star_export_entries: Vec, + + pub exported_bindings: FxHashMap, + pub exported_bindings_duplicated: Vec, + + pub export_default: Option, + pub export_default_duplicated: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NameSpan { + name: Atom, + span: Span, +} + +impl NameSpan { + #[must_use] + pub fn new(name: Atom, span: Span) -> Self { + Self { name, span } + } + + #[must_use] + pub fn name(&self) -> &Atom { + &self.name + } + + #[must_use] + pub fn span(&self) -> Span { + self.span + } +} + +/// [`ImportEntry`](https://tc39.es/ecma262/#importentry-record) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ImportEntry { + /// String value of the ModuleSpecifier of the ImportDeclaration. + pub module_request: NameSpan, + + /// The name under which the desired binding is exported by the module identified by [[ModuleRequest]]. + pub import_name: ImportImportName, + + /// The name that is used to locally access the imported value from within the importing module. + pub local_name: NameSpan, +} + +/// `ImportName` For `ImportEntry` +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ImportImportName { + Name(NameSpan), + NamespaceObject, + Default(Span), +} + +impl ImportImportName { + #[must_use] + pub fn is_default(&self) -> bool { + matches!(self, Self::Default(_)) + } + + #[must_use] + pub fn is_namespace_object(&self) -> bool { + matches!(self, Self::NamespaceObject) + } +} + +/// [`ExportEntry`](https://tc39.es/ecma262/#importentry-record) +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct ExportEntry { + /// Span for the entire export entry + pub span: Span, + + /// The String value of the ModuleSpecifier of the ExportDeclaration. + /// null if the ExportDeclaration does not have a ModuleSpecifier. + pub module_request: Option, + + /// The name under which the desired binding is exported by the module identified by [[ModuleRequest]]. + /// null if the ExportDeclaration does not have a ModuleSpecifier. + /// "all" is used for `export * as ns from "mod"`` declarations. + /// "all-but-default" is used for `export * from "mod" declarations`. + pub import_name: ExportImportName, + + /// The name used to export this binding by this module. + pub export_name: ExportExportName, + + /// The name that is used to locally access the exported value from within the importing module. + /// null if the exported value is not locally accessible from within the module. + pub local_name: ExportLocalName, +} + +/// `ImportName` for `ExportEntry` +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub enum ExportImportName { + Name(NameSpan), + /// all is used for export * as ns from "mod" declarations. + All, + /// all-but-default is used for export * from "mod" declarations. + AllButDefault, + /// the ExportDeclaration does not have a ModuleSpecifier + #[default] + Null, +} + +impl ExportImportName { + #[must_use] + pub fn is_all(&self) -> bool { + matches!(self, Self::All) + } + + #[must_use] + pub fn is_all_but_default(&self) -> bool { + matches!(self, Self::AllButDefault) + } +} + +/// `ExportName` for `ExportEntry` +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub enum ExportExportName { + Name(NameSpan), + Default(Span), + #[default] + Null, +} + +impl ExportExportName { + #[must_use] + pub fn is_default(&self) -> bool { + matches!(self, Self::Default(_)) + } + + #[must_use] + pub fn is_null(&self) -> bool { + matches!(self, Self::Null) + } +} + +/// `LocalName` for `ExportEntry` +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub enum ExportLocalName { + Name(NameSpan), + Default(Span), + #[default] + Null, +} + +impl ExportLocalName { + #[must_use] + pub fn name(&self) -> Option<&Atom> { + match self { + Self::Name(ns) => Some(ns.name()), + _ => None, + } + } + + #[must_use] + pub fn is_default(&self) -> bool { + matches!(self, Self::Default(_)) + } + + #[must_use] + pub fn is_null(&self) -> bool { + matches!(self, Self::Null) + } +} diff --git a/crates/oxc_semantic/Cargo.toml b/crates/oxc_semantic/Cargo.toml index 0356bd624..7f689947e 100644 --- a/crates/oxc_semantic/Cargo.toml +++ b/crates/oxc_semantic/Cargo.toml @@ -16,3 +16,7 @@ oxc_diagnostics = { path = "../oxc_diagnostics" } indextree = { workspace = true } bitflags = { workspace = true } rustc-hash = { workspace = true } + +[dev_dependencies] +oxc_parser = { path = "../oxc_parser" } +oxc_allocator = { path = "../oxc_allocator" } diff --git a/crates/oxc_semantic/src/builder.rs b/crates/oxc_semantic/src/builder.rs index 68f2f30e9..d55f8722e 100644 --- a/crates/oxc_semantic/src/builder.rs +++ b/crates/oxc_semantic/src/builder.rs @@ -10,6 +10,7 @@ use oxc_diagnostics::{Error, Redeclaration}; use crate::{ binder::Binder, + module_record::ModuleRecordBuilder, node::{AstNodeId, AstNodes, NodeFlags, SemanticNode}, scope::{ScopeBuilder, ScopeId}, symbol::{Reference, ReferenceFlag, SymbolFlags, SymbolId, SymbolTable}, @@ -30,6 +31,7 @@ pub struct SemanticBuilder<'a> { pub nodes: AstNodes<'a>, pub scope: ScopeBuilder, pub symbols: SymbolTable, + module_record_builder: ModuleRecordBuilder, } pub struct ScopeBuilderReturn<'a> { @@ -53,6 +55,7 @@ impl<'a> SemanticBuilder<'a> { nodes, scope, symbols: SymbolTable::default(), + module_record_builder: ModuleRecordBuilder::default(), } } @@ -64,11 +67,13 @@ impl<'a> SemanticBuilder<'a> { pub fn build(mut self, program: &'a Program<'a>, trivias: Rc) -> ScopeBuilderReturn { // AST pass self.visit_program(program); + let module_record = self.module_record_builder.build(); let semantic = Semantic { source_type: self.source_type, nodes: self.nodes, scopes: self.scope.scopes, trivias, + module_record, }; ScopeBuilderReturn { semantic, errors: self.errors } } @@ -203,6 +208,9 @@ impl<'a> SemanticBuilder<'a> { self.scope.current_scope_mut().strict_mode = true; } } + AstKind::ModuleDeclaration(decl) => { + self.module_record_builder.visit_module_declaration(decl); + } _ => {} } } diff --git a/crates/oxc_semantic/src/lib.rs b/crates/oxc_semantic/src/lib.rs index 54341c180..b3e3a01ae 100644 --- a/crates/oxc_semantic/src/lib.rs +++ b/crates/oxc_semantic/src/lib.rs @@ -3,6 +3,7 @@ mod binder; mod builder; +mod module_record; mod node; mod scope; mod symbol; @@ -11,7 +12,7 @@ use std::rc::Rc; pub use builder::SemanticBuilder; pub use node::{AstNode, AstNodes, SemanticNode}; -use oxc_ast::{SourceType, Trivias}; +use oxc_ast::{module_record::ModuleRecord, SourceType, Trivias}; pub use scope::{Scope, ScopeFlags, ScopeTree}; pub struct Semantic<'a> { @@ -22,6 +23,8 @@ pub struct Semantic<'a> { scopes: ScopeTree, trivias: Rc, + + module_record: ModuleRecord, } impl<'a> Semantic<'a> { @@ -44,4 +47,9 @@ impl<'a> Semantic<'a> { pub fn trivias(&self) -> &Trivias { &self.trivias } + + #[must_use] + pub fn module_record(&self) -> &ModuleRecord { + &self.module_record + } } diff --git a/crates/oxc_semantic/src/module_record/builder.rs b/crates/oxc_semantic/src/module_record/builder.rs new file mode 100644 index 000000000..c11d67009 --- /dev/null +++ b/crates/oxc_semantic/src/module_record/builder.rs @@ -0,0 +1,286 @@ +#[allow(clippy::wildcard_imports)] +use oxc_ast::{ + ast::*, module_record::*, syntax_directed_operations::BoundNames, Atom, GetSpan, Span, +}; + +#[derive(Debug, Default)] +pub struct ModuleRecordBuilder { + module_record: ModuleRecord, + export_entries: Vec, +} + +impl ModuleRecordBuilder { + #[must_use] + pub fn build(mut self) -> ModuleRecord { + // The `ParseModule` algorithm requires `importedBoundNames` (import entries) to be + // resolved before resovling export entries. + self.resolve_export_entries(); + self.module_record + } + + fn add_module_request(&mut self, name_span: &NameSpan) { + self.module_record + .module_requests + .entry(name_span.name().clone()) + .or_default() + .push(name_span.span()); + } + + fn add_import_entry(&mut self, entry: ImportEntry) { + self.module_record.import_entries.push(entry); + } + + fn add_export_entry(&mut self, entry: ExportEntry) { + self.export_entries.push(entry); + } + + fn append_local_export_entry(&mut self, entry: ExportEntry) { + self.module_record.local_export_entries.push(entry); + } + + fn append_indirect_export_entry(&mut self, entry: ExportEntry) { + self.module_record.indirect_export_entries.push(entry); + } + + fn append_star_export_entry(&mut self, entry: ExportEntry) { + self.module_record.star_export_entries.push(entry); + } + + fn add_export_binding(&mut self, name: Atom, span: Span) { + if let Some(old_node) = self.module_record.exported_bindings.insert(name.clone(), span) { + self.module_record.exported_bindings_duplicated.push(NameSpan::new(name, old_node)); + } + } + + fn add_default_export(&mut self, span: Span) { + if let Some(old_node) = self.module_record.export_default.replace(span) { + self.module_record.export_default_duplicated.push(old_node); + } + } + + /// [ParseModule](https://tc39.es/ecma262/#sec-parsemodule) + /// Step 10. + fn resolve_export_entries(&mut self) { + // 10. For each ExportEntry Record ee of exportEntries, do + for ee in self.export_entries.drain(..).collect::>() { + // a. If ee.[[ModuleRequest]] is null, then + if ee.module_request.is_none() { + let local_name = match &ee.local_name { + ExportLocalName::Name(name) => Some(name), + _ => None, + }; + let found_import_entry = self + .module_record + .import_entries + .iter() + .find(|import_entry| Some(&import_entry.local_name) == local_name); + match found_import_entry { + // i. If ee.[[LocalName]] is not an element of importedBoundNames, then + None => { + // 1. Append ee to localExportEntries. + self.append_local_export_entry(ee); + } + // ii. Else, + // 1. Let ie be the element of importEntries whose [[LocalName]] is the same as ee.[[LocalName]]. + Some(ie) => { + match &ie.import_name { + // 2. If ie.[[ImportName]] is namespace-object, then + ImportImportName::NamespaceObject => { + // a. NOTE: This is a re-export of an imported module namespace object. + // b. Append ee to localExportEntries. + self.append_local_export_entry(ee); + } + // 3. Else, + // a. NOTE: This is a re-export of a single name. + // Append the ExportEntry Record { [[ModuleRequest]]: ie.[[ModuleRequest]], [[ImportName]]: ie.[[ImportName]], [[LocalName]]: null, [[ExportName]]: ee.[[ExportName]] } + // to indirectExportEntries. + ImportImportName::Name(_) | ImportImportName::Default(_) => { + let export_entry = ExportEntry { + module_request: Some(ie.module_request.clone()), + import_name: match &ie.import_name { + ImportImportName::Name(name) => { + ExportImportName::Name(name.clone()) + } + // `import d from "mod"` + // `export { d }` + // ^ this is local_name of ie + ImportImportName::Default(_) => { + ExportImportName::Name(ie.local_name.clone()) + } + ImportImportName::NamespaceObject => unreachable!(), + }, + export_name: ee.export_name.clone(), + ..ExportEntry::default() + }; + self.append_indirect_export_entry(export_entry); + } + } + } + } + // b. Else if ee.[[ImportName]] is all-but-default, then + } else if ee.import_name.is_all_but_default() { + // i. Assert: ee.[[ExportName]] is null. + debug_assert!(ee.export_name.is_null()); + self.append_star_export_entry(ee); + // c. Else, + } else { + // i. Append ee to indirectExportEntries. + self.append_indirect_export_entry(ee); + } + } + } + + pub fn visit_module_declaration(&mut self, module_decl: &ModuleDeclaration) { + match &module_decl.kind { + ModuleDeclarationKind::ImportDeclaration(import_decl) => { + self.visit_import_declaration(import_decl); + } + ModuleDeclarationKind::ExportAllDeclaration(export_all_decl) => { + self.visit_export_all_declaration(export_all_decl); + } + ModuleDeclarationKind::ExportDefaultDeclaration(export_default_decl) => { + self.visit_export_default_declaration(export_default_decl); + } + ModuleDeclarationKind::ExportNamedDeclaration(export_named_decl) => { + self.visit_export_named_declaration(export_named_decl); + } + ModuleDeclarationKind::TSExportAssignment(_) + | ModuleDeclarationKind::TSNamespaceExportDeclaration(_) => { /* noop */ } + } + } + + fn visit_import_declaration(&mut self, decl: &ImportDeclaration) { + if decl.import_kind.map_or(false, |kind| kind.is_type()) { + return; + } + let module_request = NameSpan::new(decl.source.value.clone(), decl.source.span); + for specifier in &decl.specifiers { + let (import_name, local_name) = match specifier { + ImportDeclarationSpecifier::ImportSpecifier(specifier) => ( + ImportImportName::Name(NameSpan::new( + specifier.imported.name().clone(), + specifier.imported.span(), + )), + NameSpan::new(specifier.local.name.clone(), specifier.local.span), + ), + ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => ( + ImportImportName::NamespaceObject, + NameSpan::new(specifier.local.name.clone(), specifier.local.span), + ), + ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => ( + ImportImportName::Default(specifier.span), + NameSpan::new(specifier.local.name.clone(), specifier.local.span), + ), + }; + self.add_import_entry(ImportEntry { + module_request: module_request.clone(), + import_name, + local_name, + }); + } + self.add_module_request(&module_request); + } + + fn visit_export_all_declaration(&mut self, decl: &ExportAllDeclaration) { + let module_request = NameSpan::new(decl.source.value.clone(), decl.source.span); + let export_entry = ExportEntry { + module_request: Some(module_request.clone()), + import_name: decl + .exported + .as_ref() + .map_or(ExportImportName::AllButDefault, |_| ExportImportName::All), + export_name: decl.exported.as_ref().map_or(ExportExportName::Null, |exported_name| { + ExportExportName::Name(NameSpan::new( + exported_name.name().clone(), + exported_name.span(), + )) + }), + ..ExportEntry::default() + }; + self.add_export_entry(export_entry); + if let Some(exported_name) = &decl.exported { + self.add_export_binding(exported_name.name().clone(), exported_name.span()); + } + self.add_module_request(&module_request); + } + + fn visit_export_default_declaration(&mut self, decl: &ExportDefaultDeclaration) { + // ignore all TypeScript syntax as they overload + if decl.declaration.is_typescript_syntax() { + return; + } + let exported_name = &decl.exported; + self.add_default_export(exported_name.span()); + + let id = match &decl.declaration { + ExportDefaultDeclarationKind::Expression(_) => None, + ExportDefaultDeclarationKind::FunctionDeclaration(func) => func.id.as_ref(), + ExportDefaultDeclarationKind::ClassDeclaration(class) => class.id.as_ref(), + ExportDefaultDeclarationKind::TSInterfaceDeclaration(_) + | ExportDefaultDeclarationKind::TSEnumDeclaration(_) => return, + }; + let export_entry = ExportEntry { + export_name: ExportExportName::Default(exported_name.span()), + local_name: id + .as_ref() + .map_or(ExportLocalName::Default(exported_name.span()), |ident| { + ExportLocalName::Name(NameSpan::new(ident.name.clone(), ident.span)) + }), + ..ExportEntry::default() + }; + self.add_export_entry(export_entry); + } + + fn visit_export_named_declaration(&mut self, decl: &ExportNamedDeclaration) { + if decl.export_kind.map_or(false, |kind| kind.is_type()) { + return; + } + // ignore all TypeScript syntax as they overload + if decl.is_typescript_syntax() { + return; + } + + let module_request = + decl.source.as_ref().map(|source| NameSpan::new(source.value.clone(), source.span)); + + if let Some(module_request) = &module_request { + self.add_module_request(module_request); + } + + if let Some(decl) = &decl.declaration { + for ident in decl.bound_names() { + let export_entry = ExportEntry { + module_request: module_request.clone(), + export_name: ExportExportName::Name(NameSpan::new( + ident.name.clone(), + ident.span, + )), + local_name: ExportLocalName::Name(NameSpan::new( + ident.name.clone(), + ident.span, + )), + ..ExportEntry::default() + }; + self.add_export_entry(export_entry); + self.add_export_binding(ident.name.clone(), ident.span); + } + } + + for specifier in &decl.specifiers { + let export_entry = ExportEntry { + module_request: module_request.clone(), + export_name: ExportExportName::Name(NameSpan::new( + specifier.exported.name().clone(), + specifier.exported.span(), + )), + local_name: ExportLocalName::Name(NameSpan::new( + specifier.local.name().clone(), + specifier.local.span(), + )), + ..ExportEntry::default() + }; + self.add_export_entry(export_entry); + self.add_export_binding(specifier.exported.name().clone(), specifier.exported.span()); + } + } +} diff --git a/crates/oxc_semantic/src/module_record/mod.rs b/crates/oxc_semantic/src/module_record/mod.rs new file mode 100644 index 000000000..f2a3ade40 --- /dev/null +++ b/crates/oxc_semantic/src/module_record/mod.rs @@ -0,0 +1,234 @@ +mod builder; + +pub use builder::ModuleRecordBuilder; + +#[cfg(test)] +mod module_record_tests { + use oxc_allocator::Allocator; + #[allow(clippy::wildcard_imports)] + use oxc_ast::{module_record::*, SourceType, Span}; + use oxc_parser::Parser; + + use crate::SemanticBuilder; + + fn build(source_text: &str) -> ModuleRecord { + let source_type = *SourceType::default().with_module(true); + let allocator = Allocator::default(); + let ret = Parser::new(&allocator, source_text, source_type).parse(); + let program = allocator.alloc(ret.program); + let trivias = std::rc::Rc::new(ret.trivias); + let semantic_ret = SemanticBuilder::new(source_type).build(program, trivias); + semantic_ret.semantic.module_record + } + + // Table 55 gives examples of ImportEntry records fields used to represent the syntactic import forms: + // `https://tc39.es/ecma262/#table-import-forms-mapping-to-importentry-records` + + #[test] + fn import_default() { + let module_record = build("import v from 'mod'"); + let import_entry = ImportEntry { + module_request: NameSpan::new("mod".into(), Span::new(14, 19)), + import_name: ImportImportName::Default(Span::new(7, 8)), + local_name: NameSpan::new("v".into(), Span::new(7, 8)), + }; + assert_eq!(module_record.import_entries.len(), 1); + assert_eq!(module_record.import_entries[0], import_entry); + } + + #[test] + fn import_namespace() { + let module_record = build("import * as ns from 'mod'"); + let import_entry = ImportEntry { + module_request: NameSpan::new("mod".into(), Span::new(20, 25)), + import_name: ImportImportName::NamespaceObject, + local_name: NameSpan::new("ns".into(), Span::new(12, 14)), + }; + assert_eq!(module_record.import_entries.len(), 1); + assert_eq!(module_record.import_entries[0], import_entry); + } + + #[test] + fn import_specifier() { + let module_record = build("import { x } from 'mod'"); + let import_entry = ImportEntry { + module_request: NameSpan::new("mod".into(), Span::new(18, 23)), + import_name: ImportImportName::Name(NameSpan::new("x".into(), Span::new(9, 10))), + local_name: NameSpan::new("x".into(), Span::new(9, 10)), + }; + assert_eq!(module_record.import_entries.len(), 1); + assert_eq!(module_record.import_entries[0], import_entry); + } + + #[test] + fn import_specifier_alias() { + let module_record = build("import { x as v } from 'mod'"); + let import_entry = ImportEntry { + module_request: NameSpan::new("mod".into(), Span::new(23, 28)), + import_name: ImportImportName::Name(NameSpan::new("x".into(), Span::new(9, 10))), + local_name: NameSpan::new("v".into(), Span::new(14, 15)), + }; + assert_eq!(module_record.import_entries.len(), 1); + assert_eq!(module_record.import_entries[0], import_entry); + } + + #[test] + fn import_without_binding() { + let module_record = build("import 'mod'"); + assert!(module_record.import_entries.is_empty()); + } + + // Table 57 gives examples of the ExportEntry record fields used to represent the syntactic export forms + // `https://tc39.es/ecma262/#table-export-forms-mapping-to-exportentry-records` + + #[test] + fn export_star() { + // ExportDeclaration : export ExportFromClause FromClause ; + // ExportFromClause : * + let module_record = build("export * from 'mod'"); + let export_entry = ExportEntry { + module_request: Some(NameSpan::new("mod".into(), Span::new(14, 19))), + import_name: ExportImportName::AllButDefault, + ..ExportEntry::default() + }; + assert_eq!(module_record.star_export_entries.len(), 1); + assert_eq!(module_record.star_export_entries[0], export_entry); + } + + #[test] + fn export_star_as_namespace() { + // ExportDeclaration : export ExportFromClause FromClause ; + // ExportFromClause : * as ModuleExportName + let module_record = build("export * as ns from 'mod'"); + let export_entry = ExportEntry { + module_request: Some(NameSpan::new("mod".into(), Span::new(20, 25))), + import_name: ExportImportName::All, + export_name: ExportExportName::Name(NameSpan::new("ns".into(), Span::new(12, 14))), + ..ExportEntry::default() + }; + assert_eq!(module_record.indirect_export_entries.len(), 1); + assert_eq!(module_record.indirect_export_entries[0], export_entry); + } + + #[test] + fn named_exports() { + // ExportDeclaration : export NamedExports ; + // ExportSpecifier : ModuleExportName + let module_record = build("export { x }"); + let export_entry = ExportEntry { + export_name: ExportExportName::Name(NameSpan::new("x".into(), Span::new(9, 10))), + local_name: ExportLocalName::Name(NameSpan::new("x".into(), Span::new(9, 10))), + ..ExportEntry::default() + }; + assert_eq!(module_record.local_export_entries.len(), 1); + assert_eq!(module_record.local_export_entries[0], export_entry); + } + + #[test] + fn named_exports_alias() { + // ExportDeclaration : export NamedExports ; + // ExportSpecifier : ModuleExportName as ModuleExportName + let module_record = build("export { x as v }"); + let export_entry = ExportEntry { + export_name: ExportExportName::Name(NameSpan::new("v".into(), Span::new(14, 15))), + local_name: ExportLocalName::Name(NameSpan::new("x".into(), Span::new(9, 10))), + ..ExportEntry::default() + }; + assert_eq!(module_record.local_export_entries.len(), 1); + assert_eq!(module_record.local_export_entries[0], export_entry); + } + + #[test] + fn named_exports_from() { + // ExportDeclaration : export ExportFromClause FromClause ; + // ExportSpecifier : ModuleExportName + let module_record = build("export { x } from 'mod'"); + let export_entry = ExportEntry { + module_request: Some(NameSpan::new("mod".into(), Span::new(18, 23))), + export_name: ExportExportName::Name(NameSpan::new("x".into(), Span::new(9, 10))), + local_name: ExportLocalName::Name(NameSpan::new("x".into(), Span::new(9, 10))), + ..ExportEntry::default() + }; + assert_eq!(module_record.indirect_export_entries.len(), 1); + assert_eq!(module_record.indirect_export_entries[0], export_entry); + } + + #[test] + fn named_exports_alias_from() { + // ExportDeclaration : export ExportFromClause FromClause ; + // ExportSpecifier : ModuleExportName as ModuleExportName + let module_record = build("export { x as v } from 'mod'"); + let export_entry = ExportEntry { + module_request: Some(NameSpan::new("mod".into(), Span::new(23, 28))), + export_name: ExportExportName::Name(NameSpan::new("v".into(), Span::new(14, 15))), + local_name: ExportLocalName::Name(NameSpan::new("x".into(), Span::new(9, 10))), + ..ExportEntry::default() + }; + assert_eq!(module_record.indirect_export_entries.len(), 1); + assert_eq!(module_record.indirect_export_entries[0], export_entry); + } + + #[test] + fn export_declaration() { + // ExportDeclaration : export VariableStatement + let module_record = build("export var v"); + let export_entry = ExportEntry { + export_name: ExportExportName::Name(NameSpan::new("v".into(), Span::new(11, 12))), + local_name: ExportLocalName::Name(NameSpan::new("v".into(), Span::new(11, 12))), + ..ExportEntry::default() + }; + assert_eq!(module_record.local_export_entries.len(), 1); + assert_eq!(module_record.local_export_entries[0], export_entry); + } + + #[test] + fn export_default_declaration() { + // ExportDeclaration : export default HoistableDeclaration + let module_record = build("export default function f() {}"); + let export_entry = ExportEntry { + export_name: ExportExportName::Default(Span::new(7, 14)), + local_name: ExportLocalName::Name(NameSpan::new("f".into(), Span::new(24, 25))), + ..ExportEntry::default() + }; + assert_eq!(module_record.local_export_entries.len(), 1); + assert_eq!(module_record.local_export_entries[0], export_entry); + } + + #[test] + fn export_default_function_expression() { + // ExportDeclaration : export default HoistableDeclaration + let module_record = build("export default function() {}"); + let export_entry = ExportEntry { + export_name: ExportExportName::Default(Span::new(7, 14)), + local_name: ExportLocalName::Default(Span::new(7, 14)), + ..ExportEntry::default() + }; + assert_eq!(module_record.local_export_entries.len(), 1); + assert_eq!(module_record.local_export_entries[0], export_entry); + } + + #[test] + fn export_default_expression() { + // ExportDeclaration : export default HoistableDeclaration + let module_record = build("export default 42"); + let export_entry = ExportEntry { + export_name: ExportExportName::Default(Span::new(7, 14)), + local_name: ExportLocalName::Default(Span::new(7, 14)), + ..ExportEntry::default() + }; + assert_eq!(module_record.local_export_entries.len(), 1); + assert_eq!(module_record.local_export_entries[0], export_entry); + } + + #[test] + fn export_named_default() { + let module_record = build("export { default }"); + let export_entry = ExportEntry { + export_name: ExportExportName::Name(NameSpan::new("default".into(), Span::new(9, 16))), + local_name: ExportLocalName::Name(NameSpan::new("default".into(), Span::new(9, 16))), + ..ExportEntry::default() + }; + assert_eq!(module_record.local_export_entries.len(), 1); + assert_eq!(module_record.local_export_entries[0], export_entry); + } +}