Skip to content

parser: keep legal comments across the speculative arrow parse in a conditional - #40985

Merged
Jarred-Sumner merged 1 commit into
mainfrom
robobun/e588f514/ts-conditional-arrow-legal-comments
Aug 30, 2026
Merged

Jarred-Sumner merged 1 commit into
mainfrom
robobun/e588f514/ts-conditional-arrow-legal-comments

Conversation

@robobun

@robobun robobun commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • x = a ? /*! L */ (b) : T => { let y = 1 } : c aborts a debug build: panic: assertion failed: self.comments_to_preserve_before.len() >= original.comments_to_preserve_before_len in Lexer::restore (src/js_parser/lexer.rs:396). A release build drops the legal comment from the output.
  • The cause is the speculative parse from parser: accept TypeScript arrow return types inside conditionals, export type before a newline, and import type from #40818 (src/js_parser/parse/parse_skip_typescript.rs:1598). It parses the arrow body, then restores a snapshot that records comments_to_preserve_before as a length. A block body (src/js_parser/parse/mod.rs:1473) or an import() drains that buffer. A length cannot restore it.

Fix

  • P::parser_snapshot moves the buffer out of the lexer with core::mem::take. The attempt runs with an empty buffer. restore_parser_snapshot puts the original back.
  • Correct because the attempt's output is discarded. The real parse scans the same tokens again and attaches the comment to the first statement of the arrow body.
  • Verified: test/bundler/transpiler/transpiler.test.js (three new cases, each aborts the debug build without the fix). Also bundler_comments, bundler_edgecase, esbuild/ts.

Background

  • A legal comment is /*! ... */, //! ..., or a comment with @license or @preserve. The lexer collects them in comments_to_preserve_before. The next statement takes them and prints them before itself.
  • In TypeScript, (b) : c => d after a ? is an arrow function with return type c only when another : follows its body. Otherwise the : pairs with the ?. parser: accept TypeScript arrow return types inside conditionals, export type before a newline, and import type from #40818 decides this with a speculative parse: snapshot, parse the body, restore.
  • ParserSnapshot (src/js_parser/p.rs) holds everything a parse of an expression mutates. Lists are stored as lengths and truncated on restore.
Notes

…onditional

The attempt in is_type_script_arrow_return_type_after_question_and_before_colon
parses the arrow body. A block body or import() drains
lexer.comments_to_preserve_before. The lexer snapshot records only a
length, so the restore could not bring the comments back. The debug build
hit the assertion in Lexer::restore. The release build dropped the legal
comment from the output.

Move the buffer into ParserSnapshot at snapshot time and put it back on
restore.
@robobun

robobun commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

Reproduced on main (ceef547) with a debug build:

printf 'x = a ? /*! L */ (b) : T => { let y = 1 } : c\n' > /tmp/r.ts
./build/debug/bun-debug build /tmp/r.ts --no-bundle

The build aborts with panic: assertion failed: self.comments_to_preserve_before.len() >= original.comments_to_preserve_before_len in Lexer::restore. With this change it prints the arrow with the comment inside the body.

Found while I checked #34252 against #40818. The three new test cases come from the test block of #34252.

@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ad71c438-8f80-4cc0-b490-01e2a2cf2d66

📥 Commits

Reviewing files that changed from the base of the PR and between ceef547 and 2e3a1bb.

📒 Files selected for processing (2)
  • src/js_parser/p.rs
  • test/bundler/transpiler/transpiler.test.js

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


Walkthrough

The parser snapshot now preserves pending lexer comments during speculative parsing. Transpiler regression tests cover comments in conditional expressions with arrow-function and import() expression bodies.

Changes

Comment preservation

Layer / File(s) Summary
Snapshot comment state
src/js_parser/p.rs
ParserSnapshot captures pending lexer comments. Snapshot restoration returns them to the lexer.
Comment preservation regression tests
test/bundler/transpiler/transpiler.test.js
Tests verify comment output for conditional expressions with arrow-function blocks and import() bodies.

Suggested reviewers: jarred-sumner, dylan-conway

Merge Risk: ⚪ Minimal · up to 2e3a1

This localized parser change preserves legal comments during speculative conditional-arrow parsing and prevents the reported debug assertion and release-output omission; no actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: preserving legal comments during speculative arrow parsing in conditional expressions.
Description check ✅ Passed The description explains the problem, root cause, fix, and verification. It does not use the template headings exactly, but it provides the required information and is mostly complete.
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.

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

@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 snapshot fix with matching regression tests.

Checked that mem::take runs before self.lexer.snapshot(), so the captured comments_to_preserve_before_len is 0 and the debug assertion in Lexer::restore holds regardless of what the attempt drains or appends.
Verified the single caller (parse_skip_typescript.rs:1598) always reaches restore_parser_snapshot — the attempt closure's Result is captured, not ?-propagated, so the moved-out buffer cannot be lost.
ParserSnapshot is consumed by value on restore, so the saved Vec cannot be reused twice; the &self → &mut self change is safe with one &mut self-context caller.
New exp() cases sit alongside the existing conditional-arrow assertions and follow the same pattern.

Extended reasoning...

Overview

This PR fixes a snapshot/restore asymmetry in the JS parser's speculative-parse machinery for TypeScript conditional-arrow disambiguation (a ? (b) : T => body : c). ParserSnapshot previously relied on LexerSnapshot capturing comments_to_preserve_before as a length, but a block body or import() inside the attempt drains that buffer entirely — a length cannot restore a shrunk Vec, tripping a debug assertion and dropping the legal comment in release. The fix moves the buffer out with core::mem::take into the snapshot (leaving the lexer empty for the attempt) and writes it back on restore. Two files touched: src/js_parser/p.rs (~10 lines) and three new exp() assertions in test/bundler/transpiler/transpiler.test.js.

Security risks

None. This is purely internal parser state bookkeeping for legal-comment preservation during a speculative parse whose output is discarded. No user-facing API, no untrusted-input validation path, no allocation sizing, no unsafe code.

Level of scrutiny

Low-to-moderate. The change is small and mechanical, with a single caller. I traced the ordering carefully: mem::take executes before self.lexer.snapshot(), so LexerSnapshot.comments_to_preserve_before_len is captured as 0; on restore, Lexer::restore truncates whatever the attempt accumulated back to 0 (its debug_assert!(len >= 0) is trivially satisfied), and then restore_parser_snapshot overwrites with the saved Vec. The sole call site at parse_skip_typescript.rs:1598-1611 unconditionally calls restore_parser_snapshot(snapshot) before any early return, so the taken buffer can never be dropped without being reinstated. restore_parser_snapshot takes the snapshot by value, so double-restore is impossible. The &self → &mut self signature change is safe given the one caller is already in a &mut self method.

Other factors

Tests are placed in the existing describe block for TS conditional-arrow parsing, immediately after the related snapshot-restore coverage from the earlier PR, using the same exp() helper — exactly where CLAUDE.md says they belong. The three cases cover both attempt outcomes (arrow accepted, arrow rejected → plain conditional) plus the import() drain path. The added doc-comment lines and test comment are concise and explain the non-obvious "why a length cannot restore this." No CODEOWNERS entry covers these paths. Exit reason was dry_streak.

@Jarred-Sumner
Jarred-Sumner merged commit 5fadf8a into main Aug 30, 2026
11 of 12 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the robobun/e588f514/ts-conditional-arrow-legal-comments branch August 30, 2026 22:35
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