From fc08d760317d909dbda478e8f9b577dced1f5954 Mon Sep 17 00:00:00 2001 From: Dmitrii Date: Tue, 7 Jul 2026 06:24:54 +0100 Subject: [PATCH] [compiler] Fix JSX tags prefixed with `_` or `$` incorrectly treated as host elements (#36688) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #36601 ### Problem In `lowerJsxElementName` (BuildHIR.ts), the condition `if (tag.match(/^[A-Z]/))` only treats JSX tags starting with an **uppercase letter** as component references. Tags starting with `_`, `$`, or any other non-letter character fell through to the `else` branch and were incorrectly classified as `BuiltinTag` (host/intrinsic elements). This meant that `<_Bar />` or `<$Foo />` was treated like `
`, causing the compiler to skip memoization of the component and potentially producing incorrect output. ### Root Cause JSX semantics (as implemented by Babel's JSX transform) are: - Tag starts with **lowercase** letter → host/intrinsic element (string tag) - **Everything else** → component reference (in-scope identifier) The original code only handled the first half of that rule ("starts with uppercase → component") while ignoring identifiers like `_Bar` and `$Foo`. ### Fix Change: ```ts if (tag.match(/^[A-Z]/)) { ``` To: ```ts if (!tag.match(/^[a-z]/)) { ``` This correctly classifies any JSX identifier that does NOT start with a lowercase letter as a component reference, matching JSX spec semantics. ### Test Added fixture `jsx-underscore-prefix-component` that renders `<_Bar />` and verifies the compiler correctly memoizes it as a component reference. ## How did you test this change? - Added new compiler fixture test: `jsx-underscore-prefix-component` - Ran `yarn workspace babel-plugin-react-compiler lint` ✅ - Ran snap tests with the new fixture to generate expected output ✅ --- .../src/HIR/BuildHIR.ts | 2 +- .../jsx-underscore-prefix-component.expect.md | 56 +++++++++++++++++++ .../jsx-underscore-prefix-component.js | 16 ++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefix-component.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefix-component.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts index 452aa0ce329d..07e6862039ab 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -3409,7 +3409,7 @@ function lowerJsxElementName( const exprLoc = exprNode.loc ?? GeneratedSource; if (exprPath.isJSXIdentifier()) { const tag: string = exprPath.node.name; - if (tag.match(/^[A-Z]/)) { + if (!tag.match(/^[a-z]/)) { const kind = getLoadKind(builder, exprPath); return lowerValueToTemporary(builder, { kind: kind, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefix-component.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefix-component.expect.md new file mode 100644 index 000000000000..4ae800b6ac49 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefix-component.expect.md @@ -0,0 +1,56 @@ + +## Input + +```javascript +// Test that JSX tags starting with `_` or `$` are treated as component +// references (not host/builtin elements). JSX semantics: any tag NOT starting +// with a lowercase letter is a component reference. + +import {Stringify} from 'shared-runtime'; + +const _Bar = Stringify; + +function Component(props) { + return <_Bar value={props.value} />; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // Test that JSX tags starting with `_` or `$` are treated as component +// references (not host/builtin elements). JSX semantics: any tag NOT starting +// with a lowercase letter is a component reference. + +import { Stringify } from "shared-runtime"; + +const _Bar = Stringify; + +function Component(props) { + const $ = _c(2); + let t0; + if ($[0] !== props.value) { + t0 = <_Bar value={props.value} />; + $[0] = props.value; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 42 }], +}; + +``` + +### Eval output +(kind: ok)
{"value":42}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefix-component.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefix-component.js new file mode 100644 index 000000000000..234911480d39 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefix-component.js @@ -0,0 +1,16 @@ +// Test that JSX tags starting with `_` or `$` are treated as component +// references (not host/builtin elements). JSX semantics: any tag NOT starting +// with a lowercase letter is a component reference. + +import {Stringify} from 'shared-runtime'; + +const _Bar = Stringify; + +function Component(props) { + return <_Bar value={props.value} />; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +};