diff --git a/crates/oxide_builtins/src/string.rs b/crates/oxide_builtins/src/string.rs index 8d104de5..44532d00 100644 --- a/crates/oxide_builtins/src/string.rs +++ b/crates/oxide_builtins/src/string.rs @@ -222,12 +222,27 @@ pub(crate) fn make_string_array_values(vm: &mut H, parts: Vec(vm: &'a mut H, val: JsValue) -> Cow<'a, str> { +fn as_string<'a, H: VmHost>(vm: &'a mut H, val: JsValue) -> Result, JsValue> { if val.is_string() { - Cow::Borrowed(vm.string_ref(val)) - } else { - Cow::Owned(oxide_runtime_api::to_string_full(val, vm).unwrap_or_else(|_| oxide_runtime_api::to_string(val))) + return Ok(Cow::Borrowed(vm.string_ref(val))); + } + match oxide_runtime_api::to_string_full(val, vm) { + Ok(s) => Ok(Cow::Owned(s)), + Err(_) => { + // ToString 触发对象 toString/valueOf 抛出的原生异常须原样传播, + // 否则会被展平为普通 Error 丢失原始异常对象。 + if let Some(exc) = vm.take_uncaught_value() { + return Err(exc); + } + Err(crate::error::create_type_error(vm, "Cannot convert a Symbol value to a string")) + } } } @@ -508,12 +523,10 @@ fn is_regexp_obj(val: JsValue, vm: &H) -> bool { /// `String.prototype.indexOf(searchString, position)`:按 UTF-16 单元查找首次出现位置。 pub fn string_index_of(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.indexOf called with {} args", args.len()); - // 参数转换先行:search 与 position 均可能触发对象 ToString/ToNumber(&mut 路径)。 - let search = if args.len() >= 2 { - as_string(vm, vm.reg(args[1])).into_owned() - } else { - String::new() - }; + // 参数转换先行:search 缺省为 undefined(经 ToString 得 "undefined" 参与查找), + // position 缺省 0;均可能触发对象 ToString/ToNumber(&mut 路径)。 + let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() }; + let search = try_string!(as_string(vm, search_val)).into_owned(); let pos_raw = if args.len() > 2 { vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize } else { @@ -521,9 +534,6 @@ pub fn string_index_of(vm: &mut H, args: &[u8]) -> NativeResult { }; // 借用 this 原始字符串零拷贝,扫描期内不再有 &mut 调用。 let s = try_string!(this_string(vm, args)); - if args.len() < 2 { - return NativeResult::Ok(JsValue::int(-1)); - } let n = utf16_len(&s); let pos = pos_raw.min(n); @@ -548,21 +558,15 @@ pub fn string_index_of(vm: &mut H, args: &[u8]) -> NativeResult { /// `String.prototype.includes(searchString, position)`:是否包含子串。 pub fn string_includes(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.includes called with {} args", args.len()); - // 参数转换先行(&mut 路径),后借 this 扫描。 - let search = if args.len() >= 2 { - as_string(vm, vm.reg(args[1])).into_owned() - } else { - String::new() - }; + // 参数转换先行(&mut 路径):search 缺省为 undefined(经 ToString 得 "undefined")。 + let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() }; + let search = try_string!(as_string(vm, search_val)).into_owned(); let pos_raw = if args.len() > 2 { vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize } else { 0 }; let s = try_string!(this_string(vm, args)); - if args.len() < 2 { - return NativeResult::Ok(JsValue::bool(false)); - } let n = utf16_len(&s); let pos = pos_raw.min(n); @@ -790,12 +794,9 @@ pub fn string_at(vm: &mut H, args: &[u8]) -> NativeResult { /// `String.prototype.lastIndexOf(searchString, position)`:从后往前查找首次出现位置。 pub fn string_last_index_of(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.lastIndexOf called with {} args", args.len()); - // 参数转换先行(&mut 路径),后借 this 扫描。 - let search = if args.len() >= 2 { - as_string(vm, vm.reg(args[1])).into_owned() - } else { - String::new() - }; + // 参数转换先行(&mut 路径):search 缺省为 undefined(经 ToString 得 "undefined")。 + let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() }; + let search = try_string!(as_string(vm, search_val)).into_owned(); let pos_raw = if args.len() > 2 { let p = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2])); if p.is_nan() { @@ -807,9 +808,6 @@ pub fn string_last_index_of(vm: &mut H, args: &[u8]) -> NativeResult None }; let s = try_string!(this_string(vm, args)); - if args.len() < 2 { - return NativeResult::Ok(JsValue::int(-1)); - } let n = utf16_len(&s); let pos = match pos_raw { Some(p) => p.min(n), @@ -881,7 +879,7 @@ pub fn string_pad_start(vm: &mut H, args: &[u8]) -> NativeResult { None }; let pad = if args.len() > 2 { - as_string(vm, vm.reg(args[2])).into_owned() + try_string!(as_string(vm, vm.reg(args[2]))).into_owned() } else { " ".to_string() }; @@ -914,7 +912,7 @@ pub fn string_pad_end(vm: &mut H, args: &[u8]) -> NativeResult { None }; let pad = if args.len() > 2 { - as_string(vm, vm.reg(args[2])).into_owned() + try_string!(as_string(vm, vm.reg(args[2]))).into_owned() } else { " ".to_string() }; @@ -940,21 +938,15 @@ pub fn string_pad_end(vm: &mut H, args: &[u8]) -> NativeResult { /// `String.prototype.startsWith(searchString, position)`:是否以指定子串开头。 pub fn string_starts_with(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.startsWith called with {} args", args.len()); - // 参数转换先行(&mut 路径),后借 this 扫描。 - let search = if args.len() >= 2 { - as_string(vm, vm.reg(args[1])).into_owned() - } else { - String::new() - }; + // 参数转换先行(&mut 路径):search 缺省为 undefined(经 ToString 得 "undefined")。 + let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() }; + let search = try_string!(as_string(vm, search_val)).into_owned(); let pos_raw = if args.len() > 2 { vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize } else { 0 }; let s = try_string!(this_string(vm, args)); - if args.len() < 2 { - return NativeResult::Ok(JsValue::bool(false)); - } let n = utf16_len(&s); let pos = pos_raw.min(n); // 位置须对齐字符边界才有 well-formed 前缀可比;落在代理对中间时仅空串可匹配。 @@ -968,21 +960,15 @@ pub fn string_starts_with(vm: &mut H, args: &[u8]) -> NativeResult { /// `String.prototype.endsWith(searchString, endPosition)`:是否以指定子串结尾。 pub fn string_ends_with(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.endsWith called with {} args", args.len()); - // 参数转换先行(&mut 路径),后借 this 扫描。 - let search = if args.len() >= 2 { - as_string(vm, vm.reg(args[1])).into_owned() - } else { - String::new() - }; + // 参数转换先行(&mut 路径):search 缺省为 undefined(经 ToString 得 "undefined")。 + let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() }; + let search = try_string!(as_string(vm, search_val)).into_owned(); let end_pos_raw = if args.len() > 2 { vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize } else { usize::MAX }; let s = try_string!(this_string(vm, args)); - if args.len() < 2 { - return NativeResult::Ok(JsValue::bool(false)); - } let n = utf16_len(&s); let end_pos = end_pos_raw.min(n); // 截断位置须对齐字符边界才有 well-formed 后缀可比;落在代理对中间时仅空串可匹配。 @@ -1017,7 +1003,7 @@ pub fn string_split(vm: &mut H, args: &[u8]) -> NativeResult { let sep = if is_undefined_sep || has_native_re { String::new() } else { - as_string(vm, sep_val.unwrap()).into_owned() + try_string!(as_string(vm, sep_val.unwrap())).into_owned() }; // ToUint32(limit),缺省为 2^32-1。 let limit = if args.len() > 2 { @@ -1178,7 +1164,7 @@ fn string_replace_impl(vm: &mut H, args: &[u8], all: bool) -> NativeR let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; return regex_replace_fn(vm, regex, &s, replacer_val, is_global, text_arg); } - let pattern = as_string(vm, pattern_val).into_owned(); + let pattern = try_string!(as_string(vm, pattern_val)).into_owned(); return string_replace_fn(vm, &s, &pattern, replacer_val, all, text_arg); } @@ -1228,10 +1214,10 @@ fn string_replace_impl(vm: &mut H, args: &[u8], all: bool) -> NativeR } else if args.len() < 2 { "undefined".to_string() } else { - as_string(vm, pattern_val).into_owned() + try_string!(as_string(vm, pattern_val)).into_owned() }; let replacement = if args.len() > 2 { - as_string(vm, replacement_val).into_owned() + try_string!(as_string(vm, replacement_val)).into_owned() } else { "undefined".to_string() }; @@ -1244,10 +1230,10 @@ fn string_replace_impl(vm: &mut H, args: &[u8], all: bool) -> NativeR } else if args.len() < 2 { "undefined".to_string() } else { - as_string(vm, pattern_val).into_owned() + try_string!(as_string(vm, pattern_val)).into_owned() }; let replacement = if args.len() > 2 { - as_string(vm, replacement_val).into_owned() + try_string!(as_string(vm, replacement_val)).into_owned() } else { "undefined".to_string() }; @@ -1287,7 +1273,7 @@ pub fn string_match_fn(vm: &mut H, args: &[u8]) -> NativeResult { let pattern_val = if args.len() >= 2 { Some(vm.reg(args[1])) } else { None }; let is_re = pattern_val.map(|v| is_regexp_obj(v, vm)).unwrap_or(false); let pattern = match pattern_val { - Some(v) if !is_re => as_string(vm, v).into_owned(), + Some(v) if !is_re => try_string!(as_string(vm, v)).into_owned(), _ => String::new(), }; let s = try_string!(this_string(vm, args)); @@ -1339,7 +1325,7 @@ pub fn string_search(vm: &mut H, args: &[u8]) -> NativeResult { // 正则判定与参数转换先行(&mut 路径),后借 this 扫描。 let is_re = args.len() >= 2 && is_regexp_obj(pattern_val, vm); let pattern = if args.len() >= 2 && !is_re { - as_string(vm, pattern_val).into_owned() + try_string!(as_string(vm, pattern_val)).into_owned() } else { String::new() }; @@ -1471,7 +1457,7 @@ pub fn string_normalize(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.normalize called with {} args", args.len()); use unicode_normalization::UnicodeNormalization; let form = if args.len() > 1 { - as_string(vm, vm.reg(args[1])).into_owned() + try_string!(as_string(vm, vm.reg(args[1]))).into_owned() } else { "NFC".to_string() }; @@ -1512,7 +1498,7 @@ pub fn string_match_all(vm: &mut H, args: &[u8]) -> NativeResult { } pattern_val } else { - let pattern_str = as_string(vm, pattern_val); + let pattern_str = try_string!(as_string(vm, pattern_val)); let escaped = regress::escape(pattern_str.as_ref()); let rx_str = if escaped.is_empty() { String::from("(?:)") } else { format!("({})", escaped) }; let compiled = match regress::Regex::new(&rx_str) { diff --git a/crates/oxide_vm/tests/builtins_string_tests.rs b/crates/oxide_vm/tests/builtins_string_tests.rs index 71c93bd3..399e095d 100644 --- a/crates/oxide_vm/tests/builtins_string_tests.rs +++ b/crates/oxide_vm/tests/builtins_string_tests.rs @@ -222,8 +222,10 @@ fn string_search_empty_pattern_returns_zero() { #[test] fn string_no_arg_guard_family_consistent() { - // 家族无参守卫对照:search/indexOf 返回 -1,includes/startsWith/endsWith 返回 false, - // charAt 取首字符——search 与同批方法守卫位置一致、行为家族化。 + // 缺参时 searchString 按 ToString(undefined)="undefined" 参与查找——'abc' + // 不含 "undefined",indexOf 得 -1、includes/startsWith/endsWith 得 false + // ("undefined" 命中的判别断言见 string_missing_arg_searches_undefined); + // search 缺参仍走旧守卫返回 -1。 let mut vm = Vm::new(); let result = eval(&mut vm, "'abc'.search()").unwrap(); assert_eq!(result.as_int(), -1); @@ -237,6 +239,53 @@ fn string_no_arg_guard_family_consistent() { assert!(!result.as_bool()); } +#[test] +fn string_missing_arg_searches_undefined() { + // 缺参 searchString 按 ToString(undefined)="undefined" 参与查找(非固定结果短路)。 + let mut vm = Vm::new(); + let r = eval(&mut vm, "'undefined'.indexOf()").unwrap(); + assert_eq!(r.as_int(), 0); + let r = eval(&mut vm, "'abc'.indexOf()").unwrap(); + assert_eq!(r.as_int(), -1); + let r = eval(&mut vm, "'undefined'.lastIndexOf()").unwrap(); + assert_eq!(r.as_int(), 0); + let r = eval(&mut vm, "'undefined'.includes()").unwrap(); + assert!(r.as_bool()); + let r = eval(&mut vm, "''.includes()").unwrap(); + assert!(!r.as_bool()); + let r = eval(&mut vm, "'undefined'.startsWith()").unwrap(); + assert!(r.as_bool()); + let r = eval(&mut vm, "'undefined'.endsWith()").unwrap(); + assert!(r.as_bool()); + // 显式传 undefined 与缺参等价(同走 "undefined" 查找)。 + let r = eval(&mut vm, "'undefined'.indexOf(undefined)").unwrap(); + assert_eq!(r.as_int(), 0); +} + +#[test] +fn string_symbol_argument_throws_type_error() { + // Symbol 参数经 as_string 抛 TypeError(不静默降级为文本查找)。 + let mut vm = Vm::new(); + for src in [ + "'abc'.indexOf(Symbol('x'))", + "'abc'.lastIndexOf(Symbol('x'))", + "'abc'.includes(Symbol('x'))", + "'abc'.startsWith(Symbol('x'))", + "'abc'.endsWith(Symbol('x'))", + "'abc'.replace('a', Symbol('x'))", + "'abc'.replaceAll('a', Symbol('x'))", + "'abc'.split(Symbol('x'))", + "'abc'.search(Symbol('x'))", + "'abc'.match(Symbol('x'))", + "'abc'.padStart(5, Symbol('x'))", + "'abc'.padEnd(5, Symbol('x'))", + "'abc'.normalize(Symbol('x'))", + ] { + let err = eval(&mut vm, src).unwrap_err(); + assert!(err.contains("TypeError"), "{src} 应抛 TypeError,实际: {err}"); + } +} + #[test] fn string_trim_start() { let mut vm = Vm::new(); @@ -438,21 +487,19 @@ fn string_replace_all_nonglobal_regex_throws() { #[test] fn string_replace_regex_proto_subclass_fallback() { - // 类正则对象(proto 恒等 RegExp.prototype 但无编译正则): - // replace/replaceAll 统一按 ToString 文本走字符串路径;默认 toString - // 不命中源串,故此处断言结果与原串一致(判别性用例见 - // string_replace_regex_proto_subclass_custom_tostring)。 + // 类正则对象(proto 恒等 RegExp.prototype 但无编译正则)的默认 toString 是 + // RegExp.prototype.toString,对非 RegExp receiver 抛 TypeError——对象 ToString + // 异常按规范原样传播,不吞错降级为文本替换;自定义 toString 的判别性用例见 + // string_replace_regex_proto_subclass_custom_tostring。 let mut vm = Vm::new(); - let s = eval(&mut vm, "var sp = Object.create(RegExp.prototype); 'aXb'.replace(sp, 'Y')").unwrap(); - assert_eq!(to_str(&vm, s), "aXb"); - let s = eval(&mut vm, "var sp = Object.create(RegExp.prototype); 'aXb'.replaceAll(sp, 'Y')").unwrap(); - assert_eq!(to_str(&vm, s), "aXb"); - let s = eval( - &mut vm, + for src in [ + "var sp = Object.create(RegExp.prototype); 'aXb'.replace(sp, 'Y')", + "var sp = Object.create(RegExp.prototype); 'aXb'.replaceAll(sp, 'Y')", "var sp = Object.create(RegExp.prototype); 'aXb'.replace(sp, function(m){ return 'Z' })", - ) - .unwrap(); - assert_eq!(to_str(&vm, s), "aXb"); + ] { + let err = eval(&mut vm, src).unwrap_err(); + assert!(err.contains("TypeError"), "{src} 应抛 TypeError,实际: {err}"); + } } #[test] @@ -581,13 +628,11 @@ fn string_split_string_separator() { #[test] fn string_split_regex_proto_subclass_fallback() { - // 类正则对象(proto 恒等 RegExp.prototype 但无编译正则)回退字符串路径, - // 按 ToString 文本("[object]")切分,不落入逐字符切分。 + // 类正则对象(proto 恒等 RegExp.prototype 但无编译正则)的默认 toString 抛 + // TypeError(RegExp.prototype.toString 校验 receiver),split 按规范传播异常。 let mut vm = Vm::new(); - let result = eval(&mut vm, "var sp = Object.create(RegExp.prototype); 'aXb'.split(sp)").unwrap(); - let obj = unsafe { &*result.as_js_object_ptr() }; - assert_eq!(obj.prop_count(), 1); - assert_eq!(to_str(&vm, obj.get_prop_at(0)), "aXb"); + let err = eval(&mut vm, "var sp = Object.create(RegExp.prototype); 'aXb'.split(sp)").unwrap_err(); + assert!(err.contains("TypeError"), "类正则分隔符应抛 TypeError,实际: {err}"); } #[test]