Skip to content

js_parser: substitute undefined, not null, for top-level this in an ES module - #41515

Closed
robobun wants to merge 3 commits into
mainfrom
robobun/8ae21124/esm-top-level-this-undefined
Closed

robobun wants to merge 3 commits into
mainfrom
robobun/8ae21124/esm-top-level-this-undefined

Conversation

@robobun

@robobun robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • In an ES module, a top-level this evaluates to null in bun. typeof this gives "object" and this === undefined gives false. Node and the spec give undefined. This affects bun run, bun build, and Bun.Transpiler.
  • The cause is value_for_this in src/js_parser/p.rs:6029. The parser substitutes a top-level this at compile time. The comment says it must be undefined, but the code substituted E::Null.

Fix

  • Substitute E::Undefined instead of E::Null. The null_value_expr helper had no other caller, so it is removed.
  • The CommonJS branch (this is exports) and this inside functions and classes are not changed.
  • Verified: test/bundler/transpiler/transpiler.test.js (new block top-level this in an ES module is undefined, fails on main). test/bundler/esbuild/default.test.ts ThisUndefinedWarningESM now expects undefined, which is what the esbuild test expects. The transpiler, esbuild/default, and esbuild/ts suites pass.

Background

  • value_for_this runs in the visit pass for every E::This that is not nested inside a function. For a file with ES module syntax it returns a replacement expression, so no runtime fn.call(undefined) is needed.
  • typeof this and this === undefined then constant-fold from the substituted literal, which is why the wrong literal showed up in folded output as well.

[auto-merge] gate passed · iteration 0 · 3 files touched

fails on main (without fix)
ASAN without fix: 2 failed, 110 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/esbuild/default.test.ts test/bundler/transpiler/transpiler.test.js
bun test v1.4.3 (f42e98025)

test/bundler/transpiler/transpiler.test.js:
(pass) Bun.Transpiler > handles errors when parsing macros [5.47ms]
(pass) Bun.Transpiler > normalizes \r\n [6.67ms]
1
(pass) Bun.Transpiler > doesn't hang indefinitely #2746 [4.45ms]
(pass) Bun.Transpiler > property access inlining > bails out with spread [8.87ms]
(pass) Bun.Transpiler > property access inlining > bails out with multiple items [2.37ms]
(pass) Bun.Transpiler > property access inlining > works [2.78ms]
(pass) Bun.Transpiler > property access inlining > works nested [2.68ms]
(pass) Bun.Transpiler > property access inlining > bails out when the array item is an optional chain [52.94ms]
(pass) Bun.Transpiler > property access inlining > bails out or strips `this` when the index is a call/assignment target [23.60ms]
(pass) Bun.Transpiler > property access inlining > preserves runtime semantics when inlining from a literal index [306.02ms]
(pass) Bun.Transpiler > property acc
... (truncated)

release without fix: 2 failed, 110 skipped
bun test v1.4.3-canary.1 (f42e98025)

test/bundler/transpiler/transpiler.test.js:
(pass) Bun.Transpiler > handles errors when parsing macros [0.11ms]
(pass) Bun.Transpiler > normalizes \r\n [0.16ms]
1
(pass) Bun.Transpiler > doesn't hang indefinitely #2746 [0.08ms]
(pass) Bun.Transpiler > property access inlining > bails out with spread [0.11ms]
(pass) Bun.Transpiler > property access inlining > bails out with multiple items [0.03ms]
(pass) Bun.Transpiler > property access inlining > works [0.03ms]
(pass) Bun.Transpiler > property access inlining > works nested [0.02ms]
(pass) Bun.Transpiler > property access inlining > bails out when the array item is an optional chain [0.52ms]
(pass) Bun.Transpiler > property access inlining > bails out or strips `this` when the index is a call/assignment target [0.25ms]
(pass) Bun.Transpiler > property access inlining > preserves runtime semantics when inlining from a literal index [7.86ms]
(pass) Bun.Transpiler > property access inlining > bails out on optional-chain index into enum [0.42ms]
(pass) Bun.Transpiler > TypeScript > import Foo = Baz.Bar [0.06ms]
(pass) Bun.Transpiler > TypeScript > ternary should parse correctly when
... (truncated)
passes on PR (with fix)
ASAN with fix: 110 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/esbuild/default.test.ts test/bundler/transpiler/transpiler.test.js
bun test v1.4.3 (f42e98025)

test/bundler/transpiler/transpiler.test.js:
(pass) Bun.Transpiler > handles errors when parsing macros [7.51ms]
(pass) Bun.Transpiler > normalizes \r\n [7.38ms]
1
(pass) Bun.Transpiler > doesn't hang indefinitely #2746 [4.74ms]
(pass) Bun.Transpiler > property access inlining > bails out with spread [9.42ms]
(pass) Bun.Transpiler > property access inlining > bails out with multiple items [2.86ms]
(pass) Bun.Transpiler > property access inlining > works [3.13ms]
(pass) Bun.Transpiler > property access inlining > works nested [3.48ms]
(pass) Bun.Transpiler > property access inlining > bails out when the array item is an optional chain [55.52ms]
(pass) Bun.Transpiler > property access inlining > bails out or strips `this` when the index is a call/assignment target [31.13ms]
(pass) Bun.Transpiler > property access inlining > preserves runtime semantics when inlining from a literal index [442.92ms]
(pass) Bun.Transpiler > property acc
... (truncated)

release with fix: 110 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 861ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/5] cargo bun_runtime → libbun_runtime.a
�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
�[1m�[92m   Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
�[1m�[92m   Compiling�[0m bun_base64 v0.0.0 (/workspace/bun/src/base64)
�[1m�[92m   Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
�[1m�[92m   Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
�[1m�[92m   Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
�[1m�[92m   Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
�[1m�[92m   Compiling�[0m bun_brotli v0.0.0 (/workspace/bun/src/brotli)
�[1m�[92m   Compiling�[0m bun_output v0.0.0 (/workspace/bun/src/output)
�[1m�[92m   Compiling�[0m bun_clap v0.0.0 (/workspace/bun/src/clap)
�[1m�[92m   Compiling�[0m bu
... (truncated)
diff hotspot
src/js_parser/p.rs                         |  6 +-----
 test/bundler/esbuild/default.test.ts       |  2 +-
 test/bundler/transpiler/transpiler.test.js | 10 ++++++++++
 3 files changed, 12 insertions(+), 6 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                        reads  edits  tests
src/js_parser/p.rs                              2      0     16
test/bundler/esbuild/default.test.ts            1      0      5
test/bundler/transpiler/transpiler.test.js      3      3     15

…S module

The parser replaces a top-level `this` in an ES module at compile time.
It used `null`, so `this === undefined` was false and `typeof this`
was "object". The spec and Node give undefined.
@coderabbitai

coderabbitai Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: a884207a-5d3e-4e04-84b3-53d9ac7c5db6

📥 Commits

Reviewing files that changed from the base of the PR and between ee8f984 and 77be223.

📒 Files selected for processing (3)
  • src/js_parser/p.rs
  • test/bundler/esbuild/default.test.ts
  • test/bundler/transpiler/transpiler.test.js

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.


Walkthrough

Changes

The parser now lowers ES module top-level this to undefined. Bundler and transpiler tests verify module-scope output and preserve function-level this.

ESM top-level this

Layer / File(s) Summary
Update top-level this lowering
src/js_parser/p.rs
value_for_this emits undefined for ES module top-level this. The unused null_value_expr helper is removed.
Validate transformed output
test/bundler/esbuild/default.test.ts, test/bundler/transpiler/transpiler.test.js
Tests expect undefined for module-scope this and verify that function-level this remains dynamic.

Suggested reviewers: jarred-sumner

Merge Risk: ⚪ Minimal · up to c8f3c

ES modules now evaluate top-level this as undefined, aligning transpilation and bundling behavior with JavaScript semantics while preserving CommonJS and function-level behavior. No current merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the primary change: replacing null with undefined for top-level this in ES modules.
Description check ✅ Passed The description explains the problem, fix, scope, background, and verification results. It does not use the template headings exactly, but it provides the required information and is sufficiently comp…

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the claude label Sep 6, 2026
@robobun

robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:21 AM PT - Sep 6th, 2026

❌ @robobun, your commit c8f3c28 has 6 failures in Build #110986 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 41515

That installs a local version of the PR into your bun-41515 executable, so you can run:

bun-41515 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline nit, I grepped the test suite for other assertions on the old null output — default/ThisOutsideFunctionRenamedToExports and default/ThisOutsideFunctionNotRenamed don't depend on the substituted value, and no other snapshot or stdout expectation matches the old ESM this → null behavior. The CommonJS and function-scoped this paths in value_for_this are untouched.

Extended reasoning...

The confirmed inline finding covers the only cleanup issue in the diff. I additionally verified the REVIEW.md rule "grep the suite for assertions on the old behavior and update them in the same PR": the two neighboring ThisOutsideFunction* bundler tests at test/bundler/esbuild/default.test.ts:1659-1702 assert on rewriting-vs-not rather than the substituted literal, so they remain correct, and no other test expects null for ESM top-level this. The parser change itself is a one-line literal swap matching the existing comment and esbuild's behavior; the CommonJS exports branch and REPL bypass are unchanged.

Comment thread src/js_parser/p.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the orphaned #[inline] from the earlier review is now cleaned up, and the deletion is tidy.

What was reviewed: the value_for_this substitution now emits EUndefined (matches ES spec §10.2.1.2 and esbuild's js_parser.go valueForThis); confirmed null_value_expr had no other callers; the updated ThisUndefinedWarningESM snapshot matches esbuild's expected output rather than weakening a test; the new transpiler cases cover constant-folding of typeof this / this === undefined and leave function-body this untouched.

Extended reasoning...

Overview

This PR is a one-line semantic fix in src/js_parser/p.rs: the compile-time substitution of top-level this in an ES module now produces E::Undefined instead of E::Null. The now-orphaned null_value_expr() helper is deleted (with its #[inline] attribute, after the follow-up commit). One bundler snapshot in test/bundler/esbuild/default.test.ts is updated to expect undefined instead of null, and a new test block in test/bundler/transpiler/transpiler.test.js covers the raw substitution, typeof/=== constant folding, and confirms function-body this is not rewritten.

Security risks

None. This is a parser-output correctness fix affecting what literal a top-level this lowers to in ESM. No untrusted input handling, auth, crypto, or filesystem paths are touched.

Level of scrutiny

Low-to-medium. The change is a single expression swap whose correctness is directly verifiable against the ES spec (module Environment Record's [[ThisValue]] is undefined) and against esbuild's reference implementation, which the ported test (ThisUndefinedWarningESM) already tracks. The comment immediately above the changed line already documented the intended undefined behavior, so the code is being brought in line with its own stated contract. The CommonJS branch and function-scope this handling are untouched, and the new test explicitly guards the function-body case.

Other factors

The prior review's only note (orphaned #[inline] left on is_require_marker) was addressed in commit 77be223 — verified at lines 10270-10284 that the deletion is now clean. The updated snapshot is a correction toward spec/esbuild parity, not a weakening. Dead code (null_value_expr) is deleted in the same PR per REVIEW.md. No CODEOWNERS gate on these paths and no outstanding third-party objections in the timeline.

@robobun

robobun commented Sep 6, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: the diff is ready. Two CI runs (110868 and 110986) fail only on lanes this one-line parser change does not touch: test-crypto-dh-leak.js (RSS threshold on x64-asan), bun-patch.test.ts and isolated-install.test.ts (Windows install lanes), import-meta.test.js (worker crash in a parallel batch on ubuntu aarch64, passes locally with this branch), fetch-backpressure.test.ts (timeout on Windows aarch64), serve-error-handler-stream.test.ts (a concurrent fixture got a 404 on darwin aarch64), and node-dgram.test.js (pre-existing on darwin x64). These are reported for main-break triage. The transpiler, esbuild/default, and esbuild/ts suites pass locally with the fix.

@robobun

robobun commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #32173, which fixes the same bug (#32167) with the same E::Null to E::Undefined change in value_for_this, and also bumps EXPECTED_VERSION in src/jsc/RuntimeTranspilerCache.rs. The header of src/js_parser/parser.rs requires that bump for a parser output change: the cache key does not include the bun version, so without it a cached ES module keeps the null output after an upgrade. #32173 is rebased onto current main and carries the transpiler and esbuild/default test updates from this PR.

@robobun robobun closed this Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants