* Phase 4b-ets: ArkUI state → build (the ArkTS analog of react-render / * flutter-build). Assigning a reactive-decorated property (`@State count`, * `@Link selected`, …) re-runs the `@Component struct`'s `build()`, but that * hop is framework-internal — no static edge — so "onClick → markAllDone
(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield)
| 516 | * all-sibling-methods design would erase). |
| 517 | */ |
| 518 | async function arkuiStateBuildEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> { |
| 519 | let scanned255 = 0; |
| 520 | const edges: Edge[] = []; |
| 521 | const seen = new Set<string>(); |
| 522 | for (const struct of queries.iterateNodesByKind('struct')) { |
| 523 | if ((++scanned255 & 63) === 0) await onYield(); |
| 524 | if (struct.language !== 'arkts') continue; |
| 525 | const children = queries.getOutgoingEdges(struct.id, ['contains']) |
| 526 | .map((e) => queries.getNodeById(e.target)) |
| 527 | .filter((n): n is Node => !!n); |
| 528 | const build = children.find((n) => n.kind === 'method' && n.name === 'build'); |
| 529 | if (!build) continue; |
| 530 | const reactiveProps = children.filter( |
| 531 | (n) => n.kind === 'property' && (n.decorators ?? []).some((d) => ARKUI_REACTIVE_DECORATORS.has(d)) |
| 532 | ); |
| 533 | if (reactiveProps.length === 0) continue; |
| 534 | const propAlternation = reactiveProps |
| 535 | .map((p) => p.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) |
| 536 | .join('|'); |
| 537 | // `this.count = …` / `+=` / `++` / `--` / `this.todos.push(…)`. The |
| 538 | // `=(?!=)` keeps `this.done == x` comparisons out. |
| 539 | const mutationRe = new RegExp( |
| 540 | `this\\.(?:${propAlternation})\\s*(?:=(?!=)|\\+\\+|--|[+\\-*/%&|^]=|\\.(?:${ARKUI_ARRAY_MUTATORS})\\s*\\()` |
| 541 | ); |
| 542 | let added = 0; |
| 543 | for (const m of children) { |
| 544 | if (added >= MAX_CALLBACKS_PER_CHANNEL) break; |
| 545 | if (m.kind !== 'method' || m.id === build.id) continue; |
| 546 | const content = ctx.readFile(m.filePath); |
| 547 | const src = content && sliceLines(content, m.startLine, m.endLine); |
| 548 | if (!src || !mutationRe.test(stripCommentsForRegex(src, 'typescript'))) continue; |
| 549 | const key = `${m.id}>${build.id}`; |
| 550 | if (seen.has(key)) continue; |
| 551 | seen.add(key); |
| 552 | edges.push({ |
| 553 | source: m.id, target: build.id, kind: 'calls', line: m.startLine, |
| 554 | provenance: 'heuristic', |
| 555 | metadata: { synthesizedBy: 'arkui-state', via: 'state assignment', registeredAt: `${build.filePath}:${build.startLine}` }, |
| 556 | }); |
| 557 | added++; |
| 558 | } |
| 559 | } |
| 560 | return edges; |
| 561 | } |
| 562 | |
| 563 | /** Emit/subscribe call sites of HarmonyOS's `@ohos.events.emitter` bus. */ |
| 564 | const ARKUI_EMITTER_CALL_RE = /\bemitter\s*\.\s*(emit|on|once)\s*\(\s*([A-Za-z_$][\w$.]*|\{[^)]{0,120}?\beventId\s*:\s*[^,}]+[^)]*?\})/g; |
no test coverage detected