Skip to content

lexer: report JSX pragma warnings at the pragma argument - #39636

Open
robobun wants to merge 1 commit into
mainfrom
farm/e3b751f5/jsx-pragma-warning-location
Open

robobun wants to merge 1 commit into
mainfrom
farm/e3b751f5/jsx-pragma-warning-location

Conversation

@robobun

@robobun robobun commented Aug 19, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Unsupported JSX runtime: "bogus" for // @jsxRuntime bogus on line 3 is reported at rt.jsx:4:20. The argument is at rt.jsx:3:16. Block comments are off too.
  • Lexer::scan_pragma (src/js_parser/lexer.rs:2114 on main) adds self.start, the comment start, to an offset that is already absolute. The // caller also passes the offset of the @ itself. The /* */ caller adds the index of the @ twice.

Fix

  • scan_pragma, PragmaArg::scan and scan_source_mapping_url_value now take the source offset of the first byte after the @ or # and use it as is.
  • The // caller passes self.current and slices the chunk from its local contents, as the rest of that function does. This removes the StoreStr copy and the now unused Lexer::remaining. The /* */ caller tracks the chunk offset inside the comment body.
  • Correct because PragmaArg::scan locates the argument relative to that byte. esbuild 0.25.1 reports the same positions. Only diagnostics change: the parser applies pragmas from the span text and uses the range only in warnings.
  • Verified: new test in test/bundler/bun-build-api.test.ts. Bun 1.4.0 reports offsets 66, 74 and 84 instead of 41, 42 and 51. The other suites I ran are in the notes.

Background

  • A JSX pragma is a comment such as // @jsxImportSource preact. The lexer stores the argument as a Span: text plus source Range. prepare_for_visit_pass (src/js_parser/p.rs) applies the text and warns at the range.
  • scan_single_line_comment calls scan_pragma at each @ or # in a // comment. scan_comment_text does the same for a finished /* */ comment.
  • Lexer offsets are absolute. self.start is the start of the current token, here the comment. self.current is the offset after the last consumed character.
  • jsx: keep the default factory when a tsconfig or pragma factory has no members #39631 adds warnings at the @jsx and @jsxFrag spans. They get the right location through this change.
Notes

Repro on bun 1.4.0:

printf 'const x = 1;\nconst y = 2;\n// @jsxRuntime bogus\nconsole.log(<div />);\n' > rt.jsx
bun build --external '*' ./rt.jsx

Before: at rt.jsx:4:20, with line 4 printed above the caret. After: at rt.jsx:3:16, caret under bogus. The block comment form /** @jsxRuntime bogus */ moves from 4:22 (clamped to the end of the file) to 3:17. A pragma on the second line of a block comment moves from 6:22 to 4:16. esbuild 0.25.1 prints 3:15, 3:16 and 4:15 for the same three files (esbuild columns are 0-based).

Arithmetic for the // case, comment at offset 26, @ at 29: main passed 29, scan_pragma added self.start (26), and PragmaArg::scan added the pragma name (10) and the argument index in the text after it (1): 66. The argument is at 30 + 10 + 1 = 41. Bun.build exposes the range start as position.offset. The new test checks that next to line, column and lineText, for a // comment, a one line /** */ comment, and a block comment where the pragma follows another @ tag on a later line.

source_mapping_url gets the same treatment. The lexer writes it and nothing reads it, so there is nothing to test there.

Possible follow-up, kept out of this PR: after this change the text of each pragma Span is exactly source at range, so JSXPragma could hold plain Ranges and prepare_for_visit_pass could read the text with Source::text_for_range, the way it already reads all_comments. That would also remove js_ast::Span and the unread source_mapping_url. It touches the same lines of p.rs as #39631, so it is better done once that PR and this one have landed. The offset fix here is needed under either shape.

The same formulas were in js_lexer.zig before the port, so this is not a port regression.

The pragma loop in scan_comment_text now keeps pos, the offset of the chunk inside the comment, so the source offset is self.start + pos. The old loop had a match on the byte that index_of_any(rest, b"@#") found, which can only be one of those two bytes, and a .min() on i + 1, which is always in bounds. Both are gone. The clamp on the advance after scan_pragma stays. Its old comment described a case (pragma at the end of the file) in which scan_pragma returns 0 or the exact length, so the comment is gone as well.

Not changed here: in a block comment the argument runs to the next space, so /**\n * @jsxRuntime classic\n */ still yields classic\n and warns. esbuild 0.25.1 does the same. That warning now at least points at the right line.

Rebased on main after #40610, which removed the IS_JSON branch around the // pragma call. The conflict was in that arm and in the /* */ loop. Both now carry main's shape plus the offset fix. The test file had no conflict.

Suites run with the debug build: test/bundler/bundler_jsx.test.ts, bundler_comments.test.ts, transpiler/transpiler.test.js, esbuild/dce.test.ts, esbuild/tsconfig.test.ts and all of bun-build-api.test.ts. cargo fmt and cargo clippy -p bun_js_parser are clean.


no test proof · iteration 3 · 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

@coderabbitai

coderabbitai Bot commented Aug 19, 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: 8f9f7c86-e2f7-4214-8f50-15f2cacd0bc8

📥 Commits

Reviewing files that changed from the base of the PR and between 660ed00 and 462eba2.

📒 Files selected for processing (1)
  • src/js_parser/lexer.rs

Included review availability: Your plan provides up to 5 included reviews per hour; 1 remains after this review.


Walkthrough

The lexer now scans comment pragmas from direct source slices and tracks absolute offsets. JSX and source-map pragma locations use the new offset model. A concurrent regression test verifies exact @jsxRuntime warning positions.

Changes

Pragma location tracking

Layer / File(s) Summary
Direct comment pragma scanning
src/js_parser/lexer.rs
Comment pragma scanners pass direct source slices and absolute chunk offsets to JSX and source-map pragma handlers.
Absolute location calculation and regression coverage
src/js_parser/lexer.rs, test/bundler/bun-build-api.test.ts
Source-map and pragma argument ranges use absolute offsets. The test verifies warning positions across line, block, and documentation comments.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
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 and concisely describes the primary change: correcting JSX pragma warning locations.
Description check ✅ Passed The description explains the problem, implementation, behavioral impact, and verification steps. It does not use the exact template headings, but it provides the required information in equivalent sec…
Full details: Description check

Explanation

The description explains the problem, implementation, behavioral impact, and verification steps. It does not use the exact template headings, but it provides the required information in equivalent sections.


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

@robobun

robobun commented Aug 19, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. Rebased on main after #40610 (one squashed commit, f48904a736), conflict resolved in lexer.rs only.

Reproduced on bun 1.4.0 with a // @jsxRuntime bogus comment on line 3 of a file. bun build printed the warning at rt.jsx:4:20. With this branch it prints at rt.jsx:3:16, under the argument. The block comment forms move the same way. The repro commands and the before and after output are in the PR description.

The new test in test/bundler/bun-build-api.test.ts fails on bun 1.4.0 and passes with this branch, before and after the rebase. It passed on every CI lane in every build of this PR.

CI on the rebased head (build 107061): 179 of 181 jobs green. The two red jobs are outside this change and CI marks both as pre-existing on main: a LeakSanitizer report in test/bundler/transpiler/macro-test.test.ts on the x64 ASAN lane (a node_fs_binding::Binding allocation), and TypeError: Invalid URL in the Unicode 16 IDNA case of test/js/web/url/url.test.ts on macOS x64. Both are reported separately. This PR only changes comment pragma offsets in the JS lexer.

@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 — corrects the doubled-offset arithmetic so JSX pragma warnings point at the argument, matching esbuild.

What was reviewed:

  • Traced chunk_start for both callers: self.current after step() in the // path, and self.start + (end_comment_text - rest.len()) in the /* */ path — both land on the byte after @/#.
  • Verified the removed match arm and .min() on i + 1 were dead: index_of_any only returns in-bounds indices of @/#.
  • Hand-checked the test's expected offsets (41, 42, 51) and columns against the fixture strings; all three comment shapes are covered with exact assertions.
Extended reasoning...

Overview

This PR fixes incorrect source positions in JSX pragma warnings (Unsupported JSX runtime: "..."). The lexer was double-counting offsets when computing the location of a pragma argument in a comment: scan_pragma added self.start (the comment start) to a value that was already absolute in the // path, and the /* */ path additionally counted the @ index twice via i + (text.len() - rest.len()). The fix unifies both callers to pass a single chunk_start — the absolute source offset of the byte immediately after @/# — and PragmaArg::scan / scan_source_mapping_url_value add only pragma.len() + arg_index to it. Two pieces of dead code in the block-comment loop (a match on a byte that can only be @ or #, and a .min(rest.len()) on i + 1 where i < rest.len() by construction) are removed.

Security risks

None. This affects only the Range stored on pragma spans, which is used exclusively for warning locations. Pragma text (what actually configures JSX) is unchanged. No user input reaches new code paths; slice indices are all provably in bounds.

Level of scrutiny

Low-medium. The change is localized to diagnostic position arithmetic in the lexer's comment-scanning path. I traced the offset arithmetic by hand for all three test fixtures (line comment at offset 26 → argument at 41; single-line block comment → 42; multi-line block comment with a preceding @param → 51) and they match the test's indexOf("bogus") assertions. The PR description cross-checks against esbuild 0.25.1 output.

Other factors

The new test is well-constructed: it uses tempDir, covers the //, single-line /** */, and multi-line /** */ with a prior @ tag, and asserts exact line/column/offset/length/lineText values keyed by filename (order-independent via Object.fromEntries). Each fixture places the pragma after other source text so the old double-counting would land on a later line — the test would fail on Bun 1.4.0 with offsets 66/74/84 per the PR description. source_mapping_url gets the same signature change but has no reader, so it's untested (noted in the PR). The removed .min() and match are safe deletions given index_of_any's contract.

Comment thread src/js_parser/lexer.rs Outdated
Comment thread src/js_parser/lexer.rs Outdated
Comment thread src/js_parser/lexer.rs Outdated
Comment thread src/js_parser/lexer.rs Outdated
Comment thread src/js_parser/lexer.rs Outdated
@robobun

robobun commented Aug 19, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:53 PM PT - Aug 27th, 2026

❌ @robobun, your commit f48904a has 2 failures in Build #107061 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 39636

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

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

LGTM — the offset arithmetic checks out and the test pins exact positions.

What was reviewed:

  • Traced the new scan_comment_text loop against the doc-comment fixture (@param then @jsxRuntime): self.start + pos lands on offset 51, matching indexOf("bogus").
  • Confirmed &contents[chunk_start..] in the // path uses the local contents: &'a [u8] copy, so the borrowck workaround (and its per-@ StoreStr allocation) is genuinely unnecessary.
  • remaining() has no other callers; the .min(chunk.len()) clamp on the block-comment advance is preserved.
Extended reasoning...

Overview

Fixes the source location reported for JSX pragma warnings (Unsupported JSX runtime: "…"). The old scan_pragma added self.start to an offset that both callers already passed as absolute-ish, and the block-comment caller additionally counted the @ index twice. The fix collapses the two-parameter offset scheme into one chunk_start (absolute source offset of the byte after @/#), threads it unchanged through PragmaArg::scan and scan_source_mapping_url_value, rewrites the scan_comment_text pragma loop to track that offset directly, and lets the // caller slice from its local contents instead of round-tripping through StoreStr. Lexer::remaining is removed as dead. A new bun-build-api.test.ts case asserts exact offset/line/column/lineText for line, one-line block, and multi-line doc comments.

Security risks

None. This only changes the Range stored on jsx_pragma spans, which is used solely for warning locations in prepare_for_visit_pass. The pragma text (what actually configures JSX behavior) is computed from the same chunk slice as before. No untrusted-length arithmetic feeds an index — the one slice bound that could exceed the chunk (scan_pragma's return) keeps its .min(chunk.len()) clamp.

Level of scrutiny

Medium. This is inside the JS lexer, which is hot and correctness-critical, but the change is confined to diagnostic position bookkeeping and a small loop rewrite whose iteration structure is equivalent to the old one (index-of-any → advance past marker → scan → advance by consumed length). I hand-traced the doc-comment case (two @ markers, second on a later line) through the new loop and got offset 51, matching the test's indexOf("bogus"). The // path's borrow of &contents[chunk_start..] is sound because contents is a local &'a [u8] copy (line 2358), the same pattern the rest of scan_single_line_comment already uses — so dropping the StoreStr detour is safe and removes a per-@ arena copy of the rest of the file.

Other factors

The comment-cop bot flagged five long comments; all threads are resolved by 660ed00 and daa222a and the final diff carries only one-line docs. remaining() has no remaining callers (grep clean). The test uses tempDir+using, test.concurrent, and asserts exact values keyed by filename, so it fails loudly on the old offsets (66/74/84 vs 41/42/51) and covers the multi-@ block-comment path that exercises the loop's pos accumulation. PR description cross-checks against esbuild 0.25.1 output. Related bundler/transpiler suites were run per the notes.

@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 — the offset arithmetic is now a single, traceable formula and only diagnostic ranges change.

What was reviewed:

  • Traced chunk_start through both callers: self.start + pos in scan_comment_text and self.current in scan_single_line_comment both land on the byte after @/#, and PragmaArg::scan adds pragma.len() + start to reach the argument — hand-checked against all three test fixtures.
  • Confirmed the local contents: &'a [u8] in scan_single_line_comment avoids the borrowck conflict that motivated the old StoreStr detour, and that remaining() had no other callers.
  • Checked the rewritten scan_comment_text loop preserves the advance-and-clamp behavior (pos += i + 1 is always in bounds; .min(chunk.len()) on the pragma advance stays).
Extended reasoning...

Overview

This PR fixes incorrect source locations in JSX pragma warnings. The bug was double-counting: scan_pragma added self.start (the comment's absolute start) to an offset that was already absolute in one caller and mis-derived in the other. The fix collapses the two-parameter (start, offset_for_errors) model into a single chunk_start — the absolute source offset of the byte immediately after @/# — and threads it through scan_pragma, PragmaArg::scan, and scan_source_mapping_url_value. The // caller now slices directly from its local contents: &'a [u8] instead of round-tripping through an arena-allocated StoreStr, which also lets the unused Lexer::remaining() helper go. The block-comment loop in scan_comment_text is rewritten to track a single pos cursor into the comment body, dropping a dead match arm and a redundant .min().

Security risks

None. This touches only the computation of the Range attached to a pragma Span; the span's text (which drives which JSX runtime is selected) is unchanged. All indexing is into slices already bounded by the comment body or the source contents, and the one place where scan_pragma's return could theoretically exceed the chunk length is still clamped with .min(chunk.len()).

Level of scrutiny

Medium. The lexer is hot-path code, but the change is arithmetic on diagnostic offsets — the pragma text and the number of bytes the callers advance by are unchanged. I hand-verified the offset formula against all three test fixtures (line comment → 41, block comment → 42, doc comment with a preceding @param → 51) and they match indexOf("bogus") in each source string. The PR description also cross-checks against esbuild 0.25.1's 0-based columns.

Other factors

The new test in bun-build-api.test.ts asserts exact {line, column, offset, length, lineText} for three comment shapes, including a multi-line doc comment where the pragma follows another @ tag — that last case exercises the loop's advance-past-non-pragma path. The test uses tempDir, test.concurrent, and toEqual on a keyed object, matching the file's conventions. The comment-cop bot flagged over-long comments on an earlier revision; the author shortened them in 660ed00 and daa222a, and all five threads are resolved. Grep confirms remaining() has no remaining callers. The source_mapping_url path gets the same fix but is currently write-only, so no test is possible there — the PR notes this.

scan_pragma added self.start to an offset that was already absolute,
so the spans stored for @jsx, @jsxFrag, @jsxRuntime, @jsxImportSource
and sourceMappingURL pointed past the comment. The parser reports
"Unsupported JSX runtime" at that span, so the warning named the wrong
line and column.

scan_pragma and PragmaArg now take the source offset of the chunk that
follows the @ or # and use it as is. The single line comment scanner
passes self.current and slices the chunk from its local contents, which
makes Lexer::remaining unused, so it is removed. The block comment
scanner tracks the chunk offset inside the comment body directly.
@robobun
robobun force-pushed the farm/e3b751f5/jsx-pragma-warning-location branch from 462eba2 to f48904a Compare August 27, 2026 22:34

@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 — the offset arithmetic checks out and the change is diagnostic-only.

Verified chunk_start is the correct absolute source offset at both call sites: self.start + pos in scan_comment_text (since text = contents[self.start..self.end]) and self.current in scan_single_line_comment (just past the consumed @/#). PragmaArg::scan and scan_source_mapping_url_value now add only the pragma-name length and whitespace skip, which lands exactly on the argument. The removed remaining() helper had no other callers. The rewritten block-comment loop is equivalent to the old reslicing form and keeps the .min(chunk.len()) clamp. The new test pins line/column/offset/length/lineText for all three comment shapes and would fail under the old double-counted offsets.

Extended reasoning...

Overview

This PR fixes incorrect source locations in JSX pragma warnings emitted by the JS lexer. The bug was that scan_pragma added self.start (the comment's own start offset) to an offset that callers had already made absolute, so warnings like Unsupported JSX runtime: "bogus" pointed at the wrong line/column. The fix renames the parameter to chunk_start (the absolute offset of the byte after @/#), threads it through unchanged, and simplifies the block-comment pragma loop from repeated reslicing to a single pos cursor. It also drops the now-unused Lexer::remaining() and a StoreStr detour in the single-line path, and collapses chained u32::try_from casts into one usize sum. A new test in bun-build-api.test.ts builds three .jsx files (line, block, and multi-line doc comments, each with preceding source so a double-counted offset would land on a later line) and asserts exact line/column/length/offset/lineText for the warning.

Security risks

None. This is a lexer diagnostic-position fix. The pragma text extraction and application are unchanged — only the Range attached to the warning changes. No new input is parsed, no allocation size depends on the changed arithmetic, and no user-controllable value flows anywhere new.

Level of scrutiny

Moderate, because it touches src/js_parser/lexer.rs. I traced the offset arithmetic at both call sites against the actual lexer state: in scan_comment_text, text is &contents[self.start..self.end], so body[pos] sits at absolute offset self.start + pos; in scan_single_line_comment, step_with() has just consumed the @/#, leaving self.current at the following byte. Both now pass exactly that absolute offset. PragmaArg::scan then adds pragma.len() + start (whitespace skip), which is the argument's position within chunk — correct. The .min(chunk.len()) clamp on the loop advance is preserved. The single-line path now slices contents[chunk_start..] directly instead of via StoreStr::new(self.remaining()), which is behaviorally identical since remaining() was &self.contents[self.current..]. Confirmed remaining() has no other callers.

Other factors

The test is well-constructed: it uses tempDir, test.concurrent, a single .toEqual on a structured object, and indexOf("bogus") to derive expected offsets from the fixture text rather than hardcoding them. The fixtures place the pragma after earlier source lines specifically so the old double-counting would produce a visibly wrong line, satisfying the "fails for the right reason" requirement. No CODEOWNERS cover the changed files. There are no outstanding human CHANGES_REQUESTED reviews; the resolved inline threads are all from bots. The exit reason was dry_streak. A new commit landed after the earlier bot reviews, so this approval reflects the current state.

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