Skip to content

typescript: a JSX tag through a wrapper, a loader or a prop lands on what renders - #1577

Merged
swapnilpaliwal-sd merged 4 commits into
0.1.3from
loop/ty-ts-jsx-tags
Sep 28, 2026
Merged

swapnilpaliwal-sd merged 4 commits into
0.1.3from
loop/ty-ts-jsx-tags

Conversation

@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor

Follow-up to #1541. The JSX component rules produced three kinds of wrong edge:

  • Any call's argument 0 was the component. const Thing = makeThing(() => build()) made <Thing/> render build; useMemo(() => Comp, []) rendered the arrow. Only component wrappers now count: memo, forwardRef, observer, and a withX HOC (by callee name, so React.memo matches).
  • A loader rendered the module's default export even when it projected a named one. lazy(() => import("./m").then((m) => ({ default: m.Named }))) landed on the default export. The loader's returned value is now read at its root: the module itself renders its default; a .then callback, await import(...) or { default: m.Named } renders the named component. dynamic and loadable are loaders too.
  • A tag bound to a prop landed on the same-named import. ({ icon: Button }) => <Button/> (and the same with a plain call) resolved to the module's imported Button. A callee that the binder bound to a parameter or a non-module local now stays with what the local resolves to, or unresolved (typescript: a call through a destructured parameter resolves to a same-named import #1562).

Tests

  • New case 75-jsx-wrapper-guards pins each shape with controls: real wrappers still reach their argument, a loader returning the module still renders its default, an unshadowed <Button/> still reaches the import.
  • graph/test/typescript/run-tests.sh --oracle: passed 75, failed 0 (7m41s). Case 75 vs the compiler: agree 15, missing 6 (all listed as known), NEW 0.

Before / after (TypeScript call edges)

target before after
case 75 fixture 47 (JSX: 14 known, 2 multi) 46 (JSX: 11 known, 4 unresolved)
small React app 22 22 (identical set)
real React app (3 Next.js / Vite apps) 4930 4930 (identical set, 766 JSX known)

Index time on the real app: 7s before, 6s after.

#1563 added a Java case that both ground-truth oracles agree on, but left graph/test/java/expected/oracle-agreement.txt at 45 cases, so the engine (java) job fails its oracle-agreement check on 0.1.3. CI measured 46 cases compared, 46 agreeing, 0 disagreements; this records that. Refs #1401.
…sing

The bytecode oracle in CI finds three edges case 61-var-local-type does not produce: a var element of an enhanced for loop, a try-with-resources var, and var lambda parameters. Before #1401 each was a wrong external:var edge; now the receiver is an honest unknown. Record them as accepted gaps so a new miss still fails and a fixed one reports NOW-FIXED. Refs #1401.
@swapnilpaliwal-sd
swapnilpaliwal-sd force-pushed the loop/ty-ts-jsx-tags branch 2 times, most recently from d7140f5 to 7a03c15 Compare September 28, 2026 03:11
swapnilpaliwal-sd and others added 2 commits September 27, 2026 20:12
CI's second run on this branch failed 61-var-local-type with 'no oracle golden'. The golden is oracle_diff.py's report, and CI measured every value in it: oracle=20 engine=17 agree=17, the three known-missing var shapes, no extra edge. Refs #1401.
…what renders

Three wrong edges from the JSX component rules:

- Any call's argument 0 was taken as the component, so `makeThing(() => build())`
  rendered `build` and `useMemo(() => Comp, [])` rendered the arrow. Only the
  component wrappers now count: `memo`, `forwardRef`, `observer` and a `withX`
  higher-order component, read off the callee name so `React.memo` matches.
- A `lazy` / `dynamic` / `loadable` loader was matched on a namespace import
  anywhere under its return, so `import("./m").then((m) => m.Named)` rendered the
  module's default export. The returned value is read at its root; the module
  renders its default, and a projection (`.then` callback returning `m.Named`,
  `{ default: m.Named }`, `await import(...)`) renders the named component.
- A tag or call through a parameter, plain or destructured
  (`({ icon: Button }) => <Button/>`), was looked up by name in the module scope
  and landed on the same-named import. A callee bound to a local now stays with
  what the local resolves to, or unresolved.

Case 75 pins each, with controls: real wrappers still reach their argument, a
loader returning the module still renders its default, and an unshadowed tag
still reaches the import. Two React apps index to the same edge set as before.

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@swapnilpaliwal-sd
swapnilpaliwal-sd merged commit 18a6cd4 into 0.1.3 Sep 28, 2026
12 checks passed
@swapnilpaliwal-sd
swapnilpaliwal-sd deleted the loop/ty-ts-jsx-tags branch September 28, 2026 03:26
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