diff --git a/Cargo.lock b/Cargo.lock index 47bbf72f..66caf134 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1263,6 +1263,7 @@ version = "0.1.0" dependencies = [ "oxc_allocator", "oxc_ast", + "oxc_ast_visit", "oxc_diagnostics", "oxc_parser", "oxc_semantic", diff --git a/Cargo.toml b/Cargo.toml index fa8ca7a7..7b21dc14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ rust-version = "1.80" oxc_parser = "0.134.0" oxc_allocator = "0.134.0" oxc_ast = "0.134.0" +oxc_ast_visit = "0.134.0" oxc_span = "0.134.0" oxc_diagnostics = "0.134.0" oxc_syntax = "0.134.0" diff --git a/crates/oxide_parser/Cargo.toml b/crates/oxide_parser/Cargo.toml index 2ae63ce4..75e94e6a 100644 --- a/crates/oxide_parser/Cargo.toml +++ b/crates/oxide_parser/Cargo.toml @@ -11,6 +11,7 @@ oxide_log = { path = "../oxide_log" } oxc_parser.workspace = true oxc_allocator.workspace = true oxc_ast.workspace = true +oxc_ast_visit.workspace = true oxc_span.workspace = true oxc_diagnostics.workspace = true oxc_syntax.workspace = true diff --git a/crates/oxide_parser/src/lib.rs b/crates/oxide_parser/src/lib.rs index 75ba2acf..06bf3bb5 100644 --- a/crates/oxide_parser/src/lib.rs +++ b/crates/oxide_parser/src/lib.rs @@ -2,6 +2,7 @@ mod error; mod parser_log; +mod redeclaration_filter; /// 解析错误:消息 + 源文件字节区间(起始,结束)。 pub use error::OxideError; @@ -46,7 +47,13 @@ fn parse_with_source_type<'a>( .with_check_syntax_error(true) .build(&ret.program); if !semantic_ret.errors.is_empty() { - return Err(semantic_ret.errors.into_iter().map(OxideError::from).collect()); + // 重名诊断先经 AST 复核剔除跨语句列合法合并的误报(须在归一化前, + // 归一化只留首标签会丢失复核所需的新声明 span);复核后无剩余 + // 错误时按解析成功落回。 + let filtered = redeclaration_filter::filter_semantic_diagnostics(semantic_ret.errors, &ret.program); + if !filtered.is_empty() { + return Err(filtered.into_iter().map(OxideError::from).collect()); + } } Ok(ret.program) diff --git a/crates/oxide_parser/src/redeclaration_filter.rs b/crates/oxide_parser/src/redeclaration_filter.rs new file mode 100644 index 00000000..802e0816 --- /dev/null +++ b/crates/oxide_parser/src/redeclaration_filter.rs @@ -0,0 +1,223 @@ +//! 语义早期错误诊断的 AST 复核:剔除函数名与 var 声明合法合并的重名误报。 +//! +//! "Identifier `x` has already been declared" 诊断按"声明子当前作用域 + +//! 合并符号历史"粒度发射:var 声明子位于语句作用域(for-in/for-of 头、块、 +//! switch 等)且其 var 作用域内同名合并项的上一项为函数声明即判碰撞。 +//! 规范按"语句列"粒度裁定:只有作为**某个块/case 语句列直接成员**的函数 +//! 声明才计入该列的 lexically declared 名,其与该列闭包内 var 声明同名 +//! 才是 SyntaxError;脚本顶层/函数体语句列直接成员的函数声明与同 var +//! 作用域的 var 声明属提升后合法合并(var 步复用既有绑定),不应拒。 +//! +//! 关键不变式:只剔除可确认误报的既定消息形诊断;消息形不匹配、标签 +//! 缺失、模块模式、节点匹配失败一律原样保留,不吞任何真错。 + +use std::collections::HashMap; + +use oxc_ast::ast::{ + BlockStatement, Function, FunctionType, IfStatement, Program, SwitchStatement, VariableDeclaration, + VariableDeclarationKind, +}; +use oxc_ast_visit::walk::{ + walk_block_statement, walk_function_body, walk_if_statement, walk_program, walk_switch_statement, + walk_variable_declaration, +}; +use oxc_ast_visit::Visit; +use oxc_diagnostics::OxcDiagnostic; +use oxc_syntax::scope::ScopeFlags; + +/// 重名诊断消息形:前缀 + 标识符名 + 后缀。 +/// +/// 上游改文案时匹配失败、过滤静默透传(行为回到全量拒绝,不误吞真错), +/// 消息形由测试钉锁。 +const MSG_PREFIX: &str = "Identifier `"; +const MSG_SUFFIX: &str = "` has already been declared"; + +/// 对语义诊断列表做重名误报复核,返回剔除误报后的列表。 +/// +/// 须在 `OxideError` 归一化之前调用:归一化只保留首个标签,会丢失 +/// "新声明"侧 span 而失去复核能力。 +/// +/// # 边界与前提 +/// - 诊断列表可能混有非重名形诊断:逐条独立裁定,未命中剔除条件者原样保留。 +/// - 模块模式(`program.source_type` 为模块)顶层函数名为 lexical 绑定, +/// 一切 var 碰撞均为真错,整列保留。 +/// +/// # 注意事项 +/// - 无重名形诊断时直接返回原列表(常见路径零开销)。 +/// - AST 遍历只读,不改写 `program`。 +pub(crate) fn filter_semantic_diagnostics<'a>(errors: Vec, program: &Program<'a>) -> Vec { + if !errors.iter().any(|e| redeclaration_name(&e.message).is_some()) { + return errors; + } + + // 单遍收集函数声明归属与 var 声明块链,供逐诊断复核。 + let mut collector = Collector::default(); + collector.visit_program(program); + + let is_module = program.source_type.is_module(); + errors + .into_iter() + .filter(|e| verdict(e, &collector, is_module) != Some(true)) + .collect() +} + +/// 从重名形消息提取标识符名;消息形不匹配返 `None`。 +fn redeclaration_name(message: &str) -> Option<&str> { + let name = message.strip_prefix(MSG_PREFIX)?; + name.strip_suffix(MSG_SUFFIX) +} + +/// 裁定单条重名形诊断。 +/// +/// 返回 `Some(true)` = 已确认误报(可剔除);`Some(false)` = 已确认真错 +/// (保留);`None` = 不在本过滤裁定范围(消息形/标签/节点匹配失败), +/// 一律原样保留。 +/// +/// # 步骤 +/// 1. 消息形匹配取标识符名;标签取双标签(0 = 既有声明,1 = 新声明)。 +/// 2. 既有声明侧按 span 匹配同名函数声明,取其语句列归属。 +/// 3. 新声明侧在标签区间内匹配同名 var 绑定,取其块/case 链。 +/// 4. 函数不在任何块/case 语句列直接成员位 → 误报(合法合并形); +/// 在 → 真错当且仅当 var 声明位于该语句列闭包内(块链含该列)。 +fn verdict(diag: &OxcDiagnostic, col: &Collector<'_>, is_module: bool) -> Option { + let name = redeclaration_name(&diag.message)?; + let labels = diag.labels.as_deref()?; + let (existing, new) = match (labels.first(), labels.get(1)) { + (Some(a), Some(b)) => (a, b), + _ => return None, + }; + // 模块顶层函数名为 lexical 绑定,一切 var 碰撞均为真错。 + if is_module { + return Some(false); + } + + let (fn_name, owner) = col.fn_owners.get(&label_key(existing))?; + if *fn_name != name { + return None; + } + + // 新声明标签可覆盖整个声明子(含初始化器),绑定名 span 必为其子区间。 + let (start, end) = (new.offset() as u32, new.offset() as u32 + new.len() as u32); + let chain = col + .var_chains + .iter() + .find(|(span, ident, _)| *ident == name && span.0 >= start && span.1 <= end)? + .2 + .clone(); + + Some(match owner { + None => true, + Some(list) => !chain.contains(list), + }) +} + +/// span 编码为单键:起点高 32 位、终点低 32 位。 +fn label_key(label: &oxc_diagnostics::LabeledSpan) -> u64 { + let start = label.offset() as u64; + let end = start + label.len() as u64; + (start << 32) | end +} + +/// AST 收集器:记录复核所需的声明归属信息。 +/// +/// 归属性状语义(随遍历位置变化): +/// - `block_chain`:当前位置外部的块/case 容器列(最外层在前); +/// - `direct_owner`:当前语句所在语句列的块/switch 归属(程序体、函数体、 +/// static block、if/else 臂、标签语句等非块语句列为 `None`)。 +#[derive(Default)] +struct Collector<'a> { + /// 函数声明名 span → (名, 函数声明直接归属的语句列);后者 `None` 表示 + /// 直接位于程序体/函数体/static block 或非块语句(if/else 臂、标签语句), + /// 这些位置的函数名不计入任何语句列的 lexically declared 名。 + fn_owners: HashMap)>, + /// var 声明绑定名:(span, 名, 该声明处的块/case 链);覆盖解构的全部绑定。 + var_chains: Vec<((u32, u32), &'a str, Vec)>, + /// 当前位置外部的块/switch 容器 span 列(最外层在前)。 + block_chain: Vec, + /// 当前语句直接所在语句列的块/switch span;非块语句列为 `None`。 + direct_owner: Option, +} + +impl<'a> Visit<'a> for Collector<'a> { + fn visit_program(&mut self, program: &Program<'a>) { + // 程序语句列是脚本 var 作用域的顶层语句列:两项归属皆空。 + let saved = (std::mem::take(&mut self.block_chain), self.direct_owner); + self.direct_owner = None; + walk_program(self, program); + self.block_chain = saved.0; + self.direct_owner = saved.1; + } + + fn visit_function(&mut self, func: &Function<'a>, _flags: ScopeFlags) { + // 函数声明按当前 direct_owner 归属记录(仅块/switch 语句列有归属)。 + if func.r#type == FunctionType::FunctionDeclaration { + if let Some(id) = &func.id { + let span = id.span; + self.fn_owners + .insert(((span.start as u64) << 32) | span.end as u64, (id.name.as_str(), self.direct_owner)); + } + } + + // 参数默认值为表达式上下文:不含语句列归属变化。 + self.visit_formal_parameters(&func.params); + + // 函数体语句列是新 var 作用域的顶层语句列:两项归属复位。 + if let Some(body) = &func.body { + let saved = (std::mem::take(&mut self.block_chain), self.direct_owner); + self.direct_owner = None; + walk_function_body(self, body); + self.block_chain = saved.0; + self.direct_owner = saved.1; + } + } + + fn visit_block_statement(&mut self, block: &BlockStatement<'a>) { + // 块是重名裁定列:其直接成员的函数声明计入该列 lexically declared 名, + // 闭包内(含嵌套块)的 var 声明计入该列 VarDeclaredNames。 + let key = (block.span.start as u64) << 32 | block.span.end as u64; + let saved_len = self.block_chain.len(); + let saved_owner = self.direct_owner; + self.block_chain.push(key); + self.direct_owner = Some(key); + walk_block_statement(self, block); + self.block_chain.truncate(saved_len); + self.direct_owner = saved_owner; + } + + fn visit_if_statement(&mut self, if_stmt: &IfStatement<'a>) { + // if/else 臂非重名裁定语句列:臂内函数声明按外层 var 作用域绑定, + // 其名不计入任何语句列的 lexically declared 名(标签语句不同: + // 其内层语句仍为原语句列直接成员,归属继承不动)。 + let saved = self.direct_owner; + self.direct_owner = None; + walk_if_statement(self, if_stmt); + self.direct_owner = saved; + } + + fn visit_switch_statement(&mut self, switch: &SwitchStatement<'a>) { + // switch 的 CaseBlock 整体是重名裁定列:任一 case 的函数声明与 + // 任一 case 的 var 声明同列,等同同块碰撞。 + let key = (switch.span.start as u64) << 32 | switch.span.end as u64; + let saved_len = self.block_chain.len(); + let saved_owner = self.direct_owner; + self.block_chain.push(key); + self.direct_owner = Some(key); + walk_switch_statement(self, switch); + self.block_chain.truncate(saved_len); + self.direct_owner = saved_owner; + } + + fn visit_variable_declaration(&mut self, decl: &VariableDeclaration<'a>) { + // 仅 var 声明是重名诊断的"新声明"侧(let/const/using 各有其裁定臂)。 + if decl.kind == VariableDeclarationKind::Var { + for d in &decl.declarations { + let chain = self.block_chain.clone(); + for ident in d.id.get_binding_identifiers() { + self.var_chains + .push(((ident.span.start, ident.span.end), ident.name.as_str(), chain.clone())); + } + } + } + walk_variable_declaration(self, decl); + } +} diff --git a/crates/oxide_parser/tests/redeclaration_tests.rs b/crates/oxide_parser/tests/redeclaration_tests.rs new file mode 100644 index 00000000..9e14bbe9 --- /dev/null +++ b/crates/oxide_parser/tests/redeclaration_tests.rs @@ -0,0 +1,223 @@ +//! 重名误报过滤钉锁:函数名与 var 声明头跨语句列合法合并的形必须解析成功, +//! 同语句列碰撞与模块 lexical 面必须继续拒绝。 + +use oxide_parser::Allocator; + +fn parsed(source: &str) { + let allocator = Allocator::default(); + let result = oxide_parser::parse(&allocator, source); + assert!(result.is_ok(), "应解析成功:{source},实际错误:{:?}", result.err()); +} + +fn parsed_module(source: &str) { + let allocator = Allocator::default(); + let result = oxide_parser::parse_module(&allocator, source); + assert!(result.is_ok(), "应按模块解析成功:{source},实际错误:{:?}", result.err()); +} + +fn rejected(source: &str) { + let allocator = Allocator::default(); + let result = oxide_parser::parse(&allocator, source); + let errors = result.expect_err(&format!("应拒绝:{source}")); + assert!( + errors.iter().any(|e| e.message == "Identifier `x` has already been declared"), + "拒绝原因应仍为重名诊断,实际:{errors:?}" + ); +} + +fn rejected_module(source: &str) { + let allocator = Allocator::default(); + let result = oxide_parser::parse_module(&allocator, source); + let errors = result.expect_err(&format!("应按模块拒绝:{source}")); + assert!( + errors.iter().any(|e| e.message == "Identifier `x` has already been declared"), + "拒绝原因应仍为重名诊断,实际:{errors:?}" + ); +} + +// ── 合法合并形:必须解析成功 ── + +#[test] +fn script_top_function_for_in_var_header() { + // 脚本顶层函数 + for-in var 头:var 提升后与函数名同 var 作用域合法合并。 + parsed("function x(){} var o={a:1,b:2}; for (var x in o) {} x+\"|\"+x"); +} + +#[test] +fn script_top_function_for_of_var_header() { + // for-of 孪生形。 + parsed("function x(){} var o=[1,2]; for (var x of o) {} x+\"|\"+typeof x"); +} + +#[test] +fn function_body_function_for_in_var_header() { + // 函数作用域面:外层函数体内的函数声明 + 内层 for-in var 头。 + parsed("function outer(){ function x(){} var o={a:1,b:2}; for (var x in o) {} return x+\"|\"+typeof x }"); +} + +#[test] +fn script_top_function_c_for_var_header() { + // C-for init 的 var 声明子同属外层 var 作用域。 + parsed("function x(){} var o,n; for (var x=0; n<2; n++,x++) {}"); +} + +#[test] +fn script_top_function_var_in_if_block() { + // if 块内 var 提升过 if 块到外层 var 作用域。 + parsed("function x(){} if (true) { var x; } typeof x"); +} + +#[test] +fn script_top_function_var_in_bare_block() { + // 裸块面。 + parsed("function x(){} { var x; } typeof x"); +} + +#[test] +fn script_top_function_var_in_while_block() { + // while 块面。 + parsed("function x(){} while (false) { var x; } typeof x"); +} + +#[test] +fn script_top_function_generator_for_in_var_header() { + // 生成器函数声明同为 var 作用域函数名:合并形。 + parsed("function* x(){} var o={a:1}; for (var x in o) {} typeof x"); +} + +#[test] +fn if_arm_function_for_in_var_header() { + // Annex B if/else 臂函数声明按 var 作用域绑定,其名不计入任何语句列 + // 的 lexically declared 名:与外层 var 头不碰撞。 + parsed("if(true) function x(){} var o={a:1,b:2}; for (var x in o) {} typeof x"); + parsed("if(false) {} else function x(){} var o={a:1}; for (var x in o) {} typeof x"); + // 块内 if 臂同形。 + parsed("{ if(true) function x(){} var o={a:1}; for (var x in o) {} } typeof x"); +} + +#[test] +fn block_function_sibling_of_var_header() { + // 块内函数 + 块外 for-in 头:无同一语句列同时含两者,不碰撞。 + parsed("{ function x(){} } var o={a:1,b:2}; for (var x in o) {} typeof x"); +} + +#[test] +fn inner_block_function_outer_var_header() { + // 内嵌块函数 + 外层块 var 头:函数名不计入外层块的 lexically declared 名。 + parsed("{ { function x(){} } var o={a:1,b:2}; for (var x in o) {} } typeof x"); +} + +#[test] +fn labeled_statement_function_for_in_var_header() { + // 标签语句包裹的函数声明非语句列直接成员。 + parsed("a: function x(){} var o={a:1,b:2}; for (var x in o) {} typeof x"); +} + +#[test] +fn switch_case_function_var_header_outside() { + // case 内函数 + switch 外 for-in 头:var 声明不在 CaseBlock 列内。 + parsed("var o={a:1,b:2}; switch(1){ case 1: function x(){} } for (var x in o) {} typeof x"); +} + +#[test] +fn var_then_block_function_after_var_header() { + // for-in 头在前、后块内函数声明:现行语义不碰撞(v8 一致)。 + parsed("{ var o={a:1,b:2}; for (var x in o) {} { function x(){} } } typeof x"); +} + +// ── 真错面:必须继续拒绝 ── + +#[test] +fn same_block_function_and_var() { + // 同块函数声明 + var:语句列重名真错。 + rejected("{ function x(){} var x; }"); + rejected("{ function x(){} var x; } \"ok\""); +} + +#[test] +fn outer_block_function_inner_block_var() { + // 外层块函数 + 嵌套块 var:函数名计入外层列 lexically declared 名, + // var 计入该列 VarDeclaredNames(递归闭包):真错。 + rejected("{ function x(){} { var x; } }"); + rejected("{ function x(){} { for (var x in o) {} } }"); +} + +#[test] +fn same_block_let_and_var() { + // let 名与 var 同名同列:真错(标签侧非函数声明,过滤不动作)。 + rejected("{ let x; var x; }"); +} + +#[test] +fn script_top_function_and_let() { + // let 声明撞顶层函数名:声明期真错,非本过滤面。 + rejected("function x(){} let x;"); +} + +#[test] +fn class_name_with_for_in_var_header() { + // class 名建声明式组绑定,var 撞之 = 真错(既有声明侧为 class 非函数)。 + rejected("class x{} var o={a:1,b:2}; for (var x in o) {}"); +} + +#[test] +fn function_then_class_same_name() { + // class 重名臂:既有声明侧为函数、新声明侧为 class,过滤不动作。 + rejected("function x(){} class x {}"); +} + +#[test] +fn same_block_strict_function_and_var() { + // 严格模式块内函数为 lexical 绑定,同列 var 碰撞同判真错。 + rejected("{ \"use strict\"; function x(){} var o={a:1}; for (var x in o) {} }"); +} + +#[test] +fn labeled_function_in_block_with_var() { + // 块内标签语句包裹的函数声明仍属该块语句列直接成员:同列 var 碰撞真错。 + rejected("{ a: function x(){} var o={a:1,b:2}; for (var x in o) {} }"); +} + +#[test] +fn switch_cases_function_and_var() { + // CaseBlock 列:任一 case 的函数声明与任一 case 的 var 声明同列碰撞。 + rejected("var o={a:1,b:2}; switch(1){ case 1: function x(){} case 2: { for (var x in o) {} } }"); + rejected("var o={a:1,b:2}; switch(1){ case 1: { function x(){} for (var x in o) {} } }"); +} + +#[test] +fn module_var_and_function_top_level() { + // 模块顶层函数为 lexical 绑定:var 撞之 = 真错。 + rejected_module("export {}; var x; function x(){}"); + rejected_module("export {}; function x(){} var x;"); +} + +#[test] +fn module_function_and_for_in_var_header() { + // 模块面 for-in var 头同判真错。 + rejected_module("export {}; function x(){} var o={a:1,b:2}; for (var x in o){}"); +} + +// ── 透传钉:同消息形但节点形不同的诊断必须原样保留 ── + +#[test] +fn same_message_let_let_still_rejected() { + // let/let 同消息形重名:既有声明侧非函数声明,过滤须透传。 + rejected("{ let x; let x; }"); +} + +#[test] +fn strict_duplicate_params_still_rejected() { + // 严格模式重复参数同消息形:新声明侧非 var 声明子,过滤须透传。 + rejected("\"use strict\"; function f(x, x) {}"); +} + +// ── 无关面回归:普通解析不受过滤影响 ── + +#[test] +fn unrelated_shapes_unaffected() { + parsed("var x; var o={a:1}; for (var x in o) {} x+\"|\"+x"); + parsed("function x(){} var x; typeof x"); + parsed("function x(){} for (x in o) {}"); + parsed_module("export {}; function x(){} var o={a:1}; for (let x in o) {} typeof x"); +} diff --git a/crates/oxide_vm/tests/for_in_var_head_fn_merge_tests.rs b/crates/oxide_vm/tests/for_in_var_head_fn_merge_tests.rs new file mode 100644 index 00000000..a63d626f --- /dev/null +++ b/crates/oxide_vm/tests/for_in_var_head_fn_merge_tests.rs @@ -0,0 +1,74 @@ +//! 函数名与 var 声明头跨语句列合法合并的求值行为钉:var 提升步复用函数 +//! 声明既有绑定(脚本顶层全局属性 / 函数 var 环境),迭代写入同一绑定。 +//! +//! 完成值一律收敛为布尔(JsValue 的 Display 只暴露 number/bool,不暴露 +//! 字符串内容),字符串比较在引擎内完成。 + +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_parser::Allocator; +use oxide_vm::vm::Vm; + +fn eval(source: &str) -> String { + let allocator = Allocator::default(); + let program = match oxide_parser::parse(&allocator, source) { + Ok(p) => p, + Err(e) => return format!("parse error: {}", e[0].message), + }; + let module = match Compiler::new().compile(&program) { + Ok(m) => m, + Err(e) => return format!("compile error: {e}"), + }; + let mut vm = Vm::new(); + match vm.run(&Arc::new(module)) { + Ok(result) => format!("{result}"), + Err(e) => format!("vm error: {e}"), + } +} + +#[test] +fn script_top_function_for_in_var_header_reuses_global_property() { + // 脚本顶层函数 + for-in var 头:迭代重写全局属性(末键),x 不再是函数。 + assert_eq!( + eval( + "function x(){} var o={a:1,b:2}; for (var x in o) {} \ + (x === \"b\") && (typeof x === \"string\")" + ), + "true", + "var head hoists into the global var environment and reuses the function property" + ); +} + +#[test] +fn script_top_function_for_of_var_header_reuses_global_property() { + // for-of 孪生形:末元素 2 写入全局属性。 + assert_eq!( + eval("function x(){} var o=[1,2]; for (var x of o) {} (x === 2) && (typeof x === \"number\")"), + "true", + "for-of var head merges with the top-level function name binding" + ); +} + +#[test] +fn function_body_function_for_in_var_header_reuses_fn_env_binding() { + // 函数作用域面:函数 var 环境内同形合并。 + assert_eq!( + eval( + "function outer(){ function x(){} var o={a:1,b:2}; \ + for (var x in o) {} return (x === \"b\") && (typeof x === \"string\") } outer()" + ), + "true", + "var head in a for-in inside a function body reuses the function-scope binding" + ); +} + +#[test] +fn script_top_function_plain_var_keeps_function() { + // 纯 var 重名(无迭代写入):var 步零动作,x 保持函数本体。 + assert_eq!( + eval("function x(){} var x; typeof x === \"function\""), + "true", + "plain var redeclaration of a function name keeps the function binding" + ); +}