mirror of
https://github.com/lexmount/moli.git
synced 2026-09-28 16:01:39 +00:00
712 lines
28 KiB
Rust
712 lines
28 KiB
Rust
//! Syntax scanning helpers for legacy module analysis and child script binding discovery.
|
|
//!
|
|
//! The window/document ESM runtime no longer lowers modules into classic-script
|
|
//! shims. Some AST inventory helpers remain while worker/native ESM migration
|
|
//! and child script declared-name discovery are being separated.
|
|
|
|
mod ast_inventory;
|
|
mod dynamic_import;
|
|
mod exports;
|
|
mod imports;
|
|
mod types;
|
|
|
|
pub use types::{
|
|
DynamicImportRewriteKind, DynamicImportRewriteSite, ImportMetaResolveRewriteSite,
|
|
ModuleAstLowering, ModuleAstSourceFragment, ModuleAstSpan, ModuleAstStatement,
|
|
ModuleExportBinding, ModuleNamedBinding, ModulePredeclaredExportSurface,
|
|
ModuleSpecialFormRewriteSites, ParsedExportConst, ParsedExportDefaultDeclaration,
|
|
ParsedExportVariable, ParsedExportVariableBinding, ParsedExportedClass, ParsedExportedFunction,
|
|
ParsedModuleExportAll, ParsedModuleExportList, ParsedModuleStaticImport,
|
|
};
|
|
|
|
pub use ast_inventory::lower_module_source_with_ast_lowering;
|
|
|
|
pub use exports::{
|
|
parse_script_top_level_assignment_declared_names,
|
|
parse_script_top_level_lexical_declared_names, parse_script_var_and_function_declared_names,
|
|
};
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::ast_inventory::{
|
|
lower_module_source_with_ast, lower_module_source_with_ast_lowering,
|
|
};
|
|
use crate::dynamic_import::collect_module_special_form_rewrite_sites;
|
|
use crate::exports::collect_predeclared_export_surface;
|
|
use crate::{
|
|
DynamicImportRewriteKind, ModuleAstStatement,
|
|
parse_script_top_level_assignment_declared_names,
|
|
parse_script_top_level_lexical_declared_names,
|
|
parse_script_var_and_function_declared_names,
|
|
};
|
|
|
|
fn lower_one_module_statement(source: &str) -> ModuleAstStatement {
|
|
let mut statements =
|
|
lower_module_source_with_ast(source).expect("module source should lower");
|
|
assert_eq!(statements.len(), 1);
|
|
statements.remove(0)
|
|
}
|
|
|
|
fn lower_default_expression(source: &str) -> String {
|
|
let ModuleAstStatement::ExportDefaultExpr(expression) = lower_one_module_statement(source)
|
|
else {
|
|
panic!("source should lower as a default expression");
|
|
};
|
|
expression.source
|
|
}
|
|
|
|
fn lower_default_declaration(source: &str) -> crate::ParsedExportDefaultDeclaration {
|
|
let ModuleAstStatement::ExportDefaultDeclaration(declaration) =
|
|
lower_one_module_statement(source)
|
|
else {
|
|
panic!("source should lower as a default declaration");
|
|
};
|
|
declaration
|
|
}
|
|
|
|
#[test]
|
|
fn parses_side_effect_only_static_import() {
|
|
let ModuleAstStatement::StaticImport(import) =
|
|
lower_one_module_statement("import './mod.js';")
|
|
else {
|
|
panic!("source should lower as a static import");
|
|
};
|
|
assert_eq!(import.specifier, "./mod.js");
|
|
assert!(import.default_binding.is_none());
|
|
assert!(import.namespace_binding.is_none());
|
|
assert!(import.named_bindings.is_empty());
|
|
|
|
let ModuleAstStatement::StaticImport(compact_import) =
|
|
lower_one_module_statement("import{named as local}from './compact.js';")
|
|
else {
|
|
panic!("compact source should lower as a static import");
|
|
};
|
|
assert_eq!(compact_import.specifier, "./compact.js");
|
|
assert_eq!(compact_import.named_bindings.len(), 1);
|
|
assert_eq!(compact_import.named_bindings[0].imported_name, "named");
|
|
assert_eq!(compact_import.named_bindings[0].local_name, "local");
|
|
}
|
|
|
|
#[test]
|
|
fn parses_static_module_headers_with_empty_import_attributes() {
|
|
let ModuleAstStatement::StaticImport(import) =
|
|
lower_one_module_statement("import value, { named } from './mod.js' with {};")
|
|
else {
|
|
panic!("source should lower as a static import");
|
|
};
|
|
assert_eq!(import.specifier, "./mod.js");
|
|
assert_eq!(import.import_type, None);
|
|
assert_eq!(import.default_binding.as_deref(), Some("value"));
|
|
assert_eq!(import.named_bindings.len(), 1);
|
|
assert_eq!(import.named_bindings[0].imported_name, "named");
|
|
assert_eq!(import.named_bindings[0].local_name, "named");
|
|
|
|
let ModuleAstStatement::ExportList(export_list) =
|
|
lower_one_module_statement("export { named as renamed } from './mod.js' with {};")
|
|
else {
|
|
panic!("source should lower as an export list");
|
|
};
|
|
assert_eq!(export_list.specifier.as_deref(), Some("./mod.js"));
|
|
assert_eq!(export_list.import_type, None);
|
|
assert_eq!(export_list.bindings.len(), 1);
|
|
assert_eq!(export_list.bindings[0].local_name, "named");
|
|
assert_eq!(export_list.bindings[0].export_name, "renamed");
|
|
|
|
let ModuleAstStatement::ExportList(compact_export_list) =
|
|
lower_one_module_statement("export{named as renamed};")
|
|
else {
|
|
panic!("compact source should lower as an export list");
|
|
};
|
|
assert_eq!(compact_export_list.bindings.len(), 1);
|
|
assert_eq!(compact_export_list.bindings[0].local_name, "named");
|
|
assert_eq!(compact_export_list.bindings[0].export_name, "renamed");
|
|
|
|
let ModuleAstStatement::ExportAll(export_all) =
|
|
lower_one_module_statement("export * as ns from './mod.js' with {};")
|
|
else {
|
|
panic!("source should lower as an export-all");
|
|
};
|
|
assert_eq!(export_all.specifier, "./mod.js");
|
|
assert_eq!(export_all.import_type, None);
|
|
assert_eq!(export_all.namespace_export_name.as_deref(), Some("ns"));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_static_module_headers_with_json_import_attributes() {
|
|
let ModuleAstStatement::StaticImport(import) =
|
|
lower_one_module_statement("import config from './config.json' with { type: 'json' };")
|
|
else {
|
|
panic!("source should lower as a static import");
|
|
};
|
|
assert_eq!(import.specifier, "./config.json");
|
|
assert_eq!(import.import_type.as_deref(), Some("json"));
|
|
assert_eq!(import.default_binding.as_deref(), Some("config"));
|
|
|
|
let ModuleAstStatement::StaticImport(legacy) = lower_one_module_statement(
|
|
"import config from './config.json' assert { \"type\": \"json\" };",
|
|
) else {
|
|
panic!("source should lower as a static import");
|
|
};
|
|
assert_eq!(legacy.specifier, "./config.json");
|
|
assert_eq!(legacy.import_type.as_deref(), Some("json"));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_static_module_headers_with_text_import_attributes() {
|
|
let ModuleAstStatement::StaticImport(import) =
|
|
lower_one_module_statement("import content from './file.txt' with { type: 'text' };")
|
|
else {
|
|
panic!("source should lower as a static import");
|
|
};
|
|
assert_eq!(import.specifier, "./file.txt");
|
|
assert_eq!(import.import_type.as_deref(), Some("text"));
|
|
assert_eq!(import.default_binding.as_deref(), Some("content"));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_unknown_static_import_export_attributes() {
|
|
for source in [
|
|
"import config from './config.json' with { integrity: 'sha256-test' };",
|
|
"export { config } from './config.json' with { integrity: 'sha256-test' };",
|
|
"export * from './config.json' with { integrity: 'sha256-test' };",
|
|
] {
|
|
let error = lower_module_source_with_ast_lowering(source)
|
|
.expect_err("unsupported static import/export attributes should fail lowering");
|
|
assert!(
|
|
error.contains("unsupported import attribute"),
|
|
"unexpected error for {source}: {error}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn exported_variable_local_source_preserves_statement_semicolon() {
|
|
let ModuleAstStatement::ExportConst {
|
|
local_source: const_source,
|
|
..
|
|
} = lower_one_module_statement("export const value = 1;")
|
|
else {
|
|
panic!("source should lower as exported const");
|
|
};
|
|
assert_eq!(const_source.source, "const value = 1;");
|
|
|
|
let ModuleAstStatement::ExportVariable {
|
|
local_source: variable_source,
|
|
..
|
|
} = lower_one_module_statement("export let value = 1;")
|
|
else {
|
|
panic!("source should lower as exported variable");
|
|
};
|
|
assert_eq!(variable_source.source, "let value = 1;");
|
|
}
|
|
|
|
#[test]
|
|
fn lowering_drops_hashbang_trivia_before_first_statement() {
|
|
let lowering = lower_module_source_with_ast_lowering(
|
|
"#!/usr/bin/env node\nconst value = import.meta.url;",
|
|
)
|
|
.expect("hashbang module source should lower");
|
|
|
|
assert_eq!(lowering.statements.len(), 1);
|
|
let ModuleAstStatement::Body(body) = &lowering.statements[0] else {
|
|
panic!("hashbang should not become a body statement");
|
|
};
|
|
assert_eq!(body.source, "const value = import.meta.url;");
|
|
assert_eq!(body.special_forms.import_meta_urls.len(), 1);
|
|
|
|
let lowering =
|
|
lower_module_source_with_ast_lowering("#!/usr/bin/env node\nimport './dep.js';")
|
|
.expect("hashbang before import should lower");
|
|
assert_eq!(lowering.statements.len(), 1);
|
|
assert!(matches!(
|
|
lowering.statements[0],
|
|
ModuleAstStatement::StaticImport(_)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn lowering_flags_top_level_await_from_oxc_ast() {
|
|
let lowering = lower_module_source_with_ast_lowering(
|
|
[
|
|
"const literal = 'await';",
|
|
"const nested = async () => await load();",
|
|
"function nestedFunction() { return import('./inside-function.js'); }",
|
|
"const templ = `prefix:${await load()}`;",
|
|
"for await (const value of values) { consume(value); }",
|
|
"export const value = await load();",
|
|
"export default await load();",
|
|
]
|
|
.join("\n")
|
|
.as_str(),
|
|
)
|
|
.expect("module source should lower");
|
|
|
|
let ModuleAstStatement::Body(literal) = &lowering.statements[0] else {
|
|
panic!("first statement should lower as body");
|
|
};
|
|
assert!(!literal.contains_top_level_await);
|
|
|
|
let ModuleAstStatement::Body(nested) = &lowering.statements[1] else {
|
|
panic!("second statement should lower as body");
|
|
};
|
|
assert!(!nested.contains_top_level_await);
|
|
|
|
let ModuleAstStatement::Body(nested_function) = &lowering.statements[2] else {
|
|
panic!("third statement should lower as body");
|
|
};
|
|
assert!(!nested_function.contains_top_level_await);
|
|
assert_eq!(nested_function.special_forms.dynamic_imports.len(), 1);
|
|
|
|
let ModuleAstStatement::Body(template) = &lowering.statements[3] else {
|
|
panic!("fourth statement should lower as body");
|
|
};
|
|
assert!(template.contains_top_level_await);
|
|
|
|
let ModuleAstStatement::Body(for_await) = &lowering.statements[4] else {
|
|
panic!("fifth statement should lower as body");
|
|
};
|
|
assert!(for_await.contains_top_level_await);
|
|
|
|
let ModuleAstStatement::ExportConst { local_source, .. } = &lowering.statements[5] else {
|
|
panic!("sixth statement should lower as exported const");
|
|
};
|
|
assert!(local_source.contains_top_level_await);
|
|
|
|
let ModuleAstStatement::ExportDefaultExpr(expression) = &lowering.statements[6] else {
|
|
panic!("seventh statement should lower as default expression");
|
|
};
|
|
assert!(expression.contains_top_level_await);
|
|
}
|
|
|
|
#[test]
|
|
fn collects_special_form_rewrite_sites_with_oxc_ast() {
|
|
let source = [
|
|
"const literal = 'import.meta.url import(\"./ignored.js\")';",
|
|
"const templ = `url:${import.meta.url}`;",
|
|
"const resolved = import.meta.resolve(`./nested/` + 'dep' + `.js`);",
|
|
"const promise = import('./lazy.js', { with: { type: 'json' } });",
|
|
"const css = import('./styles.css', { with: { type: 'css' } });",
|
|
"const legacy = import('./legacy.js', { assert: { \"type\": \"json\" } });",
|
|
"const escaped = import('./caf\\u{e9}.js');",
|
|
"const namespace = await /* keep */ import('./awaited.js');",
|
|
"const resolvedImport = await import(import.meta.resolve('./mapped.js'));",
|
|
]
|
|
.join("\n");
|
|
|
|
let sites = collect_module_special_form_rewrite_sites(&source)
|
|
.expect("special forms should collect from AST");
|
|
|
|
assert_eq!(sites.import_meta_urls.len(), 1);
|
|
assert_eq!(
|
|
&source
|
|
[sites.import_meta_urls[0].start as usize..sites.import_meta_urls[0].end as usize],
|
|
"import.meta.url"
|
|
);
|
|
assert_eq!(sites.import_metas.len(), 1);
|
|
assert_eq!(
|
|
&source[sites.import_metas[0].start as usize..sites.import_metas[0].end as usize],
|
|
"import.meta"
|
|
);
|
|
assert_eq!(sites.dynamic_imports.len(), 6);
|
|
assert_eq!(sites.dynamic_imports[0].specifier, "./lazy.js");
|
|
assert!(!sites.dynamic_imports[0].resolve_import_meta_first);
|
|
assert_eq!(
|
|
sites.dynamic_imports[0].import_type.as_deref(),
|
|
Some("json")
|
|
);
|
|
assert_eq!(
|
|
sites.dynamic_imports[0].kind,
|
|
DynamicImportRewriteKind::Promise
|
|
);
|
|
assert_eq!(sites.dynamic_imports[1].specifier, "./styles.css");
|
|
assert_eq!(sites.dynamic_imports[1].import_type.as_deref(), Some("css"));
|
|
assert_eq!(sites.dynamic_imports[2].specifier, "./legacy.js");
|
|
assert_eq!(
|
|
sites.dynamic_imports[2].import_type.as_deref(),
|
|
Some("json")
|
|
);
|
|
assert_eq!(sites.dynamic_imports[3].specifier, "./café.js");
|
|
assert_eq!(sites.dynamic_imports[4].specifier, "./awaited.js");
|
|
assert_eq!(
|
|
sites.dynamic_imports[4].kind,
|
|
DynamicImportRewriteKind::AwaitedNamespace
|
|
);
|
|
assert_eq!(
|
|
&source[sites.dynamic_imports[4].replace_start..sites.dynamic_imports[4].replace_end],
|
|
"await /* keep */ import('./awaited.js')"
|
|
);
|
|
assert_eq!(sites.dynamic_imports[5].specifier, "./mapped.js");
|
|
assert!(sites.dynamic_imports[5].resolve_import_meta_first);
|
|
assert_eq!(
|
|
&source[sites.dynamic_imports[5].replace_start..sites.dynamic_imports[5].replace_end],
|
|
"await import(import.meta.resolve('./mapped.js'))"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn collects_special_forms_from_expression_fragments() {
|
|
let expression = "({ url: import.meta.url, mod: import('./object.js') })";
|
|
let sites = collect_module_special_form_rewrite_sites(expression)
|
|
.expect("expression fragment should collect through AST wrapper");
|
|
|
|
assert_eq!(sites.import_meta_urls.len(), 1);
|
|
assert_eq!(
|
|
&expression
|
|
[sites.import_meta_urls[0].start as usize..sites.import_meta_urls[0].end as usize],
|
|
"import.meta.url"
|
|
);
|
|
assert_eq!(sites.dynamic_imports.len(), 1);
|
|
assert_eq!(sites.dynamic_imports[0].specifier, "./object.js");
|
|
}
|
|
|
|
#[test]
|
|
fn lowering_attaches_special_form_sites_to_source_fragments() {
|
|
let lowering = lower_module_source_with_ast_lowering(
|
|
[
|
|
"const url = import.meta.url;",
|
|
"export const dep = await import('./dep.js');",
|
|
"export default import.meta.resolve('./resolved.js');",
|
|
]
|
|
.join("\n")
|
|
.as_str(),
|
|
)
|
|
.expect("module source should lower");
|
|
|
|
let ModuleAstStatement::Body(body) = &lowering.statements[0] else {
|
|
panic!("first statement should lower as body");
|
|
};
|
|
assert_eq!(body.special_forms.import_meta_urls.len(), 1);
|
|
let url_span = body.special_forms.import_meta_urls[0];
|
|
assert_eq!(
|
|
&body.source[url_span.start as usize..url_span.end as usize],
|
|
"import.meta.url"
|
|
);
|
|
|
|
let ModuleAstStatement::ExportConst { local_source, .. } = &lowering.statements[1] else {
|
|
panic!("second statement should lower as exported const");
|
|
};
|
|
assert_eq!(local_source.special_forms.dynamic_imports.len(), 1);
|
|
assert_eq!(
|
|
local_source.special_forms.dynamic_imports[0].kind,
|
|
DynamicImportRewriteKind::AwaitedNamespace
|
|
);
|
|
assert_eq!(
|
|
&local_source.source[local_source.special_forms.dynamic_imports[0].replace_start
|
|
..local_source.special_forms.dynamic_imports[0].replace_end],
|
|
"await import('./dep.js')"
|
|
);
|
|
|
|
let ModuleAstStatement::ExportDefaultExpr(expression) = &lowering.statements[2] else {
|
|
panic!("third statement should lower as default expression");
|
|
};
|
|
assert_eq!(expression.special_forms.import_metas.len(), 1);
|
|
assert_eq!(
|
|
&expression.source[expression.special_forms.import_metas[0].start as usize
|
|
..expression.special_forms.import_metas[0].end as usize],
|
|
"import.meta"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn lowering_includes_predeclared_export_surface_from_same_parse() {
|
|
let lowering = lower_module_source_with_ast_lowering(
|
|
"export const value = 1; export * from './x.js';",
|
|
)
|
|
.expect("module source should lower");
|
|
|
|
assert_eq!(
|
|
lowering.predeclared_export_surface.explicit_export_names,
|
|
vec!["value"]
|
|
);
|
|
assert!(lowering.predeclared_export_surface.has_export_star);
|
|
}
|
|
|
|
#[test]
|
|
fn ignores_special_form_words_inside_trivia_chunks() {
|
|
let trivia = [
|
|
"#!/usr/bin/env node",
|
|
"// import.meta.url",
|
|
"/* import.meta.resolve('./ignored.js') */",
|
|
"// import('./ignored.js')",
|
|
]
|
|
.join("\n");
|
|
|
|
let sites = collect_module_special_form_rewrite_sites(&trivia)
|
|
.expect("trivia-only chunks should not be parsed as executable code");
|
|
|
|
assert!(sites.import_meta_urls.is_empty());
|
|
assert!(sites.import_meta_resolves.is_empty());
|
|
assert!(sites.dynamic_imports.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_invalid_non_trivia_special_form_fragments() {
|
|
let error = collect_module_special_form_rewrite_sites("import(")
|
|
.expect_err("invalid executable fragments should stay unsupported");
|
|
|
|
assert!(error.contains("unsupported module special-form syntax"));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_non_static_special_form_specifiers() {
|
|
let dynamic_error = collect_module_special_form_rewrite_sites("const p = import(name);")
|
|
.expect_err("non-static dynamic import should stay unsupported");
|
|
assert!(dynamic_error.contains("unsupported dynamic import syntax"));
|
|
|
|
let css_dynamic = collect_module_special_form_rewrite_sites(
|
|
"const p = import(name, { with: { type: 'css' } });",
|
|
)
|
|
.expect("CSS dynamic import should stay in source for V8's host hook");
|
|
assert!(css_dynamic.dynamic_imports.is_empty());
|
|
|
|
let resolve_sites =
|
|
collect_module_special_form_rewrite_sites("const p = import.meta.resolve(name);")
|
|
.expect("dynamic import.meta.resolve should be handled by runtime import.meta");
|
|
assert_eq!(resolve_sites.import_metas.len(), 1);
|
|
|
|
let extra_arg_sites = collect_module_special_form_rewrite_sites(
|
|
"const p = import.meta.resolve('./dep.js', './base.js');",
|
|
)
|
|
.expect("multi-argument import.meta.resolve should stay a runtime call");
|
|
assert_eq!(extra_arg_sites.import_metas.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_non_static_or_ambiguous_dynamic_import_options() {
|
|
for source in [
|
|
"const p = import('./dep.js', { ...opts });",
|
|
"const p = import('./dep.js', { ['with']: { type: 'json' } });",
|
|
"const p = import('./dep.js', { unrelated });",
|
|
"const p = import('./dep.js', { with: { type: 'json' }, with: { type: 'json' } });",
|
|
"const p = import('./dep.js', { with: { type: 'json' }, assert: { type: 'json' } });",
|
|
"const p = import('./dep.js', { with: { type: 'json', type: 'css' } });",
|
|
"const p = import('./dep.js', { with: { type: getType() } });",
|
|
"const p = import('./dep.js', { with: { other: 'json' } });",
|
|
"const p = import('./dep.js', { unrelated: 'value' });",
|
|
] {
|
|
let error = collect_module_special_form_rewrite_sites(source)
|
|
.expect_err("non-static import options should stay unsupported");
|
|
assert!(
|
|
error.contains("unsupported dynamic import syntax"),
|
|
"unexpected error for {source}: {error}"
|
|
);
|
|
}
|
|
|
|
let sites = collect_module_special_form_rewrite_sites(
|
|
"const p = import('./dep.js', ({ with: { type: 'json' } }));",
|
|
)
|
|
.expect("static dynamic import options should remain supported");
|
|
assert_eq!(sites.dynamic_imports.len(), 1);
|
|
assert_eq!(
|
|
sites.dynamic_imports[0].import_type.as_deref(),
|
|
Some("json")
|
|
);
|
|
|
|
let sites = collect_module_special_form_rewrite_sites(
|
|
"const p = import('./dep.js', { with: {} });",
|
|
)
|
|
.expect("empty static dynamic import attributes should remain supported");
|
|
assert_eq!(sites.dynamic_imports.len(), 1);
|
|
assert_eq!(sites.dynamic_imports[0].import_type, None);
|
|
}
|
|
|
|
#[test]
|
|
fn decodes_export_all_namespace_and_string_literals() {
|
|
let ModuleAstStatement::ExportAll(namespace) =
|
|
lower_one_module_statement("export * as \"n\\u0073:name\" from './mod.js';")
|
|
else {
|
|
panic!("source should lower as an export-all");
|
|
};
|
|
assert_eq!(namespace.specifier, "./mod.js");
|
|
assert_eq!(namespace.namespace_export_name.as_deref(), Some("ns:name"));
|
|
|
|
let ModuleAstStatement::ExportAll(rocket_namespace) =
|
|
lower_one_module_statement("export * as \"\\uD83D\\uDE80\" from './emoji.js';")
|
|
else {
|
|
panic!("source should lower as an export-all");
|
|
};
|
|
assert_eq!(
|
|
rocket_namespace.namespace_export_name.as_deref(),
|
|
Some("\u{1F680}")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn predeclared_export_surface_uses_oxc_ast_for_export_names() {
|
|
let surface = collect_predeclared_export_surface(
|
|
r#"
|
|
export const {alpha, beta: renamed, nested: [gamma]} = source;
|
|
export function ready() {}
|
|
export default class {}
|
|
export { ready as "ready-alias" };
|
|
export * as ns from './namespace.js';
|
|
export * from './all.js';
|
|
"#,
|
|
)
|
|
.expect("module source should parse");
|
|
|
|
assert_eq!(
|
|
surface.explicit_export_names,
|
|
vec![
|
|
"alpha".to_owned(),
|
|
"renamed".to_owned(),
|
|
"gamma".to_owned(),
|
|
"ready".to_owned(),
|
|
"default".to_owned(),
|
|
"ready-alias".to_owned(),
|
|
"ns".to_owned(),
|
|
]
|
|
);
|
|
assert!(surface.has_export_star);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_default_exports_with_oxc_ast_shape() {
|
|
let expression = lower_default_expression("export default foo ? bar : baz;");
|
|
assert_eq!(expression, "foo ? bar : baz");
|
|
|
|
let anonymous_function = lower_default_declaration("export default function () {}");
|
|
assert_eq!(
|
|
anonymous_function.declaration_source.source,
|
|
"function () {}"
|
|
);
|
|
assert!(anonymous_function.is_anonymous);
|
|
|
|
let partial_body = lower_default_declaration(
|
|
"export default function makeValue() {\n return { ok: true };\n}",
|
|
);
|
|
assert_eq!(
|
|
partial_body.declaration_source.source,
|
|
"function makeValue() {\n return { ok: true };\n}"
|
|
);
|
|
assert!(!partial_body.is_anonymous);
|
|
assert_eq!(partial_body.local_name.as_deref(), Some("makeValue"));
|
|
|
|
let named_class = lower_default_declaration("export default class ValueBox {}");
|
|
assert_eq!(named_class.declaration_source.source, "class ValueBox {}");
|
|
assert!(!named_class.is_anonymous);
|
|
assert_eq!(named_class.local_name.as_deref(), Some("ValueBox"));
|
|
|
|
assert!(matches!(
|
|
lower_one_module_statement("export default function named() {}"),
|
|
ModuleAstStatement::ExportDefaultDeclaration(_)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn parses_script_var_and_function_declared_names_with_oxc() {
|
|
let names = parse_script_var_and_function_declared_names(
|
|
r#"
|
|
var { alpha, beta: gamma } = data;
|
|
for (var delta in data) {}
|
|
if (ready) { function boot() {} }
|
|
function outer() { var hidden = 1; function nested() {} }
|
|
"#,
|
|
)
|
|
.expect("script should parse");
|
|
|
|
assert_eq!(
|
|
names,
|
|
vec![
|
|
"alpha".to_owned(),
|
|
"boot".to_owned(),
|
|
"delta".to_owned(),
|
|
"gamma".to_owned(),
|
|
"outer".to_owned(),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn script_declared_names_reject_unparseable_source() {
|
|
let error = parse_script_var_and_function_declared_names(
|
|
"var O=function(){},exports={},a=1,function(){return 1},window=2;",
|
|
)
|
|
.expect_err("unparseable source should not produce guessed declarations");
|
|
|
|
assert_eq!(error, "unsupported script syntax");
|
|
}
|
|
|
|
#[test]
|
|
fn parses_top_level_assignment_declared_names_with_oxc() {
|
|
let names = parse_script_top_level_assignment_declared_names(
|
|
r#"
|
|
onload = function() {
|
|
location = "next.html";
|
|
window.nested = 1;
|
|
};
|
|
window.exported = 1;
|
|
bare = 2;
|
|
({ destructured } = value);
|
|
"#,
|
|
)
|
|
.expect("script should parse");
|
|
|
|
assert_eq!(
|
|
names,
|
|
vec![
|
|
"bare".to_owned(),
|
|
"exported".to_owned(),
|
|
"onload".to_owned(),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_top_level_lexical_declared_names_with_oxc() {
|
|
let names = parse_script_top_level_lexical_declared_names(
|
|
r#"
|
|
const wrapThreshold = 1;
|
|
let { alpha, nested: [beta] } = value;
|
|
if (ready) { const hidden = 2; }
|
|
var ignored = 3;
|
|
"#,
|
|
)
|
|
.expect("script should parse");
|
|
|
|
assert_eq!(
|
|
names,
|
|
vec![
|
|
"alpha".to_owned(),
|
|
"beta".to_owned(),
|
|
"wrapThreshold".to_owned(),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn parses_default_declaration_local_names() {
|
|
let function = lower_default_declaration("export default function localName() {}");
|
|
assert_eq!(function.local_name.as_deref(), Some("localName"));
|
|
assert!(!function.is_anonymous);
|
|
|
|
let generator = lower_default_declaration("export default async function* localGen() {}");
|
|
assert_eq!(generator.local_name.as_deref(), Some("localGen"));
|
|
assert!(!generator.is_anonymous);
|
|
|
|
let class = lower_default_declaration("export default class LocalClass {}");
|
|
assert_eq!(class.local_name.as_deref(), Some("LocalClass"));
|
|
assert!(!class.is_anonymous);
|
|
|
|
let anonymous = lower_default_declaration("export default function () {}");
|
|
assert!(anonymous.local_name.is_none());
|
|
assert!(anonymous.is_anonymous);
|
|
|
|
let anonymous_class = lower_default_declaration("export default class extends Base {}");
|
|
assert!(anonymous_class.local_name.is_none());
|
|
assert!(anonymous_class.is_anonymous);
|
|
|
|
let newline_extends_class =
|
|
lower_default_declaration("export default class extends\nBase {}");
|
|
assert!(newline_extends_class.local_name.is_none());
|
|
assert!(newline_extends_class.is_anonymous);
|
|
|
|
let extends_prefix_name = lower_default_declaration("export default class extendsFoo {}");
|
|
assert_eq!(
|
|
extends_prefix_name.local_name.as_deref(),
|
|
Some("extendsFoo")
|
|
);
|
|
assert!(!extends_prefix_name.is_anonymous);
|
|
}
|
|
}
|