Skip to content

fix(es/react-compiler): Recover spans from loc for compiler-generated nodes - #11979

Open
残月 (Chastrlove) wants to merge 3 commits into
swc-project:mainfrom
Chastrlove:fix-react-compiler-spans
Open

fix(es/react-compiler): Recover spans from loc for compiler-generated nodes#11979
残月 (Chastrlove) wants to merge 3 commits into
swc-project:mainfrom
Chastrlove:fix-react-compiler-spans

Conversation

@Chastrlove

Copy link
Copy Markdown

Description:

Function bodies compiled by the React Compiler currently lose all source mappings, so devtools breakpoints cannot bind anywhere inside a compiled component (clicking a line number in Chrome DevTools does nothing). This reproduces with plain @swc/core 1.15.43 (jsc.transform.reactCompiler: { target: "18" }) as well as through rspack 2.1's builtin:swc-loader.

Root cause: codegen_reactive_function in the React Compiler crates emits regenerated AST nodes whose BaseNode carries only a Babel-style loc (with 0-based index offsets); start/end are left unset (base_node_with_loc). The reverse bridge's span_from_base only reads start/end, so every node in a compiled function body came back as DUMMY_SP and the codegen had nothing to map.

This also broke the span-keyed preserved_ast lookup: preserved TS metadata such as JSX type arguments was silently dropped from compiled output because the span.lo keys were all dummy. The existing ast-roundtrip/jsx.tsx fixture snapshot had captured that data loss (<UI.Panel<T> became <UI.Panel); this PR updates the snapshot to the now-correct output that round-trips the type argument.

Fix: when start/end are absent, recover the span from loc.start.index / loc.end.index, converting the 0-based Babel indices back to SWC's 1-based BytePos (the forward conversion in convert_ast.rs sets index = BytePos - 1, so the coordinate spaces line up).

Measured on a 31-line TSX component with hooks (emitting with build_source_map):

mappings original lines mapped
before this fix 12 2/31 (import + fn decl only)
after this fix 85 13/31 (statement lines in the compiled body)

The remaining unmapped lines are nested-closure bodies and JSX tails whose nodes are emitted without loc upstream (BaseNode::typed) — improving that is follow-up work in the React Compiler codegen, but this change restores mappings for the bulk of compiled function bodies and makes breakpoints usable again.

BREAKING CHANGE:

None.

Related issue (if exists):

None filed; discovered while debugging missing breakpoints with rspack 2.1 + reactCompiler.

@Chastrlove
残月 (Chastrlove) requested a review from a team as a code owner July 2, 2026 06:25
@changeset-bot

changeset-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d6e90d0

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@CLAassistant

CLAassistant commented Jul 2, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bd6383f4cb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "Codex (@codex) address that feedback".

return DUMMY_SP;
};
match (loc.start.index, loc.end.index) {
(Some(start), Some(end)) => Span::new(BytePos(start + 1), BytePos(end + 1)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep loc-only spans out of rename matching

Recovering loc for every loc-only node also gives compiler-generated identifiers real span.lo values. After conversion, transform still calls apply_renames on the entire program, and that pass is keyed only by span.lo even though compiled functions already contain the compiler's renamed identifiers. When a component has a BindingRenameInfo and a generated temp/cache identifier inherits the loc of an original reference, this new span makes the rename pass rewrite the synthetic name to the user binding name, corrupting the compiled function. Please keep these recovered source-map spans from participating in the rename pass, or skip compiled regions, before enabling them globally.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch — addressed in d6e90d0. The reverse conversion now records the span.lo of every identifier whose span was recovered from loc (recovered_ident_spans), and transform removes those positions from the rename plan before apply_renames runs. Recovered spans therefore only feed source mapping; a generated identifier that inherits the loc of an original reference can no longer be rewritten to a user binding name. Identifiers in passthrough-preserved nodes keep start/end and still participate in renaming as before.

return DUMMY_SP;
};
match (loc.start.index, loc.end.index) {
(Some(start), Some(end)) => Span::new(BytePos(start + 1), BytePos(end + 1)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Rebase loc-only spans to the current source file

This conversion treats Babel loc.index as if every transformed file starts at BytePos(1). In the Compiler::process_js_file/shared SourceMap path, later SourceFiles have a non-1 start_pos, but the React Compiler bridge only passes &fm.src into convert_program, so recovered loc-only nodes in those files get byte positions in an earlier file or outside the current file range. With source maps enabled, compiled React output for any file after the first can map to the wrong source instead of the file being transformed; the recovered span needs to be rebased with the current SourceFile start position (or kept dummy when that context is unavailable).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in d6e90d0. Instead of assuming BytePos(1), the rebase offset is now derived from the root program node, which carries both the absolute start (set by the forward conversion from span.lo) and the file-relative 0-based loc.start.index — their difference is exactly the current SourceFile base. For the common per-file transform this evaluates to 1, and for a shared SourceMap it anchors recovered spans into the correct file range. Note the forward conversion's position() clamps offsets to the source text length, so file-relative loc line/columns for start_pos != 1 files have a pre-existing inaccuracy that affects diagnostics as well; that seems worth fixing separately from this PR.

@codspeed-hq

codspeed-hq Bot commented Jul 2, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 189 untouched benchmarks
⏩ 61 skipped benchmarks1


Comparing Chastrlove:fix-react-compiler-spans (d6e90d0) with main (26d8a28)2

Open in CodSpeed

Footnotes

  1. 61 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (9c8e6f3) during the generation of this report, so 26d8a28 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Chastrlove
残月 (Chastrlove) requested a review from a team as a code owner July 2, 2026 07:32
… nodes

Nodes regenerated by the React Compiler codegen carry only a Babel-style
loc (with 0-based index offsets) on their BaseNode; start/end are left
unset. span_from_base therefore returned DUMMY_SP for every node in a
compiled function body, which:

- erased all source mappings for compiled components (breakpoints in
  devtools no longer bind anywhere inside a compiled function), and
- broke the span-keyed preserved-AST lookup, silently dropping preserved
  TS metadata such as JSX type arguments (see the updated fixture).

Fall back to loc.start.index / loc.end.index (converted back to 1-based
BytePos) when start/end are absent.
…e loc offsets

Recovered spans exist only for source mapping. Record the positions of
identifiers whose spans were recovered from loc and exclude them from the
span.lo-keyed rename plan, so a generated identifier that inherits the loc
of an original reference can never be rewritten to a user binding name.

Derive the loc rebase offset from the root node (absolute start minus
file-relative loc index) instead of assuming BytePos(1), so recovered
spans stay inside the current SourceFile when it does not start at 1.
@Chastrlove
残月 (Chastrlove) force-pushed the fix-react-compiler-spans branch from 42b6d06 to d6e90d0 Compare July 2, 2026 07:44

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d6e90d073b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "Codex (@codex) address that feedback".

Comment on lines +164 to +168
if let (Some(start), Some(index)) = (
program.base.start,
program.base.loc.as_ref().and_then(|loc| loc.start.index),
) {
self.loc_base.set(start.saturating_sub(index).max(1));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Rebase loc spans from the SourceFile start

Fresh evidence in this revision: this new base is derived from program.base.loc, but the forward converter’s position() still clamps absolute BytePos values against the current fm.src.len(), so a shared SourceMap file after the first gets a root loc.index of the file length instead of 0. In that common process_js_file path, loc-only compiler-generated nodes are shifted by start - len (or collapse to the file start) rather than the current SourceFile start, so recovered spans/source maps and span-keyed preserved TS metadata can point outside the file or to the wrong node; use the actual source-file start position instead of trusting the root loc index.

Useful? React with 👍 / 👎.

@magic-akari

Copy link
Copy Markdown
Member

I’m a bit concerned that this may not be the right layer to fix this.

As I understand it, loc is expected to carry UTF-16 code-unit offsets. In the original design, we intentionally did not rely on loc, so passing byte-based values there was harmless in practice and allowed us to avoid the complexity of UTF-8/UTF-16 conversions.

However, once we start reading positions from loc, the situation changes. It would bring that previously deferred design question back onto the table and make us commit to how these positions should be interpreted.

My preference would be to avoid adding UTF-8/UTF-16 complexity here if possible. Ideally, this should be fixed on the React Compiler side, so that generated nodes always carry valid start/end values. That would match the original design and should also benefit other tools such as SWC, OXC, and Biome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants