* Phase 4b: Flutter setState → build (the Dart analog of react-render). In a * StatefulWidget's State class, `setState(() {…})` re-runs `build(context)`, but * that hop is framework-internal (Flutter calls build), so a flow like * "onPressed → _increment → setState → rebuilt UI" dead-ends at setS
(queries: QueryBuilder, ctx: ResolutionContext)
| 388 | * Flutter State classes. Over-approximation accepted (reachability-correct). |
| 389 | */ |
| 390 | function flutterBuildEdges(queries: QueryBuilder, ctx: ResolutionContext): Edge[] { |
| 391 | const edges: Edge[] = []; |
| 392 | const seen = new Set<string>(); |
| 393 | for (const cls of queries.getNodesByKind('class')) { |
| 394 | const children = queries.getOutgoingEdges(cls.id, ['contains']) |
| 395 | .map((e) => queries.getNodeById(e.target)) |
| 396 | .filter((n): n is Node => !!n && n.kind === 'method'); |
| 397 | const build = children.find((n) => n.name === 'build'); |
| 398 | if (!build || !build.filePath.endsWith('.dart')) continue; |
| 399 | let added = 0; |
| 400 | for (const m of children) { |
| 401 | if (added >= MAX_CALLBACKS_PER_CHANNEL) break; |
| 402 | if (m.id === build.id) continue; |
| 403 | const content = ctx.readFile(m.filePath); |
| 404 | const src = content && sliceLines(content, m.startLine, m.endLine); |
| 405 | if (!src || !FLUTTER_SETSTATE_RE.test(src)) continue; |
| 406 | const key = `${m.id}>${build.id}`; |
| 407 | if (seen.has(key)) continue; |
| 408 | seen.add(key); |
| 409 | edges.push({ |
| 410 | source: m.id, target: build.id, kind: 'calls', line: m.startLine, |
| 411 | provenance: 'heuristic', |
| 412 | metadata: { synthesizedBy: 'flutter-build', via: 'setState', registeredAt: `${build.filePath}:${build.startLine}` }, |
| 413 | }); |
| 414 | added++; |
| 415 | } |
| 416 | } |
| 417 | return edges; |
| 418 | } |
| 419 | |
| 420 | /** |
| 421 | * Phase 4c: C++ virtual override. A call through a base/interface pointer |
no test coverage detected