Skip to content

bundler: warn about direct eval in ES modules, with one guard for weird-code warnings - #46890

Draft
robobun wants to merge 1 commit into
mainfrom
robobun/9dadacfe/direct-eval-warning
Draft

robobun wants to merge 1 commit into
mainfrom
robobun/9dadacfe/direct-eval-warning

Conversation

@robobun

@robobun robobun commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Related to #2875

Problem

  • bun build prints nothing for a direct eval() in a bundled ES module. The file of eval uses global scope instead of local scope #2875 builds with --minify, then throws ReferenceError: bar is not defined.
  • The direct-eval block of e_call ended in a TODO (src/js_parser/visit/visit_expr.rs:1961). esbuild prints Using direct eval with a bundler is not recommended and may cause problems there.
  • The private-name warnings print for node_modules files, and twice with --minify-syntax.

Fix

  • For a bundled ES module, the block calls P::warn_direct_eval. Its note names globalThis.eval(code) and new Function.
  • warns_about_weird_code keeps these warnings out of node_modules and off the second visit of the minifier. The private-name warnings use it too.
  • Verified: test/bundler/esbuild/default.test.ts, test/bundler/bun-build-api.test.ts (9 cases, all fail on main), then all of test/bundler/.
  • Self-reviewed: 24 concerns raised, 20 addressed (Notes).

Background

Downsides

Notes

Where this comes from. A maintainer asked for this warning at the TODO. No issue asks for it. #2875 (closed) is the one report of the shape that it covers: its file still fails with --minify on main, and #40518 left the top-level names of a flat ES module renameable on purpose, as in esbuild. #13194, #14891, #19908 and #25433 are CommonJS packages under node_modules. They get no warning and this PR does not change them. #35961 was the first attempt at this line, with a debug-level note. The 90-day stale sweep closed it and no maintainer commented on it.

The rule, next to esbuild. esbuild gives the message the warning level when mode == bundle && isFileConsideredESM && !suppressWarningsAboutWeirdCode, and the debug level in all other cases. This PR emits the warning-level case only.

  • isFileConsideredESM is is_file_considered_to_have_esm_exports || has_es_module_syntax. prepare_for_visit_pass sets the two fields before the visit starts.
  • suppress_warnings_about_weird_code is true at each place that builds parser options (Remove dead code from bun_bundler, the JS parser, the help command, and PackageJSONEditor #44687 deletes it). The guard tests the whole path with path_contains_node_modules_folder. Path::is_node_module() looks only at the directory part, so it misses a file directly in node_modules/. Its 12 callers keep that gap.
  • The debug-level case is not emitted. Each bundler log starts at the warn level, and bun build has no flag that changes the level for one message.

Counts compared with esbuild 0.25.10 (--bundle --format=esm, the same in both): import only 1, export 1, import.meta only 1, top-level await only 1, .mjs with no ESM syntax 1, import type in .ts 1, export {} after the eval in .ts 1, import in .cjs 1, three calls in one file (top level, nested function, dead branch) 3, a plain script under "type": "module" 1, module.exports = eval() under "type": "module" 1, .mjs under "type": "commonjs" 1. Zero in both: CommonJS-shaped, plain script, .cjs under "type": "module", import x = require(), export =, a file with only import(), indirect forms ((0, eval)(x), eval?.(x), globalThis.eval(x), an alias, new Function), a package under node_modules, a file directly in node_modules/.

Where Bun warns but keeps the names. Bun wraps a file that has import but no export in a CommonJS closure when it has a direct eval, and pins its top-level names. For such a file only the imports are at risk. For a file with only import.meta or only import type, and for module.exports = eval() under "type": "module", nothing is at risk. The warning still prints there, as in esbuild. The note makes no claim about renamed names for this reason.

The note. Use globalThis.eval(code) when the code reads only global variables. To give the code a variable, an import or require from this file, pass it as an argument: new Function("name", code)(name). I ran the two forms with no flag, --minify-syntax, --minify and bun run: no warning, and globalThis.eval(code) stays indirect. (0, eval)(code) is not in the note: the second visit of the minifier prints const r = (0, eval)(code); return r + "!" as a direct eval, on main too. #45507 removes the second visit and the is_revisit_for_substitution field. The PR that merges second drops the field from the guard. An indirect eval runs the code as a non-strict script, so its var and function declarations become globals. The note does not say that.

What a direct eval costs when it works. It keeps every top-level declaration of its file in the bundle. A file with 200 unused functions and one exported function that evaluates a string: 7,655 B with eval(code), 88 B with globalThis.eval(code) (4,892 B and 58 B with --minify).

Where the warning shows. bun build prints it, also with --compile. Bun.build() puts it in logs and prints nothing. Bun.serve HTML routes and the dev server drop the log of a build that succeeds, as they do for each bundler warning. A file that is parsed for two targets (server entry and HTML import) gives the warning 2 times. A second build of an ES module bundle warns about a direct eval that came from node_modules (esbuild does the same). A test pins that.

Each clause has a test that fails without it. I removed one clause at a time from a local build: each of the two ES module fields, the guard call in each helper, each of the two conditions of the guard, and the log level check.

Measurements. Release builds of the merge-base (179664e) and of this PR, linux x64.

  • Non-bundle direct eval: 4 -> 4 instructions. Non-eval identifier call: 22 -> 22 for f(1) and 26 -> 26 for abcd(1). Private-name read and write that do not warn: 25 -> 25 and 26 -> 26. Method: gdb stepi, 3 runs each.
  • Bundled CJS-shaped direct eval: +5 instructions (211 -> 216), 0 new calls. Bundled node_modules ES module direct eval: +131 instructions (6 -> 137), 1 cold call, 0 allocations.
  • 1 warning per direct-eval call: logs.length is 0, 1, 101 for 0, 1, 101 calls, and CLI stderr has the same count. 31 allocations per warning end to end (mimalloc entry points counted in gdb over a CLI build, 5 runs). A file with no direct eval: 9614 -> 9614 allocations (4 of 5 runs of the PR build, one run gave 10538).
  • Release text +768 B, data +0 B. warn_direct_eval is 400 B and warn_private_name is 295 B, each at 1 address. visit_expr_in_out is 17881 -> 17610 B in each of its 2 instantiations. Frame sub rsp 0x238 -> 0x248.
  • Existing tests that had to declare the warning: 3 (the compile-argv cases). itBundled fixtures on the API backend that gain a logs entry which the harness does not check: 9. default/WarningsInsideNodeModules is no longer todo.

Self-review, what is not done.

Suites. All of test/bundler/, test/js/bun/module-graph/module-graph-compile.test.ts, test/js/node/disallow-code-generation-from-strings.test.ts, test/napi/napi.test.ts, test/cli/lint/glob.test.ts, test/js/node/module/node-module-module.test.js, test/js/node/worker_threads/worker_threads.test.ts and test/regression/issue/07740.test.ts on the release build. The changed test files on the debug build.

Found on the way, not changed here.

  • BuildMessage.level is "warn" at run time. packages/bun-types/globals.d.ts and docs/bundler/index.mdx say "warning", and the type has no notes.
  • test/bundler/bun-build-compile.test.ts:2141 writes app-with-resources (240 MB) into the working directory.
  • bundler_bytecode_portable.test.ts "encoder output is identical on every platform" fails on a release build of main in my container.

no test proof · iteration 6 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bundler/bun-build-api.test.ts

…rd-code warnings

`bun build` printed nothing for a direct `eval()` call in a bundled ES
module. The bundler can rename the top-level names of such a file and it
replaces import bindings, so the code that the eval runs can fail.

The direct-eval block of `e_call` now calls a cold helper,
`P::warn_direct_eval`, when the file is bundled and is an ES module. It
adds one warning on the `eval` token, with a note that names
`globalThis.eval(code)` and `new Function`. esbuild uses the same rule
for its `direct-eval` warning.

`P::warns_about_weird_code` is the guard of the visit-pass warnings
about suspect code: no warning inside node_modules, and none on the
second visit that the minifier makes after it moves a single-use value.
The three private-name warnings go through it too (`warn_private_name`).
Before, they printed for node_modules files, and twice with
`--minify-syntax`.

`Log::add_range_warning_fmt_with_notes` now checks the log level, like
the other warning helpers.

`default/WarningsInsideNodeModules` is no longer `todo`. Three
`compile-argv` test cases bundle a direct eval in an ES module. They now
declare the warning.
@robobun

robobun commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 9:58 AM PT - Oct 25th, 2026

✅ @robobun, your commit cd9e6b010b019e8b0ef20a5c699174fa46dd2e67 passed in Build #132641! 🎉


🧪   To try this PR locally:

bunx bun-pr 46890

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

bun-46890 --bun

@robobun

robobun commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: draft until #41658 and #43313 merge. Without those printer fixes, a warning on a long line prints one space per column, and the whole line when it is near the end of the line. Both are rebased and mergeable. CI on this PR is green. One maintainer decision is also open (first bullet of Downsides).

How to reproduce:

// entry.js
import { helper } from "./lib.js";
const secret = 42;
console.log(eval("secret + helper()"));

// lib.js
export function helper() { return 1; }

On main, bun build ./entry.js --outfile=out.js exits 0 with empty stderr, and bun out.js throws ReferenceError: helper is not defined. With this PR the build also prints warn: Using direct eval with a bundler is not recommended and may cause problems at entry.js:3:13. The bundle does not change.

@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 179664e and cd9e6b0.

Files selected for processing: 6
  • src/ast/lib.rs
  • src/js_parser/p.rs
  • src/js_parser/visit/visit_expr.rs
  • test/bundler/bun-build-api.test.ts
  • test/bundler/compile-argv.test.ts
  • test/bundler/esbuild/default.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 emits gated warnings for private-name operations and direct eval. Range warnings respect the configured log level. Bundler tests cover warning behavior across module types, eval forms, minification, and node_modules.

Changes

Bundler warning diagnostics

Layer / File(s) Summary
Warning emission and private-name diagnostics
src/ast/lib.rs, src/js_parser/p.rs, src/js_parser/visit/visit_expr.rs, test/bundler/esbuild/default.test.ts
Range warnings return without formatting when warnings are hidden by the log level. Private-name warnings use a gate that suppresses warnings during substitution revisits and for paths under node_modules. The tests check that minification does not duplicate these warnings.
Direct-eval diagnostics and bundler coverage
src/js_parser/p.rs, src/js_parser/visit/visit_expr.rs, test/bundler/bun-build-api.test.ts, test/bundler/compile-argv.test.ts, test/bundler/esbuild/default.test.ts
Bundled direct eval produces a warning for files considered to have ESM exports or ES module syntax. Tests cover module forms, eval forms, warning notes, log-level suppression, minification, and node_modules behavior.

Possibly related PRs

  • oven-sh/bun#20435: Adds a minification regression test for duplicate private-name warnings, related to this PR’s suppression of warnings on substitution revisits.
  • oven-sh/bun#45063: Changes warning handling based on logLevel, related to this PR’s range-warning log-level check.

Suggested reviewers: dylan-conway, alii

Merge Risk: Minimal · up to cd9e6

The warning and its suppression behavior have no identified issue requiring a fix before merge.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title names the primary change: a bundler warning for direct eval in ES modules. It also names the guard for weird-code warnings, which the PR adds. The title is specific and matches the changeset…
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.
Description check Passed The description clearly explains the problem, implementation, expected behavior, downsides, and verification. It does not use the exact template headings, but the required content is present through t…
Full details: Title check

Explanation

The title names the primary change: a bundler warning for direct eval in ES modules. It also names the guard for weird-code warnings, which the PR adds. The title is specific and matches the changeset. It is long at 85 characters, but it is not vague or unrelated.

Full details: Description check

Explanation

The description clearly explains the problem, implementation, expected behavior, downsides, and verification. It does not use the exact template headings, but the required content is present through the Problem, Fix, Verified, and Suites sections.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@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 findings, I also checked a few things and ruled them out: every other add_*warning* method on Log in src/ast/lib.rs already carries the Kind::Warn.should_print guard, so the new guard on add_range_warning_fmt_with_notes closes the last gap rather than leaving siblings behind; the gate is_file_considered_to_have_esm_exports || has_es_module_syntax lines up with esbuild's isFileConsideredESM (export/TLA/package type, plus import statements and import.meta via esm_import_keyword, which is restored for import() and import x = require()); and un-todo'ing default/WarningsInsideNodeModules is consistent with Bun emitting no delete-super warning at all, so the exact per-file CLI check does not depend on an unported message. The default-on CLI warning is a user-visible output change that a maintainer should sign off on.

Extended reasoning...

The change adds esbuild's direct-eval bundler warning in the Rust parser, routes the three private-name warnings and the new warning through a shared node_modules/revisit gate, and adds a missing log-level guard; it touches no security-sensitive surface. Tests cover a per-file variant matrix on the CLI backend, the JS API, logLevel suppression, and the second-build node_modules case. Two confirmed findings are posted inline, and the new warning changes default bun build stderr for ESM files that call eval, so a human decision on that default is still needed.

Comment thread src/js_parser/p.rs
Comment on lines +1035 to +1043
/// esbuild's `direct-eval` warning. The caller checks that the file is a bundled ES module.
#[cold]
#[inline(never)]
pub(crate) fn warn_direct_eval(&mut self, loc: bun_ast::Loc) {
if !self.warns_about_weird_code() {
return;
}

let r = js_lexer::range_of_identifier(self.source, loc);

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.

🔴 Users who bundle a minified ES module that calls eval outside node_modules now get a stderr line of the whole minified source plus one space per column, per eval call. The new warning in warn_direct_eval at src/js_parser/p.rs:1043 carries a range on the source line, and the printer at src/ast/lib.rs:1052-1065 echoes the full line text and then pads to the column. Fix: make the warning safe on long lines before it ships, either by landing the printer cap the PR description names (#41658) first or by truncating lineText and the padding in write_format around a window of the column, so every range warning on a minified line stays bounded. The description calls this an ordering dependency, but nothing in this diff enforces it. [also at: src/js_parser/p.rs:1044 - Users who bundle a minified ES module that lives outside node_modules (a vendored .min.js, or the output of a first bun build --minify fed into a second build or --compile) now get a ~120-byte window of the line echoed to stderr once per direct eval, plus one space per column, where the base branch printed nothing.]

Why this was flagged

A project bundles a prebuilt, minified ESM file that is not under node_modules (for example a vendored lib.min.mjs, or a bundle output from bun build --minify re-bundled). The file has export or import and a direct eval( call at column N on a one-line source. visit_expr.rs:1924 now calls warn_direct_eval for every such call, and p.rs:1043 attaches a range on that line. Log printing at src/ast/lib.rs:1052-1065 writes the full line text and then write_n_bytes(to, b' ', line_offset_for_second_line), so stderr gets roughly N bytes of source plus N spaces per eval call, with no cap in this diff. On the base branch this file produces no warning so none of this is printed. The author's note says #41658 must merge first, but this checkout has no such cap, so merging in the other order ships the cost.

Verification: normal — acknowledged in diff: the bound is accurate for the padding, and nothing in this PR's own commits caps it. Trigger: bun build (CLI, logLevel >= warn) of a bundled ES module not under node_modules whose source is one long line with a direct eval( call. write_n_bytes(to, b' ', line_offset_for_second_line) (lib.rs:1065) then writes that many spaces.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is correct. It is the second bullet of Downsides.

The cost comes from the log printer and not from this warning. Location::init_or_null_impl cuts the excerpt to about 120 bytes but keeps the column of the whole line, and Data::write_format pads the caret to that column. It also keeps the whole line when the position is in its last 80 bytes. Each located diagnostic on a long line pays this on main. #41658 and #43313 are the fixes. Both need a rebase, and I asked for that.

I did not add a bound in this diff:

To make the order a property of the PR and not only a sentence in its body, this PR is now a draft. It leaves draft when #41658 is merged. I leave this thread open until then.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Correction to my reply above: this PR waits for #43313 too, not only for #41658. #41658 bounds the caret padding. #43313 bounds the excerpt when the position is in the last 80 bytes of the line.

Measured on main with a warning that exists today (a read of a setter-only private name) on a 200 KB line: 200,268 B of stderr in the middle of the line, 400,196 B at its end.

Both PRs are rebased and mergeable now. This PR stays a draft until both are merged.

Comment thread src/js_parser/visit/visit_expr.rs Resolved

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.

2 participants