From 7942269be8bb204f574d24a7f12074241d2e7c51 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Mon, 10 Aug 2026 11:49:50 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=93=E6=9E=84=E5=93=88=E5=B8=8C?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=E5=85=A8=E9=83=A8=E5=8F=98=E4=BD=93=EF=BC=88?= =?UTF-8?q?=E6=B6=88=E9=99=A4=20code=20cache=20=E7=A1=AE=E5=AE=9A=E6=80=A7?= =?UTF-8?q?=E7=A2=B0=E6=92=9E=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_compiler/src/hash/class.rs | 9 ++ crates/oxide_compiler/src/hash/expression.rs | 78 +++++++++++ crates/oxide_compiler/src/hash/mod.rs | 5 +- crates/oxide_compiler/src/hash/statement.rs | 130 ++++++++++++++++-- crates/oxide_compiler/src/hash/target.rs | 13 +- .../tests/hash_collision_tests.rs | 92 +++++++++++++ 6 files changed, 313 insertions(+), 14 deletions(-) create mode 100644 crates/oxide_compiler/tests/hash_collision_tests.rs diff --git a/crates/oxide_compiler/src/hash/class.rs b/crates/oxide_compiler/src/hash/class.rs index 8cec5fbe..53659fff 100644 --- a/crates/oxide_compiler/src/hash/class.rs +++ b/crates/oxide_compiler/src/hash/class.rs @@ -37,6 +37,15 @@ pub(super) fn hash_class_element(element: &ClassElement, h: &mut rustc_hash::FxH statement::hash_statement(stmt, h, include_binding_names); } } + ClassElement::AccessorProperty(prop) => { + prop.r#static.hash(h); + prop.computed.hash(h); + property::hash_property_key(&prop.key, h, include_binding_names); + if let Some(value) = &prop.value { + expression::hash_expression(value, h, include_binding_names); + } + } + // TS 索引签名变体在 JS 模式(SourceType::unambiguous)下不可达,落入兜底。 _ => {} }); } diff --git a/crates/oxide_compiler/src/hash/expression.rs b/crates/oxide_compiler/src/hash/expression.rs index 927fc3b9..49706013 100644 --- a/crates/oxide_compiler/src/hash/expression.rs +++ b/crates/oxide_compiler/src/hash/expression.rs @@ -154,11 +154,89 @@ pub(super) fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher, i hash_chain_element(&chain.expression, h, include_binding_names); } Expression::Super(_) => {} + Expression::NullLiteral(_) => { + 1u8.hash(h); + } + Expression::ThisExpression(_) => { + 2u8.hash(h); + } + Expression::BigIntLiteral(big) => { + big.value.hash(h); + } Expression::RegExpLiteral(lit) => { if let Some(raw) = &lit.raw { raw.to_string().hash(h); } } + Expression::ArrayExpression(arr) => { + (arr.elements.len() as u32).hash(h); + for element in &arr.elements { + match element { + ArrayExpressionElement::SpreadElement(spread) => { + hash_expression(&spread.argument, h, include_binding_names); + } + ArrayExpressionElement::Elision(_) => { + 1u8.hash(h); + } + other => { + if let Some(expr) = other.as_expression() { + hash_expression(expr, h, include_binding_names); + } + } + } + } + } + Expression::MetaProperty(mp) => { + mp.meta.name.as_str().hash(h); + mp.property.name.as_str().hash(h); + } + Expression::AwaitExpression(ae) => { + hash_expression(&ae.argument, h, include_binding_names); + } + Expression::YieldExpression(ye) => { + ye.delegate.hash(h); + if let Some(arg) = &ye.argument { + hash_expression(arg, h, include_binding_names); + } + } + Expression::ImportExpression(ie) => { + hash_expression(&ie.source, h, include_binding_names); + if let Some(options) = &ie.options { + hash_expression(options, h, include_binding_names); + } + match &ie.phase { + Some(phase) => std::mem::discriminant(phase).hash(h), + None => 0u8.hash(h), + } + } + Expression::ParenthesizedExpression(p) => { + hash_expression(&p.expression, h, include_binding_names); + } + Expression::TSAsExpression(ts) => { + hash_expression(&ts.expression, h, include_binding_names); + } + Expression::TSSatisfiesExpression(ts) => { + hash_expression(&ts.expression, h, include_binding_names); + } + Expression::TSTypeAssertion(ts) => { + hash_expression(&ts.expression, h, include_binding_names); + } + Expression::TSNonNullExpression(ts) => { + hash_expression(&ts.expression, h, include_binding_names); + } + Expression::TSInstantiationExpression(ts) => { + hash_expression(&ts.expression, h, include_binding_names); + } + Expression::V8IntrinsicExpression(v8) => { + v8.name.name.as_str().hash(h); + (v8.arguments.len() as u32).hash(h); + for arg in &v8.arguments { + if let Some(expr) = arg.as_expression() { + hash_expression(expr, h, include_binding_names); + } + } + } + // JSX 变体在 JS 模式(SourceType::unambiguous)下不可达,落入兜底。 _ => {} }); } diff --git a/crates/oxide_compiler/src/hash/mod.rs b/crates/oxide_compiler/src/hash/mod.rs index 116626be..0c481a26 100644 --- a/crates/oxide_compiler/src/hash/mod.rs +++ b/crates/oxide_compiler/src/hash/mod.rs @@ -5,8 +5,9 @@ //! `compiled_module_hash` 纳入绑定名(精确匹配避免错误复用)。 use oxide_parser::{ - BindingPattern, ChainElement, ClassElement, Expression, ForStatementInit, ObjectPropertyKind, PropertyKey, - SimpleAssignmentTarget, Statement, + ArrayExpressionElement, BindingPattern, ChainElement, Class, ClassElement, Declaration, ExportDefaultDeclarationKind, + Expression, ForStatementInit, ForStatementLeft, Function, ImportDeclarationSpecifier, ModuleExportName, + ObjectPropertyKind, PropertyKey, SimpleAssignmentTarget, Statement, }; use std::hash::Hash; diff --git a/crates/oxide_compiler/src/hash/statement.rs b/crates/oxide_compiler/src/hash/statement.rs index f129b224..69a570a7 100644 --- a/crates/oxide_compiler/src/hash/statement.rs +++ b/crates/oxide_compiler/src/hash/statement.rs @@ -53,9 +53,26 @@ pub(super) fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher, inc expression::hash_expression(&dw.test, h, include_binding_names); } Statement::ForInStatement(fi) => { + hash_for_statement_left(&fi.left, h, include_binding_names); expression::hash_expression(&fi.right, h, include_binding_names); hash_statement(&fi.body, h, include_binding_names); } + Statement::ForOfStatement(fo) => { + fo.r#await.hash(h); + hash_for_statement_left(&fo.left, h, include_binding_names); + expression::hash_expression(&fo.right, h, include_binding_names); + hash_statement(&fo.body, h, include_binding_names); + } + Statement::WithStatement(ws) => { + expression::hash_expression(&ws.object, h, include_binding_names); + hash_statement(&ws.body, h, include_binding_names); + } + Statement::EmptyStatement(_) => { + 1u8.hash(h); + } + Statement::DebuggerStatement(_) => { + 2u8.hash(h); + } Statement::SwitchStatement(sw) => { expression::hash_expression(&sw.discriminant, h, include_binding_names); for case in &sw.cases { @@ -71,16 +88,7 @@ pub(super) fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher, inc hash_function_declaration(fd, h, include_binding_names); } Statement::ClassDeclaration(class) => { - if let Some(id) = &class.id { - id.name.as_str().hash(h); - } - class.super_class.is_some().hash(h); - if let Some(super_class) = &class.super_class { - expression::hash_expression(super_class, h, include_binding_names); - } - for element in &class.body.body { - class::hash_class_element(element, h, include_binding_names); - } + hash_class(class, h, include_binding_names); } Statement::ThrowStatement(ts) => { expression::hash_expression(&ts.argument, h, include_binding_names); @@ -88,6 +96,67 @@ pub(super) fn hash_statement(stmt: &Statement, h: &mut rustc_hash::FxHasher, inc Statement::TryStatement(ts) => { hash_try_statement(ts, h, include_binding_names); } + Statement::ImportDeclaration(imp) => { + imp.source.value.hash(h); + if let Some(specifiers) = &imp.specifiers { + (specifiers.len() as u32).hash(h); + for spec in specifiers { + match spec { + ImportDeclarationSpecifier::ImportSpecifier(s) => { + hash_module_export_name(&s.imported, h); + if include_binding_names { + s.local.name.as_str().hash(h); + } + } + ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => { + if include_binding_names { + s.local.name.as_str().hash(h); + } + } + ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => { + if include_binding_names { + s.local.name.as_str().hash(h); + } + } + } + } + } + } + Statement::ExportNamedDeclaration(exp) => { + if let Some(decl) = &exp.declaration { + hash_declaration(decl, h, include_binding_names); + } + (exp.specifiers.len() as u32).hash(h); + for spec in &exp.specifiers { + hash_module_export_name(&spec.local, h); + hash_module_export_name(&spec.exported, h); + } + if let Some(source) = &exp.source { + source.value.hash(h); + } + } + Statement::ExportDefaultDeclaration(exp) => { + match &exp.declaration { + ExportDefaultDeclarationKind::FunctionDeclaration(fd) => { + hash_function_declaration(fd, h, include_binding_names); + } + ExportDefaultDeclarationKind::ClassDeclaration(class) => { + hash_class(class, h, include_binding_names); + } + other => { + if let Some(expr) = other.as_expression() { + expression::hash_expression(expr, h, include_binding_names); + } + } + } + } + Statement::ExportAllDeclaration(exp) => { + if let Some(exported) = &exp.exported { + hash_module_export_name(exported, h); + } + exp.source.value.hash(h); + } + // TS 声明/JSX 等变体在 JS 模式(SourceType::unambiguous)下不可达,落入兜底。 _ => {} }); } @@ -125,7 +194,7 @@ fn hash_for_statement(fr: &oxide_parser::ForStatement<'_>, h: &mut rustc_hash::F } fn hash_function_declaration( - fd: &oxide_parser::Function<'_>, h: &mut rustc_hash::FxHasher, include_binding_names: bool, + fd: &Function<'_>, h: &mut rustc_hash::FxHasher, include_binding_names: bool, ) { if include_binding_names { if let Some(id) = &fd.id { @@ -147,6 +216,45 @@ fn hash_function_declaration( } } +fn hash_class(class: &Class<'_>, h: &mut rustc_hash::FxHasher, include_binding_names: bool) { + if let Some(id) = &class.id { + id.name.as_str().hash(h); + } + class.super_class.is_some().hash(h); + if let Some(super_class) = &class.super_class { + expression::hash_expression(super_class, h, include_binding_names); + } + for element in &class.body.body { + class::hash_class_element(element, h, include_binding_names); + } +} + +fn hash_declaration(decl: &Declaration<'_>, h: &mut rustc_hash::FxHasher, include_binding_names: bool) { + match decl { + Declaration::VariableDeclaration(vd) => hash_variable_declaration(vd, h, include_binding_names), + Declaration::FunctionDeclaration(fd) => hash_function_declaration(fd, h, include_binding_names), + Declaration::ClassDeclaration(class) => hash_class(class, h, include_binding_names), + _ => {} + } +} + +fn hash_module_export_name(name: &ModuleExportName<'_>, h: &mut rustc_hash::FxHasher) { + match name { + ModuleExportName::IdentifierName(ident) => ident.name.as_str().hash(h), + ModuleExportName::IdentifierReference(ident) => ident.name.as_str().hash(h), + ModuleExportName::StringLiteral(s) => s.value.hash(h), + } +} + +fn hash_for_statement_left(left: &ForStatementLeft<'_>, h: &mut rustc_hash::FxHasher, include_binding_names: bool) { + std::mem::discriminant(left).hash(h); + if let ForStatementLeft::VariableDeclaration(decl) = left { + hash_variable_declaration(decl, h, include_binding_names); + } else if let Some(target) = left.as_simple_assignment_target() { + target::hash_simple_assignment_target(target, h, include_binding_names); + } +} + fn hash_try_statement(ts: &oxide_parser::TryStatement<'_>, h: &mut rustc_hash::FxHasher, include_binding_names: bool) { for s in &ts.block.body { hash_statement(s, h, include_binding_names); diff --git a/crates/oxide_compiler/src/hash/target.rs b/crates/oxide_compiler/src/hash/target.rs index 8e2dcca3..769995a6 100644 --- a/crates/oxide_compiler/src/hash/target.rs +++ b/crates/oxide_compiler/src/hash/target.rs @@ -22,6 +22,17 @@ pub(super) fn hash_simple_assignment_target( expression::hash_expression(&member.object, h, include_binding_names); member.field.name.as_str().hash(h); } - _ => {} + SimpleAssignmentTarget::TSAsExpression(ts) => { + expression::hash_expression(&ts.expression, h, include_binding_names); + } + SimpleAssignmentTarget::TSSatisfiesExpression(ts) => { + expression::hash_expression(&ts.expression, h, include_binding_names); + } + SimpleAssignmentTarget::TSNonNullExpression(ts) => { + expression::hash_expression(&ts.expression, h, include_binding_names); + } + SimpleAssignmentTarget::TSTypeAssertion(ts) => { + expression::hash_expression(&ts.expression, h, include_binding_names); + } }); } diff --git a/crates/oxide_compiler/tests/hash_collision_tests.rs b/crates/oxide_compiler/tests/hash_collision_tests.rs new file mode 100644 index 00000000..fecba68f --- /dev/null +++ b/crates/oxide_compiler/tests/hash_collision_tests.rs @@ -0,0 +1,92 @@ +//! 结构哈希确定性碰撞的回归测试: +//! 补全 hash 的 `_ => {}` 兜底漏掉的变体后,不同 AST 不得产生相同哈希。 + +use oxide_parser::Allocator; + +fn compiled(source: &str) -> u64 { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse failed"); + oxide_compiler::compiler::compiled_module_hash(&program) +} + +fn structural(source: &str) -> u64 { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse failed"); + oxide_compiler::compiler::structural_hash(&program) +} + +#[test] +fn array_literals_distinguished() { + assert_ne!(compiled("[1, 2, 3]"), compiled("[4, 5, 6]")); + assert_ne!(compiled("[1, 2, 3]"), compiled("[1, , 3]"), "elision vs value must differ"); + assert_ne!(compiled("[1, 2, ...a]"), compiled("[1, 2, 3]"), "spread vs value must differ"); + assert_ne!(compiled("[...a]"), compiled("[...b]"), "different spread sources must differ"); +} + +#[test] +fn meta_property_distinguished() { + // `new.target` 是 MetaProperty,`a.b` 是普通成员访问,结构不同。 + assert_ne!(compiled("function f(){ return new.target }"), compiled("function f(){ return a.b }")); + // 同为 MetaProperty 时 meta/property 名字必须参与哈希(修复前两者哈希相同)。 + assert_ne!( + compiled("import './x.js'; function f(){ return import.meta }"), + compiled("import './x.js'; function f(){ return new.target }"), + "meta/property names must be part of the hash" + ); +} + +#[test] +fn parenthesized_distinguished() { + assert_ne!(compiled("(a)"), compiled("a"), "parenthesized vs bare expression must differ"); + assert_ne!(compiled("(a + b)"), compiled("a + b")); + assert_ne!(compiled("(a)"), compiled("(b)"), "inner expression must be hashed"); +} + +#[test] +fn await_yield_plain_distinguished() { + let await_h = compiled("async function f(){ await x }"); + let yield_h = compiled("function* f(){ yield x }"); + let plain_h = compiled("function f(){ x }"); + assert_ne!(await_h, yield_h); + assert_ne!(await_h, plain_h); + assert_ne!(yield_h, plain_h); + + // yield 的 delegate 标志与参数必须参与哈希。 + assert_ne!(compiled("function* f(){ yield x }"), compiled("function* f(){ yield* x }")); + assert_ne!(compiled("function* f(){ yield x }"), compiled("function* f(){ yield y }")); +} + +#[test] +fn object_literals_distinguished() { + assert_ne!(compiled("var o = { a: 1 }"), compiled("var o = { a: 2 }")); + assert_ne!(compiled("var o = { a: 1 }"), compiled("var o = { b: 1 }")); + assert_ne!(compiled("var o = { a: 1 }"), compiled("var o = { a: 1, b: 2 }"), "property count must be hashed"); +} + +#[test] +fn function_statement_hash_distinguished() { + assert_ne!(compiled("function f(){ return 1 }"), compiled("function f(){ return 2 }")); + assert_ne!(compiled("function f(){ return 1 }"), compiled("function g(){ return 1 }")); +} + +#[test] +fn for_of_with_distinguished() { + assert_ne!(compiled("for (x of xs) { body(x) }"), compiled("for (x of ys) { body(x) }")); + assert_ne!(compiled("for (x of xs) { body(x) }"), compiled("for (x in xs) { body(x) }")); + assert_ne!(compiled("with (a) { f() }"), compiled("with (b) { f() }")); +} + +#[test] +fn import_expression_distinguished() { + assert_ne!(compiled("import('./a.js')"), compiled("import('./b.js')")); + assert_ne!(compiled("import('./a.js')"), compiled("import('./a.js', { with: { type: 'json' } })")); +} + +#[test] +fn structural_hash_ignores_binding_names() { + // 结构哈希忽略绑定名:仅参数/变量改名后哈希相等。 + assert_eq!(structural("function f(x){ return x }"), structural("function f(y){ return y }")); + assert_eq!(structural("var a = 1; a + a"), structural("var b = 1; b + b")); + // 编译模块哈希纳入绑定名:改名的程序哈希不等。 + assert_ne!(compiled("function f(x){ return x }"), compiled("function f(y){ return y }")); +}