use std::{collections::HashSet, hash::BuildHasherDefault};
use indexmap::IndexSet;
use rustc_hash::FxHasher;
use boa_gc::{Finalize, Trace};
use crate::object::internal_methods::immutable_prototype::immutable_prototype_exotic_set_prototype_of;
use crate::object::internal_methods::{
ordinary_define_own_property, ordinary_delete, ordinary_get, ordinary_get_own_property,
ordinary_has_property, ordinary_own_property_keys, ordinary_try_get, InternalMethodContext,
InternalObjectMethods, ORDINARY_INTERNAL_METHODS,
};
use crate::object::{JsData, JsPrototype};
use crate::property::{PropertyDescriptor, PropertyKey};
use crate::{js_string, object::JsObject, Context, JsResult, JsString, JsValue};
use crate::{JsNativeError, Module};
use super::BindingName;
#[derive(Debug, Trace, Finalize)]
pub struct ModuleNamespace {
module: Module,
#[unsafe_ignore_trace]
exports: IndexSet<JsString, BuildHasherDefault<FxHasher>>,
}
impl JsData for ModuleNamespace {
fn internal_methods(&self) -> &'static InternalObjectMethods {
static METHODS: InternalObjectMethods = InternalObjectMethods {
__get_prototype_of__: module_namespace_exotic_get_prototype_of,
__set_prototype_of__: module_namespace_exotic_set_prototype_of,
__is_extensible__: module_namespace_exotic_is_extensible,
__prevent_extensions__: module_namespace_exotic_prevent_extensions,
__get_own_property__: module_namespace_exotic_get_own_property,
__define_own_property__: module_namespace_exotic_define_own_property,
__has_property__: module_namespace_exotic_has_property,
__try_get__: module_namespace_exotic_try_get,
__get__: module_namespace_exotic_get,
__set__: module_namespace_exotic_set,
__delete__: module_namespace_exotic_delete,
__own_property_keys__: module_namespace_exotic_own_property_keys,
..ORDINARY_INTERNAL_METHODS
};
&METHODS
}
}
impl ModuleNamespace {
pub(crate) fn create(module: Module, names: Vec<JsString>, context: &mut Context) -> JsObject {
let mut exports = names.into_iter().collect::<IndexSet<_, _>>();
exports.sort();
let namespace = context
.intrinsics()
.templates()
.namespace()
.create(Self { module, exports }, vec![js_string!("Module").into()]);
namespace
}
pub(crate) const fn exports(&self) -> &IndexSet<JsString, BuildHasherDefault<FxHasher>> {
&self.exports
}
pub(crate) const fn module(&self) -> &Module {
&self.module
}
}
#[allow(clippy::unnecessary_wraps)]
fn module_namespace_exotic_get_prototype_of(
_: &JsObject,
_: &mut Context,
) -> JsResult<JsPrototype> {
Ok(None)
}
#[allow(clippy::unnecessary_wraps)]
fn module_namespace_exotic_set_prototype_of(
obj: &JsObject,
val: JsPrototype,
context: &mut Context,
) -> JsResult<bool> {
Ok(
immutable_prototype_exotic_set_prototype_of(obj, val, context)
.expect("this must not fail per the spec"),
)
}
#[allow(clippy::unnecessary_wraps)]
fn module_namespace_exotic_is_extensible(_: &JsObject, _: &mut Context) -> JsResult<bool> {
Ok(false)
}
#[allow(clippy::unnecessary_wraps)]
fn module_namespace_exotic_prevent_extensions(_: &JsObject, _: &mut Context) -> JsResult<bool> {
Ok(true)
}
fn module_namespace_exotic_get_own_property(
obj: &JsObject,
key: &PropertyKey,
context: &mut InternalMethodContext<'_>,
) -> JsResult<Option<PropertyDescriptor>> {
let key = match key {
PropertyKey::Symbol(_) => return ordinary_get_own_property(obj, key, context),
PropertyKey::Index(idx) => js_string!(format!("{}", idx.get())),
PropertyKey::String(s) => s.clone(),
};
{
let obj = obj
.downcast_ref::<ModuleNamespace>()
.expect("internal method can only be called on module namespace objects");
let exports = obj.exports();
if !exports.contains(&key) {
return Ok(None);
}
}
let value = obj.get(key, context)?;
Ok(Some(
PropertyDescriptor::builder()
.value(value)
.writable(true)
.enumerable(true)
.configurable(false)
.build(),
))
}
fn module_namespace_exotic_define_own_property(
obj: &JsObject,
key: &PropertyKey,
desc: PropertyDescriptor,
context: &mut InternalMethodContext<'_>,
) -> JsResult<bool> {
if let PropertyKey::Symbol(_) = key {
return ordinary_define_own_property(obj, key, desc, context);
}
let Some(current) = obj.__get_own_property__(key, context)? else {
return Ok(false);
};
if desc.configurable() == Some(true)
|| desc.enumerable() == Some(false)
|| desc.is_accessor_descriptor()
|| desc.writable() == Some(false)
{
return Ok(false);
}
Ok(desc.value().map_or(true, |v| v == current.expect_value()))
}
fn module_namespace_exotic_has_property(
obj: &JsObject,
key: &PropertyKey,
context: &mut InternalMethodContext<'_>,
) -> JsResult<bool> {
let key = match key {
PropertyKey::Symbol(_) => return ordinary_has_property(obj, key, context),
PropertyKey::Index(idx) => js_string!(format!("{}", idx.get())),
PropertyKey::String(s) => s.clone(),
};
let obj = obj
.downcast_ref::<ModuleNamespace>()
.expect("internal method can only be called on module namespace objects");
let exports = obj.exports();
Ok(exports.contains(&key))
}
fn module_namespace_exotic_try_get(
obj: &JsObject,
key: &PropertyKey,
receiver: JsValue,
context: &mut InternalMethodContext<'_>,
) -> JsResult<Option<JsValue>> {
let key = match key {
PropertyKey::Symbol(_) => return ordinary_try_get(obj, key, receiver, context),
PropertyKey::Index(idx) => js_string!(format!("{}", idx.get())),
PropertyKey::String(s) => s.clone(),
};
let obj = obj
.downcast_ref::<ModuleNamespace>()
.expect("internal method can only be called on module namespace objects");
let exports = obj.exports();
let Some(export_name) = exports.get(&key).cloned() else {
return Ok(None);
};
let m = obj.module();
let binding = m
.resolve_export(
export_name.clone(),
&mut HashSet::default(),
context.interner(),
)
.expect("6. Assert: binding is a ResolvedBinding Record.");
let target_module = binding.module();
if let BindingName::Name(name) = binding.binding_name() {
let Some(env) = target_module.environment() else {
let import = export_name.to_std_string_escaped();
return Err(JsNativeError::reference()
.with_message(format!(
"cannot get import `{import}` from an uninitialized module"
))
.into());
};
let locator = env
.compile_env()
.get_binding(&name)
.expect("checked before that the name was reachable");
env.get(locator.binding_index()).map(Some).ok_or_else(|| {
let import = export_name.to_std_string_escaped();
JsNativeError::reference()
.with_message(format!("cannot get uninitialized import `{import}`"))
.into()
})
} else {
Ok(Some(target_module.namespace(context).into()))
}
}
fn module_namespace_exotic_get(
obj: &JsObject,
key: &PropertyKey,
receiver: JsValue,
context: &mut InternalMethodContext<'_>,
) -> JsResult<JsValue> {
let key = match key {
PropertyKey::Symbol(_) => return ordinary_get(obj, key, receiver, context),
PropertyKey::Index(idx) => js_string!(format!("{}", idx.get())),
PropertyKey::String(s) => s.clone(),
};
let obj = obj
.downcast_ref::<ModuleNamespace>()
.expect("internal method can only be called on module namespace objects");
let exports = obj.exports();
let Some(export_name) = exports.get(&key).cloned() else {
return Ok(JsValue::undefined());
};
let m = obj.module();
let binding = m
.resolve_export(
export_name.clone(),
&mut HashSet::default(),
context.interner(),
)
.expect("6. Assert: binding is a ResolvedBinding Record.");
let target_module = binding.module();
if let BindingName::Name(name) = binding.binding_name() {
let Some(env) = target_module.environment() else {
let import = export_name.to_std_string_escaped();
return Err(JsNativeError::reference()
.with_message(format!(
"cannot get import `{import}` from an uninitialized module"
))
.into());
};
let locator = env
.compile_env()
.get_binding(&name)
.expect("checked before that the name was reachable");
env.get(locator.binding_index()).ok_or_else(|| {
let import = export_name.to_std_string_escaped();
JsNativeError::reference()
.with_message(format!("cannot get uninitialized import `{import}`"))
.into()
})
} else {
Ok(target_module.namespace(context).into())
}
}
#[allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)]
fn module_namespace_exotic_set(
_obj: &JsObject,
_key: PropertyKey,
_value: JsValue,
_receiver: JsValue,
_context: &mut InternalMethodContext<'_>,
) -> JsResult<bool> {
Ok(false)
}
fn module_namespace_exotic_delete(
obj: &JsObject,
key: &PropertyKey,
context: &mut InternalMethodContext<'_>,
) -> JsResult<bool> {
let key = match key {
PropertyKey::Symbol(_) => return ordinary_delete(obj, key, context),
PropertyKey::Index(idx) => js_string!(format!("{}", idx.get())),
PropertyKey::String(s) => s.clone(),
};
let obj = obj
.downcast_ref::<ModuleNamespace>()
.expect("internal method can only be called on module namespace objects");
let exports = obj.exports();
Ok(!exports.contains(&key))
}
fn module_namespace_exotic_own_property_keys(
obj: &JsObject,
context: &mut Context,
) -> JsResult<Vec<PropertyKey>> {
let symbol_keys = ordinary_own_property_keys(obj, context)?;
let obj = obj
.downcast_ref::<ModuleNamespace>()
.expect("internal method can only be called on module namespace objects");
let exports = obj.exports();
Ok(exports
.iter()
.map(|k| PropertyKey::String(k.clone()))
.chain(symbol_keys)
.collect())
}