Repository navigation
React: JSX at module scope, in a top-level const or in a test callback gets no render edge #2587
Description
Summary
The JSX child-render pass only links tags written inside a named function, method or component. JSX written anywhere else produces no edge:
- at module scope:
createRoot(el).render(<App />)in an app's entry file; - in a top-level
constinitializer:const ICONS = { a: <Icon /> }; - in an anonymous callback:
it("renders", () => render(<Card />)), the shape of most React component tests.
Plain calls in those same places do get calls edges (from the file node, or from the const), so the graph shows a test file calling helpers but not the component it renders. For a real React app this means the entry point never connects to the root component, and a component's callers / impact leave out its tests.
Reproduction (verified)
On main at 8fe3156, built from source, Linux (WSL2), Node 22.23.2, WASM extractor (CODEGRAPH_KERNEL=0).
src/Card.tsx
export function Card() { return <div>card</div>; }
export function Icon() { return <svg />; }
export function setup() { return 1; }
export function helper() { return 2; }src/main.tsx
import { createRoot } from "react-dom/client";
import { Card, Icon, setup } from "./Card";
const ICONS = { a: <Icon /> };
const CFG = { s: setup() };
setup();
createRoot(document.getElementById("root")!).render(<Card />);
export function useIcons() { return [ICONS, CFG]; }src/Card.test.tsx
import { it } from "vitest";
import { render } from "@testing-library/react";
import { Card, helper } from "./Card";
it("renders", () => {
helper();
render(<Card />);
});codegraph init -i, then every edge that is not contains / imports / exports:
src/Card.test.tsx:Card.test.tsx -[calls L5]-> src/Card.tsx:function:helper
src/main.tsx:useIcons -[references]-> src/main.tsx:constant:CFG
src/main.tsx:useIcons -[references]-> src/main.tsx:constant:ICONS
src/main.tsx:CFG -[calls L4]-> src/Card.tsx:function:setup
src/main.tsx:main.tsx -[calls L5]-> src/Card.tsx:function:setup
Observed: the plain calls setup() / helper() are linked from the file node and from CFG; none of <Icon />, <Card /> (main.tsx) or <Card /> (test) is linked.
Expected: ICONS -[calls]-> Icon (as CFG -[calls]-> setup), and main.tsx -[calls]-> Card and Card.test.tsx -[calls]-> Card (as the file-level setup / helper calls), with synthesizedBy: "jsx-render".
Likely cause (inferred from reading the code)
reactJsxChildEdges picks the parents whose spans it scans for tags from PARENT_KINDS, which is method, function and component only, and skips a file that has none:
codegraph/src/resolution/callback-synthesizer.ts
Lines 1949 to 1953 in 8fe3156
async function reactJsxChildEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> { let scannedFiles = 0; const edges: Edge[] = []; const seen = new Set<string>(); const PARENT_KINDS = new Set(['method', 'function', 'component']); codegraph/src/resolution/callback-synthesizer.ts
Lines 1977 to 1980 in 8fe3156
const parents = ctx.getNodesInFile(file).filter( (n) => PARENT_KINDS.has(n.kind) && JSX_FAMILY.includes(n.language) ); if (parents.length === 0) continue;
A constant / variable node and the file node are never parents. An anonymous arrow passed to it(…) has no node of its own (its calls are attributed to the file), so its tags would have to be attributed to the file too.
A possible direction is to add constant / variable to PARENT_KINDS, and to scan the file node for tags that sit outside every function/method/component/constant span, so the attribution matches what extraction does for plain calls. A file-level parent would need the same "tag outside every other parent's span" logic, or each tag would be counted twice.
Related
- fix(react): a type argument or a name the component binds itself renders no JSX child #2442, fix(react): a JSX tag renders what the file imports under that name #2451, fix(react): a JSX tag imported from the project renders what its module exports #2563, fix(react): a tag written in a comment or a string renders no JSX child #2564 (recent changes to which child a tag resolves to; none of them changes which nodes can be parents).
- forwardRef components get no JSX render edges — callers/impact silently return empty #841 (forwardRef components getting no render edges, a different parent-shape gap).
Not checked
- The Rust kernel path. The synthesizer runs after extraction, so I expect the same result; only the WASM extractor was run.
- Class field initializers (
static defaultIcon = <Icon />) and JSX inside object-literal methods in a top-level const. - Whether giving a file node outgoing render edges changes how
codegraph_explore's Flow section or entry-point detection treat entry files.
Activity