Skip to content

Commit 6ea122a

Browse files
ssrliveCopilot
andcommitted
Implement spec-compliant ResolveExport for module resolution validation
Add validate_module_resolution() with the full ResolveExport algorithm (ES2024 §16.2.1.16.3) to detect module link-time errors: - Circular re-exports (resolve set cycle detection) - Missing named re-export bindings (returns null) - Ambiguous star-export bindings (multiple different resolutions) Key spec nuances handled: - import-then-export reclassification: 'import {x} from m; export {x}' is traced through to the import source as an indirect export entry - Namespace re-exports: 'export * as ns from m' and 'import * as ns from m; export {ns}' both resolve to the target module's namespace binding, enabling correct ambiguity comparison - Star exports skip 'default' per spec step 9 Uses ModuleResolutionContext struct to thread per-module maps: local_exports, import_bindings, export_to_local, all_reexport_deps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 357ac85 commit 6ea122a

File tree

2 files changed

+310
-0
lines changed

2 files changed

+310
-0
lines changed

src/core/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2987,6 +2987,11 @@ pub fn evaluate_script_with_unwrap<T: AsRef<str>, P: AsRef<std::path::Path>>(
29872987
vm.load_module_graph(ctx, entry_path, &requests);
29882988
// Fixup circular re-exports
29892989
vm.fixup_circular_reexports();
2990+
// Validate module resolution: check that all re-exports and
2991+
// import bindings resolve to actual exports in source modules.
2992+
if let Some((ref mk, _, ref metl, ref mrs, _, _, ref ep)) = main_module_record {
2993+
vm.validate_module_resolution(mk, &statements, metl, mrs, ep)?;
2994+
}
29902995
// Pass loaded module info to the main compiler
29912996
for (path, exports) in &vm.loaded_modules {
29922997
let mut info = HashMap::new();

src/core/vm.rs

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,22 @@ enum ModuleStatus {
609609
Evaluated,
610610
}
611611

612+
/// Result of spec-compliant ResolveExport algorithm.
613+
enum ResolveExportResult {
614+
/// Resolved to a concrete binding in (module_key, binding_name)
615+
Found(String, String),
616+
Null,
617+
Ambiguous,
618+
}
619+
620+
/// Context for module resolution validation (avoids threading many params).
621+
struct ModuleResolutionContext<'a> {
622+
local_exports: &'a std::collections::HashMap<String, std::collections::HashSet<String>>,
623+
all_reexport_deps: &'a std::collections::HashMap<String, Vec<(String, Vec<crate::core::ReexportSpec>)>>,
624+
import_bindings: &'a std::collections::HashMap<String, std::collections::HashMap<String, (String, String)>>,
625+
export_to_local: &'a std::collections::HashMap<String, std::collections::HashMap<String, String>>,
626+
}
627+
612628
#[derive(Clone)]
613629
struct StoredModuleRecord {
614630
source: String,
@@ -2643,6 +2659,295 @@ impl<'gc> VM<'gc> {
26432659
}
26442660
}
26452661

2662+
/// Validate module resolution: check that all indirect export entries and
2663+
/// import bindings resolve to actual exports in their source modules.
2664+
/// Per spec §15.2.1.16.4 step 9 (IndirectExportEntries validation).
2665+
pub fn validate_module_resolution(
2666+
&self,
2667+
main_module_key: &str,
2668+
main_statements: &[crate::core::Statement],
2669+
main_export_name_to_local: &std::collections::HashMap<String, String>,
2670+
main_reexport_sources: &[(String, Vec<crate::core::ReexportSpec>)],
2671+
entry_path: &std::path::Path,
2672+
) -> Result<(), JSError> {
2673+
use crate::core::resolve_module_path;
2674+
use crate::core::statement::{ImportSpecifier, StatementKind};
2675+
2676+
// Build per-module data structures needed for spec-compliant ResolveExport
2677+
let mut local_exports: std::collections::HashMap<String, std::collections::HashSet<String>> = std::collections::HashMap::new();
2678+
// import_bindings: module_key → { local_name → (source_module_key, import_name) }
2679+
let mut import_bindings: std::collections::HashMap<String, std::collections::HashMap<String, (String, String)>> =
2680+
std::collections::HashMap::new();
2681+
// export_to_local: module_key → { export_name → local_name }
2682+
let mut export_to_local: std::collections::HashMap<String, std::collections::HashMap<String, String>> =
2683+
std::collections::HashMap::new();
2684+
2685+
// Helper: extract import bindings from statements
2686+
fn extract_imports(
2687+
stmts: &[crate::core::Statement],
2688+
base_path: &std::path::Path,
2689+
) -> std::collections::HashMap<String, (String, String)> {
2690+
let mut map = std::collections::HashMap::new();
2691+
for stmt in stmts {
2692+
if let StatementKind::Import(specs, source) = &*stmt.kind {
2693+
let resolved = resolve_module_path(source, base_path).to_string_lossy().to_string();
2694+
for spec in specs {
2695+
match spec {
2696+
ImportSpecifier::Named(import_name, Some(local_name)) => {
2697+
map.insert(local_name.clone(), (resolved.clone(), import_name.clone()));
2698+
}
2699+
ImportSpecifier::Named(name, None) => {
2700+
map.insert(name.clone(), (resolved.clone(), name.clone()));
2701+
}
2702+
ImportSpecifier::Default(local_name) => {
2703+
map.insert(local_name.clone(), (resolved.clone(), "default".to_string()));
2704+
}
2705+
ImportSpecifier::Namespace(local_name) | ImportSpecifier::DeferredNamespace(local_name) => {
2706+
// import * as foo → namespace import; use sentinel
2707+
map.insert(local_name.clone(), (resolved.clone(), "\x00namespace".to_string()));
2708+
}
2709+
}
2710+
}
2711+
}
2712+
}
2713+
map
2714+
}
2715+
2716+
// Populate maps for all dependency modules
2717+
for (key, record) in &self.module_records {
2718+
let mut set = std::collections::HashSet::new();
2719+
for k in record.export_name_to_local.keys() {
2720+
set.insert(k.clone());
2721+
}
2722+
local_exports.insert(key.clone(), set);
2723+
import_bindings.insert(key.clone(), extract_imports(&record.statements, &record.resolved_path));
2724+
export_to_local.insert(key.clone(), record.export_name_to_local.clone());
2725+
}
2726+
// Add main module's exports and import bindings
2727+
{
2728+
let mut set = std::collections::HashSet::new();
2729+
for k in main_export_name_to_local.keys() {
2730+
set.insert(k.clone());
2731+
}
2732+
local_exports.insert(main_module_key.to_string(), set);
2733+
import_bindings.insert(main_module_key.to_string(), extract_imports(main_statements, entry_path));
2734+
export_to_local.insert(main_module_key.to_string(), main_export_name_to_local.clone());
2735+
}
2736+
2737+
// Build reexport_deps map including main module
2738+
let mut all_reexport_deps: std::collections::HashMap<String, Vec<(String, Vec<crate::core::ReexportSpec>)>> =
2739+
std::collections::HashMap::new();
2740+
{
2741+
let mut resolved_reexports = Vec::new();
2742+
for (source, specs) in main_reexport_sources {
2743+
let resolved = resolve_module_path(source, entry_path).to_string_lossy().to_string();
2744+
resolved_reexports.push((resolved, specs.clone()));
2745+
}
2746+
if !resolved_reexports.is_empty() {
2747+
all_reexport_deps.insert(main_module_key.to_string(), resolved_reexports);
2748+
}
2749+
}
2750+
// Dependency re-exports (already resolved)
2751+
for (key, deps) in &self.reexport_deps {
2752+
all_reexport_deps.insert(key.clone(), deps.clone());
2753+
}
2754+
2755+
let ctx = ModuleResolutionContext {
2756+
local_exports: &local_exports,
2757+
all_reexport_deps: &all_reexport_deps,
2758+
import_bindings: &import_bindings,
2759+
export_to_local: &export_to_local,
2760+
};
2761+
2762+
// Validate: for each module's IndirectExportEntries (named re-exports),
2763+
// resolve the export using spec-compliant ResolveExport.
2764+
for reexport_list in all_reexport_deps.values() {
2765+
for (src_key, specs) in reexport_list {
2766+
for spec in specs {
2767+
if let crate::core::ReexportSpec::Named(name, alias) = spec {
2768+
let export_name = alias.as_deref().unwrap_or(name);
2769+
let mut visited = std::collections::HashSet::new();
2770+
let result = Self::resolve_export_static(src_key, name, &ctx, &mut visited);
2771+
match result {
2772+
ResolveExportResult::Found(..) => {}
2773+
ResolveExportResult::Null => {
2774+
let display_src = self.get_module_display_name(src_key);
2775+
return Err(crate::raise_syntax_error!(format!(
2776+
"The requested module '{}' does not provide an export named '{}'",
2777+
display_src, export_name
2778+
)));
2779+
}
2780+
ResolveExportResult::Ambiguous => {
2781+
let display_src = self.get_module_display_name(src_key);
2782+
return Err(crate::raise_syntax_error!(format!(
2783+
"The requested module '{}' contains ambiguous star exports for the name '{}'",
2784+
display_src, export_name
2785+
)));
2786+
}
2787+
}
2788+
}
2789+
}
2790+
}
2791+
}
2792+
2793+
// Validate import bindings in main module
2794+
for stmt in main_statements {
2795+
if let StatementKind::Import(specs, source) = &*stmt.kind {
2796+
let resolved = resolve_module_path(source, entry_path).to_string_lossy().to_string();
2797+
Self::validate_import_specs_static(specs, &resolved, source, &ctx)?;
2798+
}
2799+
}
2800+
// Validate import bindings in dependency modules
2801+
for record in self.module_records.values() {
2802+
for stmt in &record.statements {
2803+
if let StatementKind::Import(specs, source) = &*stmt.kind {
2804+
let resolved = resolve_module_path(source, &record.resolved_path).to_string_lossy().to_string();
2805+
Self::validate_import_specs_static(specs, &resolved, source, &ctx)?;
2806+
}
2807+
}
2808+
}
2809+
2810+
Ok(())
2811+
}
2812+
2813+
/// Validate import specifiers against a source module.
2814+
fn validate_import_specs_static(
2815+
specs: &[crate::core::statement::ImportSpecifier],
2816+
resolved_key: &str,
2817+
display_source: &str,
2818+
ctx: &ModuleResolutionContext<'_>,
2819+
) -> Result<(), JSError> {
2820+
use crate::core::statement::ImportSpecifier;
2821+
for spec in specs {
2822+
let binding_name = match spec {
2823+
ImportSpecifier::Named(name, _) => Some(name.as_str()),
2824+
ImportSpecifier::Default(_) => Some("default"),
2825+
ImportSpecifier::Namespace(_) | ImportSpecifier::DeferredNamespace(_) => None,
2826+
};
2827+
if let Some(name) = binding_name {
2828+
let mut visited = std::collections::HashSet::new();
2829+
let result = Self::resolve_export_static(resolved_key, name, ctx, &mut visited);
2830+
match result {
2831+
ResolveExportResult::Found(..) => {}
2832+
ResolveExportResult::Null => {
2833+
return Err(crate::raise_syntax_error!(format!(
2834+
"The requested module '{}' does not provide an export named '{}'",
2835+
display_source, name
2836+
)));
2837+
}
2838+
ResolveExportResult::Ambiguous => {
2839+
return Err(crate::raise_syntax_error!(format!(
2840+
"The requested module '{}' contains ambiguous star exports for the name '{}'",
2841+
display_source, name
2842+
)));
2843+
}
2844+
}
2845+
}
2846+
}
2847+
Ok(())
2848+
}
2849+
2850+
/// Spec-compliant ResolveExport: traces export resolution through re-exports.
2851+
fn resolve_export_static(
2852+
module_key: &str,
2853+
export_name: &str,
2854+
ctx: &ModuleResolutionContext<'_>,
2855+
visited: &mut std::collections::HashSet<(String, String)>,
2856+
) -> ResolveExportResult {
2857+
let pair = (module_key.to_string(), export_name.to_string());
2858+
if visited.contains(&pair) {
2859+
return ResolveExportResult::Null;
2860+
}
2861+
visited.insert(pair);
2862+
2863+
// 1. Check local/direct exports
2864+
if let Some(locals) = ctx.local_exports.get(module_key)
2865+
&& locals.contains(export_name)
2866+
{
2867+
// Per spec ParseModule step 10.1.ii: if the local name is in
2868+
// importedBoundNames, it becomes an indirect export entry —
2869+
// follow the import chain to find the true origin.
2870+
if let Some(etl) = ctx.export_to_local.get(module_key)
2871+
&& let Some(local_name) = etl.get(export_name)
2872+
&& let Some(imports) = ctx.import_bindings.get(module_key)
2873+
&& let Some((src_mod, import_name)) = imports.get(local_name)
2874+
{
2875+
if import_name == "\x00namespace" {
2876+
// import * as foo; export { foo } → namespace re-export
2877+
return ResolveExportResult::Found(src_mod.clone(), "\x00namespace".to_string());
2878+
}
2879+
return Self::resolve_export_static(src_mod, import_name, ctx, visited);
2880+
}
2881+
return ResolveExportResult::Found(module_key.to_string(), export_name.to_string());
2882+
}
2883+
2884+
// 2. Check indirect export entries (named re-exports + namespace re-exports)
2885+
if let Some(reexport_list) = ctx.all_reexport_deps.get(module_key) {
2886+
for (src_key, specs) in reexport_list {
2887+
for spec in specs {
2888+
match spec {
2889+
crate::core::ReexportSpec::Named(name, alias) => {
2890+
let ename = alias.as_deref().unwrap_or(name);
2891+
if ename == export_name {
2892+
return Self::resolve_export_static(src_key, name, ctx, visited);
2893+
}
2894+
}
2895+
crate::core::ReexportSpec::Namespace(name) => {
2896+
if name == export_name {
2897+
// export * as foo from 'mod' -> per spec resolves to
2898+
// { Module: importedModule, BindingName: namespace }
2899+
return ResolveExportResult::Found(src_key.clone(), "\x00namespace".to_string());
2900+
}
2901+
}
2902+
crate::core::ReexportSpec::Star => {}
2903+
}
2904+
}
2905+
}
2906+
}
2907+
2908+
// 3. Check star re-exports (StarExportEntries)
2909+
if export_name == "default" {
2910+
return ResolveExportResult::Null;
2911+
}
2912+
let mut star_resolution: Option<(String, String)> = None;
2913+
if let Some(reexport_list) = ctx.all_reexport_deps.get(module_key) {
2914+
for (src_key, specs) in reexport_list {
2915+
let has_star = specs.iter().any(|s| matches!(s, crate::core::ReexportSpec::Star));
2916+
if !has_star {
2917+
continue;
2918+
}
2919+
let resolution = Self::resolve_export_static(src_key, export_name, ctx, visited);
2920+
match resolution {
2921+
ResolveExportResult::Ambiguous => return ResolveExportResult::Ambiguous,
2922+
ResolveExportResult::Found(ref m, ref b) => {
2923+
if let Some((ref pm, ref pb)) = star_resolution {
2924+
if m != pm || b != pb {
2925+
return ResolveExportResult::Ambiguous;
2926+
}
2927+
} else {
2928+
star_resolution = Some((m.clone(), b.clone()));
2929+
}
2930+
}
2931+
ResolveExportResult::Null => {}
2932+
}
2933+
}
2934+
}
2935+
match star_resolution {
2936+
Some((m, b)) => ResolveExportResult::Found(m, b),
2937+
None => ResolveExportResult::Null,
2938+
}
2939+
}
2940+
2941+
/// Get a human-readable display name for a module key (use relative path if possible).
2942+
fn get_module_display_name(&self, module_key: &str) -> String {
2943+
if let Some(record) = self.module_records.get(module_key)
2944+
&& let Some(fname) = record.resolved_path.file_name()
2945+
{
2946+
return format!("./{}", fname.to_string_lossy());
2947+
}
2948+
module_key.to_string()
2949+
}
2950+
26462951
/// Inject loaded module bindings into `module_locals` based on `chunk.loaded_module_vars`.
26472952
pub fn inject_loaded_module_bindings(&mut self, ctx: &GcContext<'gc>) {
26482953
let vars: Vec<(String, String, String)> = self

0 commit comments

Comments
 (0)