use std::cell::RefCell;
use std::rc::Rc;
use crate::error::{StatorError, StatorResult};
use crate::objects::map::PropertyAttributes;
use crate::objects::property_map::PropertyMap;
use crate::objects::value::JsValue;
#[derive(Debug, Clone)]
pub enum FullPropertyDescriptor {
Data {
value: JsValue,
writable: bool,
enumerable: bool,
configurable: bool,
},
Accessor {
get: JsValue,
set: JsValue,
enumerable: bool,
configurable: bool,
},
Generic {
enumerable: Option<bool>,
configurable: Option<bool>,
},
}
impl FullPropertyDescriptor {
pub fn is_data(&self) -> bool {
matches!(self, Self::Data { .. })
}
pub fn is_accessor(&self) -> bool {
matches!(self, Self::Accessor { .. })
}
pub fn is_generic(&self) -> bool {
matches!(self, Self::Generic { .. })
}
pub fn enumerable(&self) -> Option<bool> {
match self {
Self::Data { enumerable, .. } | Self::Accessor { enumerable, .. } => Some(*enumerable),
Self::Generic { enumerable, .. } => *enumerable,
}
}
pub fn configurable(&self) -> Option<bool> {
match self {
Self::Data { configurable, .. } | Self::Accessor { configurable, .. } => {
Some(*configurable)
}
Self::Generic { configurable, .. } => *configurable,
}
}
pub fn to_attributes(&self) -> PropertyAttributes {
let mut attrs = PropertyAttributes::empty();
match self {
Self::Data {
writable,
enumerable,
configurable,
..
} => {
if *writable {
attrs |= PropertyAttributes::WRITABLE;
}
if *enumerable {
attrs |= PropertyAttributes::ENUMERABLE;
}
if *configurable {
attrs |= PropertyAttributes::CONFIGURABLE;
}
}
Self::Accessor {
enumerable,
configurable,
..
} => {
if *enumerable {
attrs |= PropertyAttributes::ENUMERABLE;
}
if *configurable {
attrs |= PropertyAttributes::CONFIGURABLE;
}
}
Self::Generic {
enumerable,
configurable,
} => {
if enumerable.unwrap_or(false) {
attrs |= PropertyAttributes::ENUMERABLE;
}
if configurable.unwrap_or(false) {
attrs |= PropertyAttributes::CONFIGURABLE;
}
}
}
attrs
}
pub fn default_data() -> Self {
Self::Data {
value: JsValue::Undefined,
writable: false,
enumerable: false,
configurable: false,
}
}
pub fn from_object(obj: &JsValue) -> StatorResult<Self> {
let lookup = |key: &str| -> Option<JsValue> {
match obj {
JsValue::PlainObject(map) => map.borrow().get(key).cloned(),
_ => None,
}
};
let has_get = lookup("get").is_some();
let has_set = lookup("set").is_some();
let has_value = lookup("value").is_some();
let has_writable = lookup("writable").is_some();
if (has_get || has_set) && (has_value || has_writable) {
return Err(StatorError::TypeError(
"Invalid property descriptor. Cannot both specify accessors and a value or writable attribute".to_string(),
));
}
let enumerable = lookup("enumerable").map(|v| v.to_boolean());
let configurable = lookup("configurable").map(|v| v.to_boolean());
if has_get || has_set {
let get = lookup("get").unwrap_or(JsValue::Undefined);
let set = lookup("set").unwrap_or(JsValue::Undefined);
Ok(Self::Accessor {
get,
set,
enumerable: enumerable.unwrap_or(false),
configurable: configurable.unwrap_or(false),
})
} else if has_value || has_writable {
let value = lookup("value").unwrap_or(JsValue::Undefined);
let writable = lookup("writable").map(|v| v.to_boolean()).unwrap_or(false);
Ok(Self::Data {
value,
writable,
enumerable: enumerable.unwrap_or(false),
configurable: configurable.unwrap_or(false),
})
} else {
Ok(Self::Generic {
enumerable,
configurable,
})
}
}
pub fn to_object(&self) -> JsValue {
let mut map = PropertyMap::new();
match self {
Self::Data {
value,
writable,
enumerable,
configurable,
} => {
map.insert("value".to_string(), value.clone());
map.insert("writable".to_string(), JsValue::Boolean(*writable));
map.insert("enumerable".to_string(), JsValue::Boolean(*enumerable));
map.insert("configurable".to_string(), JsValue::Boolean(*configurable));
}
Self::Accessor {
get,
set,
enumerable,
configurable,
} => {
map.insert("get".to_string(), get.clone());
map.insert("set".to_string(), set.clone());
map.insert("enumerable".to_string(), JsValue::Boolean(*enumerable));
map.insert("configurable".to_string(), JsValue::Boolean(*configurable));
}
Self::Generic {
enumerable,
configurable,
} => {
if let Some(e) = enumerable {
map.insert("enumerable".to_string(), JsValue::Boolean(*e));
}
if let Some(c) = configurable {
map.insert("configurable".to_string(), JsValue::Boolean(*c));
}
}
}
JsValue::PlainObject(Rc::new(RefCell::new(map)))
}
pub fn validate_against(
&self,
key: &str,
current_attrs: PropertyAttributes,
) -> StatorResult<PropertyAttributes> {
let is_configurable = current_attrs.contains(PropertyAttributes::CONFIGURABLE);
if !is_configurable {
if self.configurable() == Some(true) {
return Err(StatorError::TypeError(format!(
"Cannot redefine property '{key}': \
[[Configurable]] cannot change from false to true"
)));
}
if let Some(e) = self.enumerable()
&& e != current_attrs.contains(PropertyAttributes::ENUMERABLE)
{
return Err(StatorError::TypeError(format!(
"Cannot redefine property '{key}': \
[[Enumerable]] cannot change on a non-configurable property"
)));
}
if let Self::Data { writable, .. } = self
&& *writable
&& !current_attrs.contains(PropertyAttributes::WRITABLE)
{
return Err(StatorError::TypeError(format!(
"Cannot redefine property '{key}': \
[[Writable]] cannot change from false to true"
)));
}
}
Ok(self.merge_into(current_attrs))
}
pub fn merge_into(&self, current_attrs: PropertyAttributes) -> PropertyAttributes {
match self {
Self::Data { .. } | Self::Accessor { .. } => self.to_attributes(),
Self::Generic {
enumerable,
configurable,
} => {
let mut attrs = current_attrs;
if let Some(e) = enumerable {
if *e {
attrs |= PropertyAttributes::ENUMERABLE;
} else {
attrs -= PropertyAttributes::ENUMERABLE;
}
}
if let Some(c) = configurable {
if *c {
attrs |= PropertyAttributes::CONFIGURABLE;
} else {
attrs -= PropertyAttributes::CONFIGURABLE;
}
}
attrs
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_data_descriptor_classification() {
let desc = FullPropertyDescriptor::Data {
value: JsValue::Smi(42),
writable: true,
enumerable: true,
configurable: true,
};
assert!(desc.is_data());
assert!(!desc.is_accessor());
assert!(!desc.is_generic());
}
#[test]
fn test_data_descriptor_to_attributes() {
let desc = FullPropertyDescriptor::Data {
value: JsValue::Smi(0),
writable: true,
enumerable: false,
configurable: true,
};
let attrs = desc.to_attributes();
assert!(attrs.contains(PropertyAttributes::WRITABLE));
assert!(!attrs.contains(PropertyAttributes::ENUMERABLE));
assert!(attrs.contains(PropertyAttributes::CONFIGURABLE));
}
#[test]
fn test_data_descriptor_to_object_roundtrip() {
let desc = FullPropertyDescriptor::Data {
value: JsValue::Smi(99),
writable: true,
enumerable: false,
configurable: true,
};
let obj = desc.to_object();
let back = FullPropertyDescriptor::from_object(&obj).unwrap();
assert!(back.is_data());
if let FullPropertyDescriptor::Data {
value,
writable,
enumerable,
configurable,
} = back
{
assert_eq!(value, JsValue::Smi(99));
assert!(writable);
assert!(!enumerable);
assert!(configurable);
}
}
#[test]
fn test_accessor_descriptor_classification() {
let desc = FullPropertyDescriptor::Accessor {
get: JsValue::Undefined,
set: JsValue::Undefined,
enumerable: false,
configurable: false,
};
assert!(!desc.is_data());
assert!(desc.is_accessor());
assert!(!desc.is_generic());
}
#[test]
fn test_accessor_descriptor_to_attributes_no_writable() {
let desc = FullPropertyDescriptor::Accessor {
get: JsValue::Undefined,
set: JsValue::Undefined,
enumerable: true,
configurable: true,
};
let attrs = desc.to_attributes();
assert!(!attrs.contains(PropertyAttributes::WRITABLE));
assert!(attrs.contains(PropertyAttributes::ENUMERABLE));
assert!(attrs.contains(PropertyAttributes::CONFIGURABLE));
}
#[test]
fn test_accessor_descriptor_to_object_roundtrip() {
let getter = JsValue::Boolean(true); let setter = JsValue::Boolean(false); let desc = FullPropertyDescriptor::Accessor {
get: getter.clone(),
set: setter.clone(),
enumerable: true,
configurable: false,
};
let obj = desc.to_object();
let back = FullPropertyDescriptor::from_object(&obj).unwrap();
assert!(back.is_accessor());
if let FullPropertyDescriptor::Accessor {
get,
set,
enumerable,
configurable,
} = back
{
assert_eq!(get, getter);
assert_eq!(set, setter);
assert!(enumerable);
assert!(!configurable);
}
}
#[test]
fn test_generic_descriptor_classification() {
let desc = FullPropertyDescriptor::Generic {
enumerable: Some(true),
configurable: None,
};
assert!(!desc.is_data());
assert!(!desc.is_accessor());
assert!(desc.is_generic());
}
#[test]
fn test_validate_rejects_configurable_on_nonconfigurable() {
let desc = FullPropertyDescriptor::Data {
value: JsValue::Smi(1),
writable: false,
enumerable: false,
configurable: true,
};
let current = PropertyAttributes::WRITABLE; let result = desc.validate_against("p", current);
assert!(result.is_err());
}
#[test]
fn test_validate_rejects_enumerable_change_on_nonconfigurable() {
let desc = FullPropertyDescriptor::Data {
value: JsValue::Smi(1),
writable: false,
enumerable: true, configurable: false,
};
let current = PropertyAttributes::empty(); let result = desc.validate_against("p", current);
assert!(result.is_err());
}
#[test]
fn test_validate_rejects_writable_false_to_true_on_nonconfigurable() {
let desc = FullPropertyDescriptor::Data {
value: JsValue::Smi(1),
writable: true,
enumerable: false,
configurable: false,
};
let current = PropertyAttributes::empty();
let result = desc.validate_against("p", current);
assert!(result.is_err());
}
#[test]
fn test_validate_allows_narrowing_writable_on_nonconfigurable() {
let desc = FullPropertyDescriptor::Data {
value: JsValue::Smi(1),
writable: false,
enumerable: false,
configurable: false,
};
let current = PropertyAttributes::WRITABLE;
let result = desc.validate_against("p", current);
assert!(result.is_ok());
}
#[test]
fn test_validate_allows_any_change_on_configurable() {
let desc = FullPropertyDescriptor::Data {
value: JsValue::Smi(1),
writable: true,
enumerable: true,
configurable: true,
};
let current = PropertyAttributes::CONFIGURABLE;
let result = desc.validate_against("p", current);
assert!(result.is_ok());
}
#[test]
fn test_from_object_rejects_mixed_data_accessor() {
let mut map = PropertyMap::new();
map.insert("value".to_string(), JsValue::Smi(1));
map.insert("get".to_string(), JsValue::Undefined);
let obj = JsValue::PlainObject(Rc::new(RefCell::new(map)));
let result = FullPropertyDescriptor::from_object(&obj);
assert!(result.is_err());
}
#[test]
fn test_from_object_empty_yields_generic() {
let map = PropertyMap::new();
let obj = JsValue::PlainObject(Rc::new(RefCell::new(map)));
let desc = FullPropertyDescriptor::from_object(&obj).unwrap();
assert!(desc.is_generic());
}
#[test]
fn test_default_data_all_false() {
let desc = FullPropertyDescriptor::default_data();
if let FullPropertyDescriptor::Data {
value,
writable,
enumerable,
configurable,
} = desc
{
assert_eq!(value, JsValue::Undefined);
assert!(!writable);
assert!(!enumerable);
assert!(!configurable);
} else {
panic!("expected Data");
}
}
#[test]
fn test_data_descriptor_to_object_has_all_keys() {
let desc = FullPropertyDescriptor::Data {
value: JsValue::Smi(42),
writable: true,
enumerable: false,
configurable: true,
};
let obj = desc.to_object();
if let JsValue::PlainObject(map) = obj {
let borrow = map.borrow();
assert!(borrow.contains_key("value"), "must have 'value'");
assert!(borrow.contains_key("writable"), "must have 'writable'");
assert!(borrow.contains_key("enumerable"), "must have 'enumerable'");
assert!(
borrow.contains_key("configurable"),
"must have 'configurable'"
);
assert!(!borrow.contains_key("get"), "data desc must not have 'get'");
assert!(!borrow.contains_key("set"), "data desc must not have 'set'");
} else {
panic!("expected PlainObject");
}
}
#[test]
fn test_accessor_descriptor_to_object_has_all_keys() {
let desc = FullPropertyDescriptor::Accessor {
get: JsValue::Undefined,
set: JsValue::Undefined,
enumerable: true,
configurable: false,
};
let obj = desc.to_object();
if let JsValue::PlainObject(map) = obj {
let borrow = map.borrow();
assert!(borrow.contains_key("get"), "must have 'get'");
assert!(borrow.contains_key("set"), "must have 'set'");
assert!(borrow.contains_key("enumerable"), "must have 'enumerable'");
assert!(
borrow.contains_key("configurable"),
"must have 'configurable'"
);
assert!(
!borrow.contains_key("value"),
"accessor desc must not have 'value'"
);
assert!(
!borrow.contains_key("writable"),
"accessor desc must not have 'writable'"
);
} else {
panic!("expected PlainObject");
}
}
#[test]
fn test_validate_configurable_allows_all() {
let desc = FullPropertyDescriptor::Accessor {
get: JsValue::Undefined,
set: JsValue::Undefined,
enumerable: true,
configurable: true,
};
let current = PropertyAttributes::CONFIGURABLE | PropertyAttributes::WRITABLE;
assert!(desc.validate_against("p", current).is_ok());
}
#[test]
fn test_validate_same_writable_false_on_non_configurable_ok() {
let desc = FullPropertyDescriptor::Data {
value: JsValue::Smi(1),
writable: false,
enumerable: false,
configurable: false,
};
let current = PropertyAttributes::empty();
assert!(
desc.validate_against("p", current).is_ok(),
"narrowing writable (false→false) on non-configurable should succeed"
);
}
#[test]
fn test_merge_into_data_ignores_current() {
let desc = FullPropertyDescriptor::Data {
value: JsValue::Smi(1),
writable: true,
enumerable: false,
configurable: true,
};
let current = PropertyAttributes::all();
let merged = desc.merge_into(current);
assert!(merged.contains(PropertyAttributes::WRITABLE));
assert!(!merged.contains(PropertyAttributes::ENUMERABLE));
assert!(merged.contains(PropertyAttributes::CONFIGURABLE));
}
#[test]
fn test_merge_into_generic_preserves_unspecified() {
let desc = FullPropertyDescriptor::Generic {
enumerable: Some(true),
configurable: None,
};
let current = PropertyAttributes::WRITABLE | PropertyAttributes::CONFIGURABLE;
let merged = desc.merge_into(current);
assert!(merged.contains(PropertyAttributes::WRITABLE));
assert!(merged.contains(PropertyAttributes::ENUMERABLE));
assert!(merged.contains(PropertyAttributes::CONFIGURABLE));
}
#[test]
fn test_merge_into_generic_clears_specified() {
let desc = FullPropertyDescriptor::Generic {
enumerable: None,
configurable: Some(false),
};
let current = PropertyAttributes::WRITABLE
| PropertyAttributes::ENUMERABLE
| PropertyAttributes::CONFIGURABLE;
let merged = desc.merge_into(current);
assert!(merged.contains(PropertyAttributes::WRITABLE));
assert!(merged.contains(PropertyAttributes::ENUMERABLE));
assert!(!merged.contains(PropertyAttributes::CONFIGURABLE));
}
#[test]
fn test_merge_into_generic_empty_preserves_all() {
let desc = FullPropertyDescriptor::Generic {
enumerable: None,
configurable: None,
};
let current = PropertyAttributes::WRITABLE | PropertyAttributes::ENUMERABLE;
let merged = desc.merge_into(current);
assert_eq!(merged, current);
}
}