Skip to content

js_parser: set the import.meta.hot call-drop flag only for a call target - #36875

Open
robobun wants to merge 13 commits into
mainfrom
farm/d62a550a/hot-accept-read-flag-leak
Open

robobun wants to merge 13 commits into
mainfrom
farm/d62a550a/hot-accept-read-flag-leak

Conversation

@robobun

@robobun robobun commented Aug 4, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • With HMR off, a read of an import.meta.hot method that is not a call can delete another call. if (import.meta.hot) { register(import.meta.hot.dispose); } realWork(); more(); prints if (undefined) {} more();.
  • The read sets p.method_call_must_be_replaced_with_undefined (src/js_parser/fold.rs:765, :799). The next call that the visitor reaches takes the flag and, for most callees, is deleted.

Fix

  • Both writes are now under if identifier_opts.is_call_target(). Only the callee of a call sets the flag, and that call takes it at once.
  • The transpiler cache version is now 36. A cached entry keeps the deleted call.
  • Verified: bundler_minify.test.ts, bundler_drop.test.ts (7 new tests fail on 1.4.3). Also transpiler.test.js, esbuild/dce, cli/run/transpiler-cache.
  • Self-reviewed: 32 concerns raised, 31 addressed. An earlier version kept indirect calls removed, which nobody asked for. Open: the evidence block below is stale.

Background

  • Outside the dev server import.meta.hot is undefined. Bun removes a direct import.meta.hot.accept(cb). The docs say the methods "must be called directly".
  • e_call records its callee in p.call_target before it visits it. is_call_target is true only for that node.
  • Fuzzing found this. No issue reports it. It is in each release since v1.2.5 (dev server: forgotten changes #17985).
  • Weighed: a callee-chain state in the visitors (34d735d07e) keeps indirect calls removed. It costs instructions on each . and [] visit.

Downsides

  • An indirect call is no longer removed, for example import.meta.hot.accept.call(null, fn). It prints as a call on undefined and throws TypeError at load. Main removed it through the leak.
  • Under bun build, the callback of such a call is live again, so its imports are bundled.
Notes

Shapes, bun build --no-bundle, HMR off (m is import.meta.hot.accept. The other seven methods behave the same.)

Input 1.4.3 and main This PR
const f = m; init(); start(); init() deleted kept
if (import.meta.hot) { register(import.meta.hot.dispose); } realWork(); more(); realWork() deleted kept
doSomething(m); after(); after() deleted kept
if (typeof m !== "function") { setup(); } setup() deleted kept
x[m](); after(); x[...]() deleted x[undefined]()
(import.meta.hot ? m : noop)(fn); deleted noop(fn)
m(() => { import.meta.hot.invalidate; }); a(); a() deleted kept
console.log(m); after(); with --drop=console after() deleted kept
m(cb), import.meta.hot["accept"](cb), m?.(cb) removed removed
import.meta.hot?.accept(cb) undefined?.accept(cb) undefined?.accept(cb)
m.call(null, fn), m.apply(null, [fn]), m["call"](null, fn) removed undefined.call(null, fn)
m.bind(null), m.toString() undefined undefined.bind(null)
(true ? m : x)(fn), (null ?? m)(fn), [m][0](fn) removed undefined(fn), [undefined][0](fn)
(0, m)(fn) (0, undefined)(fn), removed with --minify-syntax (0, undefined)(fn), (void 0)(fn) with --minify-syntax
m[foo()]() undefined[undefined]() undefined[foo()]()
const accept = m; accept(cb); accept(cb) deleted kept

The rows from m.call down are the accepted change. With this PR, import.meta.hot.accept.call(null, () => {}) fails at load with TypeError: undefined is not an object (evaluating '(void 0).call').

Which call the leak deletes. The flag is taken by the next call expression that the visitor reaches. That call is deleted when its visited callee is an identifier, a member access or undefined. A call with another callee shape clears the flag and stays. new and tagged templates do not take the flag.

Why not keep indirect calls removed. Earlier versions of this PR did: a callee-chain state in ExprIn, forwarded by e_dot, e_index and folded ?:, ||, &&, ??, , (last at 34d735d07e). A review of that version found these points.

  • docs/bundler/hot-reloading.mdx lists indirect use as invalid and says the methods must be called directly.
  • That version added 9 instructions to each . visit, 8 to each [] visit and 5 to each binary expression (release x64 assembly of the parser crate), for every file that Bun parses.
  • It still let [m][0](fn) throw, so the line it drew was its own.
    If a maintainer wants indirect calls to stay removed, the smaller option is to gate the two writes on the armed ExprIn bool that --drop uses, passed through the free IdentifierOpts bit.

Bundle effect of a kept call. For import.meta.hot.accept.call(null, () => import("./dev.js")), 1.4.3 prints a bundle without dev.js. This PR keeps the call, so dev.js is in the bundle.

Cache. The cache key has the source hash, the features hash and the cache version, and no Bun version. I patched the version byte of an entry that 1.4.3 wrote for a 6 KB file with the first table row, to stand for an entry from an unfixed parser at the same version. The fixed parser restored it and the program still printed only start.

Related. #45658 has the same two gate lines in the same arm, inside a larger change, and also uses cache version 36. The second of the two PRs to merge needs a rebase in that arm and the next cache version. #40857 (--drop through []) and #45566 (--drop and .bind) edit the same flag. This PR does not change --drop.

Seen and not changed here.

Suites run with the debug build. bundler_minify 71 pass, bundler_drop 15, transpiler/transpiler.test.js 238, esbuild/dce 87, bake/dev/hot 11, bake/dev-and-prod 12, cli/run/transpiler-cache 20.


[review] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 2 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/bundler_minify.test.ts
bun test v1.4.0 (3964047a3)

test/bundler/bundler_minify.test.ts:
(pass) bundler > minify/TemplateStringFolding [580.19ms]
(pass) bundler > minify/StringAdditionFolding [107.26ms]
(pass) bundler > minify/FunctionExpressionRemoveName [106.50ms]
(pass) bundler > minify/KeepNamesPreservesNames [128.93ms]
(pass) bundler > minify/KeepNamesWithMinifyIdentifiers [85.82ms]
(pass) bundler > minify/PrivateIdentifiersNameCollision [1164.19ms]
(pass) bundler > minify/MergeAdjacentVars [389.61ms]
(pass) bundler > minify/UnusedCommaAndStrictEqChains [398.83ms]
(pass) bundler > minify/Infinity [117.67ms]
(pass) bundler > minify+whitespace/Infinity [93.94ms]
(pass) bundler > minify/NumericPropertyKeysPrintedAsComputed [378.00ms]
(pass) bundler > minify/InlineArraySpread [86.36ms]
(pass) bundler > minify/ForAndWhileLoopsWithMissingBlock [369.28ms]
(pass) bundler > minify/MissingExpressionBlocks [433.44ms]
(pass) bundler > minify/BunRequireStatement [1014.57ms]
(pass) bundler > minify/SwitchUndefined [547.82ms]
(pass)
... (truncated)

release without fix: 3 FAILED
bun test v1.4.0-canary.1 (1498d7b77)

test/bundler/bundler_minify.test.ts:
(pass) bundler > minify/TemplateStringFolding [13.13ms]
(pass) bundler > minify/StringAdditionFolding [3.30ms]
(pass) bundler > minify/FunctionExpressionRemoveName [3.49ms]
(pass) bundler > minify/KeepNamesPreservesNames [3.20ms]
(pass) bundler > minify/KeepNamesWithMinifyIdentifiers [3.08ms]
(pass) bundler > minify/PrivateIdentifiersNameCollision [22.46ms]
(pass) bundler > minify/MergeAdjacentVars [17.74ms]
(pass) bundler > minify/UnusedCommaAndStrictEqChains [17.81ms]
(pass) bundler > minify/Infinity [3.28ms]
(pass) bundler > minify+whitespace/Infinity [2.90ms]
(pass) bundler > minify/NumericPropertyKeysPrintedAsComputed [16.57ms]
(pass) bundler > minify/InlineArraySpread [3.32ms]
(pass) bundler > minify/ForAndWhileLoopsWithMissingBlock [16.10ms]
(pass) bundler > minify/MissingExpressionBlocks [19.54ms]
(pass) bundler > minify/BunRequireStatement [27.67ms]
(pass) bundler > minify/SwitchUndefined [20.71ms]
(pass) bundler > minify/RequireInDeadBranch [3.55ms]
(pass) bundler > minify/TypeOfRequire [3.09ms]
(pass) bundler > minify/RequireMainToImportMetaMain [2.97ms]
(pass) bundler > minify/Con
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/bundler_minify.test.ts
bun test v1.4.0 (3964047a3)

test/bundler/bundler_minify.test.ts:
(pass) bundler > minify/TemplateStringFolding [580.02ms]
(pass) bundler > minify/StringAdditionFolding [103.83ms]
(pass) bundler > minify/FunctionExpressionRemoveName [108.87ms]
(pass) bundler > minify/KeepNamesPreservesNames [119.82ms]
(pass) bundler > minify/KeepNamesWithMinifyIdentifiers [104.53ms]
(pass) bundler > minify/PrivateIdentifiersNameCollision [1140.06ms]
(pass) bundler > minify/MergeAdjacentVars [380.38ms]
(pass) bundler > minify/UnusedCommaAndStrictEqChains [413.30ms]
(pass) bundler > minify/Infinity [99.89ms]
(pass) bundler > minify+whitespace/Infinity [94.15ms]
(pass) bundler > minify/NumericPropertyKeysPrintedAsComputed [380.06ms]
(pass) bundler > minify/InlineArraySpread [95.45ms]
(pass) bundler > minify/ForAndWhileLoopsWithMissingBlock [382.14ms]
(pass) bundler > minify/MissingExpressionBlocks [442.22ms]
(pass) bundler > minify/BunRequireStatement [1010.48ms]
(pass) bundler > minify/SwitchUndefined [538.04ms]
(pass)
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 690ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/111] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

^[[1m^[[92m   Compiling^[[0m bun_http_types v0.0.0 (/workspace/bun/src/http_types)
^[[1m^[[92m   Compiling^[[0m bun_analytics v0.0.0 (/workspace/bun/src/analytics)
^[[1m^[[92m   Compiling^[[0m bun_threading v0.0.0 (/workspace/bun/src/threading)
^[[1m^[[92m   Compiling^[[0m bun_libarchive v0.0.0 (/workspace/bun/src/libarchive)
^[[1m^[[92m   Compiling^[[0m bun_boringssl v0.0.0 (/workspace/bun/src/boringssl)
^[[1m^[[92m   Compiling^[[0m bun_glob v0.0.0 (/workspace/bun/src/glob)
^[[1m^[[92m   Compiling^[[0m bun_md v0.0.0 (/workspace/bun/src/md)
^[[1m^[[92m   Compiling^[[0m bun_ast v0.0.0 (/workspace/bun/src/ast)
^[[1m^[[92m   Compiling^[[0m bun_dns v0.0.0 (/workspace/bun/src/dns)
^[[1m^[[92m   Compiling^[[0m digest v0.10.7
^[[1m^[[92m   Compiling^[[0m cipher v0.4.4
^[[1m^[[92m   Compiling^[[0m rust-argon2 v3.0.0
^[[1m^[[92m   Compiling^[[0m bun_spawn_sys v0.0.0 (/workspac
... (truncated)
diff hotspot
src/js_parser/fold.rs               |  8 ++++--
 test/bundler/bundler_minify.test.ts | 55 +++++++++++++++++++++++++++++++++++++
 2 files changed, 61 insertions(+), 2 deletions(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                 reads  edits  tests
src/js_parser/fold.rs                    4      4      0
test/bundler/bundler_minify.test.ts      1      1      0

When HMR is disabled, reading import.meta.hot.accept (or
dispose/prune/on/off/send/decline/invalidate) without calling it would
set p.method_call_must_be_replaced_with_undefined unconditionally. That
flag is parser-wide and only consumed by e_call after visiting its
target, so the write leaked into the next unrelated call expression and
silently dropped it from the output.

Gate the flag write on identifier_opts.is_call_target() so it is only
set when an enclosing e_call will actually consume it. The --drop path
already gates on the equivalent condition.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Essentials
  • Run ID: 85820c4d-63f3-4f9a-81b5-8cbb1c47f12d
📥 Commits

Reviewing files that changed from the base of the PR and between 3964047 and 9df04b2.

📒 Files selected for processing (7)
  • src/js_parser/fold.rs
  • src/js_parser/p.rs
  • src/js_parser/parser.rs
  • src/js_parser/visit/visit_expr.rs
  • src/jsc/RuntimeTranspilerCache.rs
  • test/bundler/bundler_drop.test.ts
  • test/bundler/bundler_minify.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.

Walkthrough

The parser now tracks whether disabled import.meta.hot methods occur in call targets. Non-call reads still resolve to undefined, while calls through disabled methods are replaced. Tests cover bundling, minification, drop behavior, and runtime transpilation.

Changes

HMR property folding

Layer / File(s) Summary
Callee context and replacement state
src/js_parser/parser.rs, src/js_parser/p.rs
Parser state now represents dot and index callee paths and tracks pending method-call replacements.
Contextual HMR folding
src/js_parser/fold.rs, src/js_parser/visit/visit_expr.rs
Expression visitation propagates callee context. Disabled HMR methods request replacement only when used as applicable call targets; call handling consumes that request.
Folding validation and cache version
test/bundler/bundler_minify.test.ts, test/bundler/bundler_drop.test.ts, src/jsc/RuntimeTranspilerCache.rs
Tests cover method reads, direct and property-based calls, drop behavior, and runtime transpilation. The runtime transpiler cache version increases from 34 to 35.

Suggested reviewers: jarred-sumner

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 9df04

No actionable issue is established for this change; it is mergeable after normal checks.

Pre-merge checks: Passed 4
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 summarizes the main parser change: the import.meta.hot call-drop flag is now limited to call targets.
Description check Passed The description explains the problem, fix, scope, trade-offs, cache change, and verification results. It does not use the exact template headings, but it provides the required information in equivalen…
Full details: Description check

Explanation

The description explains the problem, fix, scope, trade-offs, cache change, and verification results. It does not use the exact template headings, but it provides the required information in equivalent sections.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added the claude label Aug 4, 2026
@robobun

robobun commented Aug 4, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:53 PM PT - Oct 17th, 2026

✅ @robobun, your commit 534c2573630aa7468065c544ead70b5bf3ec7128 passed in Build #127575! 🎉


🧪   To try this PR locally:

bunx bun-pr 36875

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

bun-36875 --bun

@robobun

robobun commented Aug 4, 2026 •

Copy link
Copy Markdown
Collaborator Author

Reproduced on 1.4.3 and on a debug build of main:

$ printf 'if (import.meta.hot) { register(import.meta.hot.dispose); }\nrealWork();\nmore();\n' > /tmp/r.ts
$ bun build --no-bundle /tmp/r.ts
if (undefined) {}
more();

With this PR:

if (undefined) {}
realWork();
more();

An indirect call of a method is no longer removed. With this PR import.meta.hot.accept.call(null, () => {}) prints undefined.call(null, () => {}) and fails at load with TypeError: undefined is not an object (evaluating '(void 0).call'). Main removed that call through the same leak. The docs say that the methods must be called directly.

PR: #36875

@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 — surgical gate on the parser-wide drop flag; the leak into unrelated calls is fixed and the direct-call stripping path is unchanged.

What was reviewed

  • Traced is_call_target at both maybe_rewrite_property_access call sites (e_dot/e_index) back to p.call_target, which e_call sets before visiting and whose consumer clears the flag on return — the gate matches the flag's lifetime exactly.
  • Confirmed the --drop writers of the same flag already gate on in_.property_access_for_method_call_maybe_should_replace_with_undefined, so this brings the import.meta.hot path in line.
  • Checked the third caller in p.rs (define-chain builder) passes default opts, so it can't reach the HotDisabled arm with is_call_target true.
  • New tests cover every affected method name, both bundled and --no-bundle, plus a re-assertion that direct calls are still stripped.
Extended reasoning...

Overview

Two-line fix in src/js_parser/fold.rs (maybe_rewrite_property_access, the ESpecial::HotEnabled|HotDisabled arm): the writes to p.method_call_must_be_replaced_with_undefined in the accept and decline|dispose|prune|invalidate|on|off|send branches are now gated on identifier_opts.is_call_target(). Two new itBundled cases in test/bundler/bundler_minify.test.ts sit next to the existing ImportMetaHotTreeShaking test.

Mechanism check

method_call_must_be_replaced_with_undefined is a parser-wide bool. Its only consumer is e_call (visit_expr.rs:1965-1968), which reads and clears it immediately after visiting its own target. is_call_target at the two visit_expr.rs call sites (887, 1343) is computed by pointer-comparing the current node against p.call_target, which e_call sets before that visit. So identifier_opts.is_call_target() is true exactly when an enclosing e_call will consume the flag on return — the gate is precise. The other two writers of this flag (visit_expr.rs:302, 1368, for --drop) already gate on the analogous in_.property_access_for_method_call_maybe_should_replace_with_undefined, so the hot path was the outlier.

The change is a strict narrowing: when is_call_target() is true, behavior is byte-identical to before (flag set, EUndefined returned, enclosing call dropped). When false, the property still folds to EUndefined but the flag stays clear, so the next unrelated call in the file survives. The one behavioral corner case I noted — import.meta.hot.accept.call(...) — now emits undefined.call(...) instead of being silently dropped, which is arguably more correct given import.meta.hot.accept is undefined when HMR is off.

Security risks

None. Bundler-output correctness only; no untrusted-input parsing, no allocation, no FFI.

Level of scrutiny

Medium — js_parser is production-critical, but the diff is a two-token guard on a niche branch (HMR-disabled import.meta.hot property access) with a well-understood consumer. The bug it fixes (silently dropping user code) is far more serious than any edge the narrowing could introduce.

Other factors

Tests are placed alongside existing coverage per repo convention, exercise every method name in the lookup table, cover both bundling modes, run the output, and negative-assert that direct calls are still stripped and import.meta.hot is gone from output. PR description reports bundler_minify, bundler_drop, and bake/dev-and-prod suites passing; robobun confirmed the repro before/after.

Comment thread src/js_parser/fold.rs Resolved

@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 — targeted fix that gates the parser-wide drop flag on is_call_target(), matching how the --drop path already guards it.

What was reviewed:

  • Traced p.call_target set in e_call (visit_expr.rs:1845) → is_call_target computed on the EDot node → passed through to maybe_rewrite_property_access; the flag is now only set when the enclosing e_call will read-and-clear it at visit_expr.rs:1965-1968.
  • Confirmed the non-call path still returns EUndefined (bare reads fold correctly), and the existing ImportMetaHotTreeShaking test plus the new FAIL-*-call assertions guard against regressing the call-stripping behavior.
  • Both callers in visit_expr.rs (EDot line 1429, EIndex line 1037) thread is_call_target through, so the gate applies to import.meta.hot["accept"] too.
Extended reasoning...

Overview

Two-line fix in src/js_parser/fold.rs wrapping p.method_call_must_be_replaced_with_undefined = true in an if identifier_opts.is_call_target() guard for both the accept branch and the decline|dispose|prune|invalidate|on|off|send branch of the HotEnabled | HotDisabled arm. Two new itBundled tests in test/bundler/bundler_minify.test.ts covering bundled+run and --no-bundle variants.

Security risks

None. This is bundler/transpiler constant folding for import.meta.hot — no auth, crypto, filesystem, or untrusted-input parsing surface.

Level of scrutiny

Low-medium. The mechanism is well-explained and I verified it against the source: e_call sets p.call_target = target.data (visit_expr.rs:1845) before visiting, the EDot/EIndex visitors compute is_call_target by pointer equality against that, and e_call consumes and clears the flag immediately after the target visit (visit_expr.rs:1965-1968). Gating on is_call_target() therefore guarantees an enclosing consumer exists. The change is strictly a narrowing — the flag is set in a subset of the cases it was before — so the only risk is that call-stripping stops working, which both the pre-existing ImportMetaHotTreeShaking test and the new not.toContain("FAIL") / not.toContain("import.meta.hot") assertions cover.

Other factors

The comment-cop inline about a paragraph-long explanatory comment was addressed in 3964047 (comment removed; thread resolved). Tests are placed next to existing import.meta.hot coverage per repo convention, exercise all eight affected method names, assert both output contents and runtime stdout, and re-verify the positive case (direct calls still stripped). PR description reports bundler_minify, bundler_drop, and bake/dev-and-prod suites green.

With HMR off, reading an import.meta.hot method set the parser-wide
"replace the method call with undefined" flag even when no call was
being visited. The next call expression in the file took the flag and
was removed.

The flag is now a type with one gated writer. The import.meta.hot
writer raises it only for a node in the callee of the call that is
being visited, as the --drop writers already did. ExprIn carries that
position as CalleeChain (None, Dots, Index). e_index forwards it, so a
call such as import.meta.hot.accept["call"](...) is still removed, and
--drop keeps following `.` only. e_index holds the flag back while it
visits its index, so a call in the index cannot take it.

The runtime transpiler cache version is bumped because cached output
of the unfixed parser has the call removed.
@robobun robobun changed the title js_parser: don't leak the import.meta.hot drop flag into the next call js_parser: gate the import.meta.hot call drop on the callee chain Oct 12, 2026
Comment thread src/js_parser/fold.rs Resolved
…ept-read-flag-leak

# Conflicts:
#	src/jsc/RuntimeTranspilerCache.rs
A call whose callee folds to a disabled import.meta.hot method, such as
`(0, import.meta.hot.accept)(cb)` with minify.syntax or
`(true ? import.meta.hot.accept : x)(cb)`, was removed before the flag
got its gate. With the gate the method read is not in the callee chain,
so the call stayed as a call on `undefined`.

The operand that takes the place of a conditional, logical or comma
expression now takes its place in the callee chain too. `--drop` does
not follow it, as it does not follow `[]`.

@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 also checked the comma-callee path added in 75a6189: visit_right_and_finish now visits the right operand of , / || / && / ?? with callee_chain.indirect(), and callee_chain_at hands the call position only to the outermost op on the binary stack, so a left-nested (a, b), c callee is not mis-marked. I also traced e_call: the flag is take()n before any argument is visited, so no argument subtree sees it raised and the new debug assertion cannot fire from that path.

Extended reasoning...

The diff replaces the parser's method_call_must_be_replaced_with_undefined bool with a MethodCallDrop newtype and threads a CalleeChain enum through ExprIn, e_dot, e_index, e_if, e_binary and the binary-expression visitor so the import.meta.hot / --drop call removal only raises inside a callee; it also bumps the runtime transpiler cache version to 36 and adds bundler and runtime-transpiler tests. No security-sensitive surface is touched. The latest commit adds comma/logical propagation that covers the case flagged in the prior run, with a dedicated minifySyntax test. One nit about missing dead-branch test coverage is posted inline; the parser hot-path change is non-trivial enough that a human look remains worthwhile.

Comment thread test/bundler/bundler_minify.test.ts Resolved

@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.

Code review completed

Nothing new to post: everything this review found is already covered by existing comments on this pull request or didn't merit a separate one.

Replace the callee-chain gate with the gate of the first version of this
change. With HMR off, the import.meta.hot arm of
maybe_rewrite_property_access sets
method_call_must_be_replaced_with_undefined only when the access is the
target of the call that is being visited. p.rs, parser.rs, visit_expr.rs
and visit_binary.rs are as on main again.

A call through a property of a disabled method, such as
import.meta.hot.accept.call(null, fn), is no longer removed. It prints
as a call on undefined. The docs say that these methods must be called
directly.

Tests: keep the read-without-call cases and the dropped-call case, add
the two other spellings of a direct call to them, and remove the cases
that pinned the removal of indirect calls.
@robobun robobun changed the title js_parser: gate the import.meta.hot call drop on the callee chain js_parser: set the import.meta.hot call-drop flag only for a call target Oct 17, 2026

@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.

Beyond the inline finding, I also checked the two --drop sites that set the same flag (src/js_parser/visit/visit_expr.rs:293 and :1352) for the same leak — both are already scoped by in_.property_access_for_method_call_maybe_should_replace_with_undefined, which e_call sets only along the callee chain, so they do not need the new gate. Note that the diff at HEAD contains only the is_call_target gate in src/js_parser/fold.rs plus the cache version bump; the MethodCallDrop / ExprIn::callee_chain mechanism the PR description describes is not in this code, and no test covers import.meta.hot.accept.call(...).

Extended reasoning...

The change gates the method_call_must_be_replaced_with_undefined flag on identifier_opts.is_call_target() at the two import.meta.hot fold sites in src/js_parser/fold.rs, bumps the runtime transpiler cache version to 36, and adds bundler, runtime-transpiler and Bun.Transpiler tests for read-without-call shapes. It touches no security-sensitive surface. The confirmed inline finding (methods invoked through .call/.apply/.bind now reach runtime as undefined.call) is the reason a human should weigh in, and the PR description describes a callee-chain design that the current diff does not contain.

Comment thread src/js_parser/fold.rs Resolved
@robobun

robobun commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

#47199 (the import.meta.hot?.x fix for #47171) carries the same two-line is_call_target() hunk in src/js_parser/fold.rs, because the optional-chain lowering depends on it. Either PR rebases cleanly over the other.

@robobun

robobun commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

Update: #47199 now gates the flag on the callee-chain flag from ExprIn (the rule the --drop defines use) instead of is_call_target, so import.meta.hot.accept.call(null, cb) stays dropped. It also ports the tests from this PR. If #47199 merges first, this PR can close.

@robobun

robobun commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator Author

Update: #47199 no longer changes the call-drop flag. At eaeaf5b it only lowers import.meta.hot?.x in the dev server, and its output with HMR off is the same as on main. This PR is the only open fix for the flag again.

This branch has not been deployed

No deployments
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.

1 participant