Skip to content

transpiler: preserve function-body "use strict" in CJS - #29539

Closed
robobun wants to merge 1 commit into
mainfrom
farm/fdc7b537/fix-use-strict-directive-stripped
Closed

transpiler: preserve function-body "use strict" in CJS#29539
robobun wants to merge 1 commit into
mainfrom
farm/fdc7b537/fix-use-strict-directive-stripped

Conversation

@robobun

@robobun robobun commented Apr 21, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Restores correct strict-mode semantics for "use strict" directives inside function bodies during CJS transpilation. Regressed in 1.3.11.

Reproduction (from #29533)

bun i random-number-csprng
echo 'import r from "random-number-csprng"; console.log(await r(69,420));' > index.ts
bun index.ts
# TypeError: Can only call X509Certificate.ca on instances of X509Certificate
#  code: "ERR_INVALID_THIS"

Minimal:

// foo.cjs
var isES5 = (function () {
  "use strict";
  return this === undefined;
})();
console.log(isES5);   // Bun 1.3.11–1.3.13: false   Node/old Bun: true

Root cause

parseStmtsUpTo (src/ast/parse.zig) unconditionally skip = true'd "use strict" during directive-prologue parsing at every scope. That was safe at module scope — the CJS wrapper in P.zig re-emits the directive at the top of the wrapper IIFE — but at function scope there is no later pass, so the function body silently ran in sloppy mode.

With bluebird's isES5 detector returning false, util.inheritedDataKeys took its pre-ES5 for..in path, which fails to filter getter-only properties. promisifyAll(require("crypto")) then read X509Certificate.prototype.ca with this = the prototype, tripping Node's ERR_INVALID_THIS guard.

Secondary bug: the joinWithComma optimisation in src/ast/visit.zig (runs under minifySyntax + DCE) dropped every S.Directive node — so even if the parser kept the directive, the bundler would re-drop it.

Fix

  • src/ast/parse.zig: only skip at p.current_scope == p.module_scope. At function scope, rewrite the S.SExpr carrying the string literal into an S.Directive so the printer re-emits "use strict"; verbatim at the top of the function body.
  • src/ast/visit.zig: append .s_directive statements to the output instead of continueing past them.

Verification

test/regression/issue/29533.test.ts (3 tests):

  1. Function-body "use strict" in .cjsthis === undefined inside the IIFE.
  2. Function-body "use strict" in CJS .jstypeof this === "string" after .call("hello"), undeclared assignment throws.
  3. Module-level "use strict" in .cjs → still enforced via the CJS wrapper (regression in the other direction).

Gate:

$ git stash push -- src/           # test file only
$ bun bd test test/regression/issue/29533.test.ts
  (fail) function-body 'use strict' is preserved in .cjs
  (fail) function-body 'use strict' enforces strict semantics in CJS .js
  (pass) module-level 'use strict' still enforces strict mode in .cjs

$ git stash pop                     # restore fix
$ bun bd test test/regression/issue/29533.test.ts
  3 pass, 0 fail

Existing test/bundler/transpiler/preserve-use-strict-cjs.test.ts and the "does not preserve use strict (for now)" transpiler test (which checks the module-level behaviour) still pass.

Closes #29533
Fixes #14251

@robobun

robobun commented Apr 21, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 1:09 PM PT - May 4th, 2026

@robobun, your commit 15dd048 has 2 failures in Build #51265 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 29539

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

bun-29539 --bun

@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Parser restricts directive-prologue recognition to entry/function-body scopes, preserves non-module directives as S.Directive, only drops module-level "use strict", and sets scope-level strictness on any "use strict" in directive-prologue. The visitor now processes .s_directive through normal statement emission. New CommonJS tests and fixtures validate behavior.

Changes

Cohort / File(s) Summary
Parser directive handling
src/ast/parse.zig
Gated directive-prologue by scope; set current_scope.strict_mode for any "use strict" in directive-prologue; only drop and record module-level "use strict" (when current_scope == module_scope); non-"use strict" directives are converted to S.Directive.
AST visitor behavior
src/ast/visit.zig
Removed early continue for .s_directive in the statement-processing/minification loop so directive statements flow through merging/minification and are appended to output like other statements.
Wrapper emission logic
src/ast/P.zig
Changed preserve_strict_mode to require file be .explicit_strict_mode and that the first emitted statement is the "use strict" directive string (not merely any .s_directive).
Regression tests & fixtures
test/regression/issue/29533.test.ts, test/regression/issue/29533-fn.fixture.cjs, test/regression/issue/29533-module.fixture.cjs
Added tests and CommonJS fixtures exercising module- and function-level "use strict" semantics, spawning processes to assert strict/sloppy behavior, directive preservation, and minification effects.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: preserving function-body "use strict" directives during CommonJS transpilation, which directly addresses the regression described in the PR.
Description check ✅ Passed The description fully covers the required template sections with comprehensive detail: what the PR does, root cause analysis, the fix approach, and verification steps with test results.
Linked Issues check ✅ Passed The code changes directly address all requirements from linked issues #29533 and #14251: preserving function-scoped "use strict" directives [#29533, #14251], fixing the parser to not unconditionally skip directives [#29533], preventing minification from dropping directives [#29533], and enabling proper strict-mode enforcement at function scope [#14251].
Out of Scope Changes check ✅ Passed All changes are directly in scope: parser fixes for directive handling, minification logic adjustment, comprehensive regression tests, and wrapper IIFE strict-mode logic—all targeting the documented regression in function-body strict-mode preservation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Strict mode does not work at function-level granularity #14251 - Directly reports that function-level "use strict" is not respected by the transpiler, which is exactly what this PR's parseStmtsUpTo fix addresses by preserving the directive at function scope instead of unconditionally skipping it.

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #14251

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. fix(transpiler): preserve "use strict" directives in function bodies #27185 - fix(transpiler): preserve "use strict" directives in function bodies (touches parse.zig and visit.zig for the same core issue)

🤖 Generated with Claude Code

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

This fix touches core CJS transpilation infrastructure (parse.zig and visit.zig). The root cause and fix are well-analyzed, but the interaction between the parser's directive handling and the CJS wrapper in P.zig is non-trivial enough to warrant a human look.

Extended reasoning...

Overview

The PR fixes a regression where function-body "use strict" directives were silently dropped during CJS transpilation. Two files are modified: src/ast/parse.zig (parser directive-prologue handling) and src/ast/visit.zig (minification/DCE pass). A regression test with 3 cases is added.

Security risks

None. This is a correctness fix for strict-mode semantics; no auth, crypto, or permission code is involved.

Level of scrutiny

The fix is small and surgical (< 20 lines of logic changed), well-documented, and accompanied by solid test coverage including a guard against regression in the opposite direction. However, parse.zig and visit.zig are core transpiler infrastructure — the change relies on the invariant that the CJS wrapper in P.zig re-emits the module-level directive, which is not verified in the diff itself. That cross-file invariant is worth a human confirming.

Other factors

No CODEOWNERS entries cover these files. The bug causes real-world breakage (bluebird, random-number-csprng). The fnBodyContainsUseStrict helper at the bottom of visit.zig already handles s_directive nodes, so the parse-side conversion is consistent with the existing visitor design. The secondary fix (preserving s_directive in the joinWithComma optimization loop) is a one-liner and clearly correct.

@robobun

robobun commented Apr 21, 2026

Copy link
Copy Markdown
Collaborator Author

Re: the CJS-wrapper invariant — the re-emit lives in src/ast/P.zig around lines 6512-6526:

const preserve_strict_mode = ...
    !(parts.items.len > 0 and
      parts.items[0].stmts.len > 0 and
      parts.items[0].stmts[0].data == .s_directive);
...
if (preserve_strict_mode) {
    remaining_stmts[0] = p.s(
        S.Directive{ .value = "use strict" },
        p.module_scope_directive_loc,
    );
    ...
}

Gated on module_scope_directive_loc, which is the one field the if (p.current_scope == p.module_scope) branch in the fix still sets. The third test ("module-level 'use strict' still enforces strict mode in .cjs") exercises this end-to-end — it would fail if the wrapper stopped re-emitting.

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/29533.test.ts`:
- Around line 1-3: Remove the extra bug-history prose from the test header and
leave only the single-line GitHub issue URL comment; specifically delete the two
trailing comment lines that start with "Bun 1.3.11..." and "so IIFEs (e.g.
bluebird's..." in the header of test/regression/issue/29533.test.ts so the file
contains only the issue URL comment as the header.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 15a0d1c5-5791-4281-9d2b-5cdca20b7ca8

📥 Commits

Reviewing files that changed from the base of the PR and between f91def2a5f0271b1b2bd36739aafc18e3d1e4e82 and c22271af9c838eef0193ba295e7069ef267d408e.

📒 Files selected for processing (5)
  • src/ast/parse.zig
  • src/ast/visit.zig
  • test/regression/issue/29533-fn.fixture.cjs
  • test/regression/issue/29533-module.fixture.cjs
  • test/regression/issue/29533.test.ts
💤 Files with no reviewable changes (1)
  • src/ast/visit.zig

Comment thread test/regression/issue/29533.test.ts 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.

The fix logic looks correct, but this touches core CJS transpilation in parse.zig and visit.zig affecting all CJS files, and there's a flagged potential duplicate PR (#27185) that should be checked before merging.

Extended reasoning...

Overview

This PR modifies two core AST processing files: src/ast/parse.zig (directive-prologue parsing in parseStmtsUpTo) and src/ast/visit.zig (minification/DCE statement visitor). The parse change gates the skip = true behavior on p.current_scope == p.module_scope, preserving function-body directives as S.Directive nodes. The visit change removes the .s_directive => continue line that was silently dropping all directive nodes during bundling passes. Two fixture files and a test file are also added.

Security risks

No security-sensitive code is touched. The change restores correct strict-mode semantics, which if anything improves correctness rather than introducing risk.

Level of scrutiny

High. parseStmtsUpTo and visitStmts are hot paths executed for every CJS file Bun processes. A subtle off-by-one or scope-comparison error could affect directive handling across all CJS transpilation. The change is small and well-reasoned, but the blast radius of a mistake is large.

Other factors

  • A potential duplicate PR (#27185) was flagged by the bot. A human should verify whether this was previously attempted and reverted, or whether the approaches differ.
  • The PR description claims 3 tests but only 2 were committed (nit flagged by the inline bug report — the missing test covers the .js-extension CJS variant, which is not a correctness problem but a coverage gap).
  • CI failures in bundler_npm.test.ts, bun-install.test.ts, and leak.test.ts appear unrelated to these changes (different subsystems, present on the autofix.ci commit), but they are still outstanding on the PR.

Comment thread test/regression/issue/29533.test.ts
@robobun
robobun force-pushed the farm/fdc7b537/fix-use-strict-directive-stripped branch from 5f3a5d4 to 9bad376 Compare April 21, 2026 09:54

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ast/parse.zig`:
- Around line 1194-1211: The code is promoting any leading string literal to
S.Directive regardless of scope; change the promotion in parseStmtsUpTo() so you
only allocate/assign S.Directive (and set skip/module_scope_directive_loc or
stmt.data = Prefill.Data.SEmpty) when p.current_scope.kind is a module or a
function scope (e.g., .entry or the function kinds like
.function_body/.function_args); for all other kinds (blocks, catch, finally,
etc.) leave the stmt as a normal expression statement so block-level string
literals are not converted to directives. Ensure the is_strict and "use asm"
branches still apply but only after verifying p.current_scope.kind permits
directive promotion.

In `@test/regression/issue/29533-fn.fixture.cjs`:
- Around line 11-19: Replace the brittle undeclared assignment in the
exports.mode probe with a unique global sentinel name (e.g.,
"__issue_29533_sentinel__") and ensure the sentinel is deleted before and after
the probe so prior tests can't affect the result; specifically, in the
exports.mode IIFE, delete the sentinel from the global object, attempt to assign
to global[<uniqueSentinel>] inside the try/catch to detect sloppy vs strict
mode, and finally clean up by deleting global[<uniqueSentinel>] in both success
and failure paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2fbc8e74-b435-4b25-b375-db35675969e8

📥 Commits

Reviewing files that changed from the base of the PR and between 5f3a5d4ae72b2fc3ff0fab4569963a3c46d321bc and 9bad376f876fc719b33e94f04ed5b2f4268848f6.

📒 Files selected for processing (5)
  • src/ast/parse.zig
  • src/ast/visit.zig
  • test/regression/issue/29533-fn.fixture.cjs
  • test/regression/issue/29533-module.fixture.cjs
  • test/regression/issue/29533.test.ts
💤 Files with no reviewable changes (1)
  • src/ast/visit.zig

Comment thread src/js_parser/ast/parse.zig
Comment thread test/regression/issue/29533-fn.fixture.cjs
Comment thread src/js_parser/ast/visit.zig
Comment thread src/ast/parse.zig Outdated
@robobun
robobun force-pushed the farm/fdc7b537/fix-use-strict-directive-stripped branch 2 times, most recently from 637ce0d to eb22e9e Compare April 21, 2026 10:45

@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 — all previously raised concerns (block-scope directive promotion, P.zig guard narrowing, missing .js-extension test) are addressed in 637ce0d.

Extended reasoning...

Overview

The PR fixes a regression introduced in Bun 1.3.11 where function-body "use strict" directives were silently dropped during CJS transpilation. Two source files are changed (parse.zig, visit.zig) plus a guard in P.zig, with five regression tests added.

Security risks

None. This is a parser/transpiler correctness fix with no auth, crypto, or permission surface.

Level of scrutiny

The changes are targeted and well-understood: a scope-kind guard in parseStmtsUpTo, removal of a continue in the DCE pass, and a narrowed string-equality check in the CJS-wrapper guard. Previous review rounds surfaced and addressed every non-trivial edge case (block-scope promotion, non-strict directive suppressing re-emission, missing .js test). The fix is now complete.

Other factors

All inline comments from both CodeRabbit and my prior reviews are marked resolved and addressed in commit 637ce0d. The CI failures shown by robobun are in unrelated tests (shell leak, file loader, bun-install, bundler_npm) and are not caused by the directive-handling changes.

Comment thread test/regression/issue/29533.test.ts
@robobun
robobun force-pushed the farm/fdc7b537/fix-use-strict-directive-stripped branch from eb22e9e to d907990 Compare April 21, 2026 11:43

@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 — all previously raised issues have been addressed.

Extended reasoning...

Overview

This PR fixes a CJS transpiler regression where function-body "use strict" directives were silently dropped. Changes touch src/ast/parse.zig (directive-prologue scoping), src/ast/visit.zig (remove blanket s_directive skip in DCE pass), src/ast/P.zig (tighten CJS-wrapper guard to check directive value, not just tag), and new regression tests.

Security risks

None. The changes operate purely on directive-handling in the parser and bundler — no auth, crypto, or permissions code is touched.

Level of scrutiny

Moderate. The fix is confined to a well-understood regression path with a clear root cause and surgical changes. The three-way interaction between parse.zig, visit.zig, and P.zig required careful reasoning, and the review cycle caught real secondary bugs (block-scope promotion, directive-value guard, missing test) which were all addressed.

Other factors

Five regression tests cover the main scenario (.cjs), the .js+package.json classifier path, the "use client" + "use strict" ordering edge case, and the block-scope minification check. CI failures visible in the timeline (bundler_npm.test.ts, ASAN SIGILL) appear to be infrastructure noise unrelated to directive handling. The bug hunting system found no new bugs in the final state.

@robobun
robobun force-pushed the farm/fdc7b537/fix-use-strict-directive-stripped branch 2 times, most recently from ad34019 to 7e4e09f Compare April 21, 2026 12:44
Comment thread test/regression/issue/29533.test.ts
@robobun
robobun force-pushed the farm/fdc7b537/fix-use-strict-directive-stripped branch from 7e4e09f to 9a09fc1 Compare April 21, 2026 13:32

@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 — all issues from prior review rounds have been addressed.

Extended reasoning...

Overview

This PR fixes a regression (introduced in Bun 1.3.11) where function-body "use strict" directives were silently dropped during CJS transpilation. The changes touch three core parser/visitor files (parse.zig, visit.zig, P.zig) and add a regression test suite.

Security risks

None. The changes affect JS directive-prologue parsing and CJS wrapper emission — no auth, crypto, or permission code is involved.

Level of scrutiny

This PR received several rounds of iterative review with multiple critical and minor bugs caught and fixed:

  • The S.Directive promotion was correctly gated to .entry and .function_body scopes only (block/catch/finally remain as SExpr for DCE)
  • The P.zig CJS-wrapper guard was narrowed to check the directive value ("use strict") not just the AST tag
  • Test coverage now includes function-body .cjs, function-body .js (package type=commonjs), module-level directive preservation, non-strict directive guard, and block-scope DCE — all via concurrent subprocess tests

Other factors

All inline comments from prior review rounds are marked resolved and confirmed in commits 5f3a5d4, 637ce0d, eb22e9ed, d907990, and 9a09fc1. The bundler_npm snapshot offset delta (+52 bytes) is expected — the minified React bundle is slightly larger because function-body "use strict" directives are now preserved. No new concerns remain.

@robobun
robobun force-pushed the farm/fdc7b537/fix-use-strict-directive-stripped branch from 9a09fc1 to dc1d48e Compare April 21, 2026 17:40

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

All previously raised issues have been addressed; LGTM.

Extended reasoning...

Overview

This PR fixes a regression (Bun 1.3.11+) where "use strict" directives inside function bodies were silently dropped during CJS transpilation. Three files are touched: src/ast/parse.zig (directive-prologue gating), src/ast/visit.zig (removes the .s_directive => continue that discarded all directives in the minify+DCE pass), and src/ast/P.zig (narrows the CJS-wrapper guard to match specifically "use strict", not any arbitrary directive). The bundler_npm.test.ts snapshot offset updates (+52 bytes) are a mechanical consequence of directives now surviving into React's CJS output.

Security risks

None. The changes affect directive parsing and CJS wrapper emission only; no security-sensitive code paths (auth, crypto, permissions) are involved.

Level of scrutiny

Medium-high: the parser and visitor are core infrastructure, but the changes are narrowly scoped — each change site is well-commented and the logic is straightforward to verify. All five rounds of review feedback were addressed with targeted fixes and corresponding regression tests.

Other factors

All my prior inline comments were resolved in commits 5f3a5d4, 637ce0d, d907990, and 9a09fc1:

  • Block-scope string literals no longer promoted to S.Directive (scope-kind guard added)
  • P.zig wrapper guard checks directive value, not just tag
  • Third .js-extension CJS test added
  • Subprocess tests converted to test.concurrent
  • Prohibited panic/ASSERTION-FAILED stderr checks removed

The test suite covers all four correctness axes: function-body "use strict" in .cjs, in .js with "type":"commonjs", module-level re-emission via the CJS wrapper, non-strict directive interplay, and block-scope DCE. No concerns remain.

@alii

alii commented Apr 22, 2026

Copy link
Copy Markdown
Member

@robobun resume

@robobun
robobun force-pushed the farm/fdc7b537/fix-use-strict-directive-stripped branch from dc1d48e to 4003f1f Compare April 22, 2026 02:24
Comment thread src/js_parser/ast/visit.zig
@robobun

robobun commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator Author

Build #51265 (15dd0481, post src/ restructure rebase) — both failed lanes so far are pre-existing main flakes; 29533.test.ts green everywhere it ran:

test lane also failing on main
test/js/web/fetch/fetch-http2-client.test.ts (ASSERTION FAILED: wasRemoved in AtomStringImpl.cpp / cross-thread ThreadLock panic) 🐧 13 x64-asan merged #30219/#30211/#30201/#30196; 3 recent deflake commits on the file
test-http-should-emit-close-when-connection-is-aborted.ts (timeout) 🪟 2019 x64 merged #30219/#30211/#30201/#30196; neighboring builds #51258/#51259

(Build #51112 was a CI outage — 30 expired + 252 waiting_failed, 0 actual failures; main's own #50956 at the same base failed identically. Build #47062's fs.test.ts Windows failures were the S_IFBLK/S_IFSOCK unified-sources macro leak from #29545, since fixed on main.)

@Jarred-Sumner
Jarred-Sumner force-pushed the farm/fdc7b537/fix-use-strict-directive-stripped branch from 4003f1f to 4ce7edd Compare May 4, 2026 10:36
parseStmtsUpTo unconditionally skipped "use strict" during directive-prologue
parsing. That was safe at module scope — the CJS wrapper in P.zig re-emits
the directive at the top of the wrapper IIFE — but at function scope there
was no later pass that put it back, so the function body silently ran sloppy.

Visible fallout: bluebird's ES5 feature detector

    var isES5 = (function(){ "use strict"; return this === undefined; })();

returned false, and promisifyAll(require("crypto")) then read
X509Certificate.prototype.ca with the wrong `this`:

    TypeError: Can only call X509Certificate.ca on instances of
    X509Certificate  [ERR_INVALID_THIS]

Changes:

- parse.zig: only skip at module scope; at function scope fall through to
  the existing generic directive-preservation branch so the printer
  re-emits it. Also gate the S.Directive promotion itself to module/
  function-body scopes per ES2015 §14.1.1 — block-scope strings stay
  S.SExpr so DCE can drop them.
- visit.zig: stop dropping S.Directive nodes in joinWithComma
  (minify_syntax + DCE). Directives carry semantics; the parse-side
  scope gate above keeps block-scope strings out of this path.
- P.zig: the CJS-wrapper "don't double-emit use strict" guard now
  checks the directive VALUE, not just the tag — otherwise a module
  that begins with e.g. "use client" would suppress re-emission and
  the wrapper IIFE would run sloppy.
- bundler_npm.test.ts: the ReactSSR bundle test hard-codes source-map
  positions and the exact filesize. React's `react.development.js`
  has a function-body `'use strict'` that was previously dropped; now
  that it is preserved the bundle grows by 52 bytes (four 13-byte
  directives) and the first two mappings shift by 13 columns. Update
  the snapshot.

Fixes #29533
Fixes #14251
@robobun
robobun force-pushed the farm/fdc7b537/fix-use-strict-directive-stripped branch from 4ce7edd to 15dd048 Compare May 4, 2026 13:47

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

All prior feedback has been addressed and the bug hunter found nothing new, but this changes directive-prologue semantics across the parser/visitor/CJS-wrapper and shifts minified output offsets, so it's worth a human sign-off.

Extended reasoning...

Overview

This PR fixes a regression where function-body "use strict" directives were dropped during CJS transpilation. It touches three coupled pieces of the JS parser: parse.zig (gates directive-prologue recognition to .entry/.function_body scopes and rewrites function-level directives to S.Directive instead of skipping them), visit.zig (removes the .s_directive => continue that dropped directives during the minify+DCE merge loop), and P.zig (narrows the CJS-wrapper "already has use strict" guard to check the directive value, not just the tag). It also updates bundler_npm.test.ts source-map column offsets and the exact-filesize assertion (+52 bytes) because preserved directives shift minified output, and adds 5 regression tests with 2 fixtures.

Security risks

None identified. This is a transpiler-output correctness fix; it does not touch auth, crypto, permissions, network, or filesystem boundaries. The only behavioural change is that some user code now runs in strict mode where it previously (incorrectly) ran sloppy — which is the intended fix and matches Node/spec semantics.

Level of scrutiny

High. The JS parser/transpiler is on the hot path for every file Bun loads or bundles. The three changes are individually small but have subtle cross-cutting interactions — several of which surfaced during review (block-scope string literals being wrongly promoted, non-strict module directives like "use client" suppressing strict-mode re-emission, the React Fast Refresh / decorator before-list reorder edge case). All raised issues except the last (acknowledged as a non-regression follow-up) were fixed in subsequent commits. The bundler_npm.test.ts offset/filesize churn is expected but is exactly the kind of thing a human reviewer should sanity-check against the actual build output.

Other factors

  • All 7 of my prior inline comments and CodeRabbit's comments are resolved.
  • CI on the most recent fully-tested commit was green for the new test on every lane; remaining failures were documented as pre-existing on main.
  • One known incomplete-fix edge case (directive displaced by nearest_stmt_list hoisting under React Fast Refresh / standard decorators) was deliberately scoped out as a follow-up — behaviour there is unchanged from before this PR.
  • No human reviewer has approved yet; alii pinged resume but did not review.

Given the parser-core scope and the output-size/sourcemap snapshot updates, I'm deferring rather than approving.

@robobun

robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

This targets the pre-Rust-rewrite .zig files (src/js_parser/ast/parse.zig, P.zig, visit.zig) which no longer exist on main, so it can't be rebased cleanly. #31807 applies the same fix (function-scope "use strict" kept as an S::Directive; directives preserved through the visit pass) to the current .rs files, plus makes X509Certificate.prototype accessors non-enumerable. Verified the #29533 random-number-csprng repro and the #31806 Bluebird repro both pass.

@robobun

robobun commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: this PR's implementation lives entirely in Zig source files that have since been removed from the tree as part of the Rust migration. The change can no longer merge cleanly and the files it edits no longer exist on main.

If the underlying issue is still present, it will need a fresh fix against the Rust implementation.

@robobun robobun closed this Jun 26, 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.

Regression in Bun 1.3.11, 1.3.12, 1.3.13 when running uncompiled code Strict mode does not work at function-level granularity

2 participants