From 7dbe8b8c1c95fcfbcf3ebe30beffb4031d46b51e Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 4 Nov 2026 03:32:18 +0000 Subject: [PATCH 1/4] js_parser: keep `typeof name` and the lookups of a body-declared `var` inside `with` A name in the body of `with (object)` is looked up on the object first. The lookup can run a proxy `has` trap, the `Symbol.unscopables` getter and a property getter. Three things dropped or moved it. The two `typeof` arms of the unused-expression helpers did not read `must_keep_due_to_with_stmt`. An unused `typeof name` was removed and `!typeof name` was folded. `find_symbol` set the flag only when its walk passed a `with` scope before the scope that lists the symbol. The scope that declares a `var` lists it, so a read of the `var` in that scope never got the flag. `find_symbol` now decides the flag after its walk, in a cold helper that runs only for a file that has a `with` statement. The helper also follows a `var` from the scope that lists it to the scope that stops hoisting. The walk no longer tests each scope for `with`. A `var` declaration with an initializer looks its name up before it evaluates the initializer. Single-use substitution moved a call into such an initializer, and so across the lookup. It no longer does for a `var` below a `with` statement, also where the declaration is printed as an assignment (a relocated top-level `var`, the initializer of a `for`-`in` head). The ahead-of-time compiler keeps an unused `typeof name` under the same rule, for `with` only. --- src/aot/compile/removable/unused.rs | 5 +- src/ast/e.rs | 6 +- src/ast/scope.rs | 3 +- src/js_parser/p.rs | 29 +- src/js_parser/scan/scan_side_effects.rs | 8 +- src/js_parser/scan/scan_symbols.rs | 65 ++++- src/js_parser/visit/visit_stmt.rs | 2 +- src/jsc/RuntimeTranspilerCache.rs | 5 +- src/sema/import_sites.rs | 6 +- src/sema/import_sites/call_sites.rs | 3 +- ...-again-quotes-the-arguments-as-printed.cjs | 14 + ...-to-a-var-of-a-with-block-after-a-call.cjs | 28 ++ ...ypeof-of-a-global-beside-a-direct-eval.cjs | 19 ++ ...d-typeof-of-a-name-inside-a-with-block.cjs | 29 ++ test/bundler/bundler_minify.test.ts | 39 +++ .../transpiler/runtime-transpiler.test.ts | 266 ++++++++++++++++++ test/bundler/transpiler/transpiler.test.js | 105 +++++++ 17 files changed, 606 insertions(+), 26 deletions(-) create mode 100644 test/aot/parallel/errors/a-call-of-a-function-through-a-with-block-that-declares-its-name-again-quotes-the-arguments-as-printed.cjs create mode 100644 test/aot/parallel/language/store-to-a-var-of-a-with-block-after-a-call.cjs create mode 100644 test/aot/parallel/tree-shaking/unused-typeof-of-a-global-beside-a-direct-eval.cjs create mode 100644 test/aot/parallel/tree-shaking/unused-typeof-of-a-name-inside-a-with-block.cjs diff --git a/src/aot/compile/removable/unused.rs b/src/aot/compile/removable/unused.rs index fcf19392732d..60a05ffc42c5 100644 --- a/src/aot/compile/removable/unused.rs +++ b/src/aot/compile/removable/unused.rs @@ -1,4 +1,5 @@ use bun_sema::bind::Parent; +use bun_sema::import_sites::is_in_with_scope; use bun_sema::util::SharedSort; use super::*; @@ -110,7 +111,9 @@ impl Removable<'_, '_> { UnOp::Typeof => { let name = hir.skip_type_assertions(operand); if matches!(hir[name].kind, ExprKind::Ident(_)) { - return self.is_name_initialized(name); + // Inside a `with` the lookup of the name can run a trap or a getter of the object. + return !is_in_with_scope(self.bound, name) + && self.is_name_initialized(name); } simplified.push(expression); self.simplify_unused(operand, simplified) diff --git a/src/ast/e.rs b/src/ast/e.rs index e00df5b0e7fc..653e4d0ad4a5 100644 --- a/src/ast/e.rs +++ b/src/ast/e.rs @@ -143,7 +143,8 @@ bitflags::bitflags! { /// /// Also if we know a typeof operator was originally an identifier, then /// we know that this typeof operator always has no side effects (even if - /// we consider the identifier by itself to have a side effect). + /// we consider the identifier by itself to have a side effect), unless the + /// identifier has `must_keep_due_to_with_stmt`. /// /// Note that there *is* actually a case where "typeof x" can throw an error: /// when "x" is being referenced inside of its TDZ (temporal dead zone). TDZ @@ -420,7 +421,8 @@ impl Identifier { /// If we're inside a "with" statement, this identifier may be a property /// access. In that case it would be incorrect to remove this identifier since - /// the property access may be a getter or setter with side effects. + /// the property access may be a getter or setter with side effects. A "typeof" + /// of it stays too: the lookup alone can run a proxy trap of the object. #[inline] pub const fn must_keep_due_to_with_stmt(self) -> bool { self.ref_.user_bit(0) diff --git a/src/ast/scope.rs b/src/ast/scope.rs index 5fd8299fb82f..0f19375cd71d 100644 --- a/src/ast/scope.rs +++ b/src/ast/scope.rs @@ -351,7 +351,8 @@ pub struct Scope { // evaluated code might reference anything that it has access to. pub contains_direct_eval: bool, - // Set on a "with" scope and every scope that encloses it, for compute_reserved_names_for_scope. + // Set on a "with" scope and every scope that encloses it, for compute_reserved_names_for_scope + // and find_symbol. pub contains_with: bool, // This is to help forbid "arguments" inside class body scopes diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index 7af9d48c2bda..b67d3d36b7de 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -2783,8 +2783,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O } js_ast::StmtData::SLocal(mut local) => { if local.decls.len_u32() > 0 { + let is_var = local.kind == js_ast::LocalKind::KVar; let first = &mut local.decls.slice_mut()[0]; - if matches!(first.binding.data, js_ast::b::B::BIdentifier(_)) { + if let js_ast::b::B::BIdentifier(id) = first.binding.data { + // A `var` declaration below a "with" statement looks its name up on + // the target before it evaluates the initializer. + if is_var && self.var_store_asks_with_target(id.r#ref) { + return false; + } if let Some(value) = first.value.as_mut() { break 'brk js_ast::StoreRef::from_bump(value); } @@ -3838,7 +3844,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O self.has_with_scope = true; } - // Marked up the chain like contains_direct_eval, for compute_reserved_names_for_scope. + // Marked up the chain like contains_direct_eval, for compute_reserved_names_for_scope + // and find_symbol. let mut scope_iter: Option> = Some(scope); while let Some(mut s) = scope_iter { if s.contains_with { @@ -6342,19 +6349,19 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O // if the result is unused and the operand has no side effects. However, it // has a special case where if the operand is an identifier expression such // as "typeof x" and "x" doesn't exist, no reference error is thrown so the - // operation has no side effects. + // operation has no side effects, unless "x" has `must_keep_due_to_with_stmt`. // // Note that there *is* actually a case where "typeof x" can throw an error: // when "x" is being referenced inside of its TDZ (temporal dead zone). TDZ // checks are not yet handled correctly by bun or esbuild, so this possibility is // currently ignored. js_ast::op::Code::UnTypeof => { - if matches!(ex.value.data, js_ast::ExprData::EIdentifier(_)) + if let js_ast::ExprData::EIdentifier(ident) = ex.value.data && ex .flags .contains(E::UnaryFlags::WAS_ORIGINALLY_TYPEOF_IDENTIFIER) { - return true; + return !ident.must_keep_due_to_with_stmt(); } return self.expr_can_be_removed_if_unused_without_dce_check(&ex.value); } @@ -8078,7 +8085,17 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O self.relocated_top_level_vars .push(LocRef { loc, ref_: r#ref }); self.record_usage(r#ref); - Expr::init_identifier(r#ref, loc) + self.var_store_target(loc, r#ref) + } + + /// The target of the assignment that replaces a `var` declaration with an initializer. + pub(crate) fn var_store_target(&self, loc: bun_ast::Loc, r#ref: Ref) -> Expr { + let target = E::Identifier::init(r#ref) + .with_must_keep_due_to_with_stmt(self.var_store_asks_with_target(r#ref)); + Expr { + loc, + data: js_ast::ExprData::EIdentifier(target), + } } // One statement could potentially expand to several statements diff --git a/src/js_parser/scan/scan_side_effects.rs b/src/js_parser/scan/scan_side_effects.rs index f541f38bad57..80093257bf4d 100644 --- a/src/js_parser/scan/scan_side_effects.rs +++ b/src/js_parser/scan/scan_side_effects.rs @@ -205,12 +205,16 @@ impl SideEffects { Op::Code::UnTypeof => { // "typeof x" must not be transformed into if "x" since doing so could // cause an exception to be thrown. Instead we can just remove it since - // "typeof x" is special-cased in the standard to never throw. - if matches!(un.value.data, ExprData::EIdentifier(_)) + // "typeof x" is special-cased in the standard to never throw, unless + // "x" has `must_keep_due_to_with_stmt`. + if let ExprData::EIdentifier(ident) = un.value.data && un .flags .contains(E::UnaryFlags::WAS_ORIGINALLY_TYPEOF_IDENTIFIER) { + if ident.must_keep_due_to_with_stmt() { + return Some(expr); + } return None; } diff --git a/src/js_parser/scan/scan_symbols.rs b/src/js_parser/scan/scan_symbols.rs index 4b5573dcf3cc..bfb87efc1c76 100644 --- a/src/js_parser/scan/scan_symbols.rs +++ b/src/js_parser/scan/scan_symbols.rs @@ -21,7 +21,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O // Every `break 'brk` below assigns `declare_loc` first; the one // early-`return` builds its own `FindSymbolResult` without reading it. let declare_loc: bun_ast::Loc; - let mut is_inside_with_scope = false; + // The scope that lists the symbol. + let mut found_in: Option> = None; let mut scope_use = true; // This function can show up in profiling. // That's part of why we do this. @@ -36,11 +37,6 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let mut did_forbid_arguments = false; while let Some(scope) = current { - // Track if we're inside a "with" statement body - if scope.kind == js_ast::scope::Kind::With { - is_inside_with_scope = true; - } - // Forbid referencing "arguments" inside class bodies if scope.forbid_arguments && !did_forbid_arguments && name == b"arguments" { let r = js_lexer::range_of_identifier(self.source, loc); @@ -55,6 +51,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O // Is the symbol a member of this scope? if let Some(member) = scope.get_member_with_hash(name, hash) { declare_loc = member.loc; + found_in = current; // Unbound globals live in the module scope. scope_use = self.track_scope_uses && (scope.parent.is_some() @@ -74,6 +71,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O if let Some(member) = exported.get(name) { if member.data.is_enum() == ts.is_enum_scope { declare_loc = member.loc; + found_in = current; // If this is an identifier from a sibling TypeScript namespace, then we're // going to have to generate a property access instead of a simple reference. // Lazily-generate an identifier that represents this property access. @@ -108,7 +106,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O return Ok(FindSymbolResult { r#ref: Ref::NONE, declare_loc: Some(loc), - is_inside_with_scope, + is_inside_with_scope: self.module_scope().contains_with + && self.lookup_asks_with_target(None, Ref::NONE), }); } @@ -145,6 +144,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O break 'brk new_ref; }; + // Only a file that has a "with" statement takes the walk. + let is_inside_with_scope = + self.module_scope().contains_with && self.lookup_asks_with_target(found_in, ref_); + // If we had to pass through a "with" statement body to get to the symbol // declaration, then this reference could potentially also refer to a // property on the target object of the "with" statement. We must not rename @@ -164,4 +167,52 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O is_inside_with_scope, }) } + + /// True when a store to the `var` `ref_`, which the current scope declares, asks the + /// target of a "with" statement for the name. + pub(crate) fn var_store_asks_with_target(&self, ref_: Ref) -> bool { + self.module_scope().contains_with + && self.lookup_asks_with_target(Some(self.current_scope), ref_) + } + + /// True when a lookup from the current scope asks the target of a "with" statement. + /// `found_in` is the scope that lists the symbol `ref_`, or `None` for a name that no + /// scope lists. A `var` is a variable of the nearest scope that stops hoisting, so a + /// "with" scope between `found_in` and that scope counts too: + /// + /// with (obj) { var foo; foo } + #[cold] + #[inline(never)] + fn lookup_asks_with_target( + &self, + found_in: Option>, + ref_: Ref, + ) -> bool { + let mut current = Some(self.current_scope); + while let Some(scope) = current { + if scope.kind == js_ast::scope::Kind::With { + return true; + } + if current == found_in { + break; + } + current = scope.parent; + } + if current.is_none() + || self.symbols[ref_.inner_index() as usize].kind != js_ast::symbol::Kind::Hoisted + { + return false; + } + + while let Some(scope) = current { + if scope.kind_stops_hoisting() { + return false; + } + if scope.kind == js_ast::scope::Kind::With { + return true; + } + current = scope.parent; + } + false + } } diff --git a/src/js_parser/visit/visit_stmt.rs b/src/js_parser/visit/visit_stmt.rs index 023c6621ae42..d53bcdf2abfc 100644 --- a/src/js_parser/visit/visit_stmt.rs +++ b/src/js_parser/visit/visit_stmt.rs @@ -2057,7 +2057,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O if let Some(val) = decl.value { let id_ref = b_id.r#ref; stmts.push(Stmt::assign( - Expr::init_identifier(id_ref, decl.binding.loc), + p.var_store_target(decl.binding.loc, id_ref), val, )); decl.value = None; diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs index cafb4f0281bf..1a0aacb57d69 100644 --- a/src/jsc/RuntimeTranspilerCache.rs +++ b/src/jsc/RuntimeTranspilerCache.rs @@ -85,7 +85,10 @@ bun_core::declare_scope!(cache, visible); /// Version 50: The body of a class with lowered class decorators names the class by the /// binding that holds what the decorators return. /// Version 51: A `bun:` specifier that names no builtin is printed as it is written. -const EXPECTED_VERSION: u32 = 51; +/// Version 52: In the body of a `with` statement, an unused `typeof name` and an unused read of +/// a `var` that the body declares are kept, and a call is not moved into the declaration of +/// such a `var`. +const EXPECTED_VERSION: u32 = 52; /// Source files smaller than this are not written to / read from the on-disk /// transpiler cache. Originally 50 KiB, which excluded almost every file in a diff --git a/src/sema/import_sites.rs b/src/sema/import_sites.rs index e6b1fb8fd846..f449840f1268 100644 --- a/src/sema/import_sites.rs +++ b/src/sema/import_sites.rs @@ -14,11 +14,9 @@ mod tests; pub use call_sites::{ RequireCall, RequireCallForm, find_module_printed_as_require, find_require_call, - is_linked_by_bundler, is_module_call_of_plugin, is_name_read_by_transpiler, -}; -pub(crate) use call_sites::{ - call_site_arguments, call_site_kind, is_free_common_js, is_in_with_scope, + is_in_with_scope, is_linked_by_bundler, is_module_call_of_plugin, is_name_read_by_transpiler, }; +pub(crate) use call_sites::{call_site_arguments, call_site_kind, is_free_common_js}; pub use encoders::{ENCODERS, Encoder}; pub use jsx::{ AutomaticJsx, JsxCallee, JsxImport, JsxImportSet, JsxMode, KeyAttribute, find_key_attribute, diff --git a/src/sema/import_sites/call_sites.rs b/src/sema/import_sites/call_sites.rs index 8481860cea04..fbc9a14e8704 100644 --- a/src/sema/import_sites/call_sites.rs +++ b/src/sema/import_sites/call_sites.rs @@ -333,7 +333,8 @@ pub(super) fn is_process( && is_free_name(hir, bound, follow_settled_bindings(hir, bound, object), global_objects)) } -pub(crate) fn is_in_with_scope(bound: &Bound<'_>, ident: ExprId) -> bool { +/// The innermost scope that makes the lookup of the name `ident` dynamic is a `with` statement or lies in one. +pub fn is_in_with_scope(bound: &Bound<'_>, ident: ExprId) -> bool { let mut scope = bound.dynamic_lookup_scope(NodeData::Expr(ident)); while let Some(at) = scope { let data = &bound.scopes[at.idx()]; diff --git a/test/aot/parallel/errors/a-call-of-a-function-through-a-with-block-that-declares-its-name-again-quotes-the-arguments-as-printed.cjs b/test/aot/parallel/errors/a-call-of-a-function-through-a-with-block-that-declares-its-name-again-quotes-the-arguments-as-printed.cjs new file mode 100644 index 000000000000..e0e27f629a79 --- /dev/null +++ b/test/aot/parallel/errors/a-call-of-a-function-through-a-with-block-that-declares-its-name-again-quotes-the-arguments-as-printed.cjs @@ -0,0 +1,14 @@ +// Inside `with`, a call of a function of the file can fail where a block of the body declares the name again with `var`: the object is asked first. The error quotes the call as Bun prints it. +function callee() {} +const scope = { callee: 5 }; +try { + with (scope) { + { + var callee; + callee(function () { return 1; }); + } + } +} catch (error) { + console.log(error.message); +} +module.exports = 1; diff --git a/test/aot/parallel/language/store-to-a-var-of-a-with-block-after-a-call.cjs b/test/aot/parallel/language/store-to-a-var-of-a-with-block-after-a-call.cjs new file mode 100644 index 000000000000..bed16033d68a --- /dev/null +++ b/test/aot/parallel/language/store-to-a-var-of-a-with-block-after-a-call.cjs @@ -0,0 +1,28 @@ +// Inside `with`, the declaration `var result = made` asks the object for `result` when it runs: behind the call that gave the object that property. So it stores in the object, as the assignment does, in a function and at the top of the file. +function make(object) { + object.result = "property"; + return "made"; +} +function assign(object) { + with (object) { + var result; + const made = make(object); + result = made; + } + return result; +} +function declare(object) { + with (object) { + const made = make(object); + var result = made; + } + return result; +} +const objects = [{}, {}, {}]; +console.log(String(assign(objects[0])), objects[0].result); +console.log(String(declare(objects[1])), objects[1].result); +with (objects[2]) { + const made = make(objects[2]); + var result = made; +} +console.log(String(result), objects[2].result); diff --git a/test/aot/parallel/tree-shaking/unused-typeof-of-a-global-beside-a-direct-eval.cjs b/test/aot/parallel/tree-shaking/unused-typeof-of-a-global-beside-a-direct-eval.cjs new file mode 100644 index 000000000000..c2a811dd3790 --- /dev/null +++ b/test/aot/parallel/tree-shaking/unused-typeof-of-a-global-beside-a-direct-eval.cjs @@ -0,0 +1,19 @@ +// Beside a direct `eval`, the `typeof` of a name that nothing uses is left out, as without the `eval`: the getter of the global does not run. Only `with` keeps such a `typeof`. The marked call makes the compiler ask what is left of each expression that nothing uses. +const seen = []; +Object.defineProperty(globalThis, "watchedGlobal", { + get() { + seen.push("getter"); + return 1; + }, + configurable: true, +}); +function pure() {} +/* @__PURE__ */ pure(); +var yes = 1; +function run() { + eval("0"); + yes && typeof watchedGlobal; + yes ? typeof watchedGlobal : 0; +} +run(); +console.log("reads:", seen.join(" ")); diff --git a/test/aot/parallel/tree-shaking/unused-typeof-of-a-name-inside-a-with-block.cjs b/test/aot/parallel/tree-shaking/unused-typeof-of-a-name-inside-a-with-block.cjs new file mode 100644 index 000000000000..6a19a3cd5298 --- /dev/null +++ b/test/aot/parallel/tree-shaking/unused-typeof-of-a-name-inside-a-with-block.cjs @@ -0,0 +1,29 @@ +// Inside `with` the `typeof` of a name looks the name up in the object, also where nothing uses what it gives: behind `&&`, `||`, `??` and in an arm of `?:`. The marked call makes the compiler ask what is left of each expression that nothing uses. +const seen = []; +const scope = new Proxy( + {}, + { + has(target, key) { + if (/^p[0-9]$/.test(key)) seen.push(key); + return false; + }, + }, +); +function pure() {} +/* @__PURE__ */ pure(); +var yes = 1, + no = 0, + nothing = null; +with (scope) { + yes && typeof p1; + no || typeof p2; + nothing ?? typeof p3; + yes ? typeof p4 : typeof p5; + { + var p6; + yes && typeof p6; + } + label: if (no) var p7; + else yes && typeof p7; +} +console.log(seen.join(" ")); diff --git a/test/bundler/bundler_minify.test.ts b/test/bundler/bundler_minify.test.ts index 37f69ce16a66..18a72c6711eb 100644 --- a/test/bundler/bundler_minify.test.ts +++ b/test/bundler/bundler_minify.test.ts @@ -88,6 +88,45 @@ describe("bundler", () => { minifyIdentifiers: false, run: { stdout: `[["arg,dep",1],["arg,dep",9]]` }, }); + // A name in a `with` body is looked up on the object first, which can run a + // proxy trap. An unused `typeof` of such a name stays, and so does an unused + // read of a `var` that the body declares. + const withLookupsSource = /* js */ ` + const lookups = []; + const scope = new Proxy({}, { + has(target, key) { + if (/^[pv][0-9]+$/.test(key)) lookups.push(key); + return false; + }, + }); + function use() {} + with (scope) { + typeof p1; + typeof p2 === "undefined"; + if (typeof p3) {} + use(!typeof p4, void typeof p5, (typeof p6, 0)); + { var v1; v1; typeof v1; } + for (var v2; ; ) { v2; break; } + label: do var v3 = 1; while (v3, 0); + } + console.log(lookups.join(" ")); + `; + itBundled("minify/WithStatementKeepsTypeofAndVarReads", { + files: { + "/entry.js": withLookupsSource, + }, + format: "cjs", + run: { stdout: "p1 p2 p3 p4 p5 p6 v1 v1 v2 v3 v3" }, + }); + itBundled("minify/WithStatementKeepsTypeofAndVarReadsMinified", { + files: { + "/entry.js": withLookupsSource, + }, + format: "cjs", + minifySyntax: true, + minifyIdentifiers: true, + run: { stdout: "p1 p2 p3 p4 p5 p6 v1 v1 v2 v3 v3" }, + }); // `bun build --no-bundle` keeps the export names. They were pinned after the // reserved names were computed, so a local in a nested function could take // one of them and shadow the export it reads. diff --git a/test/bundler/transpiler/runtime-transpiler.test.ts b/test/bundler/transpiler/runtime-transpiler.test.ts index 27e4b71f848b..fd0cdfd9bd6d 100644 --- a/test/bundler/transpiler/runtime-transpiler.test.ts +++ b/test/bundler/transpiler/runtime-transpiler.test.ts @@ -181,6 +181,272 @@ describe("json imports", () => { }); describe("with statement", () => { + // A name in the body of `with (object)` is looked up on the object first. + // The lookup can run a proxy trap or a getter, so a read stays where nothing + // uses its value. Each expected line is what Node.js prints for the same + // file. A `.cjs` file runs in sloppy mode, which `with` needs. + async function runSloppy(source: string) { + using dir = tempDir("with-statement", { "index.cjs": source }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "index.cjs"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr, exitCode }; + } + + test.concurrent("typeof of a name looks the name up on the object", async () => { + const result = await runSloppy(/* js */ ` + // Every name from p1 to p30 is looked up once, in this order. + const lookups = []; + const scope = new Proxy({}, { + has(target, key) { + if (/^(p[0-9]+|undefined|NaN|require)$/.test(key)) lookups.push(key); + return false; + }, + }); + function use() {} + var p30; + with (scope) { + typeof p1; + typeof p2 === "undefined"; + typeof p3 == "undefined"; + typeof p4 < "u"; + void typeof p5; + !typeof p6; + typeof typeof p7; + typeof p8, 0; + [typeof p9]; + [typeof p10][0]; + ({ key: typeof p11 }); + typeof p12 ? 0 : 1; + typeof p13 && 0; + typeof p14 || 0; + typeof p15 ?? 0; + void (typeof p16 !== "undefined" && p16); + if (typeof p17) {} + if (!typeof p18) use(); + while (!typeof p19) {} + label: typeof p20; + use(!typeof p21); + use(void typeof p22); + use(typeof p23 ? 0 : 1); + use((typeof p24, 0)); + use(void (typeof p25 !== "undefined" && p25)); + (function () { typeof p26; })(); + (() => { typeof p27; })(); + class Static { static { typeof p28; } } + with ({}) { typeof p29; } + typeof p30; + typeof undefined; + typeof NaN; + typeof require; + } + typeof p31; + console.log(lookups.join(" ")); + + // The object has the name: the lookup reads Symbol.unscopables and the property. + const reads = []; + const holder = { + get q1() { reads.push("q1"); return 1; }, + get q2() { reads.push("q2"); return 1; }, + get q3() { reads.push("q3"); return 1; }, + get q4() { reads.push("q4"); return 1; }, + q5: 1, + get [Symbol.unscopables]() { reads.push("unscopables"); return { q5: true }; }, + }; + var negated, kind, hidden; + with (holder) { + typeof q1; + void typeof q2; + negated = !typeof q3; + kind = typeof q4; + hidden = typeof q5; + } + console.log(reads.join(" "), negated, kind, hidden); + + // A trap that throws ends the body. + var thrown = "nothing", after = false; + const throws = new Proxy({}, { + has(target, key) { + if (key === "q6") throw new RangeError("q6"); + return false; + }, + }); + try { + with (throws) { + typeof q6; + after = true; + } + } catch (error) { + thrown = error.name; + } + console.log(thrown, after); + `); + expect(result).toEqual({ + stdout: + "p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 p13 p14 p15 p16 p17 p18 p19 p20 " + + "p21 p22 p23 p24 p25 p26 p27 p28 p29 p30 undefined NaN require\n" + + "unscopables q1 unscopables q2 unscopables q3 unscopables q4 unscopables false number undefined\n" + + "RangeError false\n", + stderr: "", + exitCode: 0, + }); + }); + + // A `var` is a variable of the function, also when a block of the body + // declares it. So a read of it in the body asks the object first. + test.concurrent("a read of a var that the body declares looks the name up on the object", async () => { + const result = await runSloppy(/* js */ ` + const lookups = []; + const scope = new Proxy({}, { + has(target, key) { + if (/^[vn][0-9]+$/.test(key)) lookups.push(key); + return false; + }, + }); + var v11; + function v12() {} + with (scope) { + var v1; v1; typeof v1; + { var v2; v2; typeof v2; } + { { var v3; v3; typeof v3; } } + for (var v4; ; ) { v4; typeof v4; break; } + for (var v5 in { key: 0 }) { v5; typeof v5; } + switch (0) { case 0: var v6; v6; typeof v6; } + try { var v7; v7; typeof v7; } finally {} + label: { var v8; v8; typeof v8; } + { var v9; } { var v9; v9; typeof v9; } + { var v10; (function () { v10; typeof v10; })(); } + { var v11; v11; typeof v11; } + { var v12; v12; typeof v12; } + with (scope) { var v13; v13; typeof v13; } + // A labeled statement declares the name. + label: if (typeof v16) var v16 = 1; + label: do var v17 = 1; while (v17, 0); + // Six and ten names in one block: the scope stores them in a larger table. + { var v20, v21, v22, v23, v24, v25; v20; typeof v25; } + { var v30, v31, v32, v33, v34, v35, v36, v37, v38, v39; v30; v34; typeof v39; } + { var v40; let n7; var v41; n7; typeof n7; v40; typeof v41; } + // These names are bindings of the body. The object is not asked. + { let n1; n1; typeof n1; } + { function n2() {} n2; typeof n2; } + try { throw 0; } catch (n3) { var n3; n3; typeof n3; } + (function () { var n4; n4; typeof n4; })(); + class Static { static { var n5; n5; typeof n5; } } + } + (function (v14) { with (scope) { var v14; v14; typeof v14; } })(); + try { throw 0; } catch (v15) { with (scope) { var v15; v15; typeof v15; } } + (function () { with (scope) { var n6; } n6; typeof n6; })(); + console.log(lookups.join(" ")); + `); + expect(result).toEqual({ + stdout: + "v1 v1 v2 v2 v3 v3 v4 v4 v5 v5 v5 v6 v6 v7 v7 v8 v8 v9 v9 v10 v10 v11 v11 v12 v12 " + + "v13 v13 v13 v13 v16 v16 v17 v17 v20 v25 v30 v34 v39 v40 v41 v14 v14 v15 v15\n", + stderr: "", + exitCode: 0, + }); + }); + + // `make()` gives the object a property `result`, so the statement after it + // stores in the object. `var result = make()` looks `result` up before the + // call and stores in the variable. + test.concurrent("a store to a var that the body declares stays after the call before it", async () => { + const result = await runSloppy(/* js */ ` + function make(object) { + object.result = "property"; + return "made"; + } + function assign(object) { + with (object) { + var result; + const made = make(object); + result = made; + } + return result; + } + function declare(object) { + with (object) { + const made = make(object); + var result = made; + } + return result; + } + function declareInLoopHead(object) { + with (object) { + const made = make(object); + for (var result = made in {}); + } + return result; + } + const objects = [{}, {}, {}, {}]; + console.log(String(assign(objects[0])), objects[0].result); + console.log(String(declare(objects[1])), objects[1].result); + console.log(String(declareInLoopHead(objects[2])), objects[2].result); + with (objects[3]) { + const made = make(objects[3]); + var result = made; + } + console.log(String(result), objects[3].result); + `); + expect(result).toEqual({ + stdout: "undefined made\nundefined made\nundefined made\nundefined made\n", + stderr: "", + exitCode: 0, + }); + }); + + // The getter of `first` adds one to `count`. `second` takes `count` before + // the getter runs. + test.concurrent("a read stays before the lookup of a var that the body declares", async () => { + const result = await runSloppy(/* js */ ` + function run(object) { + with (object) { + var first; + let count = 1; + object.bump = function () { count++; }; + const second = count; + return [first, second]; + } + } + const object = { + get first() { + this.bump(); + return "got"; + }, + }; + console.log(run(object).join(" ")); + `); + expect(result).toEqual({ stdout: "got 1\n", stderr: "", exitCode: 0 }); + }); + + // The function of a `switch` clause is printed as a `let` of that clause + // (#40839). A read from another clause that stays in the output throws. + test.todo("typeof of a function that another switch clause declares", async () => { + const result = await runSloppy(/* js */ ` + const lookups = []; + const scope = new Proxy({}, { + has(target, key) { + if (key === "inClause") lookups.push(key); + return false; + }, + }); + switch (0) { + case 0: + with (scope) { typeof inClause; } + break; + case 1: + function inClause() {} + } + console.log(lookups.join(" ")); + `); + expect(result).toEqual({ stdout: "inClause\n", stderr: "", exitCode: 0 }); + }); + test("works", () => { const { exitCode } = Bun.spawnSync({ cmd: [bunExe(), require.resolve("./with-statement-works.js")], diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index 4724a1bd2a31..23a11a6b1597 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -6885,3 +6885,108 @@ describe("same-target destructuring with an unstable target", () => { expect(exitCode).toBe(0); }); }); + +// A name in the body of a `with` statement is looked up on the object first, +// which can run a getter or a proxy trap. The lookup stays where nothing uses +// its value, and it keeps its place among the statements around it. +describe("name lookups inside a with statement", () => { + const minifier = new Bun.Transpiler({ loader: "js", minifyWhitespace: true, minify: { syntax: true } }); + const minify = source => minifier.transformSync(source); + + it("keeps an unused typeof of a name", () => { + expect(minify("with (o) { typeof x; }")).toBe("with(o)typeof x;"); + expect(minify('with (o) { typeof x === "undefined"; }')).toBe("with(o)typeof x;"); + expect(minify("with (o) { void typeof x; }")).toBe("with(o)typeof x;"); + expect(minify("with (o) { [typeof x]; }")).toBe("with(o)typeof x;"); + expect(minify("with (o) { typeof x ? a() : b(); }")).toBe("with(o)typeof x,a();"); + expect(minify("with (o) { if (typeof x) {} }")).toBe("with(o)typeof x;"); + expect(minify("with (o) { (function () { typeof x; })(); }")).toBe("with(o)(function(){typeof x})();"); + }); + + it("does not fold a typeof of a name", () => { + expect(minify("with (o) { y = !typeof x; }")).toBe("with(o)y=!typeof x;"); + expect(minify("with (o) { y = void typeof x; }")).toBe("with(o)y=void typeof x;"); + expect(minify('with (o) { y = void (typeof x !== "undefined" && x); }')).toBe('with(o)y=void(typeof x<"u"&&x);'); + expect(minify("with (o) { y = (typeof x, 1); }")).toBe("with(o)y=(typeof x,1);"); + }); + + it("keeps an unused read of a var that the body declares", () => { + expect(minify("function f(o) { with (o) { var x; x; typeof x; } }")).toBe( + "function f(o){with(o){var x;x;typeof x}}", + ); + expect(minify("function f(o) { with (o) { { var x; x; } } }")).toBe("function f(o){with(o){var x;x}}"); + expect(minify("function f(o) { with (o) { for (var x in y) { x; } } }")).toBe( + "function f(o){with(o)for(var x in y)x}", + ); + // The function declares the name too. + expect(minify("function f(o) { var x; with (o) { var x; x; } }")).toBe("function f(o){var x;with(o){var x;x}}"); + // Two blocks of the body declare the name. + expect(minify("function f(o) { with (o) { { var x; } { var x; x; } } }")).toBe( + "function f(o){with(o){var x;{var x;x}}}", + ); + // A labeled statement declares the name. + expect(minify("function f(o) { with (o) { l: do var x = 1; while (x, 0); } }")).toBe( + "function f(o){with(o)l:do var x=1;while(x,0)}", + ); + expect(minify("function f(o) { with (o) l: var x = 1, y = !typeof x; }")).toBe( + "function f(o){with(o)l:var x=1,y=!typeof x}", + ); + // The catch binding of the name is outside the `with` statement. + expect(minify("function f(o) { try {} catch (x) { with (o) { var x; x; typeof x; } } }")).toBe( + "function f(o){try{}catch(x){with(o){var x;x;typeof x}}}", + ); + }); + + it("keeps a call before the lookup of the var that the next statement stores to", () => { + expect(minify("function f(o, g) { with (o) { var r; const t = g(); r = t; } return r; }")).toBe( + "function f(o,g){with(o){var r;const t=g();r=t}return r}", + ); + expect(minify("function f(o, g) { with (o) { const t = g(); var r = t; } return r; }")).toBe( + "function f(o,g){with(o){const t=g();var r=t}return r}", + ); + // At the top level of a file the declaration is printed as an assignment. + expect(minify("with (o) { const t = g(); var r = t; }")).toBe("with(o){const t=g();r=t}var r;"); + // So is the initializer of a `for`-`in` head. + expect(minify("function f(o, g) { with (o) { const t = g(); for (var r = t in {}); } return r; }")).toBe( + "function f(o,g){with(o){const t=g();r=t;for(var r in{});}return r}", + ); + // Without the `with` statement the call moves into the declaration. + expect(minify("function f(o, g) { { const t = g(); var r = t; } return r; }")).toBe( + "function f(o,g){var r=g();return r}", + ); + }); + + it("keeps a read before the lookup of a var that the body declares", () => { + expect( + minify("function f(o) { with (o) { var r; let y = 1; o.b = () => y++; const t = y; return [r, t]; } }"), + ).toBe("function f(o){with(o){var r;let y=1;o.b=()=>y++;const t=y;return[r,t]}}"); + }); + + it("keeps a property read of a name that no scope declares", () => { + expect(minify("with (o) { Math.PI; }")).toBe("with(o)Math.PI;"); + expect(minify("with (o) { Math.PI; Math.PI; }")).toBe("with(o){Math.PI;Math.PI}"); + expect(minify("function f(o) { with (o) {} Math.PI; }")).toBe("function f(o){with(o);}"); + + const defines = new Bun.Transpiler({ + loader: "js", + minifyWhitespace: true, + minify: { syntax: true }, + define: { "FOO.bar": "1" }, + }); + expect(defines.transformSync("with (o) { y = FOO.bar; }")).toBe("with(o)y=FOO.bar;"); + expect(defines.transformSync("y = FOO.baz; with (o) { y = FOO.bar; }")).toBe("y=FOO.baz;with(o)y=FOO.bar;"); + expect(defines.transformSync("function f(o) { with (o) {} return FOO.bar; }")).toBe( + "function f(o){with(o);return 1}", + ); + }); + + it("removes a read that the object cannot see", () => { + // Outside the body. + expect(minify("typeof x; with (o) {}")).toBe("with(o);"); + expect(minify("function f(o) { with (o) { var x; } x; typeof x; }")).toBe("function f(o){with(o)var x}"); + // The name is a binding of the body. + expect(minify("with (o) { let x; x; typeof x; }")).toBe("with(o){let x}"); + expect(minify("with (o) { class x {} x; typeof x; }")).toBe("with(o){class x{}}"); + expect(minify("with (o) { (function () { var x; x; typeof x; })(); }")).toBe("with(o)(function(){var x})();"); + }); +}); From 09e6f12d612a401d6ff38b26b05167582dbfc9e7 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 8 Nov 2026 17:26:01 +0000 Subject: [PATCH 2/4] test/aot: mark the program for the error of a call through a with block as a Todo The compiler of main 2596ac5d39 quotes such a call without its arguments: the executable prints `callee()` where plain Bun prints the whole call. It does so with no `var` and no function of that name in the file too, so the cause is not in this branch. The program is run all the same and fails once the executable quotes the call as plain Bun does. --- ...t-declares-its-name-again-quotes-the-arguments-as-printed.cjs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/aot/parallel/errors/a-call-of-a-function-through-a-with-block-that-declares-its-name-again-quotes-the-arguments-as-printed.cjs b/test/aot/parallel/errors/a-call-of-a-function-through-a-with-block-that-declares-its-name-again-quotes-the-arguments-as-printed.cjs index e0e27f629a79..8ef5cc505cd5 100644 --- a/test/aot/parallel/errors/a-call-of-a-function-through-a-with-block-that-declares-its-name-again-quotes-the-arguments-as-printed.cjs +++ b/test/aot/parallel/errors/a-call-of-a-function-through-a-with-block-that-declares-its-name-again-quotes-the-arguments-as-printed.cjs @@ -1,4 +1,5 @@ // Inside `with`, a call of a function of the file can fail where a block of the body declares the name again with `var`: the object is asked first. The error quotes the call as Bun prints it. +// Todo: the error of a call of a name that a `with` object holds quotes the call without its arguments, `callee()`: also with no `var` and no function of that name in the file function callee() {} const scope = { callee: 5 }; try { From 31726eeddbd3bf56bda6ee33e5ef65ba557ad985 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 8 Nov 2026 19:52:20 +0000 Subject: [PATCH 3/4] js_parser: cut the added comments to one line each Also remove the AOT program for the error text of a call through a `with` block. It was added for a reader of the flag in the old AOT compiler. The rewritten compiler has no such reader, so the program no longer tests this change. --- src/ast/e.rs | 8 ++++---- src/ast/scope.rs | 3 +-- src/js_parser/p.rs | 6 ++---- src/js_parser/scan/scan_side_effects.rs | 3 +-- src/js_parser/scan/scan_symbols.rs | 10 ++-------- src/jsc/RuntimeTranspilerCache.rs | 4 +--- ...name-again-quotes-the-arguments-as-printed.cjs | 15 --------------- 7 files changed, 11 insertions(+), 38 deletions(-) delete mode 100644 test/aot/parallel/errors/a-call-of-a-function-through-a-with-block-that-declares-its-name-again-quotes-the-arguments-as-printed.cjs diff --git a/src/ast/e.rs b/src/ast/e.rs index 1bc325743977..1dad9d4dcd89 100644 --- a/src/ast/e.rs +++ b/src/ast/e.rs @@ -143,8 +143,8 @@ bitflags::bitflags! { /// /// Also if we know a typeof operator was originally an identifier, then /// we know that this typeof operator always has no side effects (even if - /// we consider the identifier by itself to have a side effect), unless the - /// identifier has `must_keep_due_to_with_stmt`. + /// we consider the identifier by itself to have a side effect). + /// An identifier with `must_keep_due_to_with_stmt` is the exception. /// /// Note that there *is* actually a case where "typeof x" can throw an error: /// when "x" is being referenced inside of its TDZ (temporal dead zone). TDZ @@ -421,8 +421,8 @@ impl Identifier { /// If we're inside a "with" statement, this identifier may be a property /// access. In that case it would be incorrect to remove this identifier since - /// the property access may be a getter or setter with side effects. A "typeof" - /// of it stays too: the lookup alone can run a proxy trap of the object. + /// the property access may be a getter or setter with side effects. + /// A "typeof" of it stays too: the lookup alone can run a proxy trap of the object. #[inline] pub const fn must_keep_due_to_with_stmt(self) -> bool { self.ref_.user_bit(0) diff --git a/src/ast/scope.rs b/src/ast/scope.rs index 0f19375cd71d..56d107d195b2 100644 --- a/src/ast/scope.rs +++ b/src/ast/scope.rs @@ -351,8 +351,7 @@ pub struct Scope { // evaluated code might reference anything that it has access to. pub contains_direct_eval: bool, - // Set on a "with" scope and every scope that encloses it, for compute_reserved_names_for_scope - // and find_symbol. + // Set on a "with" scope and every scope that encloses it, for the renamer and find_symbol. pub contains_with: bool, // This is to help forbid "arguments" inside class body scopes diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs index 57562f847994..d61709ef98d9 100644 --- a/src/js_parser/p.rs +++ b/src/js_parser/p.rs @@ -2778,8 +2778,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O let is_var = local.kind == js_ast::LocalKind::KVar; let first = &mut local.decls.slice_mut()[0]; if let js_ast::b::B::BIdentifier(id) = first.binding.data { - // A `var` declaration below a "with" statement looks its name up on - // the target before it evaluates the initializer. + // Below "with", a `var` is looked up before its initializer runs. if is_var && self.var_store_asks_with_target(id.r#ref) { return false; } @@ -3813,8 +3812,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O self.has_with_scope = true; } - // Marked up the chain like contains_direct_eval, for compute_reserved_names_for_scope - // and find_symbol. + // Marked up the chain like contains_direct_eval, for the renamer and find_symbol. let mut scope_iter: Option> = Some(scope); while let Some(mut s) = scope_iter { if s.contains_with { diff --git a/src/js_parser/scan/scan_side_effects.rs b/src/js_parser/scan/scan_side_effects.rs index 80093257bf4d..968b8c4d96a9 100644 --- a/src/js_parser/scan/scan_side_effects.rs +++ b/src/js_parser/scan/scan_side_effects.rs @@ -205,8 +205,7 @@ impl SideEffects { Op::Code::UnTypeof => { // "typeof x" must not be transformed into if "x" since doing so could // cause an exception to be thrown. Instead we can just remove it since - // "typeof x" is special-cased in the standard to never throw, unless - // "x" has `must_keep_due_to_with_stmt`. + // "typeof x" is special-cased in the standard to never throw. if let ExprData::EIdentifier(ident) = un.value.data && un .flags diff --git a/src/js_parser/scan/scan_symbols.rs b/src/js_parser/scan/scan_symbols.rs index bfb87efc1c76..5683abd074e8 100644 --- a/src/js_parser/scan/scan_symbols.rs +++ b/src/js_parser/scan/scan_symbols.rs @@ -168,19 +168,13 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O }) } - /// True when a store to the `var` `ref_`, which the current scope declares, asks the - /// target of a "with" statement for the name. + /// True when a store to `ref_`, a `var` of the current scope, asks a "with" target. pub(crate) fn var_store_asks_with_target(&self, ref_: Ref) -> bool { self.module_scope().contains_with && self.lookup_asks_with_target(Some(self.current_scope), ref_) } - /// True when a lookup from the current scope asks the target of a "with" statement. - /// `found_in` is the scope that lists the symbol `ref_`, or `None` for a name that no - /// scope lists. A `var` is a variable of the nearest scope that stops hoisting, so a - /// "with" scope between `found_in` and that scope counts too: - /// - /// with (obj) { var foo; foo } + /// `found_in` lists `ref_`. A `var` belongs to its function scope, so its walk goes on to there. #[cold] #[inline(never)] fn lookup_asks_with_target( diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs index 1a0aacb57d69..38f059d26f47 100644 --- a/src/jsc/RuntimeTranspilerCache.rs +++ b/src/jsc/RuntimeTranspilerCache.rs @@ -85,9 +85,7 @@ bun_core::declare_scope!(cache, visible); /// Version 50: The body of a class with lowered class decorators names the class by the /// binding that holds what the decorators return. /// Version 51: A `bun:` specifier that names no builtin is printed as it is written. -/// Version 52: In the body of a `with` statement, an unused `typeof name` and an unused read of -/// a `var` that the body declares are kept, and a call is not moved into the declaration of -/// such a `var`. +/// Version 52: Inside `with`, `typeof name` and the lookups of a body-declared `var` are kept. const EXPECTED_VERSION: u32 = 52; /// Source files smaller than this are not written to / read from the on-disk diff --git a/test/aot/parallel/errors/a-call-of-a-function-through-a-with-block-that-declares-its-name-again-quotes-the-arguments-as-printed.cjs b/test/aot/parallel/errors/a-call-of-a-function-through-a-with-block-that-declares-its-name-again-quotes-the-arguments-as-printed.cjs deleted file mode 100644 index 8ef5cc505cd5..000000000000 --- a/test/aot/parallel/errors/a-call-of-a-function-through-a-with-block-that-declares-its-name-again-quotes-the-arguments-as-printed.cjs +++ /dev/null @@ -1,15 +0,0 @@ -// Inside `with`, a call of a function of the file can fail where a block of the body declares the name again with `var`: the object is asked first. The error quotes the call as Bun prints it. -// Todo: the error of a call of a name that a `with` object holds quotes the call without its arguments, `callee()`: also with no `var` and no function of that name in the file -function callee() {} -const scope = { callee: 5 }; -try { - with (scope) { - { - var callee; - callee(function () { return 1; }); - } - } -} catch (error) { - console.log(error.message); -} -module.exports = 1; From 3f73226764cb50b4cc4ffcef2eb71194f5c42c0b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 8 Nov 2026 21:39:32 +0000 Subject: [PATCH 4/4] ci: retrigger