@colbymchenry/codegraph-darwin-arm64 1.2.0 → 1.3.0
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/bin/codegraph +5 -0
- 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/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 +51 -0
- package/lib/dist/db/queries.d.ts.map +1 -1
- package/lib/dist/db/queries.js +141 -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 +254 -3
- 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 +306 -2
- 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 +39 -7
- 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 +21 -3
- package/lib/dist/mcp/proxy.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 +74 -9
- package/lib/dist/mcp/tools.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/callback-synthesizer.d.ts.map +1 -1
- package/lib/dist/resolution/callback-synthesizer.js +672 -0
- 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/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 +39 -6
- package/lib/dist/resolution/index.d.ts.map +1 -1
- package/lib/dist/resolution/index.js +287 -50
- 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 +20 -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
|
@@ -430,6 +430,270 @@ function flutterBuildEdges(queries, ctx) {
|
|
|
430
430
|
}
|
|
431
431
|
return edges;
|
|
432
432
|
}
|
|
433
|
+
/**
|
|
434
|
+
* Reactive ArkUI property decorators: assigning a property carrying one of
|
|
435
|
+
* these re-runs the owning struct's `build()`. Covers both state models —
|
|
436
|
+
* V1 (`@Component`: State/Prop/Link/Provide/Consume/Storage*) and V2
|
|
437
|
+
* (`@ComponentV2`: Local/Provider/Consumer; `@Param` is read-only in V2 so
|
|
438
|
+
* the assignment gate never fires on it, and `@Trace` lives on `@ObservedV2`
|
|
439
|
+
* data classes, not struct properties).
|
|
440
|
+
*/
|
|
441
|
+
const ARKUI_REACTIVE_DECORATORS = new Set([
|
|
442
|
+
'State', 'Prop', 'Link', 'Provide', 'Consume', 'StorageLink', 'StorageProp',
|
|
443
|
+
'LocalStorageLink', 'LocalStorageProp', 'ObjectLink',
|
|
444
|
+
'Local', 'Provider', 'Consumer',
|
|
445
|
+
]);
|
|
446
|
+
/** ArkUI-observed array mutators — `this.todos.push(x)` re-renders like an assignment. */
|
|
447
|
+
const ARKUI_ARRAY_MUTATORS = 'push|pop|shift|unshift|splice|sort|reverse|fill';
|
|
448
|
+
/**
|
|
449
|
+
* Phase 4b-ets: ArkUI state → build (the ArkTS analog of react-render /
|
|
450
|
+
* flutter-build). Assigning a reactive-decorated property (`@State count`,
|
|
451
|
+
* `@Link selected`, …) re-runs the `@Component struct`'s `build()`, but that
|
|
452
|
+
* hop is framework-internal — no static edge — so "onClick → markAllDone →
|
|
453
|
+
* this.todos = […] → rebuilt list" dead-ends at the assignment. Bridge it:
|
|
454
|
+
* for each arkts struct with a `build()` method and at least one reactive
|
|
455
|
+
* property, link every sibling method whose body ASSIGNS (or array-mutates)
|
|
456
|
+
* one of those properties → `build`. Assignment-gated on the struct's OWN
|
|
457
|
+
* reactive property names — a method that merely reads state, or a struct
|
|
458
|
+
* with no reactive properties, gets nothing (this is the precision line the
|
|
459
|
+
* all-sibling-methods design would erase).
|
|
460
|
+
*/
|
|
461
|
+
function arkuiStateBuildEdges(queries, ctx) {
|
|
462
|
+
const edges = [];
|
|
463
|
+
const seen = new Set();
|
|
464
|
+
for (const struct of queries.getNodesByKind('struct')) {
|
|
465
|
+
if (struct.language !== 'arkts')
|
|
466
|
+
continue;
|
|
467
|
+
const children = queries.getOutgoingEdges(struct.id, ['contains'])
|
|
468
|
+
.map((e) => queries.getNodeById(e.target))
|
|
469
|
+
.filter((n) => !!n);
|
|
470
|
+
const build = children.find((n) => n.kind === 'method' && n.name === 'build');
|
|
471
|
+
if (!build)
|
|
472
|
+
continue;
|
|
473
|
+
const reactiveProps = children.filter((n) => n.kind === 'property' && (n.decorators ?? []).some((d) => ARKUI_REACTIVE_DECORATORS.has(d)));
|
|
474
|
+
if (reactiveProps.length === 0)
|
|
475
|
+
continue;
|
|
476
|
+
const propAlternation = reactiveProps
|
|
477
|
+
.map((p) => p.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
478
|
+
.join('|');
|
|
479
|
+
// `this.count = …` / `+=` / `++` / `--` / `this.todos.push(…)`. The
|
|
480
|
+
// `=(?!=)` keeps `this.done == x` comparisons out.
|
|
481
|
+
const mutationRe = new RegExp(`this\\.(?:${propAlternation})\\s*(?:=(?!=)|\\+\\+|--|[+\\-*/%&|^]=|\\.(?:${ARKUI_ARRAY_MUTATORS})\\s*\\()`);
|
|
482
|
+
let added = 0;
|
|
483
|
+
for (const m of children) {
|
|
484
|
+
if (added >= MAX_CALLBACKS_PER_CHANNEL)
|
|
485
|
+
break;
|
|
486
|
+
if (m.kind !== 'method' || m.id === build.id)
|
|
487
|
+
continue;
|
|
488
|
+
const content = ctx.readFile(m.filePath);
|
|
489
|
+
const src = content && sliceLines(content, m.startLine, m.endLine);
|
|
490
|
+
if (!src || !mutationRe.test((0, strip_comments_1.stripCommentsForRegex)(src, 'typescript')))
|
|
491
|
+
continue;
|
|
492
|
+
const key = `${m.id}>${build.id}`;
|
|
493
|
+
if (seen.has(key))
|
|
494
|
+
continue;
|
|
495
|
+
seen.add(key);
|
|
496
|
+
edges.push({
|
|
497
|
+
source: m.id, target: build.id, kind: 'calls', line: m.startLine,
|
|
498
|
+
provenance: 'heuristic',
|
|
499
|
+
metadata: { synthesizedBy: 'arkui-state', via: 'state assignment', registeredAt: `${build.filePath}:${build.startLine}` },
|
|
500
|
+
});
|
|
501
|
+
added++;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return edges;
|
|
505
|
+
}
|
|
506
|
+
/** Emit/subscribe call sites of HarmonyOS's `@ohos.events.emitter` bus. */
|
|
507
|
+
const ARKUI_EMITTER_CALL_RE = /\bemitter\s*\.\s*(emit|on|once)\s*\(\s*([A-Za-z_$][\w$.]*|\{[^)]{0,120}?\beventId\s*:\s*[^,}]+[^)]*?\})/g;
|
|
508
|
+
/** Cap per event bucket — a generic key with many parties is dynamic routing, not a static pair. */
|
|
509
|
+
const ARKUI_EMITTER_FANOUT_CAP = 8;
|
|
510
|
+
/**
|
|
511
|
+
* Phase 4b-ets2: HarmonyOS `@ohos.events.emitter` bridge. The cross-component
|
|
512
|
+
* bus — `emitter.emit(eventId)` fires `emitter.on(eventId, cb)` — is
|
|
513
|
+
* framework-internal, so an order flow riding it (OrangeShopping's
|
|
514
|
+
* add-to-cart) dead-ends at the emit. Link emit-site enclosing
|
|
515
|
+
* function/method → on/once-site enclosing function/method when both
|
|
516
|
+
* reference the SAME statically-recoverable event key.
|
|
517
|
+
*
|
|
518
|
+
* Key recovery, per call site (comment-stripped enclosing-file source): the
|
|
519
|
+
* first argument is an `{ eventId: K }` literal, a `Names.Dotted` constant, or
|
|
520
|
+
* a local whose same-file declaration is `new EventsId(K)` / `= K` — chase one
|
|
521
|
+
* level. Precision scoping learned from the samples monorepo (thousands of
|
|
522
|
+
* unrelated samples, most using eventId 1): NUMERIC keys pair within the same
|
|
523
|
+
* FILE only; NAMED keys pair within the same workspace module directory (or
|
|
524
|
+
* the whole project when it declares no modules — the single-app case), both
|
|
525
|
+
* behind a fan-out cap. Inline `on(id, (e) => {…})` arrows need no special
|
|
526
|
+
* handling — their bodies' calls already attribute to the registering method,
|
|
527
|
+
* so targeting that method keeps the chain connected.
|
|
528
|
+
*/
|
|
529
|
+
function arkuiEmitterEdges(ctx) {
|
|
530
|
+
// bucket key -> emit sites / handler sites
|
|
531
|
+
const emits = new Map();
|
|
532
|
+
const handlers = new Map();
|
|
533
|
+
const moduleDirs = (() => {
|
|
534
|
+
const ws = ctx.getWorkspacePackages?.();
|
|
535
|
+
return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
|
|
536
|
+
})();
|
|
537
|
+
const moduleScopeOf = (file) => {
|
|
538
|
+
for (const dir of moduleDirs) {
|
|
539
|
+
if (file === dir || file.startsWith(dir + '/'))
|
|
540
|
+
return dir;
|
|
541
|
+
}
|
|
542
|
+
return '';
|
|
543
|
+
};
|
|
544
|
+
for (const file of ctx.getAllFiles()) {
|
|
545
|
+
if (!file.endsWith('.ets'))
|
|
546
|
+
continue;
|
|
547
|
+
const content = ctx.readFile(file);
|
|
548
|
+
if (!content || !content.includes('emitter.'))
|
|
549
|
+
continue;
|
|
550
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
|
|
551
|
+
const nodes = ctx.getNodesInFile(file)
|
|
552
|
+
.filter((n) => n.kind === 'method' || n.kind === 'function');
|
|
553
|
+
ARKUI_EMITTER_CALL_RE.lastIndex = 0;
|
|
554
|
+
let m;
|
|
555
|
+
while ((m = ARKUI_EMITTER_CALL_RE.exec(safe))) {
|
|
556
|
+
const verb = m[1];
|
|
557
|
+
const arg = m[2].trim();
|
|
558
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
559
|
+
const encl = nodes
|
|
560
|
+
.filter((n) => n.startLine <= line && n.endLine >= line)
|
|
561
|
+
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
|
|
562
|
+
if (!encl)
|
|
563
|
+
continue;
|
|
564
|
+
// Recover the event key from the first argument.
|
|
565
|
+
let key = null;
|
|
566
|
+
const idLit = arg.startsWith('{') ? arg.match(/\beventId\s*:\s*([\w$.]+)/)?.[1] : undefined;
|
|
567
|
+
const token = idLit ?? arg;
|
|
568
|
+
if (token !== undefined) {
|
|
569
|
+
if (/^\d+$/.test(token)) {
|
|
570
|
+
key = `num:${file}:${token}`; // numeric: same-file only
|
|
571
|
+
}
|
|
572
|
+
else if (token.includes('.')) {
|
|
573
|
+
key = `name:${moduleScopeOf(file)}:${token}`;
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
// Local variable — chase its same-file declaration one level:
|
|
577
|
+
// `let x = new EventsId(K)` / `const x = K`.
|
|
578
|
+
const declRe = new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b\\s*(?::[^=\\n]+)?=\\s*(?:new\\s+[\\w$.]+\\(\\s*([^)\\n]+?)\\s*\\)|([\\w$.]+))`);
|
|
579
|
+
const decl = safe.match(declRe);
|
|
580
|
+
const inner = (decl?.[1] ?? decl?.[2])?.trim();
|
|
581
|
+
if (inner && /^\d+$/.test(inner))
|
|
582
|
+
key = `num:${file}:${inner}`;
|
|
583
|
+
else if (inner && /^[\w$.]+$/.test(inner))
|
|
584
|
+
key = `name:${moduleScopeOf(file)}:${inner}`;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (!key)
|
|
588
|
+
continue;
|
|
589
|
+
const site = { nodeId: encl.id, file, line };
|
|
590
|
+
if (verb === 'emit') {
|
|
591
|
+
(emits.get(key) ?? emits.set(key, []).get(key)).push(site);
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
(handlers.get(key) ?? handlers.set(key, []).get(key)).push(site);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const edges = [];
|
|
599
|
+
const seen = new Set();
|
|
600
|
+
for (const [key, emitSites] of emits) {
|
|
601
|
+
const handlerSites = handlers.get(key);
|
|
602
|
+
if (!handlerSites)
|
|
603
|
+
continue;
|
|
604
|
+
if (emitSites.length > ARKUI_EMITTER_FANOUT_CAP || handlerSites.length > ARKUI_EMITTER_FANOUT_CAP)
|
|
605
|
+
continue;
|
|
606
|
+
const eventLabel = key.slice(key.lastIndexOf(':') + 1);
|
|
607
|
+
for (const e of emitSites)
|
|
608
|
+
for (const h of handlerSites) {
|
|
609
|
+
if (e.nodeId === h.nodeId)
|
|
610
|
+
continue;
|
|
611
|
+
const dedupe = `${e.nodeId}>${h.nodeId}`;
|
|
612
|
+
if (seen.has(dedupe))
|
|
613
|
+
continue;
|
|
614
|
+
seen.add(dedupe);
|
|
615
|
+
edges.push({
|
|
616
|
+
source: e.nodeId, target: h.nodeId, kind: 'calls', line: e.line,
|
|
617
|
+
provenance: 'heuristic',
|
|
618
|
+
metadata: { synthesizedBy: 'arkui-emitter', event: eventLabel, registeredAt: `${h.file}:${h.line}` },
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return edges;
|
|
623
|
+
}
|
|
624
|
+
/** `router.pushUrl({ url: 'pages/Detail' })` / replaceUrl — literal urls only. */
|
|
625
|
+
const ARKUI_ROUTER_RE = /\brouter\s*\.\s*(?:pushUrl|replaceUrl)\s*\(\s*\{[^)]{0,200}?\burl\s*:\s*['"]([\w\-./]+)['"]/g;
|
|
626
|
+
/**
|
|
627
|
+
* Phase 4b-ets3: HarmonyOS page navigation. `router.pushUrl({ url:
|
|
628
|
+
* 'pages/Detail' })` reaches the `@Entry struct` of
|
|
629
|
+
* `<module>/src/main/ets/pages/Detail.ets`, but the hop is a string — no
|
|
630
|
+
* static edge — so "tap → openDetail → ???" ends at the router call. Bridge
|
|
631
|
+
* literal urls to the page struct: the url resolves against the standard
|
|
632
|
+
* `src/main/ets/` layout (what main_pages.json entries name); candidates
|
|
633
|
+
* prefer the caller's own workspace module (routes are module-scoped), and
|
|
634
|
+
* anything still ambiguous is dropped rather than guessed. Only `@Entry`
|
|
635
|
+
* structs qualify as targets — the decorator is what makes a file a page.
|
|
636
|
+
*/
|
|
637
|
+
function arkuiRouterEdges(ctx) {
|
|
638
|
+
const edges = [];
|
|
639
|
+
const seen = new Set();
|
|
640
|
+
const allFiles = ctx.getAllFiles();
|
|
641
|
+
const moduleDirs = (() => {
|
|
642
|
+
const ws = ctx.getWorkspacePackages?.();
|
|
643
|
+
return ws ? [...new Set(ws.byName.values())].sort((a, b) => b.length - a.length) : [];
|
|
644
|
+
})();
|
|
645
|
+
const moduleScopeOf = (file) => {
|
|
646
|
+
for (const dir of moduleDirs) {
|
|
647
|
+
if (file === dir || file.startsWith(dir + '/'))
|
|
648
|
+
return dir;
|
|
649
|
+
}
|
|
650
|
+
return '';
|
|
651
|
+
};
|
|
652
|
+
for (const file of allFiles) {
|
|
653
|
+
if (!file.endsWith('.ets'))
|
|
654
|
+
continue;
|
|
655
|
+
const content = ctx.readFile(file);
|
|
656
|
+
if (!content || !content.includes('router.'))
|
|
657
|
+
continue;
|
|
658
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'typescript');
|
|
659
|
+
const nodes = ctx.getNodesInFile(file)
|
|
660
|
+
.filter((n) => n.kind === 'method' || n.kind === 'function');
|
|
661
|
+
ARKUI_ROUTER_RE.lastIndex = 0;
|
|
662
|
+
let m;
|
|
663
|
+
while ((m = ARKUI_ROUTER_RE.exec(safe))) {
|
|
664
|
+
const url = m[1];
|
|
665
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
666
|
+
const encl = nodes
|
|
667
|
+
.filter((n) => n.startLine <= line && n.endLine >= line)
|
|
668
|
+
.sort((a, b) => (a.endLine - a.startLine) - (b.endLine - b.startLine))[0];
|
|
669
|
+
if (!encl)
|
|
670
|
+
continue;
|
|
671
|
+
const suffix = `/src/main/ets/${url}.ets`;
|
|
672
|
+
let candidates = allFiles.filter((f) => f.endsWith(suffix));
|
|
673
|
+
if (candidates.length > 1) {
|
|
674
|
+
const scope = moduleScopeOf(file);
|
|
675
|
+
const sameModule = candidates.filter((f) => moduleScopeOf(f) === scope);
|
|
676
|
+
if (sameModule.length > 0)
|
|
677
|
+
candidates = sameModule;
|
|
678
|
+
}
|
|
679
|
+
if (candidates.length !== 1)
|
|
680
|
+
continue; // ambiguous or unresolved — never guess
|
|
681
|
+
const page = ctx.getNodesInFile(candidates[0]).find((n) => n.kind === 'struct' && (n.decorators ?? []).includes('Entry'));
|
|
682
|
+
if (!page)
|
|
683
|
+
continue;
|
|
684
|
+
const key = `${encl.id}>${page.id}`;
|
|
685
|
+
if (seen.has(key))
|
|
686
|
+
continue;
|
|
687
|
+
seen.add(key);
|
|
688
|
+
edges.push({
|
|
689
|
+
source: encl.id, target: page.id, kind: 'calls', line,
|
|
690
|
+
provenance: 'heuristic',
|
|
691
|
+
metadata: { synthesizedBy: 'arkui-route', event: url, registeredAt: `${candidates[0]}:${page.startLine}` },
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return edges;
|
|
696
|
+
}
|
|
433
697
|
/**
|
|
434
698
|
* Phase 4c: C++ virtual override. A call through a base/interface pointer
|
|
435
699
|
* (`db->Get(...)`, `iter->Next()`) dispatches at runtime to a subclass override,
|
|
@@ -501,6 +765,7 @@ function cppOverrideEdges(queries) {
|
|
|
501
765
|
// or an `object` (Scala) so the loop also iterates those kinds.
|
|
502
766
|
const IFACE_OVERRIDE_LANGS = new Set([
|
|
503
767
|
'java', 'kotlin', 'csharp', 'typescript', 'javascript', 'swift', 'scala', 'go', 'rust',
|
|
768
|
+
'arkts',
|
|
504
769
|
]);
|
|
505
770
|
/**
|
|
506
771
|
* Go implicit interface satisfaction (#584). Go has no `implements` keyword — a
|
|
@@ -2692,6 +2957,398 @@ function sidekiqDispatchEdges(ctx) {
|
|
|
2692
2957
|
}
|
|
2693
2958
|
return edges;
|
|
2694
2959
|
}
|
|
2960
|
+
// ── Erlang behaviour-callback dispatch ────────────────────────────────────────
|
|
2961
|
+
// An Erlang behaviour is a compile-checked callback contract: the behaviour
|
|
2962
|
+
// module declares `-callback init(...) -> ...`, implementers declare
|
|
2963
|
+
// `-behaviour(B)` and export the callbacks, and the framework side dispatches
|
|
2964
|
+
// through a VARIABLE module — cowboy's `Handler:init(Req, Opts)` and
|
|
2965
|
+
// `Middleware:execute(Req, Env)` folds, ejabberd's `Mod:start/2`. Extraction
|
|
2966
|
+
// deliberately leaves var-module calls silent (no static target), so the flow
|
|
2967
|
+
// breaks at exactly the hop agents ask about (request → handler init). Bridge:
|
|
2968
|
+
//
|
|
2969
|
+
// dispatch site `Var:fn(args…)` → every in-repo implementer of the behaviour
|
|
2970
|
+
// declaring `fn` with the SITE's arity — provided exactly ONE in-repo
|
|
2971
|
+
// behaviour declares (fn, arity); a name+arity collision across behaviours
|
|
2972
|
+
// bails (silent beats wrong) — and the implementer defines and exports `fn`.
|
|
2973
|
+
//
|
|
2974
|
+
// Behaviours are discovered by scanning every Erlang file for `-callback`
|
|
2975
|
+
// declarations (not just `implements` targets), so a behaviour with zero
|
|
2976
|
+
// implementers still participates in the ambiguity gate. Fan-out control: a
|
|
2977
|
+
// mega-behaviour (ejabberd's gen_mod, ~200 mod_* implementers) would mint
|
|
2978
|
+
// hundreds of edges per site that READ as complete coverage while being
|
|
2979
|
+
// arbitrary — above the cap the site is skipped entirely and the boundary
|
|
2980
|
+
// stays visibly dynamic (explore's boundary announcer covers it) instead of
|
|
2981
|
+
// silently truncated.
|
|
2982
|
+
const ERLANG_EXT = /\.(?:erl|hrl)$/;
|
|
2983
|
+
// `Var:fn(` — variable (capitalized) module, lowercase function, immediate
|
|
2984
|
+
// open-paren. The leading char class rejects `?MODULE:fn(` (macro), `a:b(`
|
|
2985
|
+
// (static remote call, already linked), and mid-word matches.
|
|
2986
|
+
const ERLANG_DISPATCH_RE = /(^|[^?\w@'])([A-Z][A-Za-z0-9_@]*):([a-z][A-Za-z0-9_@]*)\(/g;
|
|
2987
|
+
const ERLANG_CALLBACK_DECL_RE = /(^|\n)\s*-callback\s+('[^'\n]+'|[a-z][A-Za-z0-9_@]*)\s*\(/g;
|
|
2988
|
+
const ERLANG_BEHAVIOUR_FANOUT_CAP = 24;
|
|
2989
|
+
/**
|
|
2990
|
+
* Argument count of the call/declaration whose `(` sits at `openIdx` —
|
|
2991
|
+
* top-level commas + 1, `()` → 0, unbalanced/oversized → -1. Skips nested
|
|
2992
|
+
* (), [], {}, <<>> content, `"strings"`, `'atoms'`, and `$c` char literals,
|
|
2993
|
+
* so `-callback init(fun((a, b) -> ok), #{k => v}) -> ok.` counts 2.
|
|
2994
|
+
*/
|
|
2995
|
+
function erlangArityAt(src, openIdx) {
|
|
2996
|
+
let depth = 1;
|
|
2997
|
+
let commas = 0;
|
|
2998
|
+
let sawArg = false;
|
|
2999
|
+
const limit = Math.min(src.length, openIdx + 4000);
|
|
3000
|
+
for (let i = openIdx + 1; i < limit; i++) {
|
|
3001
|
+
const ch = src[i];
|
|
3002
|
+
if (ch === '"' || ch === "'") {
|
|
3003
|
+
i++;
|
|
3004
|
+
while (i < limit && src[i] !== ch) {
|
|
3005
|
+
if (src[i] === '\\')
|
|
3006
|
+
i++;
|
|
3007
|
+
i++;
|
|
3008
|
+
}
|
|
3009
|
+
sawArg = true;
|
|
3010
|
+
continue;
|
|
3011
|
+
}
|
|
3012
|
+
if (ch === '$') {
|
|
3013
|
+
i++;
|
|
3014
|
+
if (src[i] === '\\')
|
|
3015
|
+
i++;
|
|
3016
|
+
sawArg = true;
|
|
3017
|
+
continue;
|
|
3018
|
+
}
|
|
3019
|
+
if (ch === '(' || ch === '[' || ch === '{') {
|
|
3020
|
+
depth++;
|
|
3021
|
+
sawArg = true;
|
|
3022
|
+
continue;
|
|
3023
|
+
}
|
|
3024
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
3025
|
+
depth--;
|
|
3026
|
+
if (depth === 0)
|
|
3027
|
+
return sawArg ? commas + 1 : 0;
|
|
3028
|
+
continue;
|
|
3029
|
+
}
|
|
3030
|
+
if (ch === ',' && depth === 1) {
|
|
3031
|
+
commas++;
|
|
3032
|
+
continue;
|
|
3033
|
+
}
|
|
3034
|
+
if (!/\s/.test(ch))
|
|
3035
|
+
sawArg = true;
|
|
3036
|
+
}
|
|
3037
|
+
return -1;
|
|
3038
|
+
}
|
|
3039
|
+
/**
|
|
3040
|
+
* Nix module-system option wiring. A NixOS/home-manager/nix-darwin option is
|
|
3041
|
+
* DECLARED in one module (`options.launchd.user.agents = mkOption { ... }`)
|
|
3042
|
+
* and SET in others (`launchd.user.agents.yabai = { ... }` inside a module's
|
|
3043
|
+
* config) — the connection happens by option-path unification inside the
|
|
3044
|
+
* module-system evaluator, so there is no static call/import edge to follow
|
|
3045
|
+
* and flow questions ("how does services.yabai.enable become a launchd
|
|
3046
|
+
* service?") go dark at the module boundary.
|
|
3047
|
+
*
|
|
3048
|
+
* This pass links each config-write binding to the option declaration whose
|
|
3049
|
+
* path is the longest static-segment prefix of the write path. Precision gates:
|
|
3050
|
+
* - only STATIC segments participate: plain identifiers, plus quoted segments
|
|
3051
|
+
* (`"git/config"`, `"com.apple.dock"`) as opaque verbatim tokens that match
|
|
3052
|
+
* only quote-exactly; an interpolated (`${name}`) segment ends the prefix,
|
|
3053
|
+
* so dynamic paths never match beyond their static head;
|
|
3054
|
+
* - matched prefixes must be ≥2 segments: 1-segment paths would wrongly link
|
|
3055
|
+
* every package's `meta = { ... }` attrset to nixos's `options.meta`;
|
|
3056
|
+
* - a prefix declared in more than one file is ambiguous → no edge (a wrong
|
|
3057
|
+
* edge is worse than none);
|
|
3058
|
+
* - writes physically inside an options block are declaration internals
|
|
3059
|
+
* (types, defaults, examples), never config writes → excluded.
|
|
3060
|
+
* Both declaration spellings register: flat (`options.a.b = ...`) by name, and
|
|
3061
|
+
* nested (`options = { a.b = ...; }`) by line-span containment.
|
|
3062
|
+
*/
|
|
3063
|
+
function nixLeadingPlainSegments(name) {
|
|
3064
|
+
const segs = [];
|
|
3065
|
+
let i = 0;
|
|
3066
|
+
const n = name.length;
|
|
3067
|
+
while (i < n) {
|
|
3068
|
+
if (name[i] === '"') {
|
|
3069
|
+
// Quoted segment — an opaque verbatim token (quotes kept, so it can
|
|
3070
|
+
// never collide with a plain identifier). `NSGlobalDomain."com.apple.
|
|
3071
|
+
// mouse.tapBehavior"` must match ITS OWN quoted declaration, not
|
|
3072
|
+
// whichever sibling registered the shared plain prefix first.
|
|
3073
|
+
let j = i + 1;
|
|
3074
|
+
while (j < n && name[j] !== '"') {
|
|
3075
|
+
if (name[j] === '\\')
|
|
3076
|
+
j++;
|
|
3077
|
+
j++;
|
|
3078
|
+
}
|
|
3079
|
+
if (j >= n)
|
|
3080
|
+
return segs; // unterminated — stop at the static head
|
|
3081
|
+
const tok = name.slice(i, j + 1);
|
|
3082
|
+
if (tok.includes('${'))
|
|
3083
|
+
return segs; // interpolated → dynamic → stop
|
|
3084
|
+
segs.push(tok);
|
|
3085
|
+
i = j + 1;
|
|
3086
|
+
if (i >= n)
|
|
3087
|
+
break;
|
|
3088
|
+
if (name[i] !== '.')
|
|
3089
|
+
return segs;
|
|
3090
|
+
i++;
|
|
3091
|
+
continue;
|
|
3092
|
+
}
|
|
3093
|
+
let j = i;
|
|
3094
|
+
while (j < n && name[j] !== '.') {
|
|
3095
|
+
if (name[j] === '"' || (name[j] === '$' && name[j + 1] === '{'))
|
|
3096
|
+
return segs;
|
|
3097
|
+
j++;
|
|
3098
|
+
}
|
|
3099
|
+
const seg = name.slice(i, j);
|
|
3100
|
+
if (!/^[A-Za-z_][A-Za-z0-9_'-]*$/.test(seg))
|
|
3101
|
+
return segs;
|
|
3102
|
+
segs.push(seg);
|
|
3103
|
+
i = j + 1;
|
|
3104
|
+
}
|
|
3105
|
+
return segs;
|
|
3106
|
+
}
|
|
3107
|
+
async function nixOptionPathEdges(queries, onYield) {
|
|
3108
|
+
// One streaming pass over nix bindings (variables + the odd function-valued
|
|
3109
|
+
// option); memory stays O(bindings-kept), not O(all nodes) (#610).
|
|
3110
|
+
const byFile = new Map();
|
|
3111
|
+
let scanned = 0;
|
|
3112
|
+
for (const kind of ['variable', 'function']) {
|
|
3113
|
+
for (const node of queries.iterateNodesByKind(kind)) {
|
|
3114
|
+
if ((++scanned & 0x3fff) === 0 && onYield)
|
|
3115
|
+
await onYield();
|
|
3116
|
+
if (node.language !== 'nix')
|
|
3117
|
+
continue;
|
|
3118
|
+
const segs = nixLeadingPlainSegments(node.name);
|
|
3119
|
+
if (segs.length === 0)
|
|
3120
|
+
continue;
|
|
3121
|
+
const rec = {
|
|
3122
|
+
id: node.id,
|
|
3123
|
+
filePath: node.filePath,
|
|
3124
|
+
startLine: node.startLine,
|
|
3125
|
+
endLine: node.endLine,
|
|
3126
|
+
segs,
|
|
3127
|
+
};
|
|
3128
|
+
const arr = byFile.get(node.filePath);
|
|
3129
|
+
if (arr)
|
|
3130
|
+
arr.push(rec);
|
|
3131
|
+
else
|
|
3132
|
+
byFile.set(node.filePath, [rec]);
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
// Per file: walk bindings outermost-first with a stack of active option
|
|
3136
|
+
// spans, composing nested declaration paths (`options = { services.foo = {
|
|
3137
|
+
// enable = mkOption ...; }; }` registers services.foo AND services.foo.enable).
|
|
3138
|
+
// An `options` binding nested inside another option span is a SUBMODULE's
|
|
3139
|
+
// own namespace (`attrsOf (submodule { options = ...; })`) — its internals
|
|
3140
|
+
// are not globally addressable, so the sentinel blocks registration below it
|
|
3141
|
+
// while still excluding the region from write candidates.
|
|
3142
|
+
const SUBMODULE = 'submodule';
|
|
3143
|
+
const decls = new Map();
|
|
3144
|
+
const writes = [];
|
|
3145
|
+
const register = (path, rec) => {
|
|
3146
|
+
if (path.length < 2 || path.includes(SUBMODULE))
|
|
3147
|
+
return;
|
|
3148
|
+
const key = path.join('.');
|
|
3149
|
+
const arr = decls.get(key);
|
|
3150
|
+
if (arr)
|
|
3151
|
+
arr.push(rec);
|
|
3152
|
+
else
|
|
3153
|
+
decls.set(key, [rec]);
|
|
3154
|
+
};
|
|
3155
|
+
for (const recs of byFile.values()) {
|
|
3156
|
+
recs.sort((a, b) => a.startLine - b.startLine || b.endLine - a.endLine);
|
|
3157
|
+
const stack = [];
|
|
3158
|
+
for (const rec of recs) {
|
|
3159
|
+
while (stack.length > 0 && stack[stack.length - 1].end < rec.startLine)
|
|
3160
|
+
stack.pop();
|
|
3161
|
+
// Strict containment at line granularity: a one-line nested binding is
|
|
3162
|
+
// indistinguishable from its container, so it stays unclassified (rare
|
|
3163
|
+
// in module code, where option blocks are multi-line).
|
|
3164
|
+
const enclosing = stack.length > 0 &&
|
|
3165
|
+
rec.startLine >= stack[stack.length - 1].start &&
|
|
3166
|
+
rec.endLine <= stack[stack.length - 1].end &&
|
|
3167
|
+
!(rec.startLine === stack[stack.length - 1].start && rec.endLine === stack[stack.length - 1].end)
|
|
3168
|
+
? stack[stack.length - 1]
|
|
3169
|
+
: null;
|
|
3170
|
+
if (rec.segs[0] === 'options') {
|
|
3171
|
+
const ownPath = rec.segs.slice(1); // [] for the bare `options = { ... }` spelling
|
|
3172
|
+
const prefix = enclosing ? [SUBMODULE] : ownPath;
|
|
3173
|
+
register(prefix, rec);
|
|
3174
|
+
stack.push({ start: rec.startLine, end: rec.endLine, prefix });
|
|
3175
|
+
continue;
|
|
3176
|
+
}
|
|
3177
|
+
if (enclosing) {
|
|
3178
|
+
const composed = [...enclosing.prefix, ...rec.segs];
|
|
3179
|
+
register(composed, rec);
|
|
3180
|
+
stack.push({ start: rec.startLine, end: rec.endLine, prefix: composed });
|
|
3181
|
+
continue;
|
|
3182
|
+
}
|
|
3183
|
+
if (rec.segs.length >= 2) {
|
|
3184
|
+
writes.push(rec);
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
if (decls.size === 0 || writes.length === 0)
|
|
3189
|
+
return [];
|
|
3190
|
+
const edges = [];
|
|
3191
|
+
for (const w of writes) {
|
|
3192
|
+
// `config.services.x = ...` spells the same write with an explicit prefix.
|
|
3193
|
+
const segs = w.segs[0] === 'config' ? w.segs.slice(1) : w.segs;
|
|
3194
|
+
if (segs.length < 2)
|
|
3195
|
+
continue;
|
|
3196
|
+
// Longest prefix wins; an ambiguous longest match does NOT fall back to a
|
|
3197
|
+
// shorter one (that would link `services.nginx.virtualHosts.x` to
|
|
3198
|
+
// `options.services.nginx` when virtualHosts is the contested path).
|
|
3199
|
+
for (let len = Math.min(segs.length, 6); len >= 2; len--) {
|
|
3200
|
+
const candidates = decls.get(segs.slice(0, len).join('.'));
|
|
3201
|
+
if (!candidates || candidates.length === 0)
|
|
3202
|
+
continue;
|
|
3203
|
+
const files = new Set(candidates.map((c) => c.filePath));
|
|
3204
|
+
if (files.size === 1) {
|
|
3205
|
+
const target = candidates[0];
|
|
3206
|
+
if (target.id !== w.id) {
|
|
3207
|
+
edges.push({
|
|
3208
|
+
source: w.id,
|
|
3209
|
+
target: target.id,
|
|
3210
|
+
kind: 'references',
|
|
3211
|
+
line: w.startLine,
|
|
3212
|
+
provenance: 'heuristic',
|
|
3213
|
+
metadata: {
|
|
3214
|
+
synthesizedBy: 'nix-option-path',
|
|
3215
|
+
optionPath: segs.slice(0, len).join('.'),
|
|
3216
|
+
registeredAt: `${target.filePath}:${target.startLine}`,
|
|
3217
|
+
},
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
break; // longest hit decides, matched or ambiguous
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
return edges;
|
|
3225
|
+
}
|
|
3226
|
+
function erlangBehaviourDispatchEdges(queries, ctx) {
|
|
3227
|
+
// Cheap language gate: no Erlang modules → no cost beyond one kind query.
|
|
3228
|
+
const erlangModules = queries.getNodesByKind('namespace').filter((n) => n.language === 'erlang');
|
|
3229
|
+
if (erlangModules.length === 0)
|
|
3230
|
+
return [];
|
|
3231
|
+
// Pass 1 — scan every Erlang file with `-callback` decls: behaviour module →
|
|
3232
|
+
// its (name, arity) callback set, and the global `name/arity` → declaring
|
|
3233
|
+
// behaviours map that drives the ambiguity gate.
|
|
3234
|
+
const moduleByFile = new Map();
|
|
3235
|
+
for (const ns of erlangModules) {
|
|
3236
|
+
if (!moduleByFile.has(ns.filePath))
|
|
3237
|
+
moduleByFile.set(ns.filePath, ns);
|
|
3238
|
+
}
|
|
3239
|
+
const declaringBehaviours = new Map(); // `fn/arity` → behaviour namespaces
|
|
3240
|
+
const callbackNames = new Set();
|
|
3241
|
+
for (const file of ctx.getAllFiles()) {
|
|
3242
|
+
if (!ERLANG_EXT.test(file))
|
|
3243
|
+
continue;
|
|
3244
|
+
const behaviour = moduleByFile.get(file);
|
|
3245
|
+
if (!behaviour)
|
|
3246
|
+
continue; // a .hrl or module-less file can't be a behaviour
|
|
3247
|
+
const content = ctx.readFile(file);
|
|
3248
|
+
if (!content || !content.includes('-callback'))
|
|
3249
|
+
continue;
|
|
3250
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
|
|
3251
|
+
ERLANG_CALLBACK_DECL_RE.lastIndex = 0;
|
|
3252
|
+
let m;
|
|
3253
|
+
while ((m = ERLANG_CALLBACK_DECL_RE.exec(safe))) {
|
|
3254
|
+
const name = m[2].replace(/^'|'$/g, '');
|
|
3255
|
+
const arity = erlangArityAt(safe, m.index + m[0].length - 1);
|
|
3256
|
+
if (arity < 0)
|
|
3257
|
+
continue;
|
|
3258
|
+
const key = `${name}/${arity}`;
|
|
3259
|
+
const arr = declaringBehaviours.get(key);
|
|
3260
|
+
if (arr) {
|
|
3261
|
+
if (!arr.some((b) => b.id === behaviour.id))
|
|
3262
|
+
arr.push(behaviour);
|
|
3263
|
+
}
|
|
3264
|
+
else {
|
|
3265
|
+
declaringBehaviours.set(key, [behaviour]);
|
|
3266
|
+
}
|
|
3267
|
+
callbackNames.add(name);
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
if (declaringBehaviours.size === 0)
|
|
3271
|
+
return [];
|
|
3272
|
+
// Implementer target lookup, lazy per (behaviour, fn): implementers come
|
|
3273
|
+
// from the `implements` edges extraction resolved, and the target is the
|
|
3274
|
+
// implementer module's own exported `fn` function node.
|
|
3275
|
+
const targetCache = new Map();
|
|
3276
|
+
const targetsOf = (behaviour, fn) => {
|
|
3277
|
+
const cacheKey = `${behaviour.id}#${fn}`;
|
|
3278
|
+
let targets = targetCache.get(cacheKey);
|
|
3279
|
+
if (targets)
|
|
3280
|
+
return targets;
|
|
3281
|
+
targets = [];
|
|
3282
|
+
for (const e of queries.getIncomingEdges(behaviour.id, ['implements'])) {
|
|
3283
|
+
const impl = queries.getNodeById(e.source);
|
|
3284
|
+
if (!impl || impl.language !== 'erlang' || impl.kind !== 'namespace')
|
|
3285
|
+
continue;
|
|
3286
|
+
const fnNode = ctx
|
|
3287
|
+
.getNodesInFile(impl.filePath)
|
|
3288
|
+
.find((n) => n.kind === 'function' && n.name === fn && n.isExported !== false);
|
|
3289
|
+
if (fnNode)
|
|
3290
|
+
targets.push(fnNode);
|
|
3291
|
+
}
|
|
3292
|
+
targetCache.set(cacheKey, targets);
|
|
3293
|
+
return targets;
|
|
3294
|
+
};
|
|
3295
|
+
// Pass 2 — dispatch sites. Only files containing a var-module call shape are
|
|
3296
|
+
// scanned in full.
|
|
3297
|
+
const edges = [];
|
|
3298
|
+
const seen = new Set();
|
|
3299
|
+
for (const file of ctx.getAllFiles()) {
|
|
3300
|
+
if (!ERLANG_EXT.test(file))
|
|
3301
|
+
continue;
|
|
3302
|
+
const content = ctx.readFile(file);
|
|
3303
|
+
if (!content || !/[A-Z][A-Za-z0-9_@]*:[a-z]/.test(content))
|
|
3304
|
+
continue;
|
|
3305
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'erlang');
|
|
3306
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
3307
|
+
ERLANG_DISPATCH_RE.lastIndex = 0;
|
|
3308
|
+
let m;
|
|
3309
|
+
while ((m = ERLANG_DISPATCH_RE.exec(safe))) {
|
|
3310
|
+
const fn = m[3];
|
|
3311
|
+
if (!callbackNames.has(fn))
|
|
3312
|
+
continue;
|
|
3313
|
+
const openIdx = m.index + m[0].length - 1;
|
|
3314
|
+
const arity = erlangArityAt(safe, openIdx);
|
|
3315
|
+
if (arity < 0)
|
|
3316
|
+
continue;
|
|
3317
|
+
const behaviours = declaringBehaviours.get(`${fn}/${arity}`);
|
|
3318
|
+
if (!behaviours || behaviours.length !== 1)
|
|
3319
|
+
continue; // unknown or ambiguous
|
|
3320
|
+
const behaviour = behaviours[0];
|
|
3321
|
+
const targets = targetsOf(behaviour, fn);
|
|
3322
|
+
if (targets.length === 0 || targets.length > ERLANG_BEHAVIOUR_FANOUT_CAP)
|
|
3323
|
+
continue;
|
|
3324
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
3325
|
+
const disp = enclosingFn(nodesInFile, line);
|
|
3326
|
+
if (!disp)
|
|
3327
|
+
continue;
|
|
3328
|
+
for (const target of targets) {
|
|
3329
|
+
if (target.id === disp.id)
|
|
3330
|
+
continue;
|
|
3331
|
+
const key = `${disp.id}>${target.id}`;
|
|
3332
|
+
if (seen.has(key))
|
|
3333
|
+
continue;
|
|
3334
|
+
seen.add(key);
|
|
3335
|
+
edges.push({
|
|
3336
|
+
source: disp.id,
|
|
3337
|
+
target: target.id,
|
|
3338
|
+
kind: 'calls',
|
|
3339
|
+
line,
|
|
3340
|
+
provenance: 'heuristic',
|
|
3341
|
+
metadata: {
|
|
3342
|
+
synthesizedBy: 'erlang-behaviour',
|
|
3343
|
+
via: `${behaviour.name}:${fn}/${arity}`,
|
|
3344
|
+
registeredAt: `${file}:${line}`,
|
|
3345
|
+
},
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
return edges;
|
|
3351
|
+
}
|
|
2695
3352
|
// ── Laravel events (PHP) ──────────────────────────────────────────────────────
|
|
2696
3353
|
// Laravel decouples an event dispatch from its listener(s), linked by the EVENT CLASS:
|
|
2697
3354
|
// // app/Events/PlaybackStarted.php + app/Listeners/UpdateLastfmNowPlaying.php
|
|
@@ -2894,6 +3551,12 @@ async function synthesizeCallbackEdges(queries, ctx) {
|
|
|
2894
3551
|
await yieldToLoop();
|
|
2895
3552
|
const flutterEdges = flutterBuildEdges(queries, ctx);
|
|
2896
3553
|
await yieldToLoop();
|
|
3554
|
+
const arkuiStateEdges = arkuiStateBuildEdges(queries, ctx);
|
|
3555
|
+
await yieldToLoop();
|
|
3556
|
+
const arkuiEmitter = arkuiEmitterEdges(ctx);
|
|
3557
|
+
await yieldToLoop();
|
|
3558
|
+
const arkuiRoutes = arkuiRouterEdges(ctx);
|
|
3559
|
+
await yieldToLoop();
|
|
2897
3560
|
const cppEdges = cppOverrideEdges(queries);
|
|
2898
3561
|
await yieldToLoop();
|
|
2899
3562
|
const ifaceEdges = interfaceOverrideEdges(queries);
|
|
@@ -2932,12 +3595,16 @@ async function synthesizeCallbackEdges(queries, ctx) {
|
|
|
2932
3595
|
await yieldToLoop();
|
|
2933
3596
|
const sidekiqEdges = sidekiqDispatchEdges(ctx);
|
|
2934
3597
|
await yieldToLoop();
|
|
3598
|
+
const erlangBehaviourEdges = erlangBehaviourDispatchEdges(queries, ctx);
|
|
3599
|
+
await yieldToLoop();
|
|
2935
3600
|
const laravelEdges = laravelEventEdges(ctx);
|
|
2936
3601
|
await yieldToLoop();
|
|
2937
3602
|
const cFnPtrEdges = (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(queries, ctx);
|
|
2938
3603
|
await yieldToLoop();
|
|
2939
3604
|
const goframeEdges = (0, goframe_synthesizer_1.goframeRouteEdges)(ctx);
|
|
2940
3605
|
await yieldToLoop();
|
|
3606
|
+
const nixOptionEdges = await nixOptionPathEdges(queries, yieldToLoop);
|
|
3607
|
+
await yieldToLoop();
|
|
2941
3608
|
const merged = [];
|
|
2942
3609
|
const seen = new Set();
|
|
2943
3610
|
for (const e of [
|
|
@@ -2950,6 +3617,9 @@ async function synthesizeCallbackEdges(queries, ctx) {
|
|
|
2950
3617
|
...svelteKitEdges,
|
|
2951
3618
|
...pascalEdges,
|
|
2952
3619
|
...flutterEdges,
|
|
3620
|
+
...arkuiStateEdges,
|
|
3621
|
+
...arkuiEmitter,
|
|
3622
|
+
...arkuiRoutes,
|
|
2953
3623
|
...cppEdges,
|
|
2954
3624
|
...ifaceEdges,
|
|
2955
3625
|
...kotlinExpectActual,
|
|
@@ -2969,9 +3639,11 @@ async function synthesizeCallbackEdges(queries, ctx) {
|
|
|
2969
3639
|
...springEdges,
|
|
2970
3640
|
...mediatrEdges,
|
|
2971
3641
|
...sidekiqEdges,
|
|
3642
|
+
...erlangBehaviourEdges,
|
|
2972
3643
|
...laravelEdges,
|
|
2973
3644
|
...cFnPtrEdges,
|
|
2974
3645
|
...goframeEdges,
|
|
3646
|
+
...nixOptionEdges,
|
|
2975
3647
|
]) {
|
|
2976
3648
|
const key = `${e.source}>${e.target}`;
|
|
2977
3649
|
if (seen.has(key))
|