* Phase 4b-ets3: HarmonyOS page navigation. `router.pushUrl({ url: * 'pages/Detail' })` reaches the `@Entry struct` of * ` /src/main/ets/pages/Detail.ets`, but the hop is a string — no * static edge — so "tap → openDetail → ???" ends at the router call. Bridge * literal urls to the page s
(ctx: ResolutionContext, onYield: MaybeYield)
| 692 | * structs qualify as targets — the decorator is what makes a file a page. |
| 693 | */ |
| 694 | async function arkuiRouterEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> { |
| 695 | const edges: Edge[] = []; |
| 696 | const seen = new Set<string>(); |
| 697 | |
| 698 | const allFiles = ctx.getAllFiles(); |
| 699 | const moduleDirs = (() => { |
| 700 | const ws = ctx.getWorkspacePackages?.(); |
| 701 | return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : []; |
| 702 | })(); |
| 703 | const moduleScopeOf = (file: string): string => { |
| 704 | for (const dir of moduleDirs) { |
| 705 | if (file === dir || file.startsWith(dir + '/')) return dir; |
| 706 | } |
| 707 | return ''; |
| 708 | }; |
| 709 | |
| 710 | let scannedFiles = 0; |
| 711 | for (const file of allFiles) { |
| 712 | if ((++scannedFiles & 15) === 0) await onYield(); |
| 713 | if (!file.endsWith('.ets')) continue; |
| 714 | const content = ctx.readFile(file); |
| 715 | if (!content || !content.includes('router.')) continue; |
| 716 | const safe = stripCommentsForRegex(content, 'typescript'); |
| 717 | const nodes = ctx.getNodesInFile(file) |
| 718 | .filter((n) => n.kind === 'method' || n.kind === 'function'); |
| 719 | |
| 720 | ARKUI_ROUTER_RE.lastIndex = 0; |
| 721 | let m: RegExpExecArray | null; |
| 722 | while ((m = ARKUI_ROUTER_RE.exec(safe))) { |
| 723 | const url = m[1]!; |
| 724 | const line = safe.slice(0, m.index).split('\n').length; |
| 725 | const encl = nodes |
| 726 | .filter((n) => n.startLine <= line && n.endLine >= line) |
| 727 | .sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0]; |
| 728 | if (!encl) continue; |
| 729 | |
| 730 | const suffix = `/src/main/ets/${url}.ets`; |
| 731 | let candidates = allFiles.filter((f) => f.endsWith(suffix)); |
| 732 | if (candidates.length > 1) { |
| 733 | const scope = moduleScopeOf(file); |
| 734 | const sameModule = candidates.filter((f) => moduleScopeOf(f) === scope); |
| 735 | if (sameModule.length > 0) candidates = sameModule; |
| 736 | } |
| 737 | if (candidates.length !== 1) continue; // ambiguous or unresolved — never guess |
| 738 | |
| 739 | const page = ctx.getNodesInFile(candidates[0]!).find( |
| 740 | (n) => n.kind === 'struct' && (n.decorators ?? []).includes('Entry') |
| 741 | ); |
| 742 | if (!page) continue; |
| 743 | |
| 744 | const key = `${encl.id}>${page.id}`; |
| 745 | if (seen.has(key)) continue; |
| 746 | seen.add(key); |
| 747 | edges.push({ |
| 748 | source: encl.id, target: page.id, kind: 'calls', line, |
| 749 | provenance: 'heuristic', |
| 750 | metadata: { synthesizedBy: 'arkui-route', event: url, registeredAt: `${candidates[0]}:${page.startLine}` }, |
| 751 | }); |
no test coverage detected