Skip to content

javascript: JSX tags bound to memo/forwardRef wrappers or lazy() loaders render the wrapped component - #1612

Merged
swapnilpaliwal-sd merged 1 commit into
0.1.4from
loop/ja-is1590
Sep 28, 2026
Merged

swapnilpaliwal-sd merged 1 commit into
0.1.4from
loop/ja-is1590

Conversation

@swapnilpaliwal-sd

Copy link
Copy Markdown
Contributor

Closes #1590

What changed

In JavaScript, a JSX tag whose binding holds a component wrapper or a lazy loader now renders the component behind it (graph/javascript/engine/call-edge-generation/jsx.dl):

  • wrappers: memo(C), React.memo(fn), forwardRef(fn), observer(C), withX(C), nested (memo(forwardRef(function Inner…))), reached through a const, a named or default import, export default memo(C), and re-exports;
  • loaders: lazy / dynamic / loadable whose argument returns the module (its default export renders), { default: m.Named } (block or concise body), or m.Named.

Only JSX tags use this. The wrapper call is not given a value, so Card() on a wrapped component stays ambiguous_unknown and doesn't turn into a claimed edge. Curried wrappers (connect(…)(C)) are still not modelled.

Tests

  • New case 63-jsx-wrapper-lazy (goldens blessed with --oracle, 0 defects). Before the fix, all 9 wrapper/loader tags were ambiguous_unknown. After it, all 9 are known_edge to the inner component.
  • Controls, unchanged: <Plain /> imported by name, the plain call PlainInner() (compiler EXACT), Card() called on a wrapper (stays unknown), and <Thing /> bound to an unstaged non-wrapper factory makeThing(() => …) (stays unknown).
  • JS suite run-tests.sh --oracle: 67/68. The one failure, 60-ts-emit-helpers (its lib edges), fails the same way with the original rules, so this change didn't cause it.
  • torture/run.sh: recall vs runtime 1.000. realapp/run.sh: rc 0.

Real apps

  • A real Next.js JS app (React.memo, forwardRef, dynamic(() => import())): known_edge 474→513, ambiguous_unknown 977→932, multi_inferred 20→25. No resolved edge was lost. 32 JSX elements newly resolve. 12 prop-callback calls now flow through the memo'd component (checked by hand against the call sites). Index took 7.1 s before and 13.2 s after; the second number is a first run, so it includes compiling the changed program.
  • A real React boilerplate app: no change. Its tags import through webpack module aliases, which don't resolve before this rule is reached.

…loader renders the wrapped component

A tag whose binding holds memo(C), React.memo(fn), forwardRef(fn), observer(C) or a
with* higher-order component now renders argument 0, following nested wrappers,
const bindings, named/default imports and re-exports. A tag bound to lazy(),
dynamic() or loadable() renders what the loader settles to: the module's default
export, the { default: m.Named } object, or a projected m.Named.

Only JSX tags read this; a wrapper value called as a function stays unresolved.
New case 63-jsx-wrapper-lazy with controls (plain tag, plain call, an unstaged
non-wrapper factory).

Closes #1590

Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
@swapnilpaliwal-sd
swapnilpaliwal-sd merged commit a3f0fa3 into 0.1.4 Sep 28, 2026
11 checks passed
@swapnilpaliwal-sd
swapnilpaliwal-sd deleted the loop/ja-is1590 branch September 28, 2026 06:55
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