@colbymchenry/codegraph-win32-arm64 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dist/bin/codegraph.d.ts +1 -1
- package/lib/dist/bin/codegraph.d.ts.map +1 -1
- package/lib/dist/bin/codegraph.js +232 -61
- package/lib/dist/bin/codegraph.js.map +1 -1
- package/lib/dist/bin/command-supervision.d.ts.map +1 -1
- package/lib/dist/bin/command-supervision.js +4 -1
- package/lib/dist/bin/command-supervision.js.map +1 -1
- package/lib/dist/db/index.d.ts +17 -5
- package/lib/dist/db/index.d.ts.map +1 -1
- package/lib/dist/db/index.js +68 -12
- package/lib/dist/db/index.js.map +1 -1
- package/lib/dist/db/migrations.d.ts +1 -1
- package/lib/dist/db/migrations.d.ts.map +1 -1
- package/lib/dist/db/migrations.js +19 -1
- package/lib/dist/db/migrations.js.map +1 -1
- package/lib/dist/db/queries.d.ts +76 -1
- package/lib/dist/db/queries.d.ts.map +1 -1
- package/lib/dist/db/queries.js +180 -0
- package/lib/dist/db/queries.js.map +1 -1
- package/lib/dist/db/schema.sql +19 -0
- package/lib/dist/directory.d.ts +9 -5
- package/lib/dist/directory.d.ts.map +1 -1
- package/lib/dist/directory.js +210 -19
- package/lib/dist/directory.js.map +1 -1
- package/lib/dist/extraction/cfml-extractor.d.ts +107 -0
- package/lib/dist/extraction/cfml-extractor.d.ts.map +1 -0
- package/lib/dist/extraction/cfml-extractor.js +494 -0
- package/lib/dist/extraction/cfml-extractor.js.map +1 -0
- package/lib/dist/extraction/grammars.d.ts +9 -0
- package/lib/dist/extraction/grammars.d.ts.map +1 -1
- package/lib/dist/extraction/grammars.js +115 -3
- package/lib/dist/extraction/grammars.js.map +1 -1
- package/lib/dist/extraction/index.d.ts +46 -1
- package/lib/dist/extraction/index.d.ts.map +1 -1
- package/lib/dist/extraction/index.js +338 -34
- package/lib/dist/extraction/index.js.map +1 -1
- package/lib/dist/extraction/languages/arkts.d.ts +3 -0
- package/lib/dist/extraction/languages/arkts.d.ts.map +1 -0
- package/lib/dist/extraction/languages/arkts.js +127 -0
- package/lib/dist/extraction/languages/arkts.js.map +1 -0
- package/lib/dist/extraction/languages/c-cpp.d.ts +42 -0
- package/lib/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/lib/dist/extraction/languages/c-cpp.js +369 -4
- package/lib/dist/extraction/languages/c-cpp.js.map +1 -1
- package/lib/dist/extraction/languages/cfquery.d.ts +12 -0
- package/lib/dist/extraction/languages/cfquery.d.ts.map +1 -0
- package/lib/dist/extraction/languages/cfquery.js +28 -0
- package/lib/dist/extraction/languages/cfquery.js.map +1 -0
- package/lib/dist/extraction/languages/cfscript.d.ts +3 -0
- package/lib/dist/extraction/languages/cfscript.d.ts.map +1 -0
- package/lib/dist/extraction/languages/cfscript.js +73 -0
- package/lib/dist/extraction/languages/cfscript.js.map +1 -0
- package/lib/dist/extraction/languages/cobol.d.ts +33 -0
- package/lib/dist/extraction/languages/cobol.d.ts.map +1 -0
- package/lib/dist/extraction/languages/cobol.js +499 -0
- package/lib/dist/extraction/languages/cobol.js.map +1 -0
- package/lib/dist/extraction/languages/erlang.d.ts +3 -0
- package/lib/dist/extraction/languages/erlang.d.ts.map +1 -0
- package/lib/dist/extraction/languages/erlang.js +350 -0
- package/lib/dist/extraction/languages/erlang.js.map +1 -0
- package/lib/dist/extraction/languages/index.d.ts.map +1 -1
- package/lib/dist/extraction/languages/index.js +18 -0
- package/lib/dist/extraction/languages/index.js.map +1 -1
- package/lib/dist/extraction/languages/nix.d.ts +3 -0
- package/lib/dist/extraction/languages/nix.d.ts.map +1 -0
- package/lib/dist/extraction/languages/nix.js +294 -0
- package/lib/dist/extraction/languages/nix.js.map +1 -0
- package/lib/dist/extraction/languages/solidity.d.ts +3 -0
- package/lib/dist/extraction/languages/solidity.d.ts.map +1 -0
- package/lib/dist/extraction/languages/solidity.js +293 -0
- package/lib/dist/extraction/languages/solidity.js.map +1 -0
- package/lib/dist/extraction/languages/terraform.d.ts +3 -0
- package/lib/dist/extraction/languages/terraform.d.ts.map +1 -0
- package/lib/dist/extraction/languages/terraform.js +641 -0
- package/lib/dist/extraction/languages/terraform.js.map +1 -0
- package/lib/dist/extraction/languages/vbnet.d.ts +11 -0
- package/lib/dist/extraction/languages/vbnet.d.ts.map +1 -0
- package/lib/dist/extraction/languages/vbnet.js +141 -0
- package/lib/dist/extraction/languages/vbnet.js.map +1 -0
- package/lib/dist/extraction/mybatis-extractor.d.ts +30 -10
- package/lib/dist/extraction/mybatis-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/mybatis-extractor.js +140 -38
- package/lib/dist/extraction/mybatis-extractor.js.map +1 -1
- package/lib/dist/extraction/tree-sitter-helpers.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter-helpers.js +1 -0
- package/lib/dist/extraction/tree-sitter-helpers.js.map +1 -1
- package/lib/dist/extraction/tree-sitter-types.d.ts +3 -1
- package/lib/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter.d.ts +38 -0
- package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter.js +753 -10
- package/lib/dist/extraction/tree-sitter.js.map +1 -1
- package/lib/dist/extraction/wasm/tree-sitter-arkts.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-terraform.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/lib/dist/index.d.ts +63 -1
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +327 -5
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/installer/index.d.ts.map +1 -1
- package/lib/dist/installer/index.js +3 -1
- package/lib/dist/installer/index.js.map +1 -1
- package/lib/dist/mcp/daemon.d.ts +25 -3
- package/lib/dist/mcp/daemon.d.ts.map +1 -1
- package/lib/dist/mcp/daemon.js +62 -8
- package/lib/dist/mcp/daemon.js.map +1 -1
- package/lib/dist/mcp/dynamic-boundaries.d.ts.map +1 -1
- package/lib/dist/mcp/dynamic-boundaries.js +2 -1
- package/lib/dist/mcp/dynamic-boundaries.js.map +1 -1
- package/lib/dist/mcp/early-ppid.d.ts +26 -0
- package/lib/dist/mcp/early-ppid.d.ts.map +1 -0
- package/lib/dist/mcp/early-ppid.js +29 -0
- package/lib/dist/mcp/early-ppid.js.map +1 -0
- package/lib/dist/mcp/index.d.ts.map +1 -1
- package/lib/dist/mcp/index.js +21 -4
- package/lib/dist/mcp/index.js.map +1 -1
- package/lib/dist/mcp/proxy.d.ts.map +1 -1
- package/lib/dist/mcp/proxy.js +29 -3
- package/lib/dist/mcp/proxy.js.map +1 -1
- package/lib/dist/mcp/query-pool.d.ts +14 -0
- package/lib/dist/mcp/query-pool.d.ts.map +1 -1
- package/lib/dist/mcp/query-pool.js +18 -0
- package/lib/dist/mcp/query-pool.js.map +1 -1
- package/lib/dist/mcp/session.d.ts.map +1 -1
- package/lib/dist/mcp/session.js +6 -0
- package/lib/dist/mcp/session.js.map +1 -1
- package/lib/dist/mcp/startup-handshake.d.ts +44 -0
- package/lib/dist/mcp/startup-handshake.d.ts.map +1 -0
- package/lib/dist/mcp/startup-handshake.js +73 -0
- package/lib/dist/mcp/startup-handshake.js.map +1 -0
- package/lib/dist/mcp/tools.d.ts +22 -0
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +88 -18
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/mcp/transport.d.ts.map +1 -1
- package/lib/dist/mcp/transport.js +18 -0
- package/lib/dist/mcp/transport.js.map +1 -1
- package/lib/dist/project-config.d.ts +38 -0
- package/lib/dist/project-config.d.ts.map +1 -1
- package/lib/dist/project-config.js +101 -2
- package/lib/dist/project-config.js.map +1 -1
- package/lib/dist/resolution/c-fnptr-synthesizer.d.ts +2 -1
- package/lib/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
- package/lib/dist/resolution/c-fnptr-synthesizer.js +170 -146
- package/lib/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
- package/lib/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/lib/dist/resolution/callback-synthesizer.js +982 -81
- package/lib/dist/resolution/callback-synthesizer.js.map +1 -1
- package/lib/dist/resolution/frameworks/cics.d.ts +20 -0
- package/lib/dist/resolution/frameworks/cics.d.ts.map +1 -0
- package/lib/dist/resolution/frameworks/cics.js +90 -0
- package/lib/dist/resolution/frameworks/cics.js.map +1 -0
- package/lib/dist/resolution/frameworks/index.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/index.js +6 -0
- package/lib/dist/resolution/frameworks/index.js.map +1 -1
- package/lib/dist/resolution/frameworks/java.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/java.js +14 -6
- package/lib/dist/resolution/frameworks/java.js.map +1 -1
- package/lib/dist/resolution/frameworks/terraform.d.ts +38 -0
- package/lib/dist/resolution/frameworks/terraform.d.ts.map +1 -0
- package/lib/dist/resolution/frameworks/terraform.js +277 -0
- package/lib/dist/resolution/frameworks/terraform.js.map +1 -0
- package/lib/dist/resolution/goframe-synthesizer.d.ts +2 -1
- package/lib/dist/resolution/goframe-synthesizer.d.ts.map +1 -1
- package/lib/dist/resolution/goframe-synthesizer.js +8 -3
- package/lib/dist/resolution/goframe-synthesizer.js.map +1 -1
- package/lib/dist/resolution/import-resolver.d.ts +7 -0
- package/lib/dist/resolution/import-resolver.d.ts.map +1 -1
- package/lib/dist/resolution/import-resolver.js +129 -4
- package/lib/dist/resolution/import-resolver.js.map +1 -1
- package/lib/dist/resolution/index.d.ts +49 -6
- package/lib/dist/resolution/index.d.ts.map +1 -1
- package/lib/dist/resolution/index.js +342 -65
- package/lib/dist/resolution/index.js.map +1 -1
- package/lib/dist/resolution/name-matcher.d.ts +0 -3
- package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
- package/lib/dist/resolution/name-matcher.js +225 -33
- package/lib/dist/resolution/name-matcher.js.map +1 -1
- package/lib/dist/resolution/strip-comments.d.ts +1 -1
- package/lib/dist/resolution/strip-comments.d.ts.map +1 -1
- package/lib/dist/resolution/strip-comments.js +49 -0
- package/lib/dist/resolution/strip-comments.js.map +1 -1
- package/lib/dist/resolution/types.d.ts +29 -0
- package/lib/dist/resolution/types.d.ts.map +1 -1
- package/lib/dist/resolution/workspace-packages.d.ts +10 -0
- package/lib/dist/resolution/workspace-packages.d.ts.map +1 -1
- package/lib/dist/resolution/workspace-packages.js +142 -4
- package/lib/dist/resolution/workspace-packages.js.map +1 -1
- package/lib/dist/search/identifier-segments.d.ts +60 -0
- package/lib/dist/search/identifier-segments.d.ts.map +1 -0
- package/lib/dist/search/identifier-segments.js +176 -0
- package/lib/dist/search/identifier-segments.js.map +1 -0
- package/lib/dist/sync/git-hooks.d.ts.map +1 -1
- package/lib/dist/sync/git-hooks.js +2 -0
- package/lib/dist/sync/git-hooks.js.map +1 -1
- package/lib/dist/sync/watcher.d.ts +10 -5
- package/lib/dist/sync/watcher.d.ts.map +1 -1
- package/lib/dist/sync/watcher.js +51 -14
- package/lib/dist/sync/watcher.js.map +1 -1
- package/lib/dist/sync/worktree.d.ts.map +1 -1
- package/lib/dist/sync/worktree.js +5 -0
- package/lib/dist/sync/worktree.js.map +1 -1
- package/lib/dist/types.d.ts +19 -1
- package/lib/dist/types.d.ts.map +1 -1
- package/lib/dist/types.js +10 -0
- package/lib/dist/types.js.map +1 -1
- package/lib/node_modules/.package-lock.json +1 -1
- package/lib/package.json +1 -1
- package/package.json +1 -1
- package/lib/dist/reasoning/config.d.ts +0 -45
- package/lib/dist/reasoning/config.d.ts.map +0 -1
- package/lib/dist/reasoning/config.js +0 -171
- package/lib/dist/reasoning/config.js.map +0 -1
- package/lib/dist/reasoning/credentials.d.ts +0 -5
- package/lib/dist/reasoning/credentials.d.ts.map +0 -1
- package/lib/dist/reasoning/credentials.js +0 -83
- package/lib/dist/reasoning/credentials.js.map +0 -1
- package/lib/dist/reasoning/login.d.ts +0 -21
- package/lib/dist/reasoning/login.d.ts.map +0 -1
- package/lib/dist/reasoning/login.js +0 -85
- package/lib/dist/reasoning/login.js.map +0 -1
- package/lib/dist/reasoning/reasoner.d.ts +0 -43
- package/lib/dist/reasoning/reasoner.d.ts.map +0 -1
- package/lib/dist/reasoning/reasoner.js +0 -308
- package/lib/dist/reasoning/reasoner.js.map +0 -1
|
@@ -276,10 +276,13 @@ async function closureCollectionEdges(queries, ctx, onYield) {
|
|
|
276
276
|
}
|
|
277
277
|
/** Phase 2: string-keyed EventEmitter channels (on('e', fn) ↔ emit('e')). */
|
|
278
278
|
async function eventEmitterEdges(ctx, onYield) {
|
|
279
|
+
let scannedFiles = 0;
|
|
279
280
|
const emitsByEvent = new Map(); // event → dispatcher node ids
|
|
280
281
|
const handlersByEvent = new Map(); // event → handler id → registration site (file:line)
|
|
281
282
|
let scanned = 0;
|
|
282
283
|
for (const file of ctx.getAllFiles()) {
|
|
284
|
+
if ((++scannedFiles & 15) === 0)
|
|
285
|
+
await onYield();
|
|
283
286
|
if ((++scanned & 255) === 0)
|
|
284
287
|
await onYield(); // #1091: yield mid-scan on huge graphs
|
|
285
288
|
const content = ctx.readFile(file);
|
|
@@ -353,10 +356,13 @@ async function eventEmitterEdges(ctx, onYield) {
|
|
|
353
356
|
* `this.setState`). Over-approximation (all setState methods reach render) is
|
|
354
357
|
* accepted — it's reachability-correct, like the callback channels.
|
|
355
358
|
*/
|
|
356
|
-
function reactRenderEdges(queries, ctx) {
|
|
359
|
+
async function reactRenderEdges(queries, ctx, onYield) {
|
|
360
|
+
let scanned255 = 0;
|
|
357
361
|
const edges = [];
|
|
358
362
|
const seen = new Set();
|
|
359
|
-
for (const cls of queries.
|
|
363
|
+
for (const cls of queries.iterateNodesByKind('class')) {
|
|
364
|
+
if ((++scanned255 & 63) === 0)
|
|
365
|
+
await onYield();
|
|
360
366
|
const children = queries.getOutgoingEdges(cls.id, ['contains'])
|
|
361
367
|
.map((e) => queries.getNodeById(e.target))
|
|
362
368
|
.filter((n) => !!n && n.kind === 'method');
|
|
@@ -396,10 +402,13 @@ function reactRenderEdges(queries, ctx) {
|
|
|
396
402
|
* body calls `setState(` → `build`. The setState gate + `.dart` file keep this to
|
|
397
403
|
* Flutter State classes. Over-approximation accepted (reachability-correct).
|
|
398
404
|
*/
|
|
399
|
-
function flutterBuildEdges(queries, ctx) {
|
|
405
|
+
async function flutterBuildEdges(queries, ctx, onYield) {
|
|
406
|
+
let scanned255 = 0;
|
|
400
407
|
const edges = [];
|
|
401
408
|
const seen = new Set();
|
|
402
|
-
for (const cls of queries.
|
|
409
|
+
for (const cls of queries.iterateNodesByKind('class')) {
|
|
410
|
+
if ((++scanned255 & 63) === 0)
|
|
411
|
+
await onYield();
|
|
403
412
|
const children = queries.getOutgoingEdges(cls.id, ['contains'])
|
|
404
413
|
.map((e) => queries.getNodeById(e.target))
|
|
405
414
|
.filter((n) => !!n && n.kind === 'method');
|
|
@@ -430,6 +439,279 @@ function flutterBuildEdges(queries, ctx) {
|
|
|
430
439
|
}
|
|
431
440
|
return edges;
|
|
432
441
|
}
|
|
442
|
+
/**
|
|
443
|
+
* Reactive ArkUI property decorators: assigning a property carrying one of
|
|
444
|
+
* these re-runs the owning struct's `build()`. Covers both state models —
|
|
445
|
+
* V1 (`@Component`: State/Prop/Link/Provide/Consume/Storage*) and V2
|
|
446
|
+
* (`@ComponentV2`: Local/Provider/Consumer; `@Param` is read-only in V2 so
|
|
447
|
+
* the assignment gate never fires on it, and `@Trace` lives on `@ObservedV2`
|
|
448
|
+
* data classes, not struct properties).
|
|
449
|
+
*/
|
|
450
|
+
const ARKUI_REACTIVE_DECORATORS = new Set([
|
|
451
|
+
'State', 'Prop', 'Link', 'Provide', 'Consume', 'StorageLink', 'StorageProp',
|
|
452
|
+
'LocalStorageLink', 'LocalStorageProp', 'ObjectLink',
|
|
453
|
+
'Local', 'Provider', 'Consumer',
|
|
454
|
+
]);
|
|
455
|
+
/** ArkUI-observed array mutators — `this.todos.push(x)` re-renders like an assignment. */
|
|
456
|
+
const ARKUI_ARRAY_MUTATORS = 'push|pop|shift|unshift|splice|sort|reverse|fill';
|
|
457
|
+
/**
|
|
458
|
+
* Phase 4b-ets: ArkUI state → build (the ArkTS analog of react-render /
|
|
459
|
+
* flutter-build). Assigning a reactive-decorated property (`@State count`,
|
|
460
|
+
* `@Link selected`, …) re-runs the `@Component struct`'s `build()`, but that
|
|
461
|
+
* hop is framework-internal — no static edge — so "onClick → markAllDone →
|
|
462
|
+
* this.todos = […] → rebuilt list" dead-ends at the assignment. Bridge it:
|
|
463
|
+
* for each arkts struct with a `build()` method and at least one reactive
|
|
464
|
+
* property, link every sibling method whose body ASSIGNS (or array-mutates)
|
|
465
|
+
* one of those properties → `build`. Assignment-gated on the struct's OWN
|
|
466
|
+
* reactive property names — a method that merely reads state, or a struct
|
|
467
|
+
* with no reactive properties, gets nothing (this is the precision line the
|
|
468
|
+
* all-sibling-methods design would erase).
|
|
469
|
+
*/
|
|
470
|
+
async function arkuiStateBuildEdges(queries, ctx, onYield) {
|
|
471
|
+
let scanned255 = 0;
|
|
472
|
+
const edges = [];
|
|
473
|
+
const seen = new Set();
|
|
474
|
+
for (const struct of queries.iterateNodesByKind('struct')) {
|
|
475
|
+
if ((++scanned255 & 63) === 0)
|
|
476
|
+
await onYield();
|
|
477
|
+
if (struct.language !== 'arkts')
|
|
478
|
+
continue;
|
|
479
|
+
const children = queries.getOutgoingEdges(struct.id, ['contains'])
|
|
480
|
+
.map((e) => queries.getNodeById(e.target))
|
|
481
|
+
.filter((n) => !!n);
|
|
482
|
+
const build = children.find((n) => n.kind === 'method' && n.name === 'build');
|
|
483
|
+
if (!build)
|
|
484
|
+
continue;
|
|
485
|
+
const reactiveProps = children.filter((n) => n.kind === 'property' && (n.decorators ?? []).some((d) => ARKUI_REACTIVE_DECORATORS.has(d)));
|
|
486
|
+
if (reactiveProps.length === 0)
|
|
487
|
+
continue;
|
|
488
|
+
const propAlternation = reactiveProps
|
|
489
|
+
.map((p) => p.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
490
|
+
.join('|');
|
|
491
|
+
// `this.count = …` / `+=` / `++` / `--` / `this.todos.push(…)`. The
|
|
492
|
+
// `=(?!=)` keeps `this.done == x` comparisons out.
|
|
493
|
+
const mutationRe = new RegExp(`this\\.(?:${propAlternation})\\s*(?:=(?!=)|\\+\\+|--|[+\\-*/%&|^]=|\\.(?:${ARKUI_ARRAY_MUTATORS})\\s*\\()`);
|
|
494
|
+
let added = 0;
|
|
495
|
+
for (const m of children) {
|
|
496
|
+
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
497
|
+
break;
|
|
498
|
+
if (m.kind !== 'method' || m.id === build.id)
|
|
499
|
+
continue;
|
|
500
|
+
const content = ctx.readFile(m.filePath);
|
|
501
|
+
const src = content && sliceLines(content, m.startLine, m.endLine);
|
|
502
|
+
if (!src || !mutationRe.test((0, strip_comments_1.stripCommentsForRegex)(src, 'typescript')))
|
|
503
|
+
continue;
|
|
504
|
+
const key = `${m.id}>${build.id}`;
|
|
505
|
+
if (seen.has(key))
|
|
506
|
+
continue;
|
|
507
|
+
seen.add(key);
|
|
508
|
+
edges.push({
|
|
509
|
+
source: m.id, target: build.id, kind: 'calls', line: m.startLine,
|
|
510
|
+
provenance: 'heuristic',
|
|
511
|
+
metadata: { synthesizedBy: 'arkui-state', via: 'state assignment', registeredAt: `${build.filePath}:${build.startLine}` },
|
|
512
|
+
});
|
|
513
|
+
added++;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return edges;
|
|
517
|
+
}
|
|
518
|
+
/** Emit/subscribe call sites of HarmonyOS's `@ohos.events.emitter` bus. */
|
|
519
|
+
const ARKUI_EMITTER_CALL_RE = /\bemitter\s*\.\s*(emit|on|once)\s*\(\s*([A-Za-z_$][\w$.]*|\{[^)]{0,120}?\beventId\s*:\s*[^,}]+[^)]*?\})/g;
|
|
520
|
+
/** Cap per event bucket — a generic key with many parties is dynamic routing, not a static pair. */
|
|
521
|
+
const ARKUI_EMITTER_FANOUT_CAP = 8;
|
|
522
|
+
/**
|
|
523
|
+
* Phase 4b-ets2: HarmonyOS `@ohos.events.emitter` bridge. The cross-component
|
|
524
|
+
* bus — `emitter.emit(eventId)` fires `emitter.on(eventId, cb)` — is
|
|
525
|
+
* framework-internal, so an order flow riding it (OrangeShopping's
|
|
526
|
+
* add-to-cart) dead-ends at the emit. Link emit-site enclosing
|
|
527
|
+
* function/method → on/once-site enclosing function/method when both
|
|
528
|
+
* reference the SAME statically-recoverable event key.
|
|
529
|
+
*
|
|
530
|
+
* Key recovery, per call site (comment-stripped enclosing-file source): the
|
|
531
|
+
* first argument is an `{ eventId: K }` literal, a `Names.Dotted` constant, or
|
|
532
|
+
* a local whose same-file declaration is `new EventsId(K)` / `= K` — chase one
|
|
533
|
+
* level. Precision scoping learned from the samples monorepo (thousands of
|
|
534
|
+
* unrelated samples, most using eventId 1): NUMERIC keys pair within the same
|
|
535
|
+
* FILE only; NAMED keys pair within the same workspace module directory (or
|
|
536
|
+
* the whole project when it declares no modules — the single-app case), both
|
|
537
|
+
* behind a fan-out cap. Inline `on(id, (e) => {…})` arrows need no special
|
|
538
|
+
* handling — their bodies' calls already attribute to the registering method,
|
|
539
|
+
* so targeting that method keeps the chain connected.
|
|
540
|
+
*/
|
|
541
|
+
async function arkuiEmitterEdges(ctx, onYield) {
|
|
542
|
+
let scannedFiles = 0;
|
|
543
|
+
// bucket key -> emit sites / handler sites
|
|
544
|
+
const emits = new Map();
|
|
545
|
+
const handlers = new Map();
|
|
546
|
+
const moduleDirs = (() => {
|
|
547
|
+
const ws = ctx.getWorkspacePackages?.();
|
|
548
|
+
return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
|
|
549
|
+
})();
|
|
550
|
+
const moduleScopeOf = (file) => {
|
|
551
|
+
for (const dir of moduleDirs) {
|
|
552
|
+
if (file === dir || file.startsWith(dir + '/'))
|
|
553
|
+
return dir;
|
|
554
|
+
}
|
|
555
|
+
return '';
|
|
556
|
+
};
|
|
557
|
+
for (const file of ctx.getAllFiles()) {
|
|
558
|
+
if ((++scannedFiles & 15) === 0)
|
|
559
|
+
await onYield();
|
|
560
|
+
if (!file.endsWith('.ets'))
|
|
561
|
+
continue;
|
|
562
|
+
const content = ctx.readFile(file);
|
|
563
|
+
if (!content || !content.includes('emitter.'))
|
|
564
|
+
continue;
|
|
565
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
|
|
566
|
+
const nodes = ctx.getNodesInFile(file)
|
|
567
|
+
.filter((n) => n.kind === 'method' || n.kind === 'function');
|
|
568
|
+
ARKUI_EMITTER_CALL_RE.lastIndex = 0;
|
|
569
|
+
let m;
|
|
570
|
+
while ((m = ARKUI_EMITTER_CALL_RE.exec(safe))) {
|
|
571
|
+
const verb = m[1];
|
|
572
|
+
const arg = m[2].trim();
|
|
573
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
574
|
+
const encl = nodes
|
|
575
|
+
.filter((n) => n.startLine <= line && n.endLine >= line)
|
|
576
|
+
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
|
|
577
|
+
if (!encl)
|
|
578
|
+
continue;
|
|
579
|
+
// Recover the event key from the first argument.
|
|
580
|
+
let key = null;
|
|
581
|
+
const idLit = arg.startsWith('{') ? arg.match(/\beventId\s*:\s*([\w$.]+)/)?.[1] : undefined;
|
|
582
|
+
const token = idLit ?? arg;
|
|
583
|
+
if (token !== undefined) {
|
|
584
|
+
if (/^\d+$/.test(token)) {
|
|
585
|
+
key = `num:${file}:${token}`; // numeric: same-file only
|
|
586
|
+
}
|
|
587
|
+
else if (token.includes('.')) {
|
|
588
|
+
key = `name:${moduleScopeOf(file)}:${token}`;
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
// Local variable — chase its same-file declaration one level:
|
|
592
|
+
// `let x = new EventsId(K)` / `const x = K`.
|
|
593
|
+
const declRe = new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b\\s*(?::[^=\\n]+)?=\\s*(?:new\\s+[\\w$.]+\\(\\s*([^)\\n]+?)\\s*\\)|([\\w$.]+))`);
|
|
594
|
+
const decl = safe.match(declRe);
|
|
595
|
+
const inner = (decl?.[1] ?? decl?.[2])?.trim();
|
|
596
|
+
if (inner && /^\d+$/.test(inner))
|
|
597
|
+
key = `num:${file}:${inner}`;
|
|
598
|
+
else if (inner && /^[\w$.]+$/.test(inner))
|
|
599
|
+
key = `name:${moduleScopeOf(file)}:${inner}`;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (!key)
|
|
603
|
+
continue;
|
|
604
|
+
const site = { nodeId: encl.id, file, line };
|
|
605
|
+
if (verb === 'emit') {
|
|
606
|
+
(emits.get(key) ?? emits.set(key, []).get(key)).push(site);
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
(handlers.get(key) ?? handlers.set(key, []).get(key)).push(site);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const edges = [];
|
|
614
|
+
const seen = new Set();
|
|
615
|
+
for (const [key, emitSites] of emits) {
|
|
616
|
+
const handlerSites = handlers.get(key);
|
|
617
|
+
if (!handlerSites)
|
|
618
|
+
continue;
|
|
619
|
+
if (emitSites.length > ARKUI_EMITTER_FANOUT_CAP || handlerSites.length > ARKUI_EMITTER_FANOUT_CAP)
|
|
620
|
+
continue;
|
|
621
|
+
const eventLabel = key.slice(key.lastIndexOf(':') + 1);
|
|
622
|
+
for (const e of emitSites)
|
|
623
|
+
for (const h of handlerSites) {
|
|
624
|
+
if (e.nodeId === h.nodeId)
|
|
625
|
+
continue;
|
|
626
|
+
const dedupe = `${e.nodeId}>${h.nodeId}`;
|
|
627
|
+
if (seen.has(dedupe))
|
|
628
|
+
continue;
|
|
629
|
+
seen.add(dedupe);
|
|
630
|
+
edges.push({
|
|
631
|
+
source: e.nodeId, target: h.nodeId, kind: 'calls', line: e.line,
|
|
632
|
+
provenance: 'heuristic',
|
|
633
|
+
metadata: { synthesizedBy: 'arkui-emitter', event: eventLabel, registeredAt: `${h.file}:${h.line}` },
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return edges;
|
|
638
|
+
}
|
|
639
|
+
/** `router.pushUrl({ url: 'pages/Detail' })` / replaceUrl — literal urls only. */
|
|
640
|
+
const ARKUI_ROUTER_RE = /\brouter\s*\.\s*(?:pushUrl|replaceUrl)\s*\(\s*\{[^)]{0,200}?\burl\s*:\s*['"]([\w\-./]+)['"]/g;
|
|
641
|
+
/**
|
|
642
|
+
* Phase 4b-ets3: HarmonyOS page navigation. `router.pushUrl({ url:
|
|
643
|
+
* 'pages/Detail' })` reaches the `@Entry struct` of
|
|
644
|
+
* `<module>/src/main/ets/pages/Detail.ets`, but the hop is a string — no
|
|
645
|
+
* static edge — so "tap → openDetail → ???" ends at the router call. Bridge
|
|
646
|
+
* literal urls to the page struct: the url resolves against the standard
|
|
647
|
+
* `src/main/ets/` layout (what main_pages.json entries name); candidates
|
|
648
|
+
* prefer the caller's own workspace module (routes are module-scoped), and
|
|
649
|
+
* anything still ambiguous is dropped rather than guessed. Only `@Entry`
|
|
650
|
+
* structs qualify as targets — the decorator is what makes a file a page.
|
|
651
|
+
*/
|
|
652
|
+
async function arkuiRouterEdges(ctx, onYield) {
|
|
653
|
+
const edges = [];
|
|
654
|
+
const seen = new Set();
|
|
655
|
+
const allFiles = ctx.getAllFiles();
|
|
656
|
+
const moduleDirs = (() => {
|
|
657
|
+
const ws = ctx.getWorkspacePackages?.();
|
|
658
|
+
return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
|
|
659
|
+
})();
|
|
660
|
+
const moduleScopeOf = (file) => {
|
|
661
|
+
for (const dir of moduleDirs) {
|
|
662
|
+
if (file === dir || file.startsWith(dir + '/'))
|
|
663
|
+
return dir;
|
|
664
|
+
}
|
|
665
|
+
return '';
|
|
666
|
+
};
|
|
667
|
+
let scannedFiles = 0;
|
|
668
|
+
for (const file of allFiles) {
|
|
669
|
+
if ((++scannedFiles & 15) === 0)
|
|
670
|
+
await onYield();
|
|
671
|
+
if (!file.endsWith('.ets'))
|
|
672
|
+
continue;
|
|
673
|
+
const content = ctx.readFile(file);
|
|
674
|
+
if (!content || !content.includes('router.'))
|
|
675
|
+
continue;
|
|
676
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
|
|
677
|
+
const nodes = ctx.getNodesInFile(file)
|
|
678
|
+
.filter((n) => n.kind === 'method' || n.kind === 'function');
|
|
679
|
+
ARKUI_ROUTER_RE.lastIndex = 0;
|
|
680
|
+
let m;
|
|
681
|
+
while ((m = ARKUI_ROUTER_RE.exec(safe))) {
|
|
682
|
+
const url = m[1];
|
|
683
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
684
|
+
const encl = nodes
|
|
685
|
+
.filter((n) => n.startLine <= line && n.endLine >= line)
|
|
686
|
+
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
|
|
687
|
+
if (!encl)
|
|
688
|
+
continue;
|
|
689
|
+
const suffix = `/src/main/ets/${url}.ets`;
|
|
690
|
+
let candidates = allFiles.filter((f) => f.endsWith(suffix));
|
|
691
|
+
if (candidates.length > 1) {
|
|
692
|
+
const scope = moduleScopeOf(file);
|
|
693
|
+
const sameModule = candidates.filter((f) => moduleScopeOf(f) === scope);
|
|
694
|
+
if (sameModule.length > 0)
|
|
695
|
+
candidates = sameModule;
|
|
696
|
+
}
|
|
697
|
+
if (candidates.length !== 1)
|
|
698
|
+
continue; // ambiguous or unresolved — never guess
|
|
699
|
+
const page = ctx.getNodesInFile(candidates[0]).find((n) => n.kind === 'struct' && (n.decorators ?? []).includes('Entry'));
|
|
700
|
+
if (!page)
|
|
701
|
+
continue;
|
|
702
|
+
const key = `${encl.id}>${page.id}`;
|
|
703
|
+
if (seen.has(key))
|
|
704
|
+
continue;
|
|
705
|
+
seen.add(key);
|
|
706
|
+
edges.push({
|
|
707
|
+
source: encl.id, target: page.id, kind: 'calls', line,
|
|
708
|
+
provenance: 'heuristic',
|
|
709
|
+
metadata: { synthesizedBy: 'arkui-route', event: url, registeredAt: `${candidates[0]}:${page.startLine}` },
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return edges;
|
|
714
|
+
}
|
|
433
715
|
/**
|
|
434
716
|
* Phase 4c: C++ virtual override. A call through a base/interface pointer
|
|
435
717
|
* (`db->Get(...)`, `iter->Next()`) dispatches at runtime to a subclass override,
|
|
@@ -440,14 +722,17 @@ function flutterBuildEdges(queries, ctx) {
|
|
|
440
722
|
* implementation(s). Over-approximation accepted (reachability-correct); capped
|
|
441
723
|
* per class and gated to C++ to avoid touching other languages' dispatch.
|
|
442
724
|
*/
|
|
443
|
-
function cppOverrideEdges(queries) {
|
|
725
|
+
async function cppOverrideEdges(queries, onYield) {
|
|
726
|
+
let scanned255 = 0;
|
|
444
727
|
const edges = [];
|
|
445
728
|
const seen = new Set();
|
|
446
729
|
const methodsOf = (classId) => queries
|
|
447
730
|
.getOutgoingEdges(classId, ['contains'])
|
|
448
731
|
.map((e) => queries.getNodeById(e.target))
|
|
449
732
|
.filter((n) => !!n && n.kind === 'method');
|
|
450
|
-
for (const cls of queries.
|
|
733
|
+
for (const cls of queries.iterateNodesByKind('class')) {
|
|
734
|
+
if ((++scanned255 & 63) === 0)
|
|
735
|
+
await onYield();
|
|
451
736
|
const subMethods = methodsOf(cls.id).filter((n) => n.language === 'cpp');
|
|
452
737
|
if (subMethods.length === 0)
|
|
453
738
|
continue;
|
|
@@ -501,6 +786,7 @@ function cppOverrideEdges(queries) {
|
|
|
501
786
|
// or an `object` (Scala) so the loop also iterates those kinds.
|
|
502
787
|
const IFACE_OVERRIDE_LANGS = new Set([
|
|
503
788
|
'java', 'kotlin', 'csharp', 'typescript', 'javascript', 'swift', 'scala', 'go', 'rust',
|
|
789
|
+
'arkts',
|
|
504
790
|
]);
|
|
505
791
|
/**
|
|
506
792
|
* Go implicit interface satisfaction (#584). Go has no `implements` keyword — a
|
|
@@ -514,7 +800,8 @@ const IFACE_OVERRIDE_LANGS = new Set([
|
|
|
514
800
|
* with the other dispatch synthesizers; capped per interface. Empty interfaces
|
|
515
801
|
* (`any`) are skipped so they don't match every struct.
|
|
516
802
|
*/
|
|
517
|
-
function goImplementsEdges(queries) {
|
|
803
|
+
async function goImplementsEdges(queries, onYield) {
|
|
804
|
+
let scanned255 = 0;
|
|
518
805
|
const edges = [];
|
|
519
806
|
const seen = new Set();
|
|
520
807
|
const methodNameSet = (id) => new Set(queries
|
|
@@ -522,11 +809,24 @@ function goImplementsEdges(queries) {
|
|
|
522
809
|
.map((e) => queries.getNodeById(e.target))
|
|
523
810
|
.filter((n) => !!n && n.kind === 'method')
|
|
524
811
|
.map((n) => n.name));
|
|
525
|
-
|
|
812
|
+
// Materializes GO structs only (the pass is language-gated by the caller),
|
|
813
|
+
// never the whole struct kind — that array is O(nodes) on struct-heavy
|
|
814
|
+
// repos like the Linux kernel (#1212).
|
|
815
|
+
const goStructs = [];
|
|
816
|
+
for (const s of queries.iterateNodesByKind('struct')) {
|
|
817
|
+
if ((++scanned255 & 63) === 0)
|
|
818
|
+
await onYield();
|
|
819
|
+
if (s.language === 'go')
|
|
820
|
+
goStructs.push(s);
|
|
821
|
+
}
|
|
526
822
|
const structMethods = new Map();
|
|
527
823
|
for (const s of goStructs)
|
|
528
824
|
structMethods.set(s.id, methodNameSet(s.id));
|
|
529
|
-
for (const iface of queries.
|
|
825
|
+
for (const iface of queries.iterateNodesByKind('interface')) {
|
|
826
|
+
if ((++scanned255 & 63) === 0)
|
|
827
|
+
await onYield();
|
|
828
|
+
if ((++scanned255 & 63) === 0)
|
|
829
|
+
await onYield();
|
|
530
830
|
if (iface.language !== 'go')
|
|
531
831
|
continue;
|
|
532
832
|
const want = methodNameSet(iface.id);
|
|
@@ -586,7 +886,8 @@ function goImplementsEdges(queries) {
|
|
|
586
886
|
* matching the same-file edges extraction already emits). Skips methods that
|
|
587
887
|
* already have a type parent (the same-file case). (#583, cross-file half)
|
|
588
888
|
*/
|
|
589
|
-
function goCrossFileMethodContainsEdges(queries) {
|
|
889
|
+
async function goCrossFileMethodContainsEdges(queries, onYield) {
|
|
890
|
+
let scanned255 = 0;
|
|
590
891
|
const edges = [];
|
|
591
892
|
const seen = new Set();
|
|
592
893
|
const TYPE_KINDS = new Set(['struct', 'class', 'interface', 'enum', 'type_alias']);
|
|
@@ -594,7 +895,11 @@ function goCrossFileMethodContainsEdges(queries) {
|
|
|
594
895
|
const i = p.replace(/\\/g, '/').lastIndexOf('/');
|
|
595
896
|
return i >= 0 ? p.slice(0, i) : '';
|
|
596
897
|
};
|
|
597
|
-
for (const method of queries.
|
|
898
|
+
for (const method of queries.iterateNodesByKind('method')) {
|
|
899
|
+
if ((++scanned255 & 63) === 0)
|
|
900
|
+
await onYield();
|
|
901
|
+
if ((++scanned255 & 63) === 0)
|
|
902
|
+
await onYield();
|
|
598
903
|
if (method.language !== 'go')
|
|
599
904
|
continue;
|
|
600
905
|
// The receiver type is encoded in the method's qualifiedName as `Recv::name`
|
|
@@ -664,13 +969,20 @@ const KMP_TYPE_KINDS = new Set(['class', 'interface', 'struct', 'enum', 'type_al
|
|
|
664
969
|
function kmpKindsCompatible(a, b) {
|
|
665
970
|
return a === b || (KMP_TYPE_KINDS.has(a) && KMP_TYPE_KINDS.has(b));
|
|
666
971
|
}
|
|
667
|
-
function kotlinExpectActualEdges(queries) {
|
|
972
|
+
async function kotlinExpectActualEdges(queries, onYield) {
|
|
973
|
+
let scanned255 = 0;
|
|
668
974
|
const edges = [];
|
|
669
975
|
const seen = new Set();
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
976
|
+
// SQL-side language+decorator pre-filter, streamed. The old
|
|
977
|
+
// `getAllNodes().filter(...)` hydrated the ENTIRE node table into one array
|
|
978
|
+
// just to find kotlin `actual` declarations — on a 2M-node graph that alone
|
|
979
|
+
// exceeded Node's default heap and killed the index (#1212). The LIKE
|
|
980
|
+
// pre-filter can over-match (substring), so the exact decorator check stays.
|
|
981
|
+
for (const act of queries.iterateNodesByLanguageWithDecorator('kotlin', 'actual')) {
|
|
982
|
+
if ((++scanned255 & 63) === 0)
|
|
983
|
+
await onYield();
|
|
984
|
+
if (!act.decorators?.includes('actual'))
|
|
985
|
+
continue;
|
|
674
986
|
let added = 0;
|
|
675
987
|
for (const cand of queries.getNodesByQualifiedNameExact(act.qualifiedName)) {
|
|
676
988
|
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
@@ -704,7 +1016,8 @@ function kotlinExpectActualEdges(queries) {
|
|
|
704
1016
|
}
|
|
705
1017
|
return edges;
|
|
706
1018
|
}
|
|
707
|
-
function interfaceOverrideEdges(queries) {
|
|
1019
|
+
async function interfaceOverrideEdges(queries, onYield) {
|
|
1020
|
+
let scanned255 = 0;
|
|
708
1021
|
const edges = [];
|
|
709
1022
|
const seen = new Set();
|
|
710
1023
|
const methodsOf = (classId) => queries
|
|
@@ -716,7 +1029,9 @@ function interfaceOverrideEdges(queries) {
|
|
|
716
1029
|
// types that conform to protocols. Iterate both.
|
|
717
1030
|
const concreteKinds = ['class', 'struct'];
|
|
718
1031
|
for (const kind of concreteKinds) {
|
|
719
|
-
for (const cls of queries.
|
|
1032
|
+
for (const cls of queries.iterateNodesByKind(kind)) {
|
|
1033
|
+
if ((++scanned255 & 63) === 0)
|
|
1034
|
+
await onYield();
|
|
720
1035
|
const implMethods = methodsOf(cls.id).filter((n) => IFACE_OVERRIDE_LANGS.has(n.language));
|
|
721
1036
|
if (implMethods.length === 0)
|
|
722
1037
|
continue;
|
|
@@ -790,7 +1105,8 @@ function interfaceOverrideEdges(queries) {
|
|
|
790
1105
|
* Provenance: `heuristic`, `synthesizedBy: 'go-grpc-stub-impl'`. The
|
|
791
1106
|
* stub's source line is the wiring site shown in the trace trail.
|
|
792
1107
|
*/
|
|
793
|
-
function goGrpcStubImplEdges(queries) {
|
|
1108
|
+
async function goGrpcStubImplEdges(queries, onYield) {
|
|
1109
|
+
let scanned255 = 0;
|
|
794
1110
|
const edges = [];
|
|
795
1111
|
const seen = new Set();
|
|
796
1112
|
const STUB_RE = /^Unimplemented.*Server$/;
|
|
@@ -802,7 +1118,9 @@ function goGrpcStubImplEdges(queries) {
|
|
|
802
1118
|
const methodNamesByStruct = new Map();
|
|
803
1119
|
const methodNodesByStruct = new Map();
|
|
804
1120
|
const goStructs = [];
|
|
805
|
-
for (const s of queries.
|
|
1121
|
+
for (const s of queries.iterateNodesByKind('struct')) {
|
|
1122
|
+
if ((++scanned255 & 63) === 0)
|
|
1123
|
+
await onYield();
|
|
806
1124
|
if (s.language !== 'go')
|
|
807
1125
|
continue;
|
|
808
1126
|
goStructs.push(s);
|
|
@@ -886,11 +1204,14 @@ function goGrpcStubImplEdges(queries) {
|
|
|
886
1204
|
* (or nothing) and are dropped.
|
|
887
1205
|
*/
|
|
888
1206
|
async function reactJsxChildEdges(ctx, onYield) {
|
|
1207
|
+
let scannedFiles = 0;
|
|
889
1208
|
const edges = [];
|
|
890
1209
|
const seen = new Set();
|
|
891
1210
|
const PARENT_KINDS = new Set(['method', 'function', 'component']);
|
|
892
1211
|
let scanned = 0;
|
|
893
1212
|
for (const file of ctx.getAllFiles()) {
|
|
1213
|
+
if ((++scannedFiles & 15) === 0)
|
|
1214
|
+
await onYield();
|
|
894
1215
|
if ((++scanned & 255) === 0)
|
|
895
1216
|
await onYield(); // #1091: yield mid-scan on huge graphs
|
|
896
1217
|
const content = ctx.readFile(file);
|
|
@@ -940,7 +1261,9 @@ async function reactJsxChildEdges(ctx, onYield) {
|
|
|
940
1261
|
* component, handler→function/method) keeps precision; inline arrows / `$emit`
|
|
941
1262
|
* skipped.
|
|
942
1263
|
*/
|
|
943
|
-
function vueTemplateEdges(ctx) {
|
|
1264
|
+
async function vueTemplateEdges(ctx, onYield) {
|
|
1265
|
+
let scannedFiles = 0;
|
|
1266
|
+
let scanned255 = 0;
|
|
944
1267
|
const edges = [];
|
|
945
1268
|
const seen = new Set();
|
|
946
1269
|
const COMPONENT_KINDS = new Set(['component', 'function', 'class']);
|
|
@@ -954,12 +1277,16 @@ function vueTemplateEdges(ctx) {
|
|
|
954
1277
|
// misses it (flat components match by basename and don't need this). Map each
|
|
955
1278
|
// nested component's Nuxt name → node so those template usages resolve.
|
|
956
1279
|
const nuxtComponents = new Map();
|
|
957
|
-
for (const c of ctx.getNodesByKind('component')) {
|
|
1280
|
+
for (const c of (ctx.iterateNodesByKind?.('component') ?? ctx.getNodesByKind('component'))) {
|
|
1281
|
+
if ((++scanned255 & 63) === 0)
|
|
1282
|
+
await onYield();
|
|
958
1283
|
const nn = nuxtComponentName(c.filePath);
|
|
959
1284
|
if (nn && !nuxtComponents.has(nn))
|
|
960
1285
|
nuxtComponents.set(nn, c);
|
|
961
1286
|
}
|
|
962
1287
|
for (const file of ctx.getAllFiles()) {
|
|
1288
|
+
if ((++scannedFiles & 15) === 0)
|
|
1289
|
+
await onYield();
|
|
963
1290
|
if (!file.endsWith('.vue'))
|
|
964
1291
|
continue;
|
|
965
1292
|
const content = ctx.readFile(file);
|
|
@@ -1093,13 +1420,16 @@ const RN_JVM_EMIT_RE = /\.emit\s*\(\s*"([^"]+)"\s*,/g;
|
|
|
1093
1420
|
// statement and stops at a block boundary, so the wrapper DEFINITION (whose `(`
|
|
1094
1421
|
// is followed by `… ) {`) never matches. Multi-line tolerant. (java/kotlin/swift)
|
|
1095
1422
|
const RN_NATIVE_SENDEVENT_RE = /\bsendEvent\s*\([^;{}]*?"([^"]+)"/g;
|
|
1096
|
-
function rnEventEdges(ctx) {
|
|
1423
|
+
async function rnEventEdges(ctx, onYield) {
|
|
1424
|
+
let scannedFiles = 0;
|
|
1097
1425
|
// Native dispatchers (source = the native method whose body sends the
|
|
1098
1426
|
// event) and JS handlers (target = the function/method registered as
|
|
1099
1427
|
// the listener) keyed by event name.
|
|
1100
1428
|
const nativeDispatchersByEvent = new Map();
|
|
1101
1429
|
const jsHandlersByEvent = new Map();
|
|
1102
1430
|
for (const file of ctx.getAllFiles()) {
|
|
1431
|
+
if ((++scannedFiles & 15) === 0)
|
|
1432
|
+
await onYield();
|
|
1103
1433
|
const content = ctx.readFile(file);
|
|
1104
1434
|
if (!content)
|
|
1105
1435
|
continue;
|
|
@@ -1287,11 +1617,14 @@ const FABRIC_NATIVE_SUFFIXES = ['', 'View', 'ViewManager', 'ComponentView', 'Man
|
|
|
1287
1617
|
* caller. The Expo method nodes are id-prefixed `expo-module:` and qualified
|
|
1288
1618
|
* `<file>::<module>.<method>` by the framework extractor.
|
|
1289
1619
|
*/
|
|
1290
|
-
function expoCrossPlatformEdges(queries) {
|
|
1620
|
+
async function expoCrossPlatformEdges(queries, onYield) {
|
|
1621
|
+
let scanned255 = 0;
|
|
1291
1622
|
const edges = [];
|
|
1292
1623
|
const seen = new Set();
|
|
1293
1624
|
const byKey = new Map();
|
|
1294
|
-
for (const m of queries.
|
|
1625
|
+
for (const m of queries.iterateNodesByKind('method')) {
|
|
1626
|
+
if ((++scanned255 & 63) === 0)
|
|
1627
|
+
await onYield();
|
|
1295
1628
|
if (!m.id.startsWith('expo-module:'))
|
|
1296
1629
|
continue;
|
|
1297
1630
|
const key = m.qualifiedName.split('::').pop(); // `<module>.<method>`
|
|
@@ -1340,7 +1673,8 @@ function expoCrossPlatformEdges(queries) {
|
|
|
1340
1673
|
* `getFreeDiskStorage`) — that's the JS-visible name, and how the iOS selector
|
|
1341
1674
|
* lines up with the bare Android method name.
|
|
1342
1675
|
*/
|
|
1343
|
-
function rnCrossPlatformEdges(queries) {
|
|
1676
|
+
async function rnCrossPlatformEdges(queries, onYield) {
|
|
1677
|
+
let scanned255 = 0;
|
|
1344
1678
|
const edges = [];
|
|
1345
1679
|
const seen = new Set();
|
|
1346
1680
|
const NATIVE = new Set(['java', 'kotlin', 'objc', 'cpp']);
|
|
@@ -1362,6 +1696,8 @@ function rnCrossPlatformEdges(queries) {
|
|
|
1362
1696
|
// below only runs for genuine cross-platform candidates.
|
|
1363
1697
|
const byName = new Map();
|
|
1364
1698
|
for (const m of queries.iterateNodesByKind('method')) {
|
|
1699
|
+
if ((++scanned255 & 63) === 0)
|
|
1700
|
+
await onYield();
|
|
1365
1701
|
if (!NATIVE.has(m.language))
|
|
1366
1702
|
continue;
|
|
1367
1703
|
const key = norm(m.name);
|
|
@@ -1412,17 +1748,27 @@ function rnCrossPlatformEdges(queries) {
|
|
|
1412
1748
|
}
|
|
1413
1749
|
return edges;
|
|
1414
1750
|
}
|
|
1415
|
-
function fabricNativeImplEdges(ctx) {
|
|
1751
|
+
async function fabricNativeImplEdges(ctx, onYield) {
|
|
1752
|
+
let scanned255 = 0;
|
|
1416
1753
|
const edges = [];
|
|
1417
1754
|
const seen = new Set();
|
|
1418
1755
|
// The Fabric extractor IDs are prefixed `fabric-component:` so we can
|
|
1419
|
-
// filter to just those
|
|
1420
|
-
|
|
1756
|
+
// filter to just those while streaming — never materializing the whole
|
|
1757
|
+
// `component` kind (#1212).
|
|
1758
|
+
const components = [];
|
|
1759
|
+
for (const n of (ctx.iterateNodesByKind?.('component') ?? ctx.getNodesByKind('component'))) {
|
|
1760
|
+
if ((++scanned255 & 63) === 0)
|
|
1761
|
+
await onYield();
|
|
1762
|
+
if (n.id.startsWith('fabric-component:'))
|
|
1763
|
+
components.push(n);
|
|
1764
|
+
}
|
|
1421
1765
|
if (components.length === 0)
|
|
1422
1766
|
return edges;
|
|
1423
1767
|
// Pre-index native classes by name for O(1) lookup.
|
|
1424
1768
|
const nativeClassesByName = new Map();
|
|
1425
|
-
for (const n of ctx.getNodesByKind('class')) {
|
|
1769
|
+
for (const n of (ctx.iterateNodesByKind?.('class') ?? ctx.getNodesByKind('class'))) {
|
|
1770
|
+
if ((++scanned255 & 63) === 0)
|
|
1771
|
+
await onYield();
|
|
1426
1772
|
if (n.language !== 'objc' && n.language !== 'kotlin' && n.language !== 'java' && n.language !== 'cpp')
|
|
1427
1773
|
continue;
|
|
1428
1774
|
const arr = nativeClassesByName.get(n.name);
|
|
@@ -1475,12 +1821,15 @@ function fabricNativeImplEdges(ctx) {
|
|
|
1475
1821
|
* same simple name) are dropped. We need-not bridge by package because Java
|
|
1476
1822
|
* mapper interfaces are typically uniquely named within a project.
|
|
1477
1823
|
*/
|
|
1478
|
-
function mybatisJavaXmlEdges(queries) {
|
|
1824
|
+
async function mybatisJavaXmlEdges(queries, onYield) {
|
|
1825
|
+
let scanned255 = 0;
|
|
1479
1826
|
const edges = [];
|
|
1480
1827
|
const seen = new Set();
|
|
1481
1828
|
// Index Java methods by `<ClassName>::<methodName>` for O(1) lookup.
|
|
1482
1829
|
const javaIndex = new Map();
|
|
1483
1830
|
for (const m of queries.iterateNodesByKind('method')) {
|
|
1831
|
+
if ((++scanned255 & 63) === 0)
|
|
1832
|
+
await onYield();
|
|
1484
1833
|
if (m.language !== 'java' && m.language !== 'kotlin')
|
|
1485
1834
|
continue;
|
|
1486
1835
|
const parts = m.qualifiedName.split('::');
|
|
@@ -1496,6 +1845,8 @@ function mybatisJavaXmlEdges(queries) {
|
|
|
1496
1845
|
javaIndex.set(key, [m]);
|
|
1497
1846
|
}
|
|
1498
1847
|
for (const xml of queries.iterateNodesByKind('method')) {
|
|
1848
|
+
if ((++scanned255 & 63) === 0)
|
|
1849
|
+
await onYield();
|
|
1499
1850
|
if (xml.language !== 'xml')
|
|
1500
1851
|
continue;
|
|
1501
1852
|
// Qualified name: `<namespace>::<id>`. Extract the simple class name.
|
|
@@ -1609,10 +1960,14 @@ function goHandlerIdent(expr) {
|
|
|
1609
1960
|
const m = cleaned.match(/(?:\.|^)([A-Za-z_]\w*)$/);
|
|
1610
1961
|
return m ? m[1] : null;
|
|
1611
1962
|
}
|
|
1612
|
-
function ginMiddlewareChainEdges(queries, ctx) {
|
|
1963
|
+
async function ginMiddlewareChainEdges(queries, ctx, onYield) {
|
|
1964
|
+
let scanned255 = 0;
|
|
1965
|
+
let scannedFiles = 0;
|
|
1613
1966
|
// 1. Find the chain dispatcher(s): a Go method that invokes a `handlers` slice by index.
|
|
1614
1967
|
const dispatchers = [];
|
|
1615
1968
|
for (const n of queries.iterateNodesByKind('method')) {
|
|
1969
|
+
if ((++scanned255 & 63) === 0)
|
|
1970
|
+
await onYield();
|
|
1616
1971
|
if (n.language !== 'go')
|
|
1617
1972
|
continue;
|
|
1618
1973
|
const content = ctx.readFile(n.filePath);
|
|
@@ -1627,6 +1982,8 @@ function ginMiddlewareChainEdges(queries, ctx) {
|
|
|
1627
1982
|
// closures are dropped by goHandlerIdent; the rest are HandlerFuncs.
|
|
1628
1983
|
const registered = new Map(); // name → registeredAt (file:line)
|
|
1629
1984
|
for (const file of ctx.getAllFiles()) {
|
|
1985
|
+
if ((++scannedFiles & 15) === 0)
|
|
1986
|
+
await onYield();
|
|
1630
1987
|
if (!file.endsWith('.go'))
|
|
1631
1988
|
continue;
|
|
1632
1989
|
const content = ctx.readFile(file);
|
|
@@ -1682,10 +2039,13 @@ function ginMiddlewareChainEdges(queries, ctx) {
|
|
|
1682
2039
|
* clause. Link the unit → its form so a `.dfm`/`.fmx` used only as a form
|
|
1683
2040
|
* definition isn't orphaned, and editing the form surfaces its code-behind unit.
|
|
1684
2041
|
*/
|
|
1685
|
-
function pascalFormEdges(ctx) {
|
|
2042
|
+
async function pascalFormEdges(ctx, onYield) {
|
|
2043
|
+
let scannedFiles = 0;
|
|
1686
2044
|
const edges = [];
|
|
1687
2045
|
const allFiles = new Set(ctx.getAllFiles());
|
|
1688
2046
|
for (const file of allFiles) {
|
|
2047
|
+
if ((++scannedFiles & 255) === 0)
|
|
2048
|
+
await onYield();
|
|
1689
2049
|
if (!/\.(dfm|fmx)$/i.test(file))
|
|
1690
2050
|
continue;
|
|
1691
2051
|
const pasFile = file.replace(/\.(dfm|fmx)$/i, '.pas');
|
|
@@ -1721,12 +2081,15 @@ function pascalFormEdges(ctx) {
|
|
|
1721
2081
|
* a loader's data shows the page it feeds) and the page's dependencies include
|
|
1722
2082
|
* its loader.
|
|
1723
2083
|
*/
|
|
1724
|
-
function svelteKitLoadEdges(ctx) {
|
|
2084
|
+
async function svelteKitLoadEdges(ctx, onYield) {
|
|
2085
|
+
let scannedFiles = 0;
|
|
1725
2086
|
const edges = [];
|
|
1726
2087
|
const allFiles = new Set(ctx.getAllFiles());
|
|
1727
2088
|
const HOOKS = new Set(['load', 'actions']);
|
|
1728
2089
|
const HOOK_KINDS = new Set(['function', 'method', 'constant', 'variable']);
|
|
1729
2090
|
for (const file of allFiles) {
|
|
2091
|
+
if ((++scannedFiles & 255) === 0)
|
|
2092
|
+
await onYield();
|
|
1730
2093
|
const m = file.match(/(.*\/)(\+(?:page|layout))\.svelte$/);
|
|
1731
2094
|
if (!m)
|
|
1732
2095
|
continue;
|
|
@@ -1775,10 +2138,13 @@ function svelteKitLoadEdges(ctx) {
|
|
|
1775
2138
|
const THUNK_DECL_RE = /create(?:Async)?Thunk/;
|
|
1776
2139
|
const THUNK_DISPATCH_RE = /\bdispatch\s*\(\s*([A-Za-z_]\w*)\s*[(),]/g;
|
|
1777
2140
|
const THUNK_FANOUT_CAP = 24;
|
|
1778
|
-
function reduxThunkEdges(queries, ctx) {
|
|
2141
|
+
async function reduxThunkEdges(queries, ctx, onYield) {
|
|
2142
|
+
let scanned255 = 0;
|
|
1779
2143
|
const edges = [];
|
|
1780
2144
|
const seen = new Set();
|
|
1781
2145
|
for (const node of queries.iterateNodesByKind('constant')) {
|
|
2146
|
+
if ((++scanned255 & 63) === 0)
|
|
2147
|
+
await onYield();
|
|
1782
2148
|
// Cheap gate: the initializer (captured in `signature`) must be a create(Async)Thunk call —
|
|
1783
2149
|
// avoids reading every constant's body on a large repo.
|
|
1784
2150
|
if (!node.signature || !THUNK_DECL_RE.test(node.signature))
|
|
@@ -1910,10 +2276,13 @@ function resolveRegistryHandler(ctx, name, chained) {
|
|
|
1910
2276
|
return cands.find((n) => n.kind === 'method') ?? null;
|
|
1911
2277
|
}
|
|
1912
2278
|
async function objectRegistryEdges(ctx, onYield) {
|
|
2279
|
+
let scannedFiles = 0;
|
|
1913
2280
|
const edges = [];
|
|
1914
2281
|
const seen = new Set();
|
|
1915
2282
|
let scanned = 0;
|
|
1916
2283
|
for (const file of ctx.getAllFiles()) {
|
|
2284
|
+
if ((++scannedFiles & 15) === 0)
|
|
2285
|
+
await onYield();
|
|
1917
2286
|
if ((++scanned & 255) === 0)
|
|
1918
2287
|
await onYield(); // #1091: yield mid-scan on huge graphs
|
|
1919
2288
|
if (!REGISTRY_JS_EXT.test(file))
|
|
@@ -2025,10 +2394,13 @@ function rtkEndpointNameFromHook(hook) {
|
|
|
2025
2394
|
return null;
|
|
2026
2395
|
return mid.charAt(0).toLowerCase() + mid.slice(1);
|
|
2027
2396
|
}
|
|
2028
|
-
function rtkQueryEdges(queries, ctx) {
|
|
2397
|
+
async function rtkQueryEdges(queries, ctx, onYield) {
|
|
2398
|
+
let scanned255 = 0;
|
|
2029
2399
|
const edges = [];
|
|
2030
2400
|
const seen = new Set();
|
|
2031
2401
|
for (const hook of queries.iterateNodesByKind('function')) {
|
|
2402
|
+
if ((++scanned255 & 63) === 0)
|
|
2403
|
+
await onYield();
|
|
2032
2404
|
// Only our extracted generated-hook bindings (sentinel) — not a real hook fn.
|
|
2033
2405
|
if (hook.signature !== RTK_GENERATED_HOOK_SIGNATURE)
|
|
2034
2406
|
continue;
|
|
@@ -2072,10 +2444,13 @@ const PINIA_FACTORY_RE = /\b(?:export\s+)?const\s+(\w+)\s*=\s*defineStore\s*\(/g
|
|
|
2072
2444
|
const PINIA_BIND_RE = /\bconst\s+(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/g;
|
|
2073
2445
|
const PINIA_CALL_RE = /(\w+)\s*\.\s*(\w+)\s*\(/g;
|
|
2074
2446
|
const PINIA_FANOUT_CAP = 80;
|
|
2075
|
-
function piniaStoreEdges(ctx) {
|
|
2447
|
+
async function piniaStoreEdges(ctx, onYield) {
|
|
2448
|
+
let scannedFiles = 0;
|
|
2076
2449
|
// 1. Map each `const useXStore = defineStore(...)` factory → its store file.
|
|
2077
2450
|
const factoryFile = new Map();
|
|
2078
2451
|
for (const file of ctx.getAllFiles()) {
|
|
2452
|
+
if ((++scannedFiles & 15) === 0)
|
|
2453
|
+
await onYield();
|
|
2079
2454
|
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2080
2455
|
continue;
|
|
2081
2456
|
const content = ctx.readFile(file);
|
|
@@ -2091,6 +2466,8 @@ function piniaStoreEdges(ctx) {
|
|
|
2091
2466
|
const edges = [];
|
|
2092
2467
|
const seen = new Set();
|
|
2093
2468
|
for (const file of ctx.getAllFiles()) {
|
|
2469
|
+
if ((++scannedFiles & 15) === 0)
|
|
2470
|
+
await onYield();
|
|
2094
2471
|
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2095
2472
|
continue;
|
|
2096
2473
|
const content = ctx.readFile(file);
|
|
@@ -2163,7 +2540,8 @@ const VUEX_FANOUT_CAP = 120;
|
|
|
2163
2540
|
function pathHasSegment(filePath, seg) {
|
|
2164
2541
|
return new RegExp('[\\\\/]' + seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\\\\/.]').test(filePath);
|
|
2165
2542
|
}
|
|
2166
|
-
function vuexDispatchEdges(ctx) {
|
|
2543
|
+
async function vuexDispatchEdges(ctx, onYield) {
|
|
2544
|
+
let scannedFiles = 0;
|
|
2167
2545
|
const storeFileCache = new Map();
|
|
2168
2546
|
const isStoreFile = (file) => {
|
|
2169
2547
|
let v = storeFileCache.get(file);
|
|
@@ -2201,6 +2579,8 @@ function vuexDispatchEdges(ctx) {
|
|
|
2201
2579
|
const edges = [];
|
|
2202
2580
|
const seen = new Set();
|
|
2203
2581
|
for (const file of ctx.getAllFiles()) {
|
|
2582
|
+
if ((++scannedFiles & 15) === 0)
|
|
2583
|
+
await onYield();
|
|
2204
2584
|
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2205
2585
|
continue;
|
|
2206
2586
|
const content = ctx.readFile(file);
|
|
@@ -2260,7 +2640,8 @@ const CELERY_TASK_DECORATOR_RE = /@\s*(?:[A-Za-z_][\w.]*\.)?(?:shared_task|task)
|
|
|
2260
2640
|
const CELERY_PY_EXT = /\.py$/;
|
|
2261
2641
|
const CELERY_FANOUT_CAP = 80;
|
|
2262
2642
|
const CELERY_DECORATOR_LOOKBACK = 12; // max lines above a `def` to scan for its decorators
|
|
2263
|
-
function celeryDispatchEdges(ctx) {
|
|
2643
|
+
async function celeryDispatchEdges(ctx, onYield) {
|
|
2644
|
+
let scannedFiles = 0;
|
|
2264
2645
|
// Memoize the decorator check per task-candidate node: it reads the file and scans a few
|
|
2265
2646
|
// lines above the def. Only called on names that are actually `.delay`/`.apply_async`
|
|
2266
2647
|
// receivers, so the candidate set stays small.
|
|
@@ -2304,6 +2685,8 @@ function celeryDispatchEdges(ctx) {
|
|
|
2304
2685
|
const edges = [];
|
|
2305
2686
|
const seen = new Set();
|
|
2306
2687
|
for (const file of ctx.getAllFiles()) {
|
|
2688
|
+
if ((++scannedFiles & 15) === 0)
|
|
2689
|
+
await onYield();
|
|
2307
2690
|
if (!CELERY_PY_EXT.test(file))
|
|
2308
2691
|
continue;
|
|
2309
2692
|
const content = ctx.readFile(file);
|
|
@@ -2382,15 +2765,24 @@ function springFirstParamType(sig) {
|
|
|
2382
2765
|
const type = toks[toks.length - 2].replace(/<.*$/, ''); // drop generic args
|
|
2383
2766
|
return /^[A-Z][A-Za-z0-9_]*$/.test(type) ? type : null;
|
|
2384
2767
|
}
|
|
2385
|
-
function springEventEdges(ctx) {
|
|
2768
|
+
async function springEventEdges(ctx, onYield) {
|
|
2769
|
+
let scannedFiles = 0;
|
|
2386
2770
|
// Pass 1 — event-type → listener methods, scanning only event-relevant files.
|
|
2771
|
+
// This is the ONLY full read sweep: publisher files are recorded here so
|
|
2772
|
+
// pass 2 re-reads just those instead of every .java file again (#1212 —
|
|
2773
|
+
// the double full-repo read was one of the tail's longest unyielded spans).
|
|
2387
2774
|
const listeners = new Map();
|
|
2775
|
+
const publisherFiles = [];
|
|
2388
2776
|
for (const file of ctx.getAllFiles()) {
|
|
2777
|
+
if ((++scannedFiles & 15) === 0)
|
|
2778
|
+
await onYield();
|
|
2389
2779
|
if (!SPRING_JAVA_EXT.test(file))
|
|
2390
2780
|
continue;
|
|
2391
2781
|
const content = ctx.readFile(file);
|
|
2392
2782
|
if (!content)
|
|
2393
2783
|
continue;
|
|
2784
|
+
if (content.includes('.publishEvent('))
|
|
2785
|
+
publisherFiles.push(file);
|
|
2394
2786
|
const hasAnno = content.includes('@EventListener') || content.includes('@TransactionalEventListener');
|
|
2395
2787
|
const hasAppListener = SPRING_APP_LISTENER_RE.test(content);
|
|
2396
2788
|
if (!hasAnno && !hasAppListener)
|
|
@@ -2430,12 +2822,13 @@ function springEventEdges(ctx) {
|
|
|
2430
2822
|
}
|
|
2431
2823
|
if (!listeners.size)
|
|
2432
2824
|
return [];
|
|
2433
|
-
// Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of
|
|
2825
|
+
// Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of
|
|
2826
|
+
// XEvent. Only the publisher files recorded in pass 1 are (re-)read.
|
|
2434
2827
|
const edges = [];
|
|
2435
2828
|
const seen = new Set();
|
|
2436
|
-
for (const file of
|
|
2437
|
-
if (
|
|
2438
|
-
|
|
2829
|
+
for (const file of publisherFiles) {
|
|
2830
|
+
if ((++scannedFiles & 15) === 0)
|
|
2831
|
+
await onYield();
|
|
2439
2832
|
const content = ctx.readFile(file);
|
|
2440
2833
|
if (!content || !content.includes('.publishEvent('))
|
|
2441
2834
|
continue;
|
|
@@ -2520,10 +2913,13 @@ function resolveMediatrArgType(arg, lines, methodStart, dispatchLine) {
|
|
|
2520
2913
|
}
|
|
2521
2914
|
return declType;
|
|
2522
2915
|
}
|
|
2523
|
-
function mediatrDispatchEdges(ctx) {
|
|
2916
|
+
async function mediatrDispatchEdges(ctx, onYield) {
|
|
2917
|
+
let scannedFiles = 0;
|
|
2524
2918
|
// Pass 1 — request/notification type → the Handle method of each handler class.
|
|
2525
2919
|
const handlers = new Map();
|
|
2526
2920
|
for (const file of ctx.getAllFiles()) {
|
|
2921
|
+
if ((++scannedFiles & 15) === 0)
|
|
2922
|
+
await onYield();
|
|
2527
2923
|
if (!MEDIATR_CS_EXT.test(file))
|
|
2528
2924
|
continue;
|
|
2529
2925
|
const content = ctx.readFile(file);
|
|
@@ -2557,6 +2953,8 @@ function mediatrDispatchEdges(ctx) {
|
|
|
2557
2953
|
const edges = [];
|
|
2558
2954
|
const seen = new Set();
|
|
2559
2955
|
for (const file of ctx.getAllFiles()) {
|
|
2956
|
+
if ((++scannedFiles & 15) === 0)
|
|
2957
|
+
await onYield();
|
|
2560
2958
|
if (!MEDIATR_CS_EXT.test(file))
|
|
2561
2959
|
continue;
|
|
2562
2960
|
const content = ctx.readFile(file);
|
|
@@ -2620,7 +3018,8 @@ const SIDEKIQ_DISPATCH_RE = /([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)\s*\.\
|
|
|
2620
3018
|
const SIDEKIQ_WORKER_RE = /\binclude\s+Sidekiq::(?:Job|Worker)\b/;
|
|
2621
3019
|
const SIDEKIQ_RB_EXT = /\.rb$/;
|
|
2622
3020
|
const SIDEKIQ_FANOUT_CAP = 80;
|
|
2623
|
-
function sidekiqDispatchEdges(ctx) {
|
|
3021
|
+
async function sidekiqDispatchEdges(ctx, onYield) {
|
|
3022
|
+
let scannedFiles = 0;
|
|
2624
3023
|
// class node id → its instance `perform` method (null if the class isn't a Sidekiq worker),
|
|
2625
3024
|
// memoized. Reads the class body for the mixin; only consulted for actual dispatch receivers.
|
|
2626
3025
|
const performCache = new Map();
|
|
@@ -2657,6 +3056,8 @@ function sidekiqDispatchEdges(ctx) {
|
|
|
2657
3056
|
const edges = [];
|
|
2658
3057
|
const seen = new Set();
|
|
2659
3058
|
for (const file of ctx.getAllFiles()) {
|
|
3059
|
+
if ((++scannedFiles & 15) === 0)
|
|
3060
|
+
await onYield();
|
|
2660
3061
|
if (!SIDEKIQ_RB_EXT.test(file))
|
|
2661
3062
|
continue;
|
|
2662
3063
|
const content = ctx.readFile(file);
|
|
@@ -2692,6 +3093,414 @@ function sidekiqDispatchEdges(ctx) {
|
|
|
2692
3093
|
}
|
|
2693
3094
|
return edges;
|
|
2694
3095
|
}
|
|
3096
|
+
// ── Erlang behaviour-callback dispatch ────────────────────────────────────────
|
|
3097
|
+
// An Erlang behaviour is a compile-checked callback contract: the behaviour
|
|
3098
|
+
// module declares `-callback init(...) -> ...`, implementers declare
|
|
3099
|
+
// `-behaviour(B)` and export the callbacks, and the framework side dispatches
|
|
3100
|
+
// through a VARIABLE module — cowboy's `Handler:init(Req, Opts)` and
|
|
3101
|
+
// `Middleware:execute(Req, Env)` folds, ejabberd's `Mod:start/2`. Extraction
|
|
3102
|
+
// deliberately leaves var-module calls silent (no static target), so the flow
|
|
3103
|
+
// breaks at exactly the hop agents ask about (request → handler init). Bridge:
|
|
3104
|
+
//
|
|
3105
|
+
// dispatch site `Var:fn(args…)` → every in-repo implementer of the behaviour
|
|
3106
|
+
// declaring `fn` with the SITE's arity — provided exactly ONE in-repo
|
|
3107
|
+
// behaviour declares (fn, arity); a name+arity collision across behaviours
|
|
3108
|
+
// bails (silent beats wrong) — and the implementer defines and exports `fn`.
|
|
3109
|
+
//
|
|
3110
|
+
// Behaviours are discovered by scanning every Erlang file for `-callback`
|
|
3111
|
+
// declarations (not just `implements` targets), so a behaviour with zero
|
|
3112
|
+
// implementers still participates in the ambiguity gate. Fan-out control: a
|
|
3113
|
+
// mega-behaviour (ejabberd's gen_mod, ~200 mod_* implementers) would mint
|
|
3114
|
+
// hundreds of edges per site that READ as complete coverage while being
|
|
3115
|
+
// arbitrary — above the cap the site is skipped entirely and the boundary
|
|
3116
|
+
// stays visibly dynamic (explore's boundary announcer covers it) instead of
|
|
3117
|
+
// silently truncated.
|
|
3118
|
+
const ERLANG_EXT = /\.(?:erl|hrl)$/;
|
|
3119
|
+
// `Var:fn(` — variable (capitalized) module, lowercase function, immediate
|
|
3120
|
+
// open-paren. The leading char class rejects `?MODULE:fn(` (macro), `a:b(`
|
|
3121
|
+
// (static remote call, already linked), and mid-word matches.
|
|
3122
|
+
const ERLANG_DISPATCH_RE = /(^|[^?\w@'])([A-Z][A-Za-z0-9_@]*):([a-z][A-Za-z0-9_@]*)\(/g;
|
|
3123
|
+
const ERLANG_CALLBACK_DECL_RE = /(^|\n)\s*-callback\s+('[^'\n]+'|[a-z][A-Za-z0-9_@]*)\s*\(/g;
|
|
3124
|
+
const ERLANG_BEHAVIOUR_FANOUT_CAP = 24;
|
|
3125
|
+
/**
|
|
3126
|
+
* Argument count of the call/declaration whose `(` sits at `openIdx` —
|
|
3127
|
+
* top-level commas + 1, `()` → 0, unbalanced/oversized → -1. Skips nested
|
|
3128
|
+
* (), [], {}, <<>> content, `"strings"`, `'atoms'`, and `$c` char literals,
|
|
3129
|
+
* so `-callback init(fun((a, b) -> ok), #{k => v}) -> ok.` counts 2.
|
|
3130
|
+
*/
|
|
3131
|
+
function erlangArityAt(src, openIdx) {
|
|
3132
|
+
let depth = 1;
|
|
3133
|
+
let commas = 0;
|
|
3134
|
+
let sawArg = false;
|
|
3135
|
+
const limit = Math.min(src.length, openIdx + 4000);
|
|
3136
|
+
for (let i = openIdx + 1; i < limit; i++) {
|
|
3137
|
+
const ch = src[i];
|
|
3138
|
+
if (ch === '"' || ch === "'") {
|
|
3139
|
+
i++;
|
|
3140
|
+
while (i < limit && src[i] !== ch) {
|
|
3141
|
+
if (src[i] === '\\')
|
|
3142
|
+
i++;
|
|
3143
|
+
i++;
|
|
3144
|
+
}
|
|
3145
|
+
sawArg = true;
|
|
3146
|
+
continue;
|
|
3147
|
+
}
|
|
3148
|
+
if (ch === '$') {
|
|
3149
|
+
i++;
|
|
3150
|
+
if (src[i] === '\\')
|
|
3151
|
+
i++;
|
|
3152
|
+
sawArg = true;
|
|
3153
|
+
continue;
|
|
3154
|
+
}
|
|
3155
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
3156
|
+
depth++;
|
|
3157
|
+
sawArg = true;
|
|
3158
|
+
continue;
|
|
3159
|
+
}
|
|
3160
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
3161
|
+
depth--;
|
|
3162
|
+
if (depth === 0)
|
|
3163
|
+
return sawArg ? commas + 1 : 0;
|
|
3164
|
+
continue;
|
|
3165
|
+
}
|
|
3166
|
+
if (ch === ',' && depth === 1) {
|
|
3167
|
+
commas++;
|
|
3168
|
+
continue;
|
|
3169
|
+
}
|
|
3170
|
+
if (!/\s/.test(ch))
|
|
3171
|
+
sawArg = true;
|
|
3172
|
+
}
|
|
3173
|
+
return -1;
|
|
3174
|
+
}
|
|
3175
|
+
/**
|
|
3176
|
+
* Nix module-system option wiring. A NixOS/home-manager/nix-darwin option is
|
|
3177
|
+
* DECLARED in one module (`options.launchd.user.agents = mkOption { ... }`)
|
|
3178
|
+
* and SET in others (`launchd.user.agents.yabai = { ... }` inside a module's
|
|
3179
|
+
* config) — the connection happens by option-path unification inside the
|
|
3180
|
+
* module-system evaluator, so there is no static call/import edge to follow
|
|
3181
|
+
* and flow questions ("how does services.yabai.enable become a launchd
|
|
3182
|
+
* service?") go dark at the module boundary.
|
|
3183
|
+
*
|
|
3184
|
+
* This pass links each config-write binding to the option declaration whose
|
|
3185
|
+
* path is the longest static-segment prefix of the write path. Precision gates:
|
|
3186
|
+
* - only STATIC segments participate: plain identifiers, plus quoted segments
|
|
3187
|
+
* (`"git/config"`, `"com.apple.dock"`) as opaque verbatim tokens that match
|
|
3188
|
+
* only quote-exactly; an interpolated (`${name}`) segment ends the prefix,
|
|
3189
|
+
* so dynamic paths never match beyond their static head;
|
|
3190
|
+
* - matched prefixes must be ≥2 segments: 1-segment paths would wrongly link
|
|
3191
|
+
* every package's `meta = { ... }` attrset to nixos's `options.meta`;
|
|
3192
|
+
* - a prefix declared in more than one file is ambiguous → no edge (a wrong
|
|
3193
|
+
* edge is worse than none);
|
|
3194
|
+
* - writes physically inside an options block are declaration internals
|
|
3195
|
+
* (types, defaults, examples), never config writes → excluded.
|
|
3196
|
+
* Both declaration spellings register: flat (`options.a.b = ...`) by name, and
|
|
3197
|
+
* nested (`options = { a.b = ...; }`) by line-span containment.
|
|
3198
|
+
*/
|
|
3199
|
+
function nixLeadingPlainSegments(name) {
|
|
3200
|
+
const segs = [];
|
|
3201
|
+
let i = 0;
|
|
3202
|
+
const n = name.length;
|
|
3203
|
+
while (i < n) {
|
|
3204
|
+
if (name[i] === '"') {
|
|
3205
|
+
// Quoted segment — an opaque verbatim token (quotes kept, so it can
|
|
3206
|
+
// never collide with a plain identifier). `NSGlobalDomain."com.apple.
|
|
3207
|
+
// mouse.tapBehavior"` must match ITS OWN quoted declaration, not
|
|
3208
|
+
// whichever sibling registered the shared plain prefix first.
|
|
3209
|
+
let j = i + 1;
|
|
3210
|
+
while (j < n && name[j] !== '"') {
|
|
3211
|
+
if (name[j] === '\\')
|
|
3212
|
+
j++;
|
|
3213
|
+
j++;
|
|
3214
|
+
}
|
|
3215
|
+
if (j >= n)
|
|
3216
|
+
return segs; // unterminated — stop at the static head
|
|
3217
|
+
const tok = name.slice(i, j + 1);
|
|
3218
|
+
if (tok.includes('${'))
|
|
3219
|
+
return segs; // interpolated → dynamic → stop
|
|
3220
|
+
segs.push(tok);
|
|
3221
|
+
i = j + 1;
|
|
3222
|
+
if (i >= n)
|
|
3223
|
+
break;
|
|
3224
|
+
if (name[i] !== '.')
|
|
3225
|
+
return segs;
|
|
3226
|
+
i++;
|
|
3227
|
+
continue;
|
|
3228
|
+
}
|
|
3229
|
+
let j = i;
|
|
3230
|
+
while (j < n && name[j] !== '.') {
|
|
3231
|
+
if (name[j] === '"' || (name[j] === '$' && name[j + 1] === '{'))
|
|
3232
|
+
return segs;
|
|
3233
|
+
j++;
|
|
3234
|
+
}
|
|
3235
|
+
const seg = name.slice(i, j);
|
|
3236
|
+
if (!/^[A-Za-z_][A-Za-z0-9_'-]*$/.test(seg))
|
|
3237
|
+
return segs;
|
|
3238
|
+
segs.push(seg);
|
|
3239
|
+
i = j + 1;
|
|
3240
|
+
}
|
|
3241
|
+
return segs;
|
|
3242
|
+
}
|
|
3243
|
+
async function nixOptionPathEdges(queries, onYield) {
|
|
3244
|
+
let scanned255 = 0;
|
|
3245
|
+
// One streaming pass over nix bindings (variables + the odd function-valued
|
|
3246
|
+
// option); memory stays O(bindings-kept), not O(all nodes) (#610).
|
|
3247
|
+
const byFile = new Map();
|
|
3248
|
+
let scanned = 0;
|
|
3249
|
+
for (const kind of ['variable', 'function']) {
|
|
3250
|
+
for (const node of queries.iterateNodesByKind(kind)) {
|
|
3251
|
+
if ((++scanned255 & 63) === 0)
|
|
3252
|
+
await onYield();
|
|
3253
|
+
if ((++scanned & 0x3fff) === 0 && onYield)
|
|
3254
|
+
await onYield();
|
|
3255
|
+
if (node.language !== 'nix')
|
|
3256
|
+
continue;
|
|
3257
|
+
const segs = nixLeadingPlainSegments(node.name);
|
|
3258
|
+
if (segs.length === 0)
|
|
3259
|
+
continue;
|
|
3260
|
+
const rec = {
|
|
3261
|
+
id: node.id,
|
|
3262
|
+
filePath: node.filePath,
|
|
3263
|
+
startLine: node.startLine,
|
|
3264
|
+
endLine: node.endLine,
|
|
3265
|
+
segs,
|
|
3266
|
+
};
|
|
3267
|
+
const arr = byFile.get(node.filePath);
|
|
3268
|
+
if (arr)
|
|
3269
|
+
arr.push(rec);
|
|
3270
|
+
else
|
|
3271
|
+
byFile.set(node.filePath, [rec]);
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
// Per file: walk bindings outermost-first with a stack of active option
|
|
3275
|
+
// spans, composing nested declaration paths (`options = { services.foo = {
|
|
3276
|
+
// enable = mkOption ...; }; }` registers services.foo AND services.foo.enable).
|
|
3277
|
+
// An `options` binding nested inside another option span is a SUBMODULE's
|
|
3278
|
+
// own namespace (`attrsOf (submodule { options = ...; })`) — its internals
|
|
3279
|
+
// are not globally addressable, so the sentinel blocks registration below it
|
|
3280
|
+
// while still excluding the region from write candidates.
|
|
3281
|
+
const SUBMODULE = 'submodule';
|
|
3282
|
+
const decls = new Map();
|
|
3283
|
+
const writes = [];
|
|
3284
|
+
const register = (path, rec) => {
|
|
3285
|
+
if (path.length < 2 || path.includes(SUBMODULE))
|
|
3286
|
+
return;
|
|
3287
|
+
const key = path.join('.');
|
|
3288
|
+
const arr = decls.get(key);
|
|
3289
|
+
if (arr)
|
|
3290
|
+
arr.push(rec);
|
|
3291
|
+
else
|
|
3292
|
+
decls.set(key, [rec]);
|
|
3293
|
+
};
|
|
3294
|
+
for (const recs of byFile.values()) {
|
|
3295
|
+
recs.sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
|
|
3296
|
+
const stack = [];
|
|
3297
|
+
for (const rec of recs) {
|
|
3298
|
+
while (stack.length > 0 && stack[stack.length - 1].end < rec.startLine)
|
|
3299
|
+
stack.pop();
|
|
3300
|
+
// Strict containment at line granularity: a one-line nested binding is
|
|
3301
|
+
// indistinguishable from its container, so it stays unclassified (rare
|
|
3302
|
+
// in module code, where option blocks are multi-line).
|
|
3303
|
+
const enclosing = stack.length > 0 &&
|
|
3304
|
+
rec.startLine >= stack[stack.length - 1].start &&
|
|
3305
|
+
rec.endLine <= stack[stack.length - 1].end &&
|
|
3306
|
+
!(rec.startLine === stack[stack.length - 1].start && rec.endLine === stack[stack.length - 1].end)
|
|
3307
|
+
? stack[stack.length - 1]
|
|
3308
|
+
: null;
|
|
3309
|
+
if (rec.segs[0] === 'options') {
|
|
3310
|
+
const ownPath = rec.segs.slice(1); // [] for the bare `options = { ... }` spelling
|
|
3311
|
+
const prefix = enclosing ? [SUBMODULE] : ownPath;
|
|
3312
|
+
register(prefix, rec);
|
|
3313
|
+
stack.push({ start: rec.startLine, end: rec.endLine, prefix });
|
|
3314
|
+
continue;
|
|
3315
|
+
}
|
|
3316
|
+
if (enclosing) {
|
|
3317
|
+
const composed = [...enclosing.prefix, ...rec.segs];
|
|
3318
|
+
register(composed, rec);
|
|
3319
|
+
stack.push({ start: rec.startLine, end: rec.endLine, prefix: composed });
|
|
3320
|
+
continue;
|
|
3321
|
+
}
|
|
3322
|
+
if (rec.segs.length >= 2) {
|
|
3323
|
+
writes.push(rec);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
if (decls.size === 0 || writes.length === 0)
|
|
3328
|
+
return [];
|
|
3329
|
+
const edges = [];
|
|
3330
|
+
for (const w of writes) {
|
|
3331
|
+
// `config.services.x = ...` spells the same write with an explicit prefix.
|
|
3332
|
+
const segs = w.segs[0] === 'config' ? w.segs.slice(1) : w.segs;
|
|
3333
|
+
if (segs.length < 2)
|
|
3334
|
+
continue;
|
|
3335
|
+
// Longest prefix wins; an ambiguous longest match does NOT fall back to a
|
|
3336
|
+
// shorter one (that would link `services.nginx.virtualHosts.x` to
|
|
3337
|
+
// `options.services.nginx` when virtualHosts is the contested path).
|
|
3338
|
+
for (let len = Math.min(segs.length, 6); len >= 2; len--) {
|
|
3339
|
+
const candidates = decls.get(segs.slice(0, len).join('.'));
|
|
3340
|
+
if (!candidates || candidates.length === 0)
|
|
3341
|
+
continue;
|
|
3342
|
+
const files = new Set(candidates.map((c) => c.filePath));
|
|
3343
|
+
if (files.size === 1) {
|
|
3344
|
+
const target = candidates[0];
|
|
3345
|
+
if (target.id !== w.id) {
|
|
3346
|
+
edges.push({
|
|
3347
|
+
source: w.id,
|
|
3348
|
+
target: target.id,
|
|
3349
|
+
kind: 'references',
|
|
3350
|
+
line: w.startLine,
|
|
3351
|
+
provenance: 'heuristic',
|
|
3352
|
+
metadata: {
|
|
3353
|
+
synthesizedBy: 'nix-option-path',
|
|
3354
|
+
optionPath: segs.slice(0, len).join('.'),
|
|
3355
|
+
registeredAt: `${target.filePath}:${target.startLine}`,
|
|
3356
|
+
},
|
|
3357
|
+
});
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
break; // longest hit decides, matched or ambiguous
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
return edges;
|
|
3364
|
+
}
|
|
3365
|
+
async function erlangBehaviourDispatchEdges(queries, ctx, onYield) {
|
|
3366
|
+
let scannedFiles = 0;
|
|
3367
|
+
let scanned255 = 0;
|
|
3368
|
+
// Cheap language gate: no Erlang modules → no cost beyond one streamed
|
|
3369
|
+
// kind scan (never a materialized array of every namespace — #1212).
|
|
3370
|
+
const erlangModules = [];
|
|
3371
|
+
for (const n of queries.iterateNodesByKind('namespace')) {
|
|
3372
|
+
if ((++scanned255 & 63) === 0)
|
|
3373
|
+
await onYield();
|
|
3374
|
+
if (n.language === 'erlang')
|
|
3375
|
+
erlangModules.push(n);
|
|
3376
|
+
}
|
|
3377
|
+
if (erlangModules.length === 0)
|
|
3378
|
+
return [];
|
|
3379
|
+
// Pass 1 — scan every Erlang file with `-callback` decls: behaviour module →
|
|
3380
|
+
// its (name, arity) callback set, and the global `name/arity` → declaring
|
|
3381
|
+
// behaviours map that drives the ambiguity gate.
|
|
3382
|
+
const moduleByFile = new Map();
|
|
3383
|
+
for (const ns of erlangModules) {
|
|
3384
|
+
if (!moduleByFile.has(ns.filePath))
|
|
3385
|
+
moduleByFile.set(ns.filePath, ns);
|
|
3386
|
+
}
|
|
3387
|
+
const declaringBehaviours = new Map(); // `fn/arity` → behaviour namespaces
|
|
3388
|
+
const callbackNames = new Set();
|
|
3389
|
+
for (const file of ctx.getAllFiles()) {
|
|
3390
|
+
if ((++scannedFiles & 15) === 0)
|
|
3391
|
+
await onYield();
|
|
3392
|
+
if (!ERLANG_EXT.test(file))
|
|
3393
|
+
continue;
|
|
3394
|
+
const behaviour = moduleByFile.get(file);
|
|
3395
|
+
if (!behaviour)
|
|
3396
|
+
continue; // a .hrl or module-less file can't be a behaviour
|
|
3397
|
+
const content = ctx.readFile(file);
|
|
3398
|
+
if (!content || !content.includes('-callback'))
|
|
3399
|
+
continue;
|
|
3400
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
|
|
3401
|
+
ERLANG_CALLBACK_DECL_RE.lastIndex = 0;
|
|
3402
|
+
let m;
|
|
3403
|
+
while ((m = ERLANG_CALLBACK_DECL_RE.exec(safe))) {
|
|
3404
|
+
const name = m[2].replace(/^'|'$/g, '');
|
|
3405
|
+
const arity = erlangArityAt(safe, m.index + m[0].length - 1);
|
|
3406
|
+
if (arity < 0)
|
|
3407
|
+
continue;
|
|
3408
|
+
const key = `${name}/${arity}`;
|
|
3409
|
+
const arr = declaringBehaviours.get(key);
|
|
3410
|
+
if (arr) {
|
|
3411
|
+
if (!arr.some((b) => b.id === behaviour.id))
|
|
3412
|
+
arr.push(behaviour);
|
|
3413
|
+
}
|
|
3414
|
+
else {
|
|
3415
|
+
declaringBehaviours.set(key, [behaviour]);
|
|
3416
|
+
}
|
|
3417
|
+
callbackNames.add(name);
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
if (declaringBehaviours.size === 0)
|
|
3421
|
+
return [];
|
|
3422
|
+
// Implementer target lookup, lazy per (behaviour, fn): implementers come
|
|
3423
|
+
// from the `implements` edges extraction resolved, and the target is the
|
|
3424
|
+
// implementer module's own exported `fn` function node.
|
|
3425
|
+
const targetCache = new Map();
|
|
3426
|
+
const targetsOf = (behaviour, fn) => {
|
|
3427
|
+
const cacheKey = `${behaviour.id}#${fn}`;
|
|
3428
|
+
let targets = targetCache.get(cacheKey);
|
|
3429
|
+
if (targets)
|
|
3430
|
+
return targets;
|
|
3431
|
+
targets = [];
|
|
3432
|
+
for (const e of queries.getIncomingEdges(behaviour.id, ['implements'])) {
|
|
3433
|
+
const impl = queries.getNodeById(e.source);
|
|
3434
|
+
if (!impl || impl.language !== 'erlang' || impl.kind !== 'namespace')
|
|
3435
|
+
continue;
|
|
3436
|
+
const fnNode = ctx
|
|
3437
|
+
.getNodesInFile(impl.filePath)
|
|
3438
|
+
.find((n) => n.kind === 'function' && n.name === fn && n.isExported !== false);
|
|
3439
|
+
if (fnNode)
|
|
3440
|
+
targets.push(fnNode);
|
|
3441
|
+
}
|
|
3442
|
+
targetCache.set(cacheKey, targets);
|
|
3443
|
+
return targets;
|
|
3444
|
+
};
|
|
3445
|
+
// Pass 2 — dispatch sites. Only files containing a var-module call shape are
|
|
3446
|
+
// scanned in full.
|
|
3447
|
+
const edges = [];
|
|
3448
|
+
const seen = new Set();
|
|
3449
|
+
for (const file of ctx.getAllFiles()) {
|
|
3450
|
+
if ((++scannedFiles & 15) === 0)
|
|
3451
|
+
await onYield();
|
|
3452
|
+
if (!ERLANG_EXT.test(file))
|
|
3453
|
+
continue;
|
|
3454
|
+
const content = ctx.readFile(file);
|
|
3455
|
+
if (!content || !/[A-Z][A-Za-z0-9_@]*:[a-z]/.test(content))
|
|
3456
|
+
continue;
|
|
3457
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
|
|
3458
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
3459
|
+
ERLANG_DISPATCH_RE.lastIndex = 0;
|
|
3460
|
+
let m;
|
|
3461
|
+
while ((m = ERLANG_DISPATCH_RE.exec(safe))) {
|
|
3462
|
+
const fn = m[3];
|
|
3463
|
+
if (!callbackNames.has(fn))
|
|
3464
|
+
continue;
|
|
3465
|
+
const openIdx = m.index + m[0].length - 1;
|
|
3466
|
+
const arity = erlangArityAt(safe, openIdx);
|
|
3467
|
+
if (arity < 0)
|
|
3468
|
+
continue;
|
|
3469
|
+
const behaviours = declaringBehaviours.get(`${fn}/${arity}`);
|
|
3470
|
+
if (!behaviours || behaviours.length !== 1)
|
|
3471
|
+
continue; // unknown or ambiguous
|
|
3472
|
+
const behaviour = behaviours[0];
|
|
3473
|
+
const targets = targetsOf(behaviour, fn);
|
|
3474
|
+
if (targets.length === 0 || targets.length > ERLANG_BEHAVIOUR_FANOUT_CAP)
|
|
3475
|
+
continue;
|
|
3476
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
3477
|
+
const disp = enclosingFn(nodesInFile, line);
|
|
3478
|
+
if (!disp)
|
|
3479
|
+
continue;
|
|
3480
|
+
for (const target of targets) {
|
|
3481
|
+
if (target.id === disp.id)
|
|
3482
|
+
continue;
|
|
3483
|
+
const key = `${disp.id}>${target.id}`;
|
|
3484
|
+
if (seen.has(key))
|
|
3485
|
+
continue;
|
|
3486
|
+
seen.add(key);
|
|
3487
|
+
edges.push({
|
|
3488
|
+
source: disp.id,
|
|
3489
|
+
target: target.id,
|
|
3490
|
+
kind: 'calls',
|
|
3491
|
+
line,
|
|
3492
|
+
provenance: 'heuristic',
|
|
3493
|
+
metadata: {
|
|
3494
|
+
synthesizedBy: 'erlang-behaviour',
|
|
3495
|
+
via: `${behaviour.name}:${fn}/${arity}`,
|
|
3496
|
+
registeredAt: `${file}:${line}`,
|
|
3497
|
+
},
|
|
3498
|
+
});
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
return edges;
|
|
3503
|
+
}
|
|
2695
3504
|
// ── Laravel events (PHP) ──────────────────────────────────────────────────────
|
|
2696
3505
|
// Laravel decouples an event dispatch from its listener(s), linked by the EVENT CLASS:
|
|
2697
3506
|
// // app/Events/PlaybackStarted.php + app/Listeners/UpdateLastfmNowPlaying.php
|
|
@@ -2738,7 +3547,8 @@ function phpArrayBody(src, openIdx) {
|
|
|
2738
3547
|
}
|
|
2739
3548
|
return null;
|
|
2740
3549
|
}
|
|
2741
|
-
function laravelEventEdges(ctx) {
|
|
3550
|
+
async function laravelEventEdges(ctx, onYield) {
|
|
3551
|
+
let scannedFiles = 0;
|
|
2742
3552
|
// event short name → its listener `handle` methods (deduped by node id).
|
|
2743
3553
|
const listeners = new Map();
|
|
2744
3554
|
const add = (event, handle) => {
|
|
@@ -2755,6 +3565,8 @@ function laravelEventEdges(ctx) {
|
|
|
2755
3565
|
&& n.startLine >= cls.startLine && n.startLine <= (cls.endLine ?? cls.startLine)) ?? null;
|
|
2756
3566
|
// Pass 1 — build the event→handle map from both registration mechanisms.
|
|
2757
3567
|
for (const file of ctx.getAllFiles()) {
|
|
3568
|
+
if ((++scannedFiles & 15) === 0)
|
|
3569
|
+
await onYield();
|
|
2758
3570
|
if (!LARAVEL_PHP_EXT.test(file))
|
|
2759
3571
|
continue;
|
|
2760
3572
|
const content = ctx.readFile(file);
|
|
@@ -2800,6 +3612,8 @@ function laravelEventEdges(ctx) {
|
|
|
2800
3612
|
const edges = [];
|
|
2801
3613
|
const seen = new Set();
|
|
2802
3614
|
for (const file of ctx.getAllFiles()) {
|
|
3615
|
+
if ((++scannedFiles & 15) === 0)
|
|
3616
|
+
await onYield();
|
|
2803
3617
|
if (!LARAVEL_PHP_EXT.test(file))
|
|
2804
3618
|
continue;
|
|
2805
3619
|
const content = ctx.readFile(file);
|
|
@@ -2859,85 +3673,160 @@ async function synthesizeCallbackEdges(queries, ctx) {
|
|
|
2859
3673
|
// that itself hangs (a real wedge) never reaches the next yield, so the
|
|
2860
3674
|
// watchdog still catches that. See ./cooperative-yield.
|
|
2861
3675
|
const yieldToLoop = (0, cooperative_yield_1.createYielder)();
|
|
3676
|
+
// Per-pass wall-clock timing to stderr, opt-in via CODEGRAPH_SYNTH_TIMINGS
|
|
3677
|
+
// (=1: passes over 250ms; =all: every pass). This is the diagnostic that
|
|
3678
|
+
// located both the #1091/#1122 watchdog stalls and the #1212 OOM — keep it.
|
|
3679
|
+
const markT = { t: Date.now() };
|
|
3680
|
+
const __mark = (label) => {
|
|
3681
|
+
const now = Date.now();
|
|
3682
|
+
const dt = now - markT.t;
|
|
3683
|
+
markT.t = now;
|
|
3684
|
+
if (process.env.CODEGRAPH_SYNTH_TIMINGS && (dt > 250 || process.env.CODEGRAPH_SYNTH_TIMINGS === 'all')) {
|
|
3685
|
+
console.error(`[synth-timing] ${label}: ${dt}ms`);
|
|
3686
|
+
}
|
|
3687
|
+
};
|
|
3688
|
+
// Language gating: one indexed DISTINCT over the files table lets a pass
|
|
3689
|
+
// whose own filters reference a specific language/extension be skipped
|
|
3690
|
+
// outright when the project has no such files — its result is provably
|
|
3691
|
+
// empty, so skipping is behavior-identical and the cost drops to zero
|
|
3692
|
+
// (the Kotlin pass was the OOM culprit on the pure-C Linux kernel, #1212).
|
|
3693
|
+
// Passes without an explicit language filter always run.
|
|
3694
|
+
const langs = queries.getDistinctFileLanguages();
|
|
3695
|
+
const has = (...ls) => ls.some((l) => langs.has(l));
|
|
3696
|
+
const JS_FAMILY = ['typescript', 'javascript', 'tsx', 'jsx'];
|
|
3697
|
+
const NONE = [];
|
|
2862
3698
|
// Cross-file Go method→type `contains` edges must be synthesized AND persisted
|
|
2863
3699
|
// FIRST: a method declared in a different file from its receiver type is
|
|
2864
3700
|
// otherwise orphaned from the struct, and goImplementsEdges (next) derives a
|
|
2865
3701
|
// struct's method set from its `contains` edges — so without this it would
|
|
2866
3702
|
// under-count the interfaces a cross-file struct satisfies. (#583)
|
|
2867
|
-
const goMethodContains = goCrossFileMethodContainsEdges(queries);
|
|
2868
|
-
|
|
2869
|
-
queries.insertEdges(goMethodContains);
|
|
3703
|
+
const goMethodContains = has('go') ? await goCrossFileMethodContainsEdges(queries, yieldToLoop) : NONE;
|
|
3704
|
+
for (let i = 0; i < goMethodContains.length; i += 2000) {
|
|
3705
|
+
queries.insertEdges(goMethodContains.slice(i, i + 2000));
|
|
3706
|
+
await yieldToLoop();
|
|
3707
|
+
}
|
|
2870
3708
|
await yieldToLoop();
|
|
3709
|
+
__mark('goMethodContains');
|
|
2871
3710
|
// Go implicit `implements` edges must be synthesized AND persisted next: the
|
|
2872
3711
|
// interface-dispatch bridge below reads `implements` edges from the DB, and
|
|
2873
3712
|
// Go has none statically. (Other languages already have static implements
|
|
2874
3713
|
// edges from extraction, so they don't need this pre-pass.)
|
|
2875
|
-
const goImpl = goImplementsEdges(queries);
|
|
2876
|
-
|
|
2877
|
-
queries.insertEdges(goImpl);
|
|
3714
|
+
const goImpl = has('go') ? await goImplementsEdges(queries, yieldToLoop) : NONE;
|
|
3715
|
+
for (let i = 0; i < goImpl.length; i += 2000) {
|
|
3716
|
+
queries.insertEdges(goImpl.slice(i, i + 2000));
|
|
3717
|
+
await yieldToLoop();
|
|
3718
|
+
}
|
|
2878
3719
|
await yieldToLoop();
|
|
3720
|
+
__mark('goImplements');
|
|
2879
3721
|
const fieldEdges = await fieldChannelEdges(queries, ctx, yieldToLoop);
|
|
2880
3722
|
await yieldToLoop();
|
|
3723
|
+
__mark('fieldEdges');
|
|
2881
3724
|
const closureCollEdges = await closureCollectionEdges(queries, ctx, yieldToLoop);
|
|
2882
3725
|
await yieldToLoop();
|
|
3726
|
+
__mark('closureCollEdges');
|
|
2883
3727
|
const emitterEdges = await eventEmitterEdges(ctx, yieldToLoop);
|
|
2884
3728
|
await yieldToLoop();
|
|
2885
|
-
|
|
3729
|
+
__mark('emitterEdges');
|
|
3730
|
+
const renderEdges = await reactRenderEdges(queries, ctx, yieldToLoop);
|
|
2886
3731
|
await yieldToLoop();
|
|
3732
|
+
__mark('renderEdges');
|
|
2887
3733
|
const jsxEdges = await reactJsxChildEdges(ctx, yieldToLoop);
|
|
2888
3734
|
await yieldToLoop();
|
|
2889
|
-
|
|
3735
|
+
__mark('jsxEdges');
|
|
3736
|
+
const vueEdges = has('vue') ? await vueTemplateEdges(ctx, yieldToLoop) : NONE;
|
|
3737
|
+
await yieldToLoop();
|
|
3738
|
+
__mark('vueEdges');
|
|
3739
|
+
const svelteKitEdges = has('svelte') ? await svelteKitLoadEdges(ctx, yieldToLoop) : NONE;
|
|
3740
|
+
await yieldToLoop();
|
|
3741
|
+
__mark('svelteKitEdges');
|
|
3742
|
+
const pascalEdges = await pascalFormEdges(ctx, yieldToLoop);
|
|
2890
3743
|
await yieldToLoop();
|
|
2891
|
-
|
|
3744
|
+
__mark('pascalEdges');
|
|
3745
|
+
const flutterEdges = has('dart') ? await flutterBuildEdges(queries, ctx, yieldToLoop) : NONE;
|
|
2892
3746
|
await yieldToLoop();
|
|
2893
|
-
|
|
3747
|
+
__mark('flutterEdges');
|
|
3748
|
+
const arkuiStateEdges = has('arkts') ? await arkuiStateBuildEdges(queries, ctx, yieldToLoop) : NONE;
|
|
2894
3749
|
await yieldToLoop();
|
|
2895
|
-
|
|
3750
|
+
__mark('arkuiStateEdges');
|
|
3751
|
+
const arkuiEmitter = has('arkts') ? await arkuiEmitterEdges(ctx, yieldToLoop) : NONE;
|
|
2896
3752
|
await yieldToLoop();
|
|
2897
|
-
|
|
3753
|
+
__mark('arkuiEmitter');
|
|
3754
|
+
const arkuiRoutes = has('arkts') ? await arkuiRouterEdges(ctx, yieldToLoop) : NONE;
|
|
2898
3755
|
await yieldToLoop();
|
|
2899
|
-
|
|
3756
|
+
__mark('arkuiRoutes');
|
|
3757
|
+
const cppEdges = has('cpp') ? await cppOverrideEdges(queries, yieldToLoop) : NONE;
|
|
2900
3758
|
await yieldToLoop();
|
|
2901
|
-
|
|
3759
|
+
__mark('cppEdges');
|
|
3760
|
+
const ifaceEdges = has('java', 'kotlin', 'csharp', 'swift', 'scala', 'go', 'rust', 'arkts', ...JS_FAMILY)
|
|
3761
|
+
? await interfaceOverrideEdges(queries, yieldToLoop) : NONE;
|
|
2902
3762
|
await yieldToLoop();
|
|
2903
|
-
|
|
3763
|
+
__mark('ifaceEdges');
|
|
3764
|
+
const kotlinExpectActual = has('kotlin') ? await kotlinExpectActualEdges(queries, yieldToLoop) : NONE;
|
|
2904
3765
|
await yieldToLoop();
|
|
2905
|
-
|
|
3766
|
+
__mark('kotlinExpectActual');
|
|
3767
|
+
const goGrpcEdges = has('go') ? await goGrpcStubImplEdges(queries, yieldToLoop) : NONE;
|
|
2906
3768
|
await yieldToLoop();
|
|
2907
|
-
|
|
3769
|
+
__mark('goGrpcEdges');
|
|
3770
|
+
const rnEventEdgesList = has(...JS_FAMILY) ? await rnEventEdges(ctx, yieldToLoop) : NONE;
|
|
2908
3771
|
await yieldToLoop();
|
|
2909
|
-
|
|
3772
|
+
__mark('rnEventEdgesList');
|
|
3773
|
+
const fabricNativeEdges = await fabricNativeImplEdges(ctx, yieldToLoop);
|
|
2910
3774
|
await yieldToLoop();
|
|
2911
|
-
|
|
3775
|
+
__mark('fabricNativeEdges');
|
|
3776
|
+
const expoXPlatEdges = await expoCrossPlatformEdges(queries, yieldToLoop);
|
|
2912
3777
|
await yieldToLoop();
|
|
2913
|
-
|
|
3778
|
+
__mark('expoXPlatEdges');
|
|
3779
|
+
const rnXPlatEdges = await rnCrossPlatformEdges(queries, yieldToLoop);
|
|
2914
3780
|
await yieldToLoop();
|
|
2915
|
-
|
|
3781
|
+
__mark('rnXPlatEdges');
|
|
3782
|
+
const mybatisEdges = has('java', 'kotlin') && has('xml') ? await mybatisJavaXmlEdges(queries, yieldToLoop) : NONE;
|
|
2916
3783
|
await yieldToLoop();
|
|
2917
|
-
|
|
3784
|
+
__mark('mybatisEdges');
|
|
3785
|
+
const ginEdges = has('go') ? await ginMiddlewareChainEdges(queries, ctx, yieldToLoop) : NONE;
|
|
2918
3786
|
await yieldToLoop();
|
|
3787
|
+
__mark('ginEdges');
|
|
3788
|
+
const thunkEdges = has(...JS_FAMILY) ? await reduxThunkEdges(queries, ctx, yieldToLoop) : NONE;
|
|
3789
|
+
await yieldToLoop();
|
|
3790
|
+
__mark('thunkEdges');
|
|
2919
3791
|
const registryEdges = await objectRegistryEdges(ctx, yieldToLoop);
|
|
2920
3792
|
await yieldToLoop();
|
|
2921
|
-
|
|
3793
|
+
__mark('registryEdges');
|
|
3794
|
+
const rtkEdges = has(...JS_FAMILY) ? await rtkQueryEdges(queries, ctx, yieldToLoop) : NONE;
|
|
3795
|
+
await yieldToLoop();
|
|
3796
|
+
__mark('rtkEdges');
|
|
3797
|
+
const piniaEdges = has('vue', ...JS_FAMILY) ? await piniaStoreEdges(ctx, yieldToLoop) : NONE;
|
|
2922
3798
|
await yieldToLoop();
|
|
2923
|
-
|
|
3799
|
+
__mark('piniaEdges');
|
|
3800
|
+
const vuexEdges = has('vue', ...JS_FAMILY) ? await vuexDispatchEdges(ctx, yieldToLoop) : NONE;
|
|
2924
3801
|
await yieldToLoop();
|
|
2925
|
-
|
|
3802
|
+
__mark('vuexEdges');
|
|
3803
|
+
const celeryEdges = has('python') ? await celeryDispatchEdges(ctx, yieldToLoop) : NONE;
|
|
2926
3804
|
await yieldToLoop();
|
|
2927
|
-
|
|
3805
|
+
__mark('celeryEdges');
|
|
3806
|
+
const springEdges = has('java') ? await springEventEdges(ctx, yieldToLoop) : NONE;
|
|
2928
3807
|
await yieldToLoop();
|
|
2929
|
-
|
|
3808
|
+
__mark('springEdges');
|
|
3809
|
+
const mediatrEdges = has('csharp') ? await mediatrDispatchEdges(ctx, yieldToLoop) : NONE;
|
|
2930
3810
|
await yieldToLoop();
|
|
2931
|
-
|
|
3811
|
+
__mark('mediatrEdges');
|
|
3812
|
+
const sidekiqEdges = has('ruby') ? await sidekiqDispatchEdges(ctx, yieldToLoop) : NONE;
|
|
2932
3813
|
await yieldToLoop();
|
|
2933
|
-
|
|
3814
|
+
__mark('sidekiqEdges');
|
|
3815
|
+
const erlangBehaviourEdges = has('erlang') ? await erlangBehaviourDispatchEdges(queries, ctx, yieldToLoop) : NONE;
|
|
2934
3816
|
await yieldToLoop();
|
|
2935
|
-
|
|
3817
|
+
__mark('erlangBehaviourEdges');
|
|
3818
|
+
const laravelEdges = has('php') ? await laravelEventEdges(ctx, yieldToLoop) : NONE;
|
|
2936
3819
|
await yieldToLoop();
|
|
2937
|
-
|
|
3820
|
+
__mark('laravelEdges');
|
|
3821
|
+
const cFnPtrEdges = has('c', 'cpp') ? await (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(queries, ctx, yieldToLoop) : NONE;
|
|
2938
3822
|
await yieldToLoop();
|
|
2939
|
-
|
|
3823
|
+
__mark('cFnPtrEdges');
|
|
3824
|
+
const goframeEdges = has('go') ? await (0, goframe_synthesizer_1.goframeRouteEdges)(ctx, yieldToLoop) : NONE;
|
|
2940
3825
|
await yieldToLoop();
|
|
3826
|
+
__mark('goframeEdges');
|
|
3827
|
+
const nixOptionEdges = has('nix') ? await nixOptionPathEdges(queries, yieldToLoop) : NONE;
|
|
3828
|
+
await yieldToLoop();
|
|
3829
|
+
__mark('nixOptionEdges');
|
|
2941
3830
|
const merged = [];
|
|
2942
3831
|
const seen = new Set();
|
|
2943
3832
|
for (const e of [
|
|
@@ -2950,6 +3839,9 @@ async function synthesizeCallbackEdges(queries, ctx) {
|
|
|
2950
3839
|
...svelteKitEdges,
|
|
2951
3840
|
...pascalEdges,
|
|
2952
3841
|
...flutterEdges,
|
|
3842
|
+
...arkuiStateEdges,
|
|
3843
|
+
...arkuiEmitter,
|
|
3844
|
+
...arkuiRoutes,
|
|
2953
3845
|
...cppEdges,
|
|
2954
3846
|
...ifaceEdges,
|
|
2955
3847
|
...kotlinExpectActual,
|
|
@@ -2969,9 +3861,11 @@ async function synthesizeCallbackEdges(queries, ctx) {
|
|
|
2969
3861
|
...springEdges,
|
|
2970
3862
|
...mediatrEdges,
|
|
2971
3863
|
...sidekiqEdges,
|
|
3864
|
+
...erlangBehaviourEdges,
|
|
2972
3865
|
...laravelEdges,
|
|
2973
3866
|
...cFnPtrEdges,
|
|
2974
3867
|
...goframeEdges,
|
|
3868
|
+
...nixOptionEdges,
|
|
2975
3869
|
]) {
|
|
2976
3870
|
const key = `${e.source}>${e.target}`;
|
|
2977
3871
|
if (seen.has(key))
|
|
@@ -2979,8 +3873,15 @@ async function synthesizeCallbackEdges(queries, ctx) {
|
|
|
2979
3873
|
seen.add(key);
|
|
2980
3874
|
merged.push(e);
|
|
2981
3875
|
}
|
|
2982
|
-
|
|
2983
|
-
|
|
3876
|
+
__mark('dedupe-merge');
|
|
3877
|
+
// Chunked insert with yields: on the Linux kernel the merged synthesized
|
|
3878
|
+
// edge set is ~275k rows, and one transaction for all of them was a 20s
|
|
3879
|
+
// unyielded main-thread span (#1212 follow-up) — the last one in the tail.
|
|
3880
|
+
for (let i = 0; i < merged.length; i += 2000) {
|
|
3881
|
+
queries.insertEdges(merged.slice(i, i + 2000));
|
|
3882
|
+
await yieldToLoop();
|
|
3883
|
+
}
|
|
3884
|
+
__mark('insertMergedEdges');
|
|
2984
3885
|
return merged.length + goImpl.length + goMethodContains.length;
|
|
2985
3886
|
}
|
|
2986
3887
|
//# sourceMappingURL=callback-synthesizer.js.map
|