Skip to content

React: JSX at module scope, in a top-level const or in a test callback gets no render edge #2587

Open

Description

@inth3shadows

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 const initializer: 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:

  • 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']);
  • 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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions