From d1c844911b8bae9f532ea22ed723fa4597cf6885 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 00:44:31 +0000 Subject: [PATCH 1/4] Fix parsing of reserved keywords (true/false) as class method names The class element parser was missing `TokenKind::BooleanLiteral(_)` in 4 token kind pattern matches, preventing `true` and `false` from being used as class method identifiers. The `PropertyName` parser already handled boolean literals correctly, so only the dispatch patterns in `class_decl/mod.rs` needed updating. Fixes #4605 https://claude.ai/code/session_017EhjrAqAAkwhGjQkH7uWZy --- core/engine/src/tests/class.rs | 53 +++++++++++++++++++ .../declaration/hoistable/class_decl/mod.rs | 4 ++ .../src/parser/tests/format/function/class.rs | 20 +++++++ 3 files changed, 77 insertions(+) diff --git a/core/engine/src/tests/class.rs b/core/engine/src/tests/class.rs index f15bbaccc32..b77cd4f4e4b 100644 --- a/core/engine/src/tests/class.rs +++ b/core/engine/src/tests/class.rs @@ -117,3 +117,56 @@ fn property_initializer_reference_escaped_variable() { TestAction::assert_eq("Z.getD()", js_str!("D")), ]); } + +// https://github.com/boa-dev/boa/issues/4605 +#[test] +fn class_boolean_literal_method_names() { + run_test_actions([ + TestAction::run(indoc! {r#" + class A { + true() { return 1; } + false() { return 2; } + null() { return 3; } + } + var a = new A(); + "#}), + TestAction::assert_eq("a.true()", 1), + TestAction::assert_eq("a.false()", 2), + TestAction::assert_eq("a.null()", 3), + ]); +} + +// https://github.com/boa-dev/boa/issues/4605 +#[test] +fn class_boolean_literal_static_method_names() { + run_test_actions([ + TestAction::run(indoc! {r#" + class B { + static true() { return 10; } + static false() { return 20; } + } + "#}), + TestAction::assert_eq("B.true()", 10), + TestAction::assert_eq("B.false()", 20), + ]); +} + +// https://github.com/boa-dev/boa/issues/4605 +#[test] +fn class_boolean_literal_getter_setter_names() { + run_test_actions([ + TestAction::run(indoc! {r#" + class C { + get true() { return this._true; } + set true(v) { this._true = v; } + get false() { return this._false; } + set false(v) { this._false = v; } + } + var c = new C(); + c.true = 42; + c.false = 84; + "#}), + TestAction::assert_eq("c.true", 42), + TestAction::assert_eq("c.false", 84), + ]); +} diff --git a/core/parser/src/parser/statement/declaration/hoistable/class_decl/mod.rs b/core/parser/src/parser/statement/declaration/hoistable/class_decl/mod.rs index d8837384b57..c16b10a325b 100644 --- a/core/parser/src/parser/statement/declaration/hoistable/class_decl/mod.rs +++ b/core/parser/src/parser/statement/declaration/hoistable/class_decl/mod.rs @@ -548,6 +548,7 @@ where | TokenKind::NumericLiteral(_) | TokenKind::Keyword(_) | TokenKind::NullLiteral(_) + | TokenKind::BooleanLiteral(_) | TokenKind::PrivateIdentifier(_) | TokenKind::Punctuator( Punctuator::OpenBracket | Punctuator::Mul | Punctuator::OpenBlock, @@ -918,6 +919,7 @@ where | TokenKind::NumericLiteral(_) | TokenKind::Keyword(_) | TokenKind::NullLiteral(_) + | TokenKind::BooleanLiteral(_) | TokenKind::Punctuator(Punctuator::OpenBracket) => { let name_position = token.span().start(); let name = PropertyName::new(self.allow_yield, self.allow_await) @@ -1020,6 +1022,7 @@ where | TokenKind::NumericLiteral(_) | TokenKind::Keyword(_) | TokenKind::NullLiteral(_) + | TokenKind::BooleanLiteral(_) | TokenKind::Punctuator(Punctuator::OpenBracket) => { let name = PropertyName::new(self.allow_yield, self.allow_await) .parse(cursor, interner)?; @@ -1152,6 +1155,7 @@ where | TokenKind::NumericLiteral(_) | TokenKind::Keyword(_) | TokenKind::NullLiteral(_) + | TokenKind::BooleanLiteral(_) | TokenKind::Punctuator(Punctuator::OpenBracket) => { let start = token.span().start(); let name = PropertyName::new(self.allow_yield, self.allow_await) diff --git a/core/parser/src/parser/tests/format/function/class.rs b/core/parser/src/parser/tests/format/function/class.rs index 21e8c3e42a1..f2faedc513f 100644 --- a/core/parser/src/parser/tests/format/function/class.rs +++ b/core/parser/src/parser/tests/format/function/class.rs @@ -104,3 +104,23 @@ fn class_declaration_elements_private_static() { "#, ); } + +// https://github.com/boa-dev/boa/issues/4605 +#[test] +fn class_declaration_boolean_literal_method_names() { + test_formatting( + r#" + class A { + true() {} + false() {} + null() {} + get true() {} + set true(value) {} + get false() {} + set false(value) {} + static true() {} + static false() {} + } + "#, + ); +} From a6bffc0a479ca2c14b0dbe8ec24dc7bb1ffced79 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 00:45:32 +0000 Subject: [PATCH 2/4] Update Cargo.lock https://claude.ai/code/session_017EhjrAqAAkwhGjQkH7uWZy --- Cargo.lock | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index bd6ba9a3d05..4e47e11df5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -607,6 +607,7 @@ dependencies = [ "rstest", "rustc-hash 2.1.1", "serde_json", + "temp-env", "test-case", "textwrap", "url", @@ -4147,6 +4148,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" +[[package]] +name = "temp-env" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96374855068f47402c3121c6eed88d29cb1de8f3ab27090e273e420bdabcf050" +dependencies = [ + "parking_lot", +] + [[package]] name = "temporal_rs" version = "0.1.2" From 9d876a063b5b86ec91001db02ac726290f8a2674 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 04:46:30 +0000 Subject: [PATCH 3/4] Fix missing backtrace/position on errors caught by async module handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the VM's `handle_error` encounters a catchable error, and there's an exception handler in the current frame (e.g. the async module evaluation handler), it was storing the error without capturing any backtrace or position info. This meant errors like "not a callable function" TypeErrors created in Rust code had no source location when surfaced through module promise rejection. Two changes: 1. Capture the backtrace at the top of `handle_error`, before checking for exception handlers, so it's always available. 2. Inject the caller position from the shadow stack into native errors that lack one. This position is preserved through the JsError → JS Error object → JsValue → JsError round-trip that occurs during promise rejection, since it's stored on the Error object's internal data. https://claude.ai/code/session_017EhjrAqAAkwhGjQkH7uWZy --- core/engine/src/error.rs | 13 +++++++++++++ core/engine/src/vm/mod.rs | 23 ++++++++++++++++------- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/core/engine/src/error.rs b/core/engine/src/error.rs index 4c24ad51107..8a89ff0e4ca 100644 --- a/core/engine/src/error.rs +++ b/core/engine/src/error.rs @@ -716,6 +716,19 @@ impl JsError { self } + /// Injects the caller position from the shadow stack into the native + /// error's position field, if it doesn't already have one. This ensures + /// that errors created in Rust (e.g. `JsNativeError::typ()`) carry + /// source position information that survives conversion to a JS Error + /// object through promise rejection. + pub(crate) fn inject_position(&mut self, position: Option) { + if let Repr::Native(err) = &mut self.inner { + if err.position.0.is_none() { + err.position = IgnoreEq(position); + } + } + } + /// Is the [`JsError`] catchable in JavaScript. #[inline] pub(crate) const fn is_catchable(&self) -> bool { diff --git a/core/engine/src/vm/mod.rs b/core/engine/src/vm/mod.rs index b9d328c6391..587bd4a9138 100644 --- a/core/engine/src/vm/mod.rs +++ b/core/engine/src/vm/mod.rs @@ -667,16 +667,25 @@ impl Context { } fn handle_error(&mut self, mut err: JsError) -> ControlFlow { + // Capture the backtrace early, before any exception handler check, + // so that errors caught by internal handlers (e.g. async module + // evaluation) still carry source position information. + if err.backtrace.is_none() { + err.backtrace = Some( + self.vm + .shadow_stack + .take(self.vm.runtime_limits.backtrace_limit(), self.vm.frame.pc), + ); + } + + // Inject caller position into native errors that don't have one + // (e.g. TypeErrors created in Rust code). This ensures the position + // survives conversion to a JS Error object through promise rejection. + err.inject_position(self.vm.shadow_stack.caller_position()); + // If we hit the execution step limit, bubble up the error to the // (Rust) caller instead of trying to handle as an exception. if !err.is_catchable() { - if err.backtrace.is_none() { - err.backtrace = Some( - self.vm - .shadow_stack - .take(self.vm.runtime_limits.backtrace_limit(), self.vm.frame.pc), - ); - } let mut frame = None; let mut env_fp = self.vm.frame.environments.len(); From e5d1495e8162a0d250353ceaeb698e6bfe62add7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Feb 2026 05:15:29 +0000 Subject: [PATCH 4/4] Revert "Fix missing backtrace/position on errors caught by async module handler" This reverts commit 9d876a063b5b86ec91001db02ac726290f8a2674. --- core/engine/src/error.rs | 13 ------------- core/engine/src/vm/mod.rs | 23 +++++++---------------- 2 files changed, 7 insertions(+), 29 deletions(-) diff --git a/core/engine/src/error.rs b/core/engine/src/error.rs index 8a89ff0e4ca..4c24ad51107 100644 --- a/core/engine/src/error.rs +++ b/core/engine/src/error.rs @@ -716,19 +716,6 @@ impl JsError { self } - /// Injects the caller position from the shadow stack into the native - /// error's position field, if it doesn't already have one. This ensures - /// that errors created in Rust (e.g. `JsNativeError::typ()`) carry - /// source position information that survives conversion to a JS Error - /// object through promise rejection. - pub(crate) fn inject_position(&mut self, position: Option) { - if let Repr::Native(err) = &mut self.inner { - if err.position.0.is_none() { - err.position = IgnoreEq(position); - } - } - } - /// Is the [`JsError`] catchable in JavaScript. #[inline] pub(crate) const fn is_catchable(&self) -> bool { diff --git a/core/engine/src/vm/mod.rs b/core/engine/src/vm/mod.rs index 587bd4a9138..b9d328c6391 100644 --- a/core/engine/src/vm/mod.rs +++ b/core/engine/src/vm/mod.rs @@ -667,25 +667,16 @@ impl Context { } fn handle_error(&mut self, mut err: JsError) -> ControlFlow { - // Capture the backtrace early, before any exception handler check, - // so that errors caught by internal handlers (e.g. async module - // evaluation) still carry source position information. - if err.backtrace.is_none() { - err.backtrace = Some( - self.vm - .shadow_stack - .take(self.vm.runtime_limits.backtrace_limit(), self.vm.frame.pc), - ); - } - - // Inject caller position into native errors that don't have one - // (e.g. TypeErrors created in Rust code). This ensures the position - // survives conversion to a JS Error object through promise rejection. - err.inject_position(self.vm.shadow_stack.caller_position()); - // If we hit the execution step limit, bubble up the error to the // (Rust) caller instead of trying to handle as an exception. if !err.is_catchable() { + if err.backtrace.is_none() { + err.backtrace = Some( + self.vm + .shadow_stack + .take(self.vm.runtime_limits.backtrace_limit(), self.vm.frame.pc), + ); + } let mut frame = None; let mut env_fp = self.vm.frame.environments.len();