(ctx: ResolutionContext, onYield: MaybeYield)
| 2226 | } |
| 2227 | |
| 2228 | async function objectRegistryEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> { |
| 2229 | let scannedFiles = 0; |
| 2230 | const edges: Edge[] = []; |
| 2231 | const seen = new Set<string>(); |
| 2232 | let scanned = 0; |
| 2233 | for (const file of ctx.getAllFiles()) { |
| 2234 | if ((++scannedFiles & 15) === 0) await onYield(); |
| 2235 | if ((++scanned & 255) === 0) await onYield(); // #1091: yield mid-scan on huge graphs |
| 2236 | if (!REGISTRY_JS_EXT.test(file)) continue; |
| 2237 | const content = ctx.readFile(file); |
| 2238 | // Cheap pre-filter: a computed member access BY NAME (`ident[ident`) — the dispatch shape. |
| 2239 | if (!content || !/[\w$]\s*\[\s*[A-Za-z_$]/.test(content)) continue; |
| 2240 | // Skip minified/generated bundles (draco, three.min, base64…): their pervasive `h[x](...)` |
| 2241 | // calls + single-letter `{a:b}` literals are a false-positive minefield. Average line |
| 2242 | // length is the reliable tell — real source ~30–80, minified in the hundreds/thousands. |
| 2243 | const newlines = (content.match(/\n/g)?.length ?? 0) + 1; |
| 2244 | if (content.length / newlines > 200) continue; |
| 2245 | const safe = stripCommentsForRegex(content, /\.(?:jsx?|mjs|cjs)$/.test(file) ? 'javascript' : 'typescript'); |
| 2246 | |
| 2247 | // 1. Dispatch sites: `(new )?<ref>[<ident-key>]` followed by a call or a chained method. |
| 2248 | // A quoted-string key (`['save']`) does NOT match — that's a static access, not dispatch. |
| 2249 | REGISTRY_DISPATCH_RE.lastIndex = 0; |
| 2250 | const dispatches: Array<{ ref: string; line: number; chained: string | null }> = []; |
| 2251 | let dm: RegExpExecArray | null; |
| 2252 | while ((dm = REGISTRY_DISPATCH_RE.exec(safe))) { |
| 2253 | const win = safe.slice(dm.index, dm.index + 160); |
| 2254 | const cm = /\]\s*\([^)]*\)\s*\.\s*([A-Za-z_$][\w$]*)/.exec(win) || /\]\s*\.\s*([A-Za-z_$][\w$]*)/.exec(win); |
| 2255 | dispatches.push({ ref: dm[1]!, line: safe.slice(0, dm.index).split('\n').length, chained: cm ? cm[1]! : null }); |
| 2256 | } |
| 2257 | if (!dispatches.length) continue; |
| 2258 | // Normalize a leading `this.` so a class FIELD-INITIALIZER registry (`commands = {…}`) |
| 2259 | // matches a `this.commands[k]` dispatch, not just the constructor form `this.commands = {…}`. |
| 2260 | const norm = (r: string) => r.replace(/^this\./, ''); |
| 2261 | const refs = new Set(dispatches.map((d) => norm(d.ref))); |
| 2262 | |
| 2263 | // 2. Registries: an object literal assigned to a dispatched ref, ≥2 entries resolving to callables. |
| 2264 | REGISTRY_ASSIGN_RE.lastIndex = 0; |
| 2265 | const registries = new Map<string, { names: string[]; line: number }>(); |
| 2266 | let am: RegExpExecArray | null; |
| 2267 | while ((am = REGISTRY_ASSIGN_RE.exec(safe))) { |
| 2268 | const lhs = norm(am[1] ?? am[2]!); |
| 2269 | if (!refs.has(lhs) || registries.has(lhs)) continue; |
| 2270 | const body = braceBody(safe, am.index + am[0].length - 1); |
| 2271 | if (!body) continue; |
| 2272 | const names = registryEntryNames(body); // depth-0 `key: Identifier` entries only |
| 2273 | if (names.length >= REGISTRY_MIN_ENTRIES) { |
| 2274 | registries.set(lhs, { names, line: safe.slice(0, am.index).split('\n').length }); |
| 2275 | } |
| 2276 | } |
| 2277 | if (!registries.size) continue; |
| 2278 | |
| 2279 | // 3. Link each dispatcher → each registered handler's callable entry. |
| 2280 | const nodesInFile = ctx.getNodesInFile(file); |
| 2281 | for (const d of dispatches) { |
| 2282 | const reg = registries.get(norm(d.ref)); |
| 2283 | if (!reg) continue; |
| 2284 | const disp = enclosingFn(nodesInFile, d.line); |
| 2285 | if (!disp) continue; |
no test coverage detected