* Phase 4: React class-component re-render. `this.setState(...)` re-runs the * component's `render()`, but that hop is React-internal — no static edge — so a * flow like "mutation → setState → canvas repaint" dead-ends at setState even * though `render → getRenderableElements → …` is fully call-c
(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield)
| 398 | * accepted — it's reachability-correct, like the callback channels. |
| 399 | */ |
| 400 | async function reactRenderEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> { |
| 401 | let scanned255 = 0; |
| 402 | const edges: Edge[] = []; |
| 403 | const seen = new Set<string>(); |
| 404 | // A class can only emit here if it CONTAINS a method named `render` — so one |
| 405 | // indexed name lookup bounds the candidate set up front, and the class scan |
| 406 | // below skips everything else before its per-class edge/node queries. On a |
| 407 | // repo with few/no render methods (any non-React codebase) this collapses |
| 408 | // the pass from every-class fan-out to ~zero DB work, with identical output: |
| 409 | // the skipped classes fail the same `render` check today, just after paying |
| 410 | // for their children. (Not a language gate: `render` + `this.setState(` in |
| 411 | // Java — e.g. Litho — legitimately matches today and still does.) |
| 412 | const renderOwners = new Set<string>(); |
| 413 | for (const n of ctx.getNodesByName('render')) { |
| 414 | if (n.kind !== 'method') continue; |
| 415 | for (const e of queries.getIncomingEdges(n.id, ['contains'])) renderOwners.add(e.source); |
| 416 | } |
| 417 | if (renderOwners.size === 0) return edges; |
| 418 | for (const cls of queries.iterateNodesByKind('class')) { |
| 419 | if ((++scanned255 & 63) === 0) await onYield(); |
| 420 | if (!renderOwners.has(cls.id)) continue; |
| 421 | const children = queries.getOutgoingEdges(cls.id, ['contains']) |
| 422 | .map((e) => queries.getNodeById(e.target)) |
| 423 | .filter((n): n is Node => !!n && n.kind === 'method'); |
| 424 | const render = children.find((n) => n.name === 'render'); |
| 425 | if (!render) continue; |
| 426 | let added = 0; |
| 427 | for (const m of children) { |
| 428 | if (added >= MAX_CALLBACKS_PER_CHANNEL) break; |
| 429 | if (m.id === render.id) continue; |
| 430 | const content = ctx.readFile(m.filePath); |
| 431 | const src = content && sliceLines(content, m.startLine, m.endLine); |
| 432 | if (!src || !SETSTATE_RE.test(src)) continue; |
| 433 | const key = `${m.id}>${render.id}`; |
| 434 | if (seen.has(key)) continue; |
| 435 | seen.add(key); |
| 436 | edges.push({ |
| 437 | source: m.id, target: render.id, kind: 'calls', line: m.startLine, |
| 438 | provenance: 'heuristic', |
| 439 | metadata: { synthesizedBy: 'react-render', via: 'setState', registeredAt: `${render.filePath}:${render.startLine}` }, |
| 440 | }); |
| 441 | added++; |
| 442 | } |
| 443 | } |
| 444 | return edges; |
| 445 | } |
| 446 | |
| 447 | /** |
| 448 | * Phase 4b: Flutter setState → build (the Dart analog of react-render). In a |
no test coverage detected