From 36c1ef12083eb19cbd50e4078e91910dfe62da91 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Fri, 14 Aug 2026 21:15:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20replaceAll=20=E7=B1=BB=E6=AD=A3=E5=88=99?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E7=BB=9F=E4=B8=80=E8=B5=B0=20ToString=20?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E8=B7=AF=E5=BE=84=E4=B8=8E=20replace=20?= =?UTF-8?q?=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/string.rs | 11 +--- .../oxide_vm/tests/builtins_string_tests.rs | 63 ++++++++++++++++++- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/crates/oxide_builtins/src/string.rs b/crates/oxide_builtins/src/string.rs index eceae781..e80bbfc1 100644 --- a/crates/oxide_builtins/src/string.rs +++ b/crates/oxide_builtins/src/string.rs @@ -1013,8 +1013,8 @@ pub fn string_split(vm: &mut H, args: &[u8]) -> NativeResult { /// (`s.replace()` 全缺省等价于把 "undefined" 替换为 "undefined",结果与 /// 原串一致;`s.replace("b")` 得到 "aundefinedc" 而非 "ac")。 /// - replaceAll 遇非 global 正则抛 TypeError;类正则对象(proto 恒等 -/// RegExp.prototype 但无编译正则)replaceAll 返回原串、replace 按 ToString -/// 文本走字符串路径。 +/// RegExp.prototype 但无编译正则)replace/replaceAll 统一按 ToString 文本走 +/// 字符串路径。 /// /// # 注意事项 /// - 分支 B 的 `&H` 共享借用与 `&mut` 互斥由编译器强制,三 `&str` 同源可共存 @@ -1066,13 +1066,8 @@ fn string_replace_impl(vm: &mut H, args: &[u8], all: bool) -> NativeR None }; - // 类正则对象无编译正则:replaceAll 按现状返回原串(receiver 转换先行保错误次序)。 - if all && is_re && !has_native_re { - let s = try_string!(this_string(vm, args)).into_owned(); - return NativeResult::Ok(vm.new_string_owned(s)); - } - // replaceAll 要求正则带 global:非 global 正则直接抛 TypeError(规范 flags 检查)。 + // 类正则对象(无编译正则)无 flags 概念,统一走 as_string 文本路径,不在此检查。 if all && has_native_re && !is_global { return NativeResult::Err(crate::error::create_type_error( vm, diff --git a/crates/oxide_vm/tests/builtins_string_tests.rs b/crates/oxide_vm/tests/builtins_string_tests.rs index 3569a955..b4d038b0 100644 --- a/crates/oxide_vm/tests/builtins_string_tests.rs +++ b/crates/oxide_vm/tests/builtins_string_tests.rs @@ -398,7 +398,9 @@ fn string_replace_all_nonglobal_regex_throws() { #[test] fn string_replace_regex_proto_subclass_fallback() { // 类正则对象(proto 恒等 RegExp.prototype 但无编译正则): - // replace 走字符串文本路径,replaceAll 返回原串。 + // replace/replaceAll 统一按 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"); @@ -412,6 +414,37 @@ fn string_replace_regex_proto_subclass_fallback() { assert_eq!(to_str(&vm, s), "aXb"); } +#[test] +fn string_replace_regex_proto_subclass_custom_tostring() { + // 判别性回归:自定义 toString 命中源串的类正则对象,replace/replaceAll + // 均按 ToString 文本替换(不返回原串、不抛 TypeError),两入口对称。 + let mut vm = Vm::new(); + let s = eval( + &mut vm, + "var sp = Object.create(RegExp.prototype); sp.toString = function(){ return 'X' }; 'aXb'.replace(sp, 'Y')", + ) + .unwrap(); + assert_eq!(to_str(&vm, s), "aYb"); + let s = eval( + &mut vm, + "var sp = Object.create(RegExp.prototype); sp.toString = function(){ return 'X' }; 'aXb'.replaceAll(sp, 'Y')", + ) + .unwrap(); + assert_eq!(to_str(&vm, s), "aYb"); + let s = eval( + &mut vm, + "var sp = Object.create(RegExp.prototype); sp.toString = function(){ return 'X' }; 'aXb'.replace(sp, function(){ return 'Z' })", + ) + .unwrap(); + assert_eq!(to_str(&vm, s), "aZb"); + let s = eval( + &mut vm, + "var sp = Object.create(RegExp.prototype); sp.toString = function(){ return 'X' }; 'aXb'.replaceAll(sp, function(){ return 'Z' })", + ) + .unwrap(); + assert_eq!(to_str(&vm, s), "aZb"); +} + #[test] fn string_replace_boxed_receiver_and_replacement() { // boxed receiver 走对象 ToString 路径,boxed replacement 走 ToString 转换。 @@ -434,6 +467,34 @@ fn string_replace_astral_and_empty_pattern() { assert_eq!(to_str(&vm, s), "-a-b-c-"); } +#[test] +fn string_replace_numeric_pattern() { + // 数字 pattern 经 ToString 文本替换("1"),replace/replaceAll 均走字符串路径。 + let mut vm = Vm::new(); + let s = eval(&mut vm, "'a1b'.replace(1, 'X')").unwrap(); + assert_eq!(to_str(&vm, s), "aXb"); + let s = eval(&mut vm, "'a1b1c'.replaceAll(1, 'X')").unwrap(); + assert_eq!(to_str(&vm, s), "aXbXc"); +} + +#[test] +fn string_replace_all_boxed_string_pattern() { + // boxed String pattern 经 ToString 文本替换。 + let mut vm = Vm::new(); + let s = eval(&mut vm, "'abc'.replaceAll(new String('b'), 'X')").unwrap(); + assert_eq!(to_str(&vm, s), "aXc"); +} + +#[test] +fn string_replace_empty_receiver() { + // 空串 receiver + 空 pattern:仅位置 0 一次匹配。 + let mut vm = Vm::new(); + let s = eval(&mut vm, "''.replace('', 'x')").unwrap(); + assert_eq!(to_str(&vm, s), "x"); + let s = eval(&mut vm, "''.replaceAll('', 'x')").unwrap(); + assert_eq!(to_str(&vm, s), "x"); +} + // ── split 测试 ── #[test]