Skip to content

parser(ts): a plain JSX tag like <div onClick={h}> gets an element row (still no call site) - #1969

Open
swapnilpaliwal-sd wants to merge 2 commits into
0.2.1from
fix/ts-intrinsic-jsx-attributes
Open

swapnilpaliwal-sd wants to merge 2 commits into
0.2.1from
fix/ts-intrinsic-jsx-attributes

Conversation

@swapnilpaliwal-sd

@swapnilpaliwal-sd swapnilpaliwal-sd commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1970

Symptom

In a .tsx file, a plain tag such as <div onClick={h} ref={r}> produced no expression row. Its attribute values were there only as unattached embedded expressions, so the IR could not say that a handler or a ref callback sits on a plain tag. The JavaScript parser records the same tag as an element (JSX_INTRINSIC_ELEMENT).

Change

  • New TsExpressionKind.JSX_INTRINSIC_ELEMENT for a tag that names no declaration (<div>, <my-el>, <svg:circle>).
  • The row has no child and no call site. It is never added to the call nodes, so nothing can resolve it into JSX.IntrinsicElements.
  • The walker roots a nested plain tag the way it already roots a nested component tag. This covers plain tags inside another element's children and inside a function passed in an attribute.
  • Component tags are unchanged: they keep their JSX_ELEMENT / JSX_SELF_CLOSING rows and JSX_COMPONENT_CALL sites.
  • Added to the schema's kind domain.

Control (must not change)

The control is component tags (<Badge/>) and the call graph. Before/after on two public React+TS apps, with warm indexes:

small app (125 TS files) larger app (~2.3k TS files)
graph call_sites 1,155 → 1,155 101,316 → 101,316
call_edges by tier identical (known 384, ambiguous 752, callback 170, intrinsic 8) identical (known 30,572, ambiguous 69,691, callback 13,644, multi 987, intrinsic 194, ambient 2)
full sorted edge diff (tier, kind, caller, callee, site) empty* empty
IR JSX_COMPONENT_CALL sites 209 → 209 5,169 → 5,169
component element rows 376 → 376 5,356 → 5,356
plain-tag element rows 0 → 386 (of 386) 0 → 513 (of 513)
attributes with a value row on a plain tag (on*, ref) 0 → 173 (8, 12) 0 → 414 (44, 30)
index wall time 4.5 s → 4.2 s 80 s → 75 s

* For the small app the diff is by tier, kind and site.

On the larger app, the attributes on component tags that have a value row went from 7,395 to 7,409. All 14 extra values are a plain tag passed as a prop (fallback={<div />}), which now has a row.

Tag name on the plain element row

A JSX_INTRINSIC_ELEMENT row said a plain tag was there but not which one, so <a>, <form> and <iframe> looked the same in the IR. The row now carries the tag as written (div, my-el, svg:circle) in potentialQualifiedName, set through TsExpressionRegistry.setJsxTagName from a new jsxTagText helper.

  • Component rows are unchanged: the tag is still the METHOD_NAME child that resolves, and potentialQualifiedName stays empty.
  • No engine projection reads this column for expression rows. All 30 ts_expression / lib_ts_expression atoms under graph/ bind column 17 to _. The plugin's reference index reads the column only for IDENTIFIER_REFERENCE / PROPERTY_ACCESS rows.

Control, before and after this commit on a small and a large public React+TS app:

small app large app
graph call_sites 1,155 → 1,155 101,316 → 101,316
call_edges by tier identical (1,314 total) identical (115,090 total)
full sorted edge list (tier, kind, callee, caller, site) byte-identical byte-identical
symbols / refs / literals identical identical
expression rows 9,586 → 9,586, same ids 728,214 → 728,214, same ids
cells that changed only potentialQualifiedName on plain-tag rows only potentialQualifiedName on plain-tag rows
other IR files byte-identical byte-identical
plain-tag rows with a tag 386 / 386 (100%) 513 / 513 (100%)
component rows with a potentialQualifiedName 0 / 376 0 / 5,356

Tests

  • parser: npx tsx src/test/typescript-tests.ts passed 49/49. "JSX brace expressions are walked" now also checks the following. Run against the old source, the extended check fails on all 6 plain tags.
    • Every plain tag has exactly one childless JSX_INTRINSIC_ELEMENT row and no call site.
    • Every component keeps its JSX_ELEMENT / JSX_SELF_CLOSING row.
    • Each attribute value on a plain tag (onClick={save}, ref={setRef}, x && <input onChange={save}/>, <my-el data-x={...}/>) has a row.
  • graph/test/typescript/run-tests.sh passed 101/101, with goldens unchanged.
  • Tag check: "JSX brace expressions are walked" now adds <svg:circle r="1" />. Using the AST as the expectation, it asserts that every plain element's row carries its tag as written and that every component row keeps an empty potentialQualifiedName. With the extractor line removed, the check fails on all 7 plain elements (0 with their tag). With the line in place, the parser suite passes 49/49 and graph/test/typescript/run-tests.sh passes 101/101.
  • oss-scrub-gate was not run: it needs $JS_SCRUB_DENYLIST.

swapnilpaliwal-sd and others added 2 commits October 15, 2026 11:29
`<div onClick={h} ref={r}>` produced no expression row in the TypeScript
parser, so nothing in the IR said a handler or a ref callback sat on a
plain tag: an attribute's value existed only as an unattached embedded
expression. The JavaScript parser records the same tag as an element.

An intrinsic tag (`<div>`, `<my-el>`, `<svg:circle>`) is now a
JSX_INTRINSIC_ELEMENT row with no child and no call site, rooted wherever
it sits (a return, a ternary branch, another element's children, a
function inside an attribute). It is never added to the call nodes, so
nothing can resolve it into JSX.IntrinsicElements. Component tags keep
their JSX_ELEMENT / JSX_SELF_CLOSING rows and JSX_COMPONENT_CALL sites.
A JSX_INTRINSIC_ELEMENT row said a plain tag was there but not which
one: `<a>`, `<form>` and `<iframe>` were indistinguishable, so nothing
reading the IR could ask about a particular element without going back
to the source.

The row now carries the tag as written (`div`, `my-el`, `svg:circle`)
in potentialQualifiedName. A component element is unchanged: its tag is
still the METHOD_NAME child that resolves, and its own
potentialQualifiedName stays empty. No engine projection reads this
column for expression rows, so call sites and call edges cannot move.
@swapnilpaliwal-sd
swapnilpaliwal-sd force-pushed the fix/ts-intrinsic-jsx-attributes branch from 956d168 to 47dacac Compare October 15, 2026 18:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant