* Phase 4b-ets2: HarmonyOS `@ohos.events.emitter` bridge. The cross-component * bus — `emitter.emit(eventId)` fires `emitter.on(eventId, cb)` — is * framework-internal, so an order flow riding it (OrangeShopping's * add-to-cart) dead-ends at the emit. Link emit-site enclosing * function/method →
(ctx: ResolutionContext, onYield: MaybeYield)
| 586 | * so targeting that method keeps the chain connected. |
| 587 | */ |
| 588 | async function arkuiEmitterEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> { |
| 589 | let scannedFiles = 0; |
| 590 | interface Site { nodeId: string; file: string; line: number } |
| 591 | // bucket key -> emit sites / handler sites |
| 592 | const emits = new Map<string, Site[]>(); |
| 593 | const handlers = new Map<string, Site[]>(); |
| 594 | |
| 595 | const moduleDirs = (() => { |
| 596 | const ws = ctx.getWorkspacePackages?.(); |
| 597 | return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : []; |
| 598 | })(); |
| 599 | const moduleScopeOf = (file: string): string => { |
| 600 | for (const dir of moduleDirs) { |
| 601 | if (file === dir || file.startsWith(dir + '/')) return dir; |
| 602 | } |
| 603 | return ''; |
| 604 | }; |
| 605 | |
| 606 | for (const file of ctx.getAllFiles()) { |
| 607 | if ((++scannedFiles & 15) === 0) await onYield(); |
| 608 | if (!file.endsWith('.ets')) continue; |
| 609 | const content = ctx.readFile(file); |
| 610 | if (!content || !content.includes('emitter.')) continue; |
| 611 | const safe = stripCommentsForRegex(content, 'typescript'); |
| 612 | const nodes = ctx.getNodesInFile(file) |
| 613 | .filter((n) => n.kind === 'method' || n.kind === 'function'); |
| 614 | |
| 615 | ARKUI_EMITTER_CALL_RE.lastIndex = 0; |
| 616 | let m: RegExpExecArray | null; |
| 617 | while ((m = ARKUI_EMITTER_CALL_RE.exec(safe))) { |
| 618 | const verb = m[1]!; |
| 619 | const arg = m[2]!.trim(); |
| 620 | const line = safe.slice(0, m.index).split('\n').length; |
| 621 | const encl = nodes |
| 622 | .filter((n) => n.startLine <= line && n.endLine >= line) |
| 623 | .sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0]; |
| 624 | if (!encl) continue; |
| 625 | |
| 626 | // Recover the event key from the first argument. |
| 627 | let key: string | null = null; |
| 628 | const idLit = arg.startsWith('{') ? arg.match(/\beventId\s*:\s*([\w$.]+)/)?.[1] : undefined; |
| 629 | const token = idLit ?? arg; |
| 630 | if (token !== undefined) { |
| 631 | if (/^\d+$/.test(token)) { |
| 632 | key = `num:${file}:${token}`; // numeric: same-file only |
| 633 | } else if (token.includes('.')) { |
| 634 | key = `name:${moduleScopeOf(file)}:${token}`; |
| 635 | } else { |
| 636 | // Local variable — chase its same-file declaration one level: |
| 637 | // `let x = new EventsId(K)` / `const x = K`. |
| 638 | const declRe = new RegExp( |
| 639 | `\\b${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b\\s*(?::[^=\\n]+)?=\\s*(?:new\\s+[\\w$.]+\\(\\s*([^)\\n]+?)\\s*\\)|([\\w$.]+))` |
| 640 | ); |
| 641 | const decl = safe.match(declRe); |
| 642 | const inner = (decl?.[1] ?? decl?.[2])?.trim(); |
| 643 | if (inner && /^\d+$/.test(inner)) key = `num:${file}:${inner}`; |
| 644 | else if (inner && /^[\w$.]+$/.test(inner)) key = `name:${moduleScopeOf(file)}:${inner}`; |
| 645 | } |
no test coverage detected