From 3931b981322ff9969486d9a8409e3943567d2d1f Mon Sep 17 00:00:00 2001 From: Dunqing <29533304+Dunqing@users.noreply.github.com> Date: Tue, 24 Mar 2026 09:22:51 +0000 Subject: [PATCH] fix(transformer): ignore `@jsxImportSource` inside inline code spans in comments (#20674) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Fix JSX pragma scanner incorrectly matching `@jsxImportSource` inside backtick code spans (e.g. `` `@jsxImportSource custom/source` ``) in doc comments - Add validation that `@` must be preceded by whitespace, `*`, or be at the start of the comment ## Problem The `memchr`-based `@` search found pragma directives anywhere in a comment, including inside inline code spans. This caused: 1. A `@jsxImportSource` inside a doc comment overriding the actual file-level pragma 2. The trailing backtick being included in the import specifier (`` custom/source` ``) This broke Vite 8 dev server when `.tsx` files had JSDoc comments mentioning `@jsxImportSource`. ## Design | Tool | `@` position check | |------|-------------------| | esbuild | None — matches `@jsx` anywhere in comment | | Babel | Full start-of-line regex (`^\s*(?:\*\s*)?@jsx`) | | **Oxc (this PR)** | Preceding byte must be whitespace, `*`, or start of comment | We intentionally sit between esbuild and Babel — checking only the immediately preceding byte is sufficient for the backtick case without being as strict as Babel's full start-of-line regex. Fixes #20669 ## Test plan - [x] Unit tests added for backtick-wrapped pragmas - [x] Conformance test fixture added (`issue-20669`) - [x] All existing transformer tests pass - [x] Clippy clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- crates/oxc_transformer/src/jsx/comments.rs | 26 +++++++++++++++++++ .../snapshots/oxc.snap.md | 4 +-- .../fixtures/issues/issue-20669/input.jsx | 8 ++++++ .../fixtures/issues/issue-20669/options.json | 4 +++ .../fixtures/issues/issue-20669/output.js | 7 +++++ 5 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/issues/issue-20669/input.jsx create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/issues/issue-20669/options.json create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/issues/issue-20669/output.js diff --git a/crates/oxc_transformer/src/jsx/comments.rs b/crates/oxc_transformer/src/jsx/comments.rs index 0959005f83d20..ad2f0f94fc399 100644 --- a/crates/oxc_transformer/src/jsx/comments.rs +++ b/crates/oxc_transformer/src/jsx/comments.rs @@ -103,6 +103,23 @@ fn find_jsx_pragma(mut comment_str: &str) -> Option<(PragmaType, &str, &str)> { // to find `@` characters, and then checking if `@` is followed by `jsx` separately. let at_sign_index = memchr(b'@', comment_str.as_bytes())?; + // `@` must be preceded by whitespace or `*` (or be at start of comment) to count + // as a pragma. This avoids matching inside inline code spans like `` `@jsxImportSource foo` ``. + // Note: esbuild does no preceding-character check at all (matches `@jsx` anywhere). + // We are intentionally stricter here — only checking the immediately preceding byte, + // which is sufficient for the backtick case without being as strict as Babel's + // full start-of-line regex. + // + if at_sign_index > 0 { + let prev_byte = comment_str.as_bytes()[at_sign_index - 1]; + if !matches!(prev_byte, b' ' | b'\t' | b'\r' | b'\n' | b'*') { + // SAFETY: Byte at `at_sign_index` is `@`, so `at_sign_index + 1` is either within + // string or end of string, and on a UTF-8 char boundary. + comment_str = unsafe { comment_str.get_unchecked(at_sign_index + 1..) }; + continue; + } + } + // Check `@` is start of `@jsx`. // Note: Checking 4 bytes including leading `@` is faster than checking the 3 bytes after `@`, // because 4 bytes is a `u32`. @@ -240,6 +257,15 @@ mod tests { ("@jsx @jsx h", &[(PragmaType::Jsx, "@jsx")]), // Multiple `@` signs ("@@@@@jsx h", &[(PragmaType::Jsx, "h")]), + // Pragma inside backticks (inline code span) should not be recognized + ("`@jsxImportSource custom/source`", &[]), + ("`@jsx h`", &[]), + ("This mentions `@jsxImportSource custom/source` in docs", &[]), + // But valid pragma before backtick-wrapped text should still work + ( + "@jsxImportSource react\n * This mentions `@jsxImportSource custom/source` in docs", + &[(PragmaType::JsxImportSource, "react")], + ), ]; let prefixes = ["", " ", "\n\n", "*\n* "]; diff --git a/tasks/transform_conformance/snapshots/oxc.snap.md b/tasks/transform_conformance/snapshots/oxc.snap.md index f90c37200f6f5..81caf736bec83 100644 --- a/tasks/transform_conformance/snapshots/oxc.snap.md +++ b/tasks/transform_conformance/snapshots/oxc.snap.md @@ -1,6 +1,6 @@ commit: 0124e7c7 -Passed: 208/344 +Passed: 209/345 # All Passed: * babel-plugin-transform-class-static-block @@ -540,7 +540,7 @@ after transform: [ReferenceId(0), ReferenceId(1), ReferenceId(4), ReferenceId(9) rebuilt : [ReferenceId(5)] -# babel-plugin-transform-react-jsx (47/50) +# babel-plugin-transform-react-jsx (48/51) * refresh/import-after-component/input.js Missing ScopeId Missing ReferenceId: "useFoo" diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/issues/issue-20669/input.jsx b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/issues/issue-20669/input.jsx new file mode 100644 index 0000000000000..5a478d0bcde99 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/issues/issue-20669/input.jsx @@ -0,0 +1,8 @@ +/** @jsxImportSource react */ + +/** + * This comment mentions `@jsxImportSource custom/source` in docs. + */ +function App() { + return
hello
; +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/issues/issue-20669/options.json b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/issues/issue-20669/options.json new file mode 100644 index 0000000000000..1c3260292f20a --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/issues/issue-20669/options.json @@ -0,0 +1,4 @@ +{ + "plugins": [["transform-react-jsx", { "runtime": "automatic" }]], + "sourceType": "module" +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/issues/issue-20669/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/issues/issue-20669/output.js new file mode 100644 index 0000000000000..1fcd5ac416df3 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-react-jsx/test/fixtures/issues/issue-20669/output.js @@ -0,0 +1,7 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +/** + * This comment mentions `@jsxImportSource custom/source` in docs. + */ +function App() { + return _jsx("div", { children: "hello" }); +}