fix: String 方法 Symbol 参数抛 TypeError 且缺参按 undefined 查找

as_string 由兜底转换改 Result 化:Symbol 与其余转换失败抛
TypeError,对象 ToString 抛出的原生异常经 take_uncaught_value
原样传播,不再静默降级为空串/占位文本。17 处调用点统一经
try_string! 抛错。

indexOf/includes/startsWith/endsWith/lastIndexOf 删除缺参短路:
searchString 缺省按 ToString(undefined)="undefined" 参与查找,
'undefined'.indexOf() 现得 0。

类正则对象(Object.create(RegExp.prototype))默认 toString 抛
TypeError 的 replace/split 用例随异常传播行为更新断言。
This commit is contained in:
2026-08-16 02:08:13 +08:00
parent 0c6aa7fbef
commit 5de959e2bd
2 changed files with 113 additions and 82 deletions
+47 -61
View File
@@ -222,12 +222,27 @@ pub(crate) fn make_string_array_values<H: VmHost>(vm: &mut H, parts: Vec<JsValue
}
/// 取参数字符串内容:原始字符串零拷贝借用,其余经完整 ToString(对象 ToPrimitive)。
/// Symbol 与其余转换失败按规范抛 TypeError;对象 ToString 抛出的原生异常原样传播。
///
/// # 边界与前提
/// - 失败返回异常值,调用方经 `try_string!` 原样抛出。
///
/// # 注意事项
/// 返回借用绑定本次 `&mut` 借用;需与 `this_string` 借用并存时先 `into_owned` 落地。
fn as_string<'a, H: VmHost>(vm: &'a mut H, val: JsValue) -> Cow<'a, str> {
fn as_string<'a, H: VmHost>(vm: &'a mut H, val: JsValue) -> Result<Cow<'a, str>, 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<H: VmHost>(val: JsValue, vm: &H) -> bool {
/// `String.prototype.indexOf(searchString, position)`:按 UTF-16 单元查找首次出现位置。
pub fn string_index_of<H: VmHost>(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<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
/// `String.prototype.includes(searchString, position)`:是否包含子串。
pub fn string_includes<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
/// `String.prototype.lastIndexOf(searchString, position)`:从后往前查找首次出现位置。
pub fn string_last_index_of<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
/// `String.prototype.startsWith(searchString, position)`:是否以指定子串开头。
pub fn string_starts_with<H: VmHost>(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<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
/// `String.prototype.endsWith(searchString, endPosition)`:是否以指定子串结尾。
pub fn string_ends_with<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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<H: VmHost>(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) {
+66 -21
View File
@@ -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]