Skip to content

js_parser: sanitize auto-generated default export name for digit-named modules#31403

Merged
Jarred-Sumner merged 8 commits into
mainfrom
farm/1629c71d/fix-digit-default-name
May 26, 2026
Merged

js_parser: sanitize auto-generated default export name for digit-named modules#31403
Jarred-Sumner merged 8 commits into
mainfrom
farm/1629c71d/fix-digit-default-name

Conversation

@robobun

@robobun robobun commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Fixes #31401

Repro

// 1.ts
export default function () {}
$ bun run 1.ts
SyntaxError: No identifiers allowed directly after numeric literal

Two conditions are both required: the module filename starts with a digit (1.ts, 9abc.ts, …) and the source has an anonymous default function declaration (export default function () {}). Named defaults, anonymous classes, and arrows are unaffected.

Cause

An anonymous export default function () {} is given an auto-generated name derived from the module filename (<name>_default). The transpile / bun run path does not run the renamer (only bundling / minify_identifiers do), so the generated name is emitted verbatim:

$ bun build --no-bundle 1.ts
export default function 1_default() {}   # invalid identifier

create_default_name built this name with the non-allocating fmt_identifier() formatter. Unlike the allocating nonUniqueNameString helper the Zig reference used (MutableString.ensureValidIdentifier), fmt_identifier does not prepend an underscore when the name starts with a non-identifier-start character — so the leading 1 passed through unchanged, yielding 1_default. JSC's lexer then rejects 1_default as a numeric literal followed by an identifier.

The bundler path was fine because its NumberRenamer runs ensure_valid_identifier on every name, rewriting 1_default_1_default. This is effectively a regression of #2946 on the no-renamer path: names must be generated valid up-front there.

Fix

In create_default_name, sanitize the filename-derived base with MutableString::ensure_valid_identifier (which carries the #2946 leading-underscore fix) before appending _default, matching the Zig createDefaultName and the bundler output:

$ bun build --no-bundle 1.ts
export default function _1_default() {}   # valid

Non-digit filenames are unchanged (foo.tsfoo_default).

Verification

  • bun run 1.ts now exits 0.
  • New regression test test/regression/issue/31401.test.ts covers the bun run, import, and transpile-only paths. Fails with USE_SYSTEM_BUN=1 (3 fail), passes with the debug build (3 pass).
  • test/bundler/bundler_regressions.test.ts (incl. InvalidIdentifierInFileName#2946) and test/bundler/transpiler/transpiler.test.js (156 pass) remain green.

An anonymous `export default function () {}` is given an auto-generated
name derived from the module filename (`<name>_default`). The transpile
and `bun run` paths do not run the renamer, so the generated name is
emitted verbatim. `create_default_name` built it with the non-allocating
`fmt_identifier()` formatter, which — unlike the allocating
`nonUniqueNameString` helper the Zig reference used — does not prepend an
underscore when the name starts with a non-identifier-start character.

For a module named `1.ts` this produced `function 1_default()`, an
invalid identifier that JSC's lexer rejected with 'No identifiers allowed
directly after numeric literal'.

Use `MutableString::ensure_valid_identifier` (the #2946 fix) to sanitize
the filename-derived base before appending `_default`, matching the
bundler's NumberRenamer output (`_1_default`).
@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 31 minutes and 4 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ea20e180-ee7a-49ba-9c58-69da3210492c

📥 Commits

Reviewing files that changed from the base of the PR and between 52a84cd and 9beb90a.

📒 Files selected for processing (3)
  • src/bun_core/fmt.rs
  • src/js_parser/p.rs
  • test/regression/issue/31401.test.ts

Walkthrough

The PR fixes a parser bug where modules with digit-prefixed filenames (e.g., 1.ts) containing anonymous default exports generated invalid identifiers. The create_default_name function now sanitizes the filename base before appending the "_default" suffix, ensuring valid identifier generation across execution paths.

Changes

Identifier sanitization for digit-prefixed modules

Layer / File(s) Summary
Identifier sanitization in create_default_name
src/js_parser/p.rs
create_default_name switches from fmt_identifier() + format! to explicit sanitization via MutableString::ensure_valid_identifier on the filename base, then concatenates the sanitized identifier with "_default" directly in the bump arena.
Regression tests for digit-prefixed module filenames
test/regression/issue/31401.test.ts
Three tests validate the fix: direct execution of digit-named modules confirms no syntax error and correct output, module import verifies the imported function type, and bundled output confirms the sanitized identifier (_1_default) appears in the transpiled code.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: sanitizing auto-generated default export names for digit-named modules.
Description check ✅ Passed The PR description comprehensively covers the issue, root cause, fix, and verification across all required aspects.
Linked Issues check ✅ Passed The code changes address issue #31401 by sanitizing auto-generated default export names for digit-prefixed modules, fixing the SyntaxError.
Out of Scope Changes check ✅ Passed All changes directly address the objectives: one fix to the parser and one regression test for the identified issue.

✏️ 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.

@robobun

robobun commented May 25, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 7:12 PM PT - May 25th, 2026

@robobun, your commit 9beb90a9466af70be71e20a70634a2888f9182be passed in Build #58073! 🎉


🧪   To try this PR locally:

bunx bun-pr 31403

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

bun-31403 --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.

I didn't find any issues — the fix correctly swaps fmt_identifier() for MutableString::ensure_valid_identifier (matching both the Zig reference and the existing Source::identifier_name pattern), and the regression tests are solid. Deferring only because this is in the parser's transpile path, so it's worth a quick human glance.

Extended reasoning...

Overview

This PR fixes a regression where bun run 1.ts (or any module whose filename starts with a digit) containing export default function () {} fails with SyntaxError: No identifiers allowed directly after numeric literal. The root cause is that create_default_name in src/js_parser/p.rs used the non-allocating fmt_identifier() Display formatter to build the <name>_default symbol, but that formatter (src/bun_core/fmt.rs:1284-1288) does not prepend _ when the first character is a non-identifier-start — unlike MutableString::ensure_valid_identifier (src/bun_core/string/MutableString.rs:201-207), which carries the #2946 fix. On the no-renamer path (transpile / bun run), the unsanitized 1_default was emitted verbatim and rejected by JSC's lexer.

The fix replaces the formatter with non_unique_name_string_base() + ensure_valid_identifier(), then copies the result plus the _default suffix into the bump arena. This is the exact same pattern already used by Source::identifier_name (src/ast/lib.rs:2922-2924) and matches the Zig createDefaultName reference implementation. The arena allocation uses alloc_slice_fill_copy(sanitized.len() + SUFFIX.len(), 0u8) with correctly-bounded copy_from_slice calls. The intermediate Box<[u8]> from ensure_valid_identifier is dropped at end of block. The ? propagation of AllocErrorbun_core::Error is already established by the identical pattern in identifier_name.

A new regression test (test/regression/issue/31401.test.ts) covers three paths: bun run 1.ts, importing a digit-named module from another module, and bun build --no-bundle output verification. The PR description confirms these fail under USE_SYSTEM_BUN=1 and pass with the fix, and that existing bundler regression tests (including #2946) and the transpiler suite remain green.

Security risks

None. The change only affects the auto-generated symbol name for anonymous default function exports. Input is the module's own filename base, output is an internal AST symbol name. No user-controlled data flows into anything security-sensitive; if anything, the fix makes the sanitization stricter.

Level of scrutiny

Moderate. The change itself is small (~10 LOC), mechanical, and mirrors an established in-tree pattern exactly. However, create_default_name lives in the JS parser and runs on every transpiled file containing an anonymous default function export — this is production-critical runtime code for Bun. While I'm confident the fix is correct, parser changes in a production runtime warrant a human reviewer's eyes rather than bot-only approval.

Other factors

  • No CODEOWNERS entry covers src/js_parser/.
  • No prior human reviews or outstanding comments on the PR.
  • The bug hunting system found no issues.
  • The diagnosis in the PR description is accurate (I verified the divergence between fmt_identifier and ensure_valid_identifier at the relevant lines).

Normalize CRLF in spawned-process stdout assertions and drop the
over-strict empty-stderr check (replaced with the meaningful
'no numeric-literal SyntaxError' invariant) so the test is robust on
the Windows baseline lane.

@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 looks correct and well-tested, but this touches the JS parser hot path and CI hasn't reported green on the latest commit yet (the Windows transpiler.test.js failure was on 0f04b583, and 6b87f027 only touched the new regression test) — worth a human look before merging.

Extended reasoning...

Overview

This PR fixes #31401 by changing create_default_name in src/js_parser/p.rs to sanitize the filename-derived base via MutableString::ensure_valid_identifier (which prepends _ for non-identifier-start leading chars per #2946) instead of the non-allocating fmt_identifier() formatter, which lacks that prefix logic. A new regression test covers bun run, import, and build --no-bundle paths for digit-named modules with anonymous default function exports.

Security risks

None. This is purely identifier-name generation for emitted JS; no untrusted input handling, auth, crypto, or filesystem boundaries are touched.

Level of scrutiny

Medium-high. The diff is small (~15 lines) and the logic is straightforward, but create_default_name runs in the JS parser for every module with an anonymous default export, on the no-renamer path used by bun run. There is also a subtle behavioral side-effect: ensure_valid_identifier additionally applies strict_mode_reserved_word_remap, so e.g. let.ts will now yield _let_default instead of let_default. That is harmless (both are valid identifiers) and actually matches the Zig reference more closely, but it is a change in emitted output.

Other factors

  • Several callers wrap create_default_name(...).expect("unreachable"); the newly-introduced ? only propagates AllocError (OOM), which is already treated as unreachable elsewhere, so no new panic surface.
  • robobun reported test/bundler/transpiler/transpiler.test.js failing on two Windows lanes for commit 0f04b583. The follow-up commit 6b87f027 only hardened the new 31401.test.ts (CRLF normalization, looser stderr check) and did not touch transpiler.test.js, so it is unclear whether that failure is flaky or real; CI for the latest commit has not yet been reported in the thread.
  • No CODEOWNERS entry for src/js_parser/.

Given the parser-hot-path location and the not-yet-confirmed-green Windows CI, deferring to a human reviewer rather than auto-approving.

robobun and others added 2 commits May 25, 2026 21:28
Launch the digit-named modules via `bun <absolute-path>` instead of
`bun run <relative>` + cwd. The direct-file form (AutoCommand) is the
path already exercised on Windows CI by test/regression/issue/08965, and
avoids the run-command's script-resolution step for a bare relative
filename. The fix under test lives in the parser and runs identically
regardless of how the entry file is launched.

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/regression/issue/31401.test.ts`:
- Around line 1-3: Replace raw stdout assertions (e.g.,
expect(stdout).toBe(...), expect(stdout).toContain(...)) with normalized
snapshot assertions using normalizeBunSnapshot: call
expect(normalizeBunSnapshot(stdout)).toMatchSnapshot() (or
toMatchInlineSnapshot() if desired). Update the three occurrences referenced
(the assertions around lines noted in the comment) to use normalizeBunSnapshot
and snapshot matchers; keep existing variables (stdout, stderr) and test names
intact and only change the assertion style.
🪄 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: bf5013fd-e6d5-4c6d-a756-6b5051e19845

📥 Commits

Reviewing files that changed from the base of the PR and between fc26825 and 52a84cd.

📒 Files selected for processing (2)
  • src/js_parser/p.rs
  • test/regression/issue/31401.test.ts

Comment thread test/regression/issue/31401.test.ts
Comment thread src/js_parser/p.rs Outdated
robobun added 2 commits May 25, 2026 21:55
Adopt the repo convention of normalizing spawned-process stdout through
normalizeBunSnapshot (which collapses CRLF among other things) instead of
ad-hoc replaceAll. The bug is still caught by the stderr
'No identifiers allowed directly after numeric literal' assertion, which
is independent of stdout formatting.

@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 aligns create_default_name with the Zig reference sanitizer, with solid regression coverage.

Extended reasoning...

Overview

This PR fixes a single function, create_default_name in src/js_parser/p.rs, swapping the non-allocating fmt_identifier() formatter for MutableString::ensure_valid_identifier so that filename-derived default-export names are valid identifiers even when the filename starts with a digit (e.g. 1.ts_1_default instead of the invalid 1_default). It also adds a 3-case regression test (bun run, import, build --no-bundle) at test/regression/issue/31401.test.ts.

Security risks

None. The change only affects how an internal symbol name is constructed from a module filename for printing; no auth, crypto, permissions, or user-input parsing surface is touched. The sanitizer being swapped in is strictly stricter than the one being replaced.

Level of scrutiny

Low. This is a ~10-line mechanical fix that brings the Rust port back in line with the documented Zig reference (nonUniqueNameString = ensureValidIdentifier(nonUniqueNameStringBase())). I verified ensure_valid_identifier always returns a non-empty result (empty → _), so the arena slice math is sound, and the ? correctly propagates the existing AllocErrorbun_core::Error path. Non-digit filenames are unchanged since the sanitizer is a no-op for already-valid-start identifiers.

Other factors

All review feedback has been addressed and resolved: the CodeRabbit normalizeBunSnapshot suggestion was adopted in b103c2a, and my earlier non-blocking heads-up about two sibling fmt_identifier call sites was acknowledged as intentionally out-of-scope (neither is observable on the no-renamer path). The PR description confirms the existing #2946 bundler regression test and transpiler suite remain green, and the new test fails on system Bun / passes on the patched build as expected.

@robobun

robobun commented May 25, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: diff is green, failures are pre-existing flake on unrelated lanes

The only repeatedly-failing test across builds #58031 and #58043 is test/bundler/transpiler/transpiler.test.js"Bun.Transpiler.transformSync stack overflows" on the windows-2019-x64(-baseline) lanes. It fails with Received function did not throw — i.e. the transpiler didn't blow the stack on the lots-of-for-loop.js fixture, which is a stack-depth/platform-timing flake.

This is unrelated to this PR:

  • The fixture (test/bundler/transpiler/fixtures/lots-of-for-loop.js) contains no export default, so it never invokes create_default_name — the only function this PR changes.
  • Both "stack overflows" tests pass locally with the patched debug build.
  • This PR's own regression test (test/regression/issue/31401.test.ts) passes on every lane, including Windows, and is clean under ASAN.

Other red lanes were also unrelated flake:

  • test/js/bun/s3/s3-stream-cancel-leak.test.ts (debian-13-x64-asan) — a LeakSanitizer SIGABRT whose allocation stack is entirely in ReadableStream.rs/Blob.rs/s3/client.rs (zero frames in the parser).
  • update_interactive_install.test.ts, cpu-prof.test.ts, bun-install.test.ts — tagged flaky by Buildkite (ran with retries).

The diff itself is green. This needs a maintainer to merge (or a re-run of the flaky Windows lane).

@Jarred-Sumner Jarred-Sumner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Delete your code comments.

fmt_identifier shoudl handle numbers? Isn't that the correct fix?

…t char

Per review feedback, fix the root formatter instead of special-casing
create_default_name. `fmt_identifier` (FormatValidIdentifier) left the
first character untouched when it was not a valid identifier start (e.g. a
digit from a module named `1.ts`), emitting an invalid identifier like
`1_default`. Prepend `_` in that case, matching
MutableString::ensure_valid_identifier (the #2946 fix). create_default_name
reverts to its original one-liner now that the formatter is correct.
@robobun

robobun commented May 26, 2026

Copy link
Copy Markdown
Collaborator Author

Done in d47b834 — you're right, that's the correct fix. fmt_identifier now prepends _ when the name starts with a non-identifier-start char (e.g. a digit), matching MutableString::ensure_valid_identifier. Reverted create_default_name to its original one-liner and deleted the added comments.

This also fixes the sibling fmt_identifier call sites at the root. Verified: digit-named module → _1_default, non-digit unchanged (foo_default), and the bundler regressions (#2946) + full transpiler suite stay green.

Note: the flaky transpiler.test.js "stack overflows" CI failure was unrelated — it's fixed on main by #31382 (deepened the fixture).

@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 — moving the leading-underscore fix into fmt_identifier itself is the cleaner root-cause fix and resolves the sibling call sites I'd flagged earlier.

Extended reasoning...

Overview

This PR fixes #31401: bun run 1.ts with export default function () {} produced SyntaxError: No identifiers allowed directly after numeric literal because the auto-generated name 1_default is not a valid identifier on the no-renamer (transpile/run) path.

The final diff is minimal:

  • src/bun_core/fmt.rs (+4): FormatValidIdentifier::fmt now prepends _ when the first character is not a valid identifier-start (the start_i == 0 branch). This mirrors MutableString::ensure_valid_identifier line-for-line, including the same #2946 comment.
  • src/js_parser/p.rs (−4): removes a now-stale port-note comment; create_default_name is otherwise unchanged.
  • test/regression/issue/31401.test.ts (+66): three concurrent tests covering bun run, import, and build --no-bundle for digit-named modules.

Security risks

None. This only affects how a filename-derived string is sanitized into an internal JS identifier. No auth, crypto, filesystem, or network surface.

Level of scrutiny

Low–medium. It's a 4-line logic addition that brings the non-allocating formatter in line with the allocating reference (ensure_valid_identifier). I audited all fmt_identifier / FormatValidIdentifier call sites: bare callers (create_default_name, the namespace_ref sites in p.rs/parse_stmt.rs, scanImportsAndExports.rs) now emit valid identifiers up-front instead of relying on the bundler renamer to repair them; prefixed callers (require_{}, export_{}, ${}) gain at most a cosmetic extra _ in internal generated names, which are still valid and go through the renamer anyway.

Other factors

  • The author pivoted from a call-site patch to fixing fmt_identifier at the root after review discussion — this directly resolves the non-blocking sibling-site concern I'd raised on the previous revision.
  • All inline review threads (CodeRabbit's normalizeBunSnapshot request, my sibling-site note) are addressed and resolved.
  • Tests follow house style: tempDir with using, bunEnv/bunExe, normalizeBunSnapshot for stdout, stderr regression marker checked, exit code asserted last.
  • Existing bundler regressions (#2946) and the transpiler suite were verified green by the author; CI failures were unrelated pre-existing flakes (Windows stack-overflow fixture, S3 ASAN leak) with no parser frames.
  • No CODEOWNERS for these paths; bug-hunting system found nothing.

@Jarred-Sumner Jarred-Sumner merged commit 146afa3 into main May 26, 2026
76 of 77 checks passed
@Jarred-Sumner Jarred-Sumner deleted the farm/1629c71d/fix-digit-default-name branch May 26, 2026 01:33
springmin pushed a commit to springmin/bun that referenced this pull request May 26, 2026
* oven/main (10 new commits):
  Optimize TextEncoder.encode: restore SIMD ASCII fast paths lost in the Rust port (oven-sh#31385)
  js_parser: sanitize auto-generated default export name for digit-named modules (oven-sh#31403)
  fetch: run checkServerIdentity before writing the request (oven-sh#31325)
  ffi: avoid copying the threadsafe callback wrapper on the calling thread (oven-sh#31332)
  install: gate the exit-callback cache teardown to the main thread (oven-sh#31376)
  fix(node:module): don't register native helpers as their own constructors (oven-sh#31393)
  css: escape custom pseudo-class/element names when printing (oven-sh#31404)
  Deepen the lots-of-for-loop fixture so the transpiler stack-overflow tests throw on Windows (oven-sh#31382)
  Hardening: input validation and bounds tightening across 36 subsystems (round 4) (oven-sh#31339)
  Speed up FormData multipart serialization (oven-sh#31379)

Auto-merged: src/install/PackageManager.rs, src/runtime/cli/upgrade_command.rs, src/runtime/webcore/Blob.rs, src/sys/lib.rs
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.

No identifiers allowed directly after numeric literal

2 participants