@colbymchenry/codegraph-darwin-x64 1.0.0 → 1.1.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/lib/dist/bin/codegraph.js +258 -17
- package/lib/dist/bin/codegraph.js.map +1 -1
- package/lib/dist/bin/fatal-handler.d.ts +20 -0
- package/lib/dist/bin/fatal-handler.d.ts.map +1 -0
- package/lib/dist/bin/fatal-handler.js +118 -0
- package/lib/dist/bin/fatal-handler.js.map +1 -0
- package/lib/dist/db/index.d.ts +22 -1
- package/lib/dist/db/index.d.ts.map +1 -1
- package/lib/dist/db/index.js +46 -1
- package/lib/dist/db/index.js.map +1 -1
- package/lib/dist/db/queries.d.ts +14 -0
- package/lib/dist/db/queries.d.ts.map +1 -1
- package/lib/dist/db/queries.js +25 -0
- package/lib/dist/db/queries.js.map +1 -1
- package/lib/dist/directory.d.ts +58 -0
- package/lib/dist/directory.d.ts.map +1 -1
- package/lib/dist/directory.js +165 -0
- package/lib/dist/directory.js.map +1 -1
- package/lib/dist/extraction/grammars.d.ts +11 -3
- package/lib/dist/extraction/grammars.d.ts.map +1 -1
- package/lib/dist/extraction/grammars.js +14 -5
- package/lib/dist/extraction/grammars.js.map +1 -1
- package/lib/dist/extraction/index.d.ts.map +1 -1
- package/lib/dist/extraction/index.js +202 -32
- package/lib/dist/extraction/index.js.map +1 -1
- package/lib/dist/extraction/languages/c-cpp.d.ts.map +1 -1
- package/lib/dist/extraction/languages/c-cpp.js +47 -2
- package/lib/dist/extraction/languages/c-cpp.js.map +1 -1
- package/lib/dist/extraction/languages/csharp.d.ts.map +1 -1
- package/lib/dist/extraction/languages/csharp.js +20 -0
- package/lib/dist/extraction/languages/csharp.js.map +1 -1
- package/lib/dist/extraction/languages/dart.d.ts.map +1 -1
- package/lib/dist/extraction/languages/dart.js +22 -0
- package/lib/dist/extraction/languages/dart.js.map +1 -1
- package/lib/dist/extraction/languages/java.d.ts.map +1 -1
- package/lib/dist/extraction/languages/java.js +213 -9
- package/lib/dist/extraction/languages/java.js.map +1 -1
- package/lib/dist/extraction/languages/kotlin.d.ts.map +1 -1
- package/lib/dist/extraction/languages/kotlin.js +51 -0
- package/lib/dist/extraction/languages/kotlin.js.map +1 -1
- package/lib/dist/extraction/languages/scala.d.ts.map +1 -1
- package/lib/dist/extraction/languages/scala.js +19 -9
- package/lib/dist/extraction/languages/scala.js.map +1 -1
- package/lib/dist/extraction/parse-worker.js +4 -1
- package/lib/dist/extraction/parse-worker.js.map +1 -1
- package/lib/dist/extraction/tree-sitter-types.d.ts +13 -0
- package/lib/dist/extraction/tree-sitter-types.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter.d.ts +119 -0
- package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/lib/dist/extraction/tree-sitter.js +890 -11
- package/lib/dist/extraction/tree-sitter.js.map +1 -1
- package/lib/dist/index.d.ts +33 -0
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +68 -7
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/installer/index.d.ts.map +1 -1
- package/lib/dist/installer/index.js +33 -56
- package/lib/dist/installer/index.js.map +1 -1
- package/lib/dist/installer/instructions-template.d.ts +3 -3
- package/lib/dist/installer/instructions-template.d.ts.map +1 -1
- package/lib/dist/installer/instructions-template.js +4 -4
- package/lib/dist/installer/targets/claude.d.ts +18 -12
- package/lib/dist/installer/targets/claude.d.ts.map +1 -1
- package/lib/dist/installer/targets/claude.js +78 -6
- package/lib/dist/installer/targets/claude.js.map +1 -1
- package/lib/dist/installer/targets/shared.d.ts +12 -2
- package/lib/dist/installer/targets/shared.d.ts.map +1 -1
- package/lib/dist/installer/targets/shared.js +13 -12
- package/lib/dist/installer/targets/shared.js.map +1 -1
- package/lib/dist/installer/targets/types.d.ts +7 -0
- package/lib/dist/installer/targets/types.d.ts.map +1 -1
- package/lib/dist/mcp/daemon-manager.d.ts +42 -0
- package/lib/dist/mcp/daemon-manager.d.ts.map +1 -0
- package/lib/dist/mcp/daemon-manager.js +129 -0
- package/lib/dist/mcp/daemon-manager.js.map +1 -0
- package/lib/dist/mcp/daemon-registry.d.ts +47 -0
- package/lib/dist/mcp/daemon-registry.d.ts.map +1 -0
- package/lib/dist/mcp/daemon-registry.js +229 -0
- package/lib/dist/mcp/daemon-registry.js.map +1 -0
- package/lib/dist/mcp/daemon.d.ts.map +1 -1
- package/lib/dist/mcp/daemon.js +5 -0
- package/lib/dist/mcp/daemon.js.map +1 -1
- package/lib/dist/mcp/engine.d.ts.map +1 -1
- package/lib/dist/mcp/engine.js +8 -0
- package/lib/dist/mcp/engine.js.map +1 -1
- package/lib/dist/mcp/index.d.ts +1 -0
- package/lib/dist/mcp/index.d.ts.map +1 -1
- package/lib/dist/mcp/index.js +13 -0
- package/lib/dist/mcp/index.js.map +1 -1
- package/lib/dist/mcp/liveness-watchdog.d.ts +18 -0
- package/lib/dist/mcp/liveness-watchdog.d.ts.map +1 -0
- package/lib/dist/mcp/liveness-watchdog.js +207 -0
- package/lib/dist/mcp/liveness-watchdog.js.map +1 -0
- package/lib/dist/mcp/server-instructions.d.ts +18 -14
- package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
- package/lib/dist/mcp/server-instructions.js +57 -52
- package/lib/dist/mcp/server-instructions.js.map +1 -1
- package/lib/dist/mcp/session.d.ts.map +1 -1
- package/lib/dist/mcp/session.js +23 -18
- package/lib/dist/mcp/session.js.map +1 -1
- package/lib/dist/mcp/tools.d.ts +51 -1
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +585 -151
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/project-config.d.ts +19 -0
- package/lib/dist/project-config.d.ts.map +1 -0
- package/lib/dist/project-config.js +180 -0
- package/lib/dist/project-config.js.map +1 -0
- package/lib/dist/reasoning/config.d.ts +45 -0
- package/lib/dist/reasoning/config.d.ts.map +1 -0
- package/lib/dist/reasoning/config.js +171 -0
- package/lib/dist/reasoning/config.js.map +1 -0
- package/lib/dist/reasoning/credentials.d.ts +5 -0
- package/lib/dist/reasoning/credentials.d.ts.map +1 -0
- package/lib/dist/reasoning/credentials.js +83 -0
- package/lib/dist/reasoning/credentials.js.map +1 -0
- package/lib/dist/reasoning/login.d.ts +21 -0
- package/lib/dist/reasoning/login.d.ts.map +1 -0
- package/lib/dist/reasoning/login.js +85 -0
- package/lib/dist/reasoning/login.js.map +1 -0
- package/lib/dist/reasoning/reasoner.d.ts +43 -0
- package/lib/dist/reasoning/reasoner.d.ts.map +1 -0
- package/lib/dist/reasoning/reasoner.js +308 -0
- package/lib/dist/reasoning/reasoner.js.map +1 -0
- package/lib/dist/resolution/c-fnptr-synthesizer.d.ts +33 -0
- package/lib/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -0
- package/lib/dist/resolution/c-fnptr-synthesizer.js +352 -0
- package/lib/dist/resolution/c-fnptr-synthesizer.js.map +1 -0
- package/lib/dist/resolution/callback-synthesizer.d.ts +6 -1
- package/lib/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/lib/dist/resolution/callback-synthesizer.js +1109 -1
- package/lib/dist/resolution/callback-synthesizer.js.map +1 -1
- package/lib/dist/resolution/frameworks/goframe.d.ts +41 -0
- package/lib/dist/resolution/frameworks/goframe.d.ts.map +1 -0
- package/lib/dist/resolution/frameworks/goframe.js +112 -0
- package/lib/dist/resolution/frameworks/goframe.js.map +1 -0
- package/lib/dist/resolution/frameworks/index.d.ts +1 -0
- package/lib/dist/resolution/frameworks/index.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/index.js +5 -1
- package/lib/dist/resolution/frameworks/index.js.map +1 -1
- package/lib/dist/resolution/frameworks/react.d.ts.map +1 -1
- package/lib/dist/resolution/frameworks/react.js +17 -60
- package/lib/dist/resolution/frameworks/react.js.map +1 -1
- package/lib/dist/resolution/goframe-synthesizer.d.ts +28 -0
- package/lib/dist/resolution/goframe-synthesizer.d.ts.map +1 -0
- package/lib/dist/resolution/goframe-synthesizer.js +158 -0
- package/lib/dist/resolution/goframe-synthesizer.js.map +1 -0
- package/lib/dist/resolution/import-resolver.d.ts.map +1 -1
- package/lib/dist/resolution/import-resolver.js +56 -0
- package/lib/dist/resolution/import-resolver.js.map +1 -1
- package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
- package/lib/dist/resolution/name-matcher.js +48 -8
- 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 +2 -0
- package/lib/dist/resolution/strip-comments.js.map +1 -1
- package/lib/dist/sync/watcher.d.ts +68 -1
- package/lib/dist/sync/watcher.d.ts.map +1 -1
- package/lib/dist/sync/watcher.js +212 -14
- package/lib/dist/sync/watcher.js.map +1 -1
- package/lib/dist/telemetry/index.d.ts +0 -3
- package/lib/dist/telemetry/index.d.ts.map +1 -1
- package/lib/dist/telemetry/index.js +4 -7
- package/lib/dist/telemetry/index.js.map +1 -1
- package/lib/dist/upgrade/index.d.ts.map +1 -1
- package/lib/dist/upgrade/index.js +40 -4
- package/lib/dist/upgrade/index.js.map +1 -1
- package/lib/dist/utils.d.ts +14 -1
- package/lib/dist/utils.d.ts.map +1 -1
- package/lib/dist/utils.js +20 -2
- package/lib/dist/utils.js.map +1 -1
- package/lib/node_modules/.package-lock.json +1 -1
- package/lib/package.json +2 -2
- package/package.json +1 -1
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.synthesizeCallbackEdges = synthesizeCallbackEdges;
|
|
4
4
|
const generated_detection_1 = require("../extraction/generated-detection");
|
|
5
5
|
const strip_comments_1 = require("./strip-comments");
|
|
6
|
+
const c_fnptr_synthesizer_1 = require("./c-fnptr-synthesizer");
|
|
7
|
+
const goframe_synthesizer_1 = require("./goframe-synthesizer");
|
|
6
8
|
const REGISTRAR_NAME = /^(on[A-Z]\w*|subscribe|addListener|addEventListener|register|watch|listen|addCallback)$/;
|
|
7
9
|
const DISPATCHER_NAME = /(emit|trigger|notify|dispatch|fire|publish|flush)/i;
|
|
8
10
|
const MAX_CALLBACKS_PER_CHANNEL = 40;
|
|
@@ -1741,10 +1743,1092 @@ function svelteKitLoadEdges(ctx) {
|
|
|
1741
1743
|
}
|
|
1742
1744
|
return edges;
|
|
1743
1745
|
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Redux-thunk dispatch chain. `export const X = createAsyncThunk(prefix, async (a, api) => {...})`
|
|
1748
|
+
* (or a wrapper like trezor's `createThunk(...)`) passes the async body as an ARGUMENT, so
|
|
1749
|
+
* tree-sitter never extracts it as a function node: `X` is a `constant` whose body's calls are
|
|
1750
|
+
* ORPHANED. The `dispatch(nextThunk(...))` calls that drive a thunk chain forward therefore produce
|
|
1751
|
+
* no edges, so `callees(X)` is empty and a flow `dispatch(X(...)) → X → nextThunk` dead-ends at the
|
|
1752
|
+
* constant (validated on trezor-suite: the signXxxThunk constants had ZERO outgoing edges). Bridge
|
|
1753
|
+
* it: body-scan each thunk constant for `dispatch(Y(...))` and link `X → Y`, so the dispatch chain
|
|
1754
|
+
* connects. High-precision — the `dispatch(` keyword plus `Y` must resolve to a function/constant/
|
|
1755
|
+
* method node; capped; gated on thunk constants existing so it never runs on non-RTK repos.
|
|
1756
|
+
* Cross-file by design (a suite thunk dispatches a wallet-core thunk). Provenance `heuristic`,
|
|
1757
|
+
* `synthesizedBy:'redux-thunk'`; `registeredAt` is the dispatch site.
|
|
1758
|
+
*/
|
|
1759
|
+
const THUNK_DECL_RE = /create(?:Async)?Thunk/;
|
|
1760
|
+
const THUNK_DISPATCH_RE = /\bdispatch\s*\(\s*([A-Za-z_]\w*)\s*[(),]/g;
|
|
1761
|
+
const THUNK_FANOUT_CAP = 24;
|
|
1762
|
+
function reduxThunkEdges(queries, ctx) {
|
|
1763
|
+
const edges = [];
|
|
1764
|
+
const seen = new Set();
|
|
1765
|
+
for (const node of queries.iterateNodesByKind('constant')) {
|
|
1766
|
+
// Cheap gate: the initializer (captured in `signature`) must be a create(Async)Thunk call —
|
|
1767
|
+
// avoids reading every constant's body on a large repo.
|
|
1768
|
+
if (!node.signature || !THUNK_DECL_RE.test(node.signature))
|
|
1769
|
+
continue;
|
|
1770
|
+
const content = ctx.readFile(node.filePath);
|
|
1771
|
+
const src = content && sliceLines(content, node.startLine, node.endLine);
|
|
1772
|
+
if (!src)
|
|
1773
|
+
continue;
|
|
1774
|
+
// Thunks are TS/JS-family (same // and /* */ comment syntax); map to a CommentLang.
|
|
1775
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(src, node.language === 'javascript' || node.language === 'jsx' ? 'javascript' : 'typescript');
|
|
1776
|
+
THUNK_DISPATCH_RE.lastIndex = 0;
|
|
1777
|
+
let m;
|
|
1778
|
+
let added = 0;
|
|
1779
|
+
while ((m = THUNK_DISPATCH_RE.exec(safe)) && added < THUNK_FANOUT_CAP) {
|
|
1780
|
+
const name = m[1];
|
|
1781
|
+
if (name === node.name)
|
|
1782
|
+
continue; // self-dispatch (recursive thunk) — skip
|
|
1783
|
+
// Resolve the dispatched name, PREFERRING the thunk/action-creator over a same-named
|
|
1784
|
+
// service function. `dispatch(X(...))` dispatches a thunk or an action-creator (both
|
|
1785
|
+
// `constant`s) — never an unrelated helper that merely shares the name. On octo-call,
|
|
1786
|
+
// `leaveCall` is BOTH a `createAsyncThunk` const AND a service function, and the bare
|
|
1787
|
+
// `.find()` picked the function (wrong). Order: thunk const > other const > same-file
|
|
1788
|
+
// callable > first match. A single candidate (no collision) is unaffected.
|
|
1789
|
+
const cands = ctx
|
|
1790
|
+
.getNodesByName(name)
|
|
1791
|
+
.filter((n) => n.kind === 'constant' || n.kind === 'function' || n.kind === 'method');
|
|
1792
|
+
const target = cands.find((n) => !!n.signature && THUNK_DECL_RE.test(n.signature)) ??
|
|
1793
|
+
cands.find((n) => n.kind === 'constant') ??
|
|
1794
|
+
cands.find((n) => n.filePath === node.filePath) ??
|
|
1795
|
+
cands[0];
|
|
1796
|
+
if (!target || target.id === node.id)
|
|
1797
|
+
continue;
|
|
1798
|
+
const key = `${node.id}>${target.id}`;
|
|
1799
|
+
if (seen.has(key))
|
|
1800
|
+
continue;
|
|
1801
|
+
seen.add(key);
|
|
1802
|
+
const line = node.startLine + safe.slice(0, m.index).split('\n').length - 1;
|
|
1803
|
+
edges.push({
|
|
1804
|
+
source: node.id,
|
|
1805
|
+
target: target.id,
|
|
1806
|
+
kind: 'calls',
|
|
1807
|
+
line,
|
|
1808
|
+
provenance: 'heuristic',
|
|
1809
|
+
metadata: { synthesizedBy: 'redux-thunk', via: name, registeredAt: `${node.filePath}:${line}` },
|
|
1810
|
+
});
|
|
1811
|
+
added++;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
return edges;
|
|
1815
|
+
}
|
|
1816
|
+
// ── Object-literal registry dispatch ─────────────────────────────────────────
|
|
1817
|
+
// A command/handler registry maps string keys → handler class/function symbols in an
|
|
1818
|
+
// object literal, then dispatches by a RUNTIME key static parsing can't follow:
|
|
1819
|
+
// this.commands = { [Cmd.ADD]: AddObjectCommand, ... } // registration
|
|
1820
|
+
// new this.commands[command](args).execute() // dynamic dispatch
|
|
1821
|
+
// Bridge it like gin-middleware-chain: link each dispatching function → each registered
|
|
1822
|
+
// handler's callable entry (a class's execute/run/handle/… method — preferring the method
|
|
1823
|
+
// chained at the dispatch site — or the function value). Scoped to a registry + dispatch in
|
|
1824
|
+
// the SAME file (the cross-file barrel-namespace variant, e.g. trezor's getMethod, is
|
|
1825
|
+
// deferred). Gated on a real object literal with ≥2 entries that RESOLVE to callables (a
|
|
1826
|
+
// `{ width: 5 }` literal resolves to nothing → no edges); fan-out capped.
|
|
1827
|
+
const REGISTRY_ASSIGN_RE = /(?:(?:const|let|var)\s+([A-Za-z_$][\w$]*)|((?:this\.)?[A-Za-z_$][\w$]*))\s*=\s*\{/g;
|
|
1828
|
+
const REGISTRY_DISPATCH_RE = /(?:\bnew\s+)?((?:this\.)?[A-Za-z_$][\w$]*)\s*\[\s*([A-Za-z_$][\w$.]*)\s*\]\s*(?:\(|\.[A-Za-z_$])/g;
|
|
1829
|
+
const REGISTRY_MIN_ENTRIES = 2;
|
|
1830
|
+
const REGISTRY_FANOUT_CAP = 40;
|
|
1831
|
+
const REGISTRY_CLASS_ENTRY = new Set(['execute', 'run', 'handle', 'perform', 'process', 'call', 'apply', 'dispatch']);
|
|
1832
|
+
const REGISTRY_JS_EXT = /\.(?:ts|tsx|js|jsx|mjs|cjs)$/;
|
|
1833
|
+
/** From the index of an opening `{`, return the brace-balanced body up to its matching `}`. */
|
|
1834
|
+
function braceBody(src, openIdx) {
|
|
1835
|
+
let depth = 0;
|
|
1836
|
+
for (let i = openIdx; i < src.length; i++) {
|
|
1837
|
+
if (src[i] === '{')
|
|
1838
|
+
depth++;
|
|
1839
|
+
else if (src[i] === '}' && --depth === 0)
|
|
1840
|
+
return src.slice(openIdx + 1, i);
|
|
1841
|
+
}
|
|
1842
|
+
return null;
|
|
1843
|
+
}
|
|
1844
|
+
/** Top-level `key: Identifier` entries of an object-literal body. DEPTH-AWARE: only depth-0
|
|
1845
|
+
* segments are considered, so method-shorthand bodies (`number(a,b){…}`), arrow values
|
|
1846
|
+
* (`x: () => …`), and nested objects (`x: { … }`) don't leak their inner `k: v` pairs as
|
|
1847
|
+
* bogus handlers. The per-segment anchor (`^… key: Ident …$`) keeps only pure identifier
|
|
1848
|
+
* values — a data value (`x: 5`), call, or arrow fails to match. */
|
|
1849
|
+
function registryEntryNames(body) {
|
|
1850
|
+
const segs = [];
|
|
1851
|
+
let depth = 0;
|
|
1852
|
+
let start = 0;
|
|
1853
|
+
for (let i = 0; i < body.length; i++) {
|
|
1854
|
+
const c = body[i];
|
|
1855
|
+
if (c === '{' || c === '(' || c === '[')
|
|
1856
|
+
depth++;
|
|
1857
|
+
else if (c === '}' || c === ')' || c === ']')
|
|
1858
|
+
depth--;
|
|
1859
|
+
else if (c === ',' && depth === 0) {
|
|
1860
|
+
segs.push(body.slice(start, i));
|
|
1861
|
+
start = i + 1;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
segs.push(body.slice(start));
|
|
1865
|
+
const names = [];
|
|
1866
|
+
for (const seg of segs) {
|
|
1867
|
+
const m = /^\s*(?:\[[^\]]+\]|['"]?[\w$]+['"]?)\s*:\s*([A-Za-z_$][\w$]*)\s*$/.exec(seg);
|
|
1868
|
+
if (m && m[1].length >= 3 && !names.includes(m[1]))
|
|
1869
|
+
names.push(m[1]);
|
|
1870
|
+
}
|
|
1871
|
+
return names;
|
|
1872
|
+
}
|
|
1873
|
+
/** Resolve a registered handler name to its callable entry: a function value, or a class's
|
|
1874
|
+
* `execute`-like method (preferring the method chained at the dispatch site), else the class. */
|
|
1875
|
+
function resolveRegistryHandler(ctx, name, chained) {
|
|
1876
|
+
const cands = ctx.getNodesByName(name);
|
|
1877
|
+
const fn = cands.find((n) => n.kind === 'function');
|
|
1878
|
+
if (fn)
|
|
1879
|
+
return fn;
|
|
1880
|
+
const cls = cands.find((n) => n.kind === 'class' || n.kind === 'struct');
|
|
1881
|
+
if (cls) {
|
|
1882
|
+
const methods = ctx
|
|
1883
|
+
.getNodesInFile(cls.filePath)
|
|
1884
|
+
.filter((n) => n.kind === 'method' && n.startLine >= cls.startLine && n.startLine <= (cls.endLine ?? cls.startLine));
|
|
1885
|
+
const want = chained && REGISTRY_CLASS_ENTRY.has(chained) ? chained : null;
|
|
1886
|
+
const entry = (want && methods.find((m) => m.name === want)) ||
|
|
1887
|
+
methods.find((m) => REGISTRY_CLASS_ENTRY.has(m.name)) ||
|
|
1888
|
+
methods.find((m) => m.name === 'constructor');
|
|
1889
|
+
return entry ?? cls;
|
|
1890
|
+
}
|
|
1891
|
+
// Require a CALLABLE target — a registry dispatched as `reg[k](…)` invokes a function/
|
|
1892
|
+
// method, never a data `constant` (dropping it removes false positives like a `{ x: URL }`
|
|
1893
|
+
// entry resolving to the global URL constant).
|
|
1894
|
+
return cands.find((n) => n.kind === 'method') ?? null;
|
|
1895
|
+
}
|
|
1896
|
+
function objectRegistryEdges(ctx) {
|
|
1897
|
+
const edges = [];
|
|
1898
|
+
const seen = new Set();
|
|
1899
|
+
for (const file of ctx.getAllFiles()) {
|
|
1900
|
+
if (!REGISTRY_JS_EXT.test(file))
|
|
1901
|
+
continue;
|
|
1902
|
+
const content = ctx.readFile(file);
|
|
1903
|
+
// Cheap pre-filter: a computed member access BY NAME (`ident[ident`) — the dispatch shape.
|
|
1904
|
+
if (!content || !/[\w$]\s*\[\s*[A-Za-z_$]/.test(content))
|
|
1905
|
+
continue;
|
|
1906
|
+
// Skip minified/generated bundles (draco, three.min, base64…): their pervasive `h[x](...)`
|
|
1907
|
+
// calls + single-letter `{a:b}` literals are a false-positive minefield. Average line
|
|
1908
|
+
// length is the reliable tell — real source ~30–80, minified in the hundreds/thousands.
|
|
1909
|
+
const newlines = (content.match(/\n/g)?.length ?? 0) + 1;
|
|
1910
|
+
if (content.length / newlines > 200)
|
|
1911
|
+
continue;
|
|
1912
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, /\.(?:jsx?|mjs|cjs)$/.test(file) ? 'javascript' : 'typescript');
|
|
1913
|
+
// 1. Dispatch sites: `(new )?<ref>[<ident-key>]` followed by a call or a chained method.
|
|
1914
|
+
// A quoted-string key (`['save']`) does NOT match — that's a static access, not dispatch.
|
|
1915
|
+
REGISTRY_DISPATCH_RE.lastIndex = 0;
|
|
1916
|
+
const dispatches = [];
|
|
1917
|
+
let dm;
|
|
1918
|
+
while ((dm = REGISTRY_DISPATCH_RE.exec(safe))) {
|
|
1919
|
+
const win = safe.slice(dm.index, dm.index + 160);
|
|
1920
|
+
const cm = /\]\s*\([^)]*\)\s*\.\s*([A-Za-z_$][\w$]*)/.exec(win) || /\]\s*\.\s*([A-Za-z_$][\w$]*)/.exec(win);
|
|
1921
|
+
dispatches.push({ ref: dm[1], line: safe.slice(0, dm.index).split('\n').length, chained: cm ? cm[1] : null });
|
|
1922
|
+
}
|
|
1923
|
+
if (!dispatches.length)
|
|
1924
|
+
continue;
|
|
1925
|
+
// Normalize a leading `this.` so a class FIELD-INITIALIZER registry (`commands = {…}`)
|
|
1926
|
+
// matches a `this.commands[k]` dispatch, not just the constructor form `this.commands = {…}`.
|
|
1927
|
+
const norm = (r) => r.replace(/^this\./, '');
|
|
1928
|
+
const refs = new Set(dispatches.map((d) => norm(d.ref)));
|
|
1929
|
+
// 2. Registries: an object literal assigned to a dispatched ref, ≥2 entries resolving to callables.
|
|
1930
|
+
REGISTRY_ASSIGN_RE.lastIndex = 0;
|
|
1931
|
+
const registries = new Map();
|
|
1932
|
+
let am;
|
|
1933
|
+
while ((am = REGISTRY_ASSIGN_RE.exec(safe))) {
|
|
1934
|
+
const lhs = norm(am[1] ?? am[2]);
|
|
1935
|
+
if (!refs.has(lhs) || registries.has(lhs))
|
|
1936
|
+
continue;
|
|
1937
|
+
const body = braceBody(safe, am.index + am[0].length - 1);
|
|
1938
|
+
if (!body)
|
|
1939
|
+
continue;
|
|
1940
|
+
const names = registryEntryNames(body); // depth-0 `key: Identifier` entries only
|
|
1941
|
+
if (names.length >= REGISTRY_MIN_ENTRIES) {
|
|
1942
|
+
registries.set(lhs, { names, line: safe.slice(0, am.index).split('\n').length });
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
if (!registries.size)
|
|
1946
|
+
continue;
|
|
1947
|
+
// 3. Link each dispatcher → each registered handler's callable entry.
|
|
1948
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
1949
|
+
for (const d of dispatches) {
|
|
1950
|
+
const reg = registries.get(norm(d.ref));
|
|
1951
|
+
if (!reg)
|
|
1952
|
+
continue;
|
|
1953
|
+
const disp = enclosingFn(nodesInFile, d.line);
|
|
1954
|
+
if (!disp)
|
|
1955
|
+
continue;
|
|
1956
|
+
let added = 0;
|
|
1957
|
+
for (const name of reg.names) {
|
|
1958
|
+
if (added >= REGISTRY_FANOUT_CAP)
|
|
1959
|
+
break;
|
|
1960
|
+
const target = resolveRegistryHandler(ctx, name, d.chained);
|
|
1961
|
+
if (!target || target.id === disp.id)
|
|
1962
|
+
continue;
|
|
1963
|
+
const key = `${disp.id}>${target.id}`;
|
|
1964
|
+
if (seen.has(key))
|
|
1965
|
+
continue;
|
|
1966
|
+
seen.add(key);
|
|
1967
|
+
edges.push({
|
|
1968
|
+
source: disp.id,
|
|
1969
|
+
target: target.id,
|
|
1970
|
+
kind: 'calls',
|
|
1971
|
+
line: d.line,
|
|
1972
|
+
provenance: 'heuristic',
|
|
1973
|
+
metadata: { synthesizedBy: 'object-registry', via: name, registeredAt: `${file}:${reg.line}` },
|
|
1974
|
+
});
|
|
1975
|
+
added++;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
return edges;
|
|
1980
|
+
}
|
|
1981
|
+
// ── RTK Query generated-hook → endpoint ──────────────────────────────────────
|
|
1982
|
+
// RTK Query generates one `useGetXQuery`/`useUpdateYMutation` hook per endpoint
|
|
1983
|
+
// (`createApi({ endpoints: b => ({ getX: b.query(...) }) })`). Components call the
|
|
1984
|
+
// hook; the fetch logic lives in the endpoint's queryFn. The hook↔endpoint link is
|
|
1985
|
+
// pure NAMING CONVENTION (no static edge): strip `use` + the optional `Lazy`
|
|
1986
|
+
// variant + the `Query|Mutation` suffix, lowercase the head → the endpoint key.
|
|
1987
|
+
// Both are extracted as function nodes (the hook from its `export const {…}=api`
|
|
1988
|
+
// binding, carrying a sentinel signature; the endpoint from the createApi object),
|
|
1989
|
+
// so bridging hook→endpoint connects `component → useGetXQuery → getX → queryFn`.
|
|
1990
|
+
// Gated on the extraction sentinel so it only ever fires on genuinely-generated
|
|
1991
|
+
// hooks (never a hand-written `useFooQuery`), and on a SAME-FILE endpoint (RTK
|
|
1992
|
+
// colocates the hooks and their api in one module) — 0 on any non-RTK repo.
|
|
1993
|
+
const RTK_HOOK_DERIVE_RE = /^use([A-Z][A-Za-z0-9]*?)(?:Query|Mutation)$/;
|
|
1994
|
+
// MUST match the signature set in tree-sitter.ts `extractRtkHookBindings`.
|
|
1995
|
+
const RTK_GENERATED_HOOK_SIGNATURE = '= RTK Query generated hook';
|
|
1996
|
+
/** Derive the endpoint key from a generated-hook name (`useLazyGetRecordsQuery`
|
|
1997
|
+
* → `getRecords`), or null if it doesn't fit the convention. */
|
|
1998
|
+
function rtkEndpointNameFromHook(hook) {
|
|
1999
|
+
const m = RTK_HOOK_DERIVE_RE.exec(hook);
|
|
2000
|
+
if (!m)
|
|
2001
|
+
return null;
|
|
2002
|
+
let mid = m[1];
|
|
2003
|
+
if (mid.startsWith('Lazy'))
|
|
2004
|
+
mid = mid.slice(4); // useLazyGetX → getX (same endpoint)
|
|
2005
|
+
if (!mid)
|
|
2006
|
+
return null;
|
|
2007
|
+
return mid.charAt(0).toLowerCase() + mid.slice(1);
|
|
2008
|
+
}
|
|
2009
|
+
function rtkQueryEdges(queries, ctx) {
|
|
2010
|
+
const edges = [];
|
|
2011
|
+
const seen = new Set();
|
|
2012
|
+
for (const hook of queries.iterateNodesByKind('function')) {
|
|
2013
|
+
// Only our extracted generated-hook bindings (sentinel) — not a real hook fn.
|
|
2014
|
+
if (hook.signature !== RTK_GENERATED_HOOK_SIGNATURE)
|
|
2015
|
+
continue;
|
|
2016
|
+
const endpointName = rtkEndpointNameFromHook(hook.name);
|
|
2017
|
+
if (!endpointName)
|
|
2018
|
+
continue;
|
|
2019
|
+
// The endpoint is a same-file function by the derived name (RTK colocates the
|
|
2020
|
+
// api definition and its generated-hook exports in one module).
|
|
2021
|
+
const target = ctx
|
|
2022
|
+
.getNodesByName(endpointName)
|
|
2023
|
+
.find((n) => n.kind === 'function' && n.filePath === hook.filePath);
|
|
2024
|
+
if (!target || target.id === hook.id)
|
|
2025
|
+
continue;
|
|
2026
|
+
const key = `${hook.id}>${target.id}`;
|
|
2027
|
+
if (seen.has(key))
|
|
2028
|
+
continue;
|
|
2029
|
+
seen.add(key);
|
|
2030
|
+
edges.push({
|
|
2031
|
+
source: hook.id,
|
|
2032
|
+
target: target.id,
|
|
2033
|
+
kind: 'calls',
|
|
2034
|
+
line: hook.startLine,
|
|
2035
|
+
provenance: 'heuristic',
|
|
2036
|
+
metadata: { synthesizedBy: 'rtk-query', via: endpointName, registeredAt: `${hook.filePath}:${hook.startLine}` },
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
return edges;
|
|
2040
|
+
}
|
|
2041
|
+
// ── Pinia useStore().action() dispatch bridge ────────────────────────────────
|
|
2042
|
+
// A Pinia store factory `export const useXStore = defineStore(...)` exposes its
|
|
2043
|
+
// actions as methods on the store instance; a consumer does `const s = useXStore()`
|
|
2044
|
+
// then `s.action()`. The call is a method-on-instance with no static edge to the
|
|
2045
|
+
// action (which lives in the store's module). Bridge it: map each factory → its
|
|
2046
|
+
// file, bind `const <var> = useXStore()` per consumer file, and link the enclosing
|
|
2047
|
+
// function → the `<var>.method()` action node IN THE STORE'S FILE. The same-store-
|
|
2048
|
+
// file gate keeps it precise (a Pinia built-in like `$patch` or an unrelated
|
|
2049
|
+
// same-named method resolves to nothing). Covers both the options and setup store
|
|
2050
|
+
// forms uniformly (the action is a function node in the store file either way).
|
|
2051
|
+
const PINIA_CONSUMER_EXT = /\.(?:ts|tsx|js|jsx|mjs|cjs|vue)$/;
|
|
2052
|
+
const PINIA_FACTORY_RE = /\b(?:export\s+)?const\s+(\w+)\s*=\s*defineStore\s*\(/g;
|
|
2053
|
+
const PINIA_BIND_RE = /\bconst\s+(\w+)\s*=\s*(?:await\s+)?(\w+)\s*\(/g;
|
|
2054
|
+
const PINIA_CALL_RE = /(\w+)\s*\.\s*(\w+)\s*\(/g;
|
|
2055
|
+
const PINIA_FANOUT_CAP = 80;
|
|
2056
|
+
function piniaStoreEdges(ctx) {
|
|
2057
|
+
// 1. Map each `const useXStore = defineStore(...)` factory → its store file.
|
|
2058
|
+
const factoryFile = new Map();
|
|
2059
|
+
for (const file of ctx.getAllFiles()) {
|
|
2060
|
+
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2061
|
+
continue;
|
|
2062
|
+
const content = ctx.readFile(file);
|
|
2063
|
+
if (!content || !content.includes('defineStore'))
|
|
2064
|
+
continue;
|
|
2065
|
+
PINIA_FACTORY_RE.lastIndex = 0;
|
|
2066
|
+
let m;
|
|
2067
|
+
while ((m = PINIA_FACTORY_RE.exec(content)))
|
|
2068
|
+
factoryFile.set(m[1], file);
|
|
2069
|
+
}
|
|
2070
|
+
if (!factoryFile.size)
|
|
2071
|
+
return [];
|
|
2072
|
+
const edges = [];
|
|
2073
|
+
const seen = new Set();
|
|
2074
|
+
for (const file of ctx.getAllFiles()) {
|
|
2075
|
+
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2076
|
+
continue;
|
|
2077
|
+
const content = ctx.readFile(file);
|
|
2078
|
+
if (!content || !content.includes('Store'))
|
|
2079
|
+
continue;
|
|
2080
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, /\.(?:jsx?|mjs|cjs)$/.test(file) ? 'javascript' : 'typescript');
|
|
2081
|
+
// 2. Bind store vars in this file: `const <var> = <known-factory>(...)`.
|
|
2082
|
+
const varStore = new Map();
|
|
2083
|
+
PINIA_BIND_RE.lastIndex = 0;
|
|
2084
|
+
let bm;
|
|
2085
|
+
while ((bm = PINIA_BIND_RE.exec(safe))) {
|
|
2086
|
+
const sf = factoryFile.get(bm[2]);
|
|
2087
|
+
if (sf)
|
|
2088
|
+
varStore.set(bm[1], sf);
|
|
2089
|
+
}
|
|
2090
|
+
if (!varStore.size)
|
|
2091
|
+
continue;
|
|
2092
|
+
// 3. Link `<var>.<method>(` → the action function node in the store's file.
|
|
2093
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
2094
|
+
const fallbackDispatcher = nodesInFile.find((n) => n.kind === 'component'); // .vue top-level setup
|
|
2095
|
+
PINIA_CALL_RE.lastIndex = 0;
|
|
2096
|
+
let cm;
|
|
2097
|
+
let added = 0;
|
|
2098
|
+
while ((cm = PINIA_CALL_RE.exec(safe)) && added < PINIA_FANOUT_CAP) {
|
|
2099
|
+
const storeFile = varStore.get(cm[1]);
|
|
2100
|
+
if (!storeFile)
|
|
2101
|
+
continue;
|
|
2102
|
+
const method = cm[2];
|
|
2103
|
+
const line = safe.slice(0, cm.index).split('\n').length;
|
|
2104
|
+
const disp = enclosingFn(nodesInFile, line) ?? fallbackDispatcher;
|
|
2105
|
+
if (!disp)
|
|
2106
|
+
continue;
|
|
2107
|
+
const target = ctx
|
|
2108
|
+
.getNodesByName(method)
|
|
2109
|
+
.find((n) => n.kind === 'function' && n.filePath === storeFile);
|
|
2110
|
+
if (!target || target.id === disp.id)
|
|
2111
|
+
continue;
|
|
2112
|
+
const key = `${disp.id}>${target.id}`;
|
|
2113
|
+
if (seen.has(key))
|
|
2114
|
+
continue;
|
|
2115
|
+
seen.add(key);
|
|
2116
|
+
edges.push({
|
|
2117
|
+
source: disp.id,
|
|
2118
|
+
target: target.id,
|
|
2119
|
+
kind: 'calls',
|
|
2120
|
+
line,
|
|
2121
|
+
provenance: 'heuristic',
|
|
2122
|
+
metadata: { synthesizedBy: 'pinia-store', via: method, registeredAt: `${file}:${line}` },
|
|
2123
|
+
});
|
|
2124
|
+
added++;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
return edges;
|
|
2128
|
+
}
|
|
2129
|
+
// ── Vuex string-keyed dispatch / commit bridge ───────────────────────────────
|
|
2130
|
+
// Vuex dispatches actions/mutations by a runtime STRING key: `dispatch('user/login')`
|
|
2131
|
+
// / `commit('SET_TOKEN')` / `this.$store.dispatch('app/toggleDevice')`. The action
|
|
2132
|
+
// & mutation definitions are object-literal methods in store module files (now
|
|
2133
|
+
// extracted as function nodes). Bridge the string key to its node: the LAST `/`
|
|
2134
|
+
// segment is the action/mutation name; the preceding segment is the namespace
|
|
2135
|
+
// (≈ the store module's file). Resolve the name to a function node IN A STORE FILE
|
|
2136
|
+
// (the store-file gate excludes a same-named `api/` helper — `getInfo`/`login`
|
|
2137
|
+
// commonly collide), disambiguated by the namespace appearing in the path (or, for
|
|
2138
|
+
// a root key, the same file — Vuex's local-module `commit('M')` inside an action).
|
|
2139
|
+
const VUEX_DISPATCH_RE = /\b(?:dispatch|commit)\s*\(\s*['"]([A-Za-z][\w/]*)['"]/g;
|
|
2140
|
+
const VUEX_STORE_SIGNAL = /\bdefineStore\b|\bcreateStore\b|\bVuex\b|\bmutations\b|\bactions\b|\bgetters\b|\bnamespaced\b/g;
|
|
2141
|
+
const VUEX_FANOUT_CAP = 120;
|
|
2142
|
+
/** A path segment (dir or filename stem) equals `seg` — `…/modules/user.js` has
|
|
2143
|
+
* the segment `user` for namespace `user`. */
|
|
2144
|
+
function pathHasSegment(filePath, seg) {
|
|
2145
|
+
return new RegExp('[\\\\/]' + seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '[\\\\/.]').test(filePath);
|
|
2146
|
+
}
|
|
2147
|
+
function vuexDispatchEdges(ctx) {
|
|
2148
|
+
const storeFileCache = new Map();
|
|
2149
|
+
const isStoreFile = (file) => {
|
|
2150
|
+
let v = storeFileCache.get(file);
|
|
2151
|
+
if (v === undefined) {
|
|
2152
|
+
const c = ctx.readFile(file);
|
|
2153
|
+
const seen = new Set();
|
|
2154
|
+
if (c) {
|
|
2155
|
+
VUEX_STORE_SIGNAL.lastIndex = 0;
|
|
2156
|
+
let sm;
|
|
2157
|
+
while ((sm = VUEX_STORE_SIGNAL.exec(c))) {
|
|
2158
|
+
seen.add(sm[0]);
|
|
2159
|
+
if (seen.size >= 2)
|
|
2160
|
+
break;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
v = seen.size >= 2;
|
|
2164
|
+
storeFileCache.set(file, v);
|
|
2165
|
+
}
|
|
2166
|
+
return v;
|
|
2167
|
+
};
|
|
2168
|
+
const resolve = (key, dispatchFile) => {
|
|
2169
|
+
const segs = key.split('/');
|
|
2170
|
+
const action = segs[segs.length - 1];
|
|
2171
|
+
const cands = ctx.getNodesByName(action).filter((n) => n.kind === 'function' && isStoreFile(n.filePath));
|
|
2172
|
+
if (!cands.length)
|
|
2173
|
+
return null;
|
|
2174
|
+
if (segs.length > 1) {
|
|
2175
|
+
const mod = segs[segs.length - 2]; // immediate namespace ≈ the module file
|
|
2176
|
+
return cands.find((c) => pathHasSegment(c.filePath, mod)) ?? (cands.length === 1 ? cands[0] : null);
|
|
2177
|
+
}
|
|
2178
|
+
// Root key: a local `commit('M')` inside an action targets the same module file;
|
|
2179
|
+
// otherwise accept only an unambiguous single store-wide match.
|
|
2180
|
+
return cands.find((c) => c.filePath === dispatchFile) ?? (cands.length === 1 ? cands[0] : null);
|
|
2181
|
+
};
|
|
2182
|
+
const edges = [];
|
|
2183
|
+
const seen = new Set();
|
|
2184
|
+
for (const file of ctx.getAllFiles()) {
|
|
2185
|
+
if (!PINIA_CONSUMER_EXT.test(file))
|
|
2186
|
+
continue;
|
|
2187
|
+
const content = ctx.readFile(file);
|
|
2188
|
+
if (!content || (!content.includes('dispatch(') && !content.includes('commit(')))
|
|
2189
|
+
continue;
|
|
2190
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, /\.(?:jsx?|mjs|cjs)$/.test(file) ? 'javascript' : 'typescript');
|
|
2191
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
2192
|
+
const fallback = nodesInFile.find((n) => n.kind === 'component'); // .vue top-level
|
|
2193
|
+
VUEX_DISPATCH_RE.lastIndex = 0;
|
|
2194
|
+
let m;
|
|
2195
|
+
let added = 0;
|
|
2196
|
+
while ((m = VUEX_DISPATCH_RE.exec(safe)) && added < VUEX_FANOUT_CAP) {
|
|
2197
|
+
const key = m[1];
|
|
2198
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
2199
|
+
const disp = enclosingFn(nodesInFile, line) ?? fallback;
|
|
2200
|
+
if (!disp)
|
|
2201
|
+
continue;
|
|
2202
|
+
const target = resolve(key, file);
|
|
2203
|
+
if (!target || target.id === disp.id)
|
|
2204
|
+
continue;
|
|
2205
|
+
const edgeKey = `${disp.id}>${target.id}`;
|
|
2206
|
+
if (seen.has(edgeKey))
|
|
2207
|
+
continue;
|
|
2208
|
+
seen.add(edgeKey);
|
|
2209
|
+
edges.push({
|
|
2210
|
+
source: disp.id,
|
|
2211
|
+
target: target.id,
|
|
2212
|
+
kind: 'calls',
|
|
2213
|
+
line,
|
|
2214
|
+
provenance: 'heuristic',
|
|
2215
|
+
metadata: { synthesizedBy: 'vuex-dispatch', via: key, registeredAt: `${file}:${line}` },
|
|
2216
|
+
});
|
|
2217
|
+
added++;
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
return edges;
|
|
2221
|
+
}
|
|
2222
|
+
// ── Celery task dispatch (Python) ─────────────────────────────────────────────
|
|
2223
|
+
// Celery decouples a task's call site from its body through async dispatch:
|
|
2224
|
+
// # tasks.py
|
|
2225
|
+
// @shared_task # also @app.task / @celery_app.task / @<app>.task / @task
|
|
2226
|
+
// def process(account_ids): ...
|
|
2227
|
+
// # views.py — a DIFFERENT module
|
|
2228
|
+
// process.apply_async(kwargs={...}) # or process.delay(...) — dynamic, no static edge
|
|
2229
|
+
// Bridge it: link the enclosing function/method at each `.delay(`/`.apply_async(` site → the
|
|
2230
|
+
// task function body. Precision rests on the DECORATOR gate — the dispatched name must resolve
|
|
2231
|
+
// to a Python function carrying a celery task decorator (read from the source lines above its
|
|
2232
|
+
// `def`, since the def's own startLine excludes the decorator). A `.delay()` on a non-task
|
|
2233
|
+
// object resolves to no task node → no edge, so a Celery-free repo yields 0. Same-file /
|
|
2234
|
+
// unique-candidate disambiguation like vuex. (Canvas forms — `group(t).delay()`, `t.s()`/`.si()`
|
|
2235
|
+
// — have no single identifier before `.delay`/`.apply_async`, so they're skipped, not mis-bridged.)
|
|
2236
|
+
const CELERY_DISPATCH_RE = /\b([A-Za-z_]\w*)\s*\.\s*(?:delay|apply_async)\s*\(/g;
|
|
2237
|
+
// A task decorator: bare `@shared_task`/`@task` or attribute `@app.task`/`@celery_app.task`,
|
|
2238
|
+
// each optionally called with args. `\b`-bounded and `@`-anchored so `@mytask`, or a symbol
|
|
2239
|
+
// merely named `task`, can't match. No `/g`, so `.test()` is stateless across reuse.
|
|
2240
|
+
const CELERY_TASK_DECORATOR_RE = /@\s*(?:[A-Za-z_][\w.]*\.)?(?:shared_task|task)\b/;
|
|
2241
|
+
const CELERY_PY_EXT = /\.py$/;
|
|
2242
|
+
const CELERY_FANOUT_CAP = 80;
|
|
2243
|
+
const CELERY_DECORATOR_LOOKBACK = 12; // max lines above a `def` to scan for its decorators
|
|
2244
|
+
function celeryDispatchEdges(ctx) {
|
|
2245
|
+
// Memoize the decorator check per task-candidate node: it reads the file and scans a few
|
|
2246
|
+
// lines above the def. Only called on names that are actually `.delay`/`.apply_async`
|
|
2247
|
+
// receivers, so the candidate set stays small.
|
|
2248
|
+
const taskCache = new Map();
|
|
2249
|
+
const isCeleryTask = (node) => {
|
|
2250
|
+
let v = taskCache.get(node.id);
|
|
2251
|
+
if (v !== undefined)
|
|
2252
|
+
return v;
|
|
2253
|
+
v = false;
|
|
2254
|
+
if (node.kind === 'function' && CELERY_PY_EXT.test(node.filePath)) {
|
|
2255
|
+
const content = ctx.readFile(node.filePath);
|
|
2256
|
+
if (content) {
|
|
2257
|
+
const lines = content.split('\n');
|
|
2258
|
+
// startLine is the `def` line (decorators sit ABOVE it). Walk upward, stopping at the
|
|
2259
|
+
// previous declaration so a non-task def can never inherit the prior def's decorator.
|
|
2260
|
+
const stop = Math.max(0, node.startLine - 1 - CELERY_DECORATOR_LOOKBACK);
|
|
2261
|
+
for (let i = node.startLine - 2; i >= stop; i--) {
|
|
2262
|
+
const t = (lines[i] ?? '').trim();
|
|
2263
|
+
if (/^(?:async\s+def|def|class)\b/.test(t))
|
|
2264
|
+
break; // previous decl → stop
|
|
2265
|
+
if (CELERY_TASK_DECORATOR_RE.test(t)) {
|
|
2266
|
+
v = true;
|
|
2267
|
+
break;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
taskCache.set(node.id, v);
|
|
2273
|
+
return v;
|
|
2274
|
+
};
|
|
2275
|
+
const resolve = (name, dispatchFile) => {
|
|
2276
|
+
const cands = ctx.getNodesByName(name).filter((n) => n.kind === 'function' && isCeleryTask(n));
|
|
2277
|
+
if (!cands.length)
|
|
2278
|
+
return null;
|
|
2279
|
+
if (cands.length === 1)
|
|
2280
|
+
return cands[0];
|
|
2281
|
+
// Cross-module name collision: prefer a task defined in the dispatching file, else bail
|
|
2282
|
+
// (ambiguous — precision over recall, like vuex's root-key resolution).
|
|
2283
|
+
return cands.find((c) => c.filePath === dispatchFile) ?? null;
|
|
2284
|
+
};
|
|
2285
|
+
const edges = [];
|
|
2286
|
+
const seen = new Set();
|
|
2287
|
+
for (const file of ctx.getAllFiles()) {
|
|
2288
|
+
if (!CELERY_PY_EXT.test(file))
|
|
2289
|
+
continue;
|
|
2290
|
+
const content = ctx.readFile(file);
|
|
2291
|
+
if (!content || (!content.includes('.delay(') && !content.includes('.apply_async(')))
|
|
2292
|
+
continue;
|
|
2293
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'python');
|
|
2294
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
2295
|
+
CELERY_DISPATCH_RE.lastIndex = 0;
|
|
2296
|
+
let m;
|
|
2297
|
+
let added = 0;
|
|
2298
|
+
while ((m = CELERY_DISPATCH_RE.exec(safe)) && added < CELERY_FANOUT_CAP) {
|
|
2299
|
+
const name = m[1];
|
|
2300
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
2301
|
+
const disp = enclosingFn(nodesInFile, line);
|
|
2302
|
+
if (!disp)
|
|
2303
|
+
continue; // module-level dispatch — no source symbol to attribute
|
|
2304
|
+
const target = resolve(name, file);
|
|
2305
|
+
if (!target || target.id === disp.id)
|
|
2306
|
+
continue;
|
|
2307
|
+
const key = `${disp.id}>${target.id}`;
|
|
2308
|
+
if (seen.has(key))
|
|
2309
|
+
continue;
|
|
2310
|
+
seen.add(key);
|
|
2311
|
+
edges.push({
|
|
2312
|
+
source: disp.id,
|
|
2313
|
+
target: target.id,
|
|
2314
|
+
kind: 'calls',
|
|
2315
|
+
line,
|
|
2316
|
+
provenance: 'heuristic',
|
|
2317
|
+
metadata: { synthesizedBy: 'celery-dispatch', via: name, registeredAt: `${file}:${line}` },
|
|
2318
|
+
});
|
|
2319
|
+
added++;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
return edges;
|
|
2323
|
+
}
|
|
2324
|
+
// ── Spring application events (Java) ──────────────────────────────────────────
|
|
2325
|
+
// Spring decouples an event PUBLISHER from its LISTENER(s) through the application
|
|
2326
|
+
// event bus, linked by the EVENT TYPE (not a name):
|
|
2327
|
+
// // SomeService.java
|
|
2328
|
+
// eventPublisher.publishEvent(new PasswordChangedEvent(this, username)); // publish
|
|
2329
|
+
// // RememberMeTokenRevoker.java — a DIFFERENT file
|
|
2330
|
+
// @EventListener(PasswordChangedEvent.class) // listen
|
|
2331
|
+
// public void onPasswordChanged(PasswordChangedEvent event) { ... }
|
|
2332
|
+
// Bridge it: link the enclosing method at each `publishEvent(new XEvent(...))` site →
|
|
2333
|
+
// every listener method of XEvent. Listeners are `@EventListener` / `@TransactionalEventListener`
|
|
2334
|
+
// methods (event type = the first param type, or the `@EventListener(X.class)` value form) and
|
|
2335
|
+
// the older `class … implements ApplicationListener<X> { void onApplicationEvent(X e) }`. Keyed
|
|
2336
|
+
// by exact type name, usually cross-file. A repo with no `@EventListener`/`publishEvent` yields 0.
|
|
2337
|
+
// (Java method nodes INCLUDE their leading annotations in the range — startLine is the first
|
|
2338
|
+
// `@…` line — so the annotation block is scanned DOWNWARD from startLine, bounded to consecutive
|
|
2339
|
+
// `@`-lines so it can't bleed into an adjacent method.)
|
|
2340
|
+
const SPRING_PUBLISH_RE = /\.publishEvent\s*\(\s*new\s+([A-Z][A-Za-z0-9_]*)/g;
|
|
2341
|
+
const SPRING_LISTENER_ANNO_RE = /@(?:EventListener|TransactionalEventListener)\b/;
|
|
2342
|
+
const SPRING_ANNO_TYPE_RE = /@(?:EventListener|TransactionalEventListener)\s*\(\s*([A-Z][A-Za-z0-9_]*)\.class/;
|
|
2343
|
+
const SPRING_APP_LISTENER_RE = /\bApplicationListener\s*</;
|
|
2344
|
+
const SPRING_JAVA_EXT = /\.java$/;
|
|
2345
|
+
const SPRING_FANOUT_CAP = 80;
|
|
2346
|
+
/** The first parameter's type from a Java method `signature` (`"void (XEvent e)"` → `XEvent`).
|
|
2347
|
+
* Skips a leading `final`/`@Anno`, strips generics, and requires a PascalCase class name (event
|
|
2348
|
+
* types are classes) — so a no-arg or primitive-param method yields null. */
|
|
2349
|
+
function springFirstParamType(sig) {
|
|
2350
|
+
if (!sig)
|
|
2351
|
+
return null;
|
|
2352
|
+
const open = sig.indexOf('(');
|
|
2353
|
+
if (open < 0)
|
|
2354
|
+
return null;
|
|
2355
|
+
const close = sig.indexOf(')', open);
|
|
2356
|
+
const inner = sig.slice(open + 1, close < 0 ? sig.length : close).trim();
|
|
2357
|
+
if (!inner)
|
|
2358
|
+
return null;
|
|
2359
|
+
const first = inner.split(',')[0].trim();
|
|
2360
|
+
const toks = first.split(/\s+/).filter((t) => t && t !== 'final' && !t.startsWith('@'));
|
|
2361
|
+
if (toks.length < 2)
|
|
2362
|
+
return null; // need `Type name`
|
|
2363
|
+
const type = toks[toks.length - 2].replace(/<.*$/, ''); // drop generic args
|
|
2364
|
+
return /^[A-Z][A-Za-z0-9_]*$/.test(type) ? type : null;
|
|
2365
|
+
}
|
|
2366
|
+
function springEventEdges(ctx) {
|
|
2367
|
+
// Pass 1 — event-type → listener methods, scanning only event-relevant files.
|
|
2368
|
+
const listeners = new Map();
|
|
2369
|
+
for (const file of ctx.getAllFiles()) {
|
|
2370
|
+
if (!SPRING_JAVA_EXT.test(file))
|
|
2371
|
+
continue;
|
|
2372
|
+
const content = ctx.readFile(file);
|
|
2373
|
+
if (!content)
|
|
2374
|
+
continue;
|
|
2375
|
+
const hasAnno = content.includes('@EventListener') || content.includes('@TransactionalEventListener');
|
|
2376
|
+
const hasAppListener = SPRING_APP_LISTENER_RE.test(content);
|
|
2377
|
+
if (!hasAnno && !hasAppListener)
|
|
2378
|
+
continue;
|
|
2379
|
+
const lines = content.split('\n');
|
|
2380
|
+
for (const node of ctx.getNodesInFile(file)) {
|
|
2381
|
+
if (node.kind !== 'method')
|
|
2382
|
+
continue;
|
|
2383
|
+
// Collect this method's own leading annotation block (consecutive `@`-lines from startLine).
|
|
2384
|
+
const annoLines = [];
|
|
2385
|
+
for (let i = node.startLine - 1; i < lines.length && i < node.startLine + 7; i++) {
|
|
2386
|
+
const t = (lines[i] ?? '').trim();
|
|
2387
|
+
if (!t.startsWith('@'))
|
|
2388
|
+
break; // reached the declaration → stop (no bleed into next method)
|
|
2389
|
+
annoLines.push(t);
|
|
2390
|
+
}
|
|
2391
|
+
const head = annoLines.join('\n');
|
|
2392
|
+
const annotated = hasAnno && SPRING_LISTENER_ANNO_RE.test(head);
|
|
2393
|
+
const isAppListener = hasAppListener && node.name === 'onApplicationEvent';
|
|
2394
|
+
if (!annotated && !isAppListener)
|
|
2395
|
+
continue;
|
|
2396
|
+
let type = springFirstParamType(node.signature);
|
|
2397
|
+
if (!type && annotated) {
|
|
2398
|
+
const m = SPRING_ANNO_TYPE_RE.exec(head);
|
|
2399
|
+
if (m)
|
|
2400
|
+
type = m[1];
|
|
2401
|
+
}
|
|
2402
|
+
if (!type)
|
|
2403
|
+
continue;
|
|
2404
|
+
let arr = listeners.get(type);
|
|
2405
|
+
if (!arr) {
|
|
2406
|
+
arr = [];
|
|
2407
|
+
listeners.set(type, arr);
|
|
2408
|
+
}
|
|
2409
|
+
arr.push(node);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
if (!listeners.size)
|
|
2413
|
+
return [];
|
|
2414
|
+
// Pass 2 — link each publishEvent(new XEvent(...)) site → every listener of XEvent.
|
|
2415
|
+
const edges = [];
|
|
2416
|
+
const seen = new Set();
|
|
2417
|
+
for (const file of ctx.getAllFiles()) {
|
|
2418
|
+
if (!SPRING_JAVA_EXT.test(file))
|
|
2419
|
+
continue;
|
|
2420
|
+
const content = ctx.readFile(file);
|
|
2421
|
+
if (!content || !content.includes('.publishEvent('))
|
|
2422
|
+
continue;
|
|
2423
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'java');
|
|
2424
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
2425
|
+
SPRING_PUBLISH_RE.lastIndex = 0;
|
|
2426
|
+
let m;
|
|
2427
|
+
let added = 0;
|
|
2428
|
+
while ((m = SPRING_PUBLISH_RE.exec(safe)) && added < SPRING_FANOUT_CAP) {
|
|
2429
|
+
const targets = listeners.get(m[1]);
|
|
2430
|
+
if (!targets || !targets.length)
|
|
2431
|
+
continue;
|
|
2432
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
2433
|
+
const disp = enclosingFn(nodesInFile, line);
|
|
2434
|
+
if (!disp)
|
|
2435
|
+
continue;
|
|
2436
|
+
for (const target of targets) {
|
|
2437
|
+
if (target.id === disp.id)
|
|
2438
|
+
continue;
|
|
2439
|
+
const key = `${disp.id}>${target.id}`;
|
|
2440
|
+
if (seen.has(key))
|
|
2441
|
+
continue;
|
|
2442
|
+
seen.add(key);
|
|
2443
|
+
edges.push({
|
|
2444
|
+
source: disp.id,
|
|
2445
|
+
target: target.id,
|
|
2446
|
+
kind: 'calls',
|
|
2447
|
+
line,
|
|
2448
|
+
provenance: 'heuristic',
|
|
2449
|
+
metadata: { synthesizedBy: 'spring-event', via: m[1], registeredAt: `${file}:${line}` },
|
|
2450
|
+
});
|
|
2451
|
+
added++;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
return edges;
|
|
2456
|
+
}
|
|
2457
|
+
// ── MediatR request/notification dispatch (C#/.NET) ───────────────────────────
|
|
2458
|
+
// MediatR decouples a Send/Publish call site from its Handle method through a mediator,
|
|
2459
|
+
// linked by the request/notification TYPE (the IRequestHandler<T,…> generic):
|
|
2460
|
+
// // CancelOrderCommandHandler.cs — the handler
|
|
2461
|
+
// public class CancelOrderCommandHandler : IRequestHandler<CancelOrderCommand, bool> {
|
|
2462
|
+
// public async Task<bool> Handle(CancelOrderCommand request, CancellationToken ct) { … }
|
|
2463
|
+
// // some controller — the dispatch (usually a DIFFERENT file)
|
|
2464
|
+
// var command = new CancelOrderCommand(orderId); await _mediator.Send(command);
|
|
2465
|
+
// Bridge it: link the enclosing method at each mediator `.Send(x)`/`.Publish(x)` site → the
|
|
2466
|
+
// `Handle` method of the handler for x's type. The sent type is resolved from the argument —
|
|
2467
|
+
// inline `new X(…)`, a local `var v = new X(…)`, or a parameter/local declared `X v` — bounded
|
|
2468
|
+
// to the enclosing method. Precision rests on TWO gates: the receiver must be mediator-ish
|
|
2469
|
+
// (`mediator`/`sender`/`publisher`, so MAUI `MessagingCenter.Send` is ignored) AND the resolved
|
|
2470
|
+
// type must be a known handler request type (so a same-named non-request DTO is never bridged).
|
|
2471
|
+
// C# has no `signature` on method nodes, so the handler's request type is read from the class
|
|
2472
|
+
// base-list source (`: IRequestHandler<X,…>`), not a param signature.
|
|
2473
|
+
const MEDIATR_HANDLER_BASE_RE = /(?:IRequestHandler|INotificationHandler)\s*<\s*([A-Za-z_]\w*)/;
|
|
2474
|
+
const MEDIATR_DISPATCH_RE = /([A-Za-z_][\w.]*)\s*\.\s*(?:Send|Publish)\s*\(\s*(new\s+[A-Z]\w*|[A-Za-z_]\w*)/g;
|
|
2475
|
+
const MEDIATR_RECEIVER_RE = /(?:mediator|sender|publisher)/i;
|
|
2476
|
+
const MEDIATR_CS_EXT = /\.cs$/;
|
|
2477
|
+
const MEDIATR_FANOUT_CAP = 80;
|
|
2478
|
+
const MEDIATR_HANDLER_DECL_LOOKAHEAD = 4; // lines from a class startLine to find a wrapped base list
|
|
2479
|
+
/** The type sent at a MediatR `.Send(arg)`/`.Publish(arg)` site: an inline `new X(…)`, else
|
|
2480
|
+
* `arg` as an identifier resolved within the enclosing method — a `… arg = new X(…)` assignment
|
|
2481
|
+
* (wins), or a parameter/local declared `X arg`. Returns null when the type can't be seen. */
|
|
2482
|
+
function resolveMediatrArgType(arg, lines, methodStart, dispatchLine) {
|
|
2483
|
+
const inl = /^new\s+([A-Z]\w*)/.exec(arg);
|
|
2484
|
+
if (inl)
|
|
2485
|
+
return inl[1];
|
|
2486
|
+
if (!/^[A-Za-z_]\w*$/.test(arg))
|
|
2487
|
+
return null;
|
|
2488
|
+
const assignRe = new RegExp(`\\b${arg}\\b\\s*=\\s*new\\s+([A-Z]\\w*)`);
|
|
2489
|
+
const declRe = new RegExp(`\\b([A-Z]\\w*)\\b\\s+${arg}\\b`);
|
|
2490
|
+
let declType = null;
|
|
2491
|
+
for (let i = Math.max(0, methodStart - 1); i < dispatchLine && i < lines.length; i++) {
|
|
2492
|
+
const ln = lines[i] ?? '';
|
|
2493
|
+
const a = assignRe.exec(ln);
|
|
2494
|
+
if (a)
|
|
2495
|
+
return a[1]; // an explicit `arg = new X` is the most specific — take it
|
|
2496
|
+
if (!declType) {
|
|
2497
|
+
const d = declRe.exec(ln);
|
|
2498
|
+
if (d)
|
|
2499
|
+
declType = d[1]; // a `X arg` declaration — remember, but keep scanning for an assignment
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
return declType;
|
|
2503
|
+
}
|
|
2504
|
+
function mediatrDispatchEdges(ctx) {
|
|
2505
|
+
// Pass 1 — request/notification type → the Handle method of each handler class.
|
|
2506
|
+
const handlers = new Map();
|
|
2507
|
+
for (const file of ctx.getAllFiles()) {
|
|
2508
|
+
if (!MEDIATR_CS_EXT.test(file))
|
|
2509
|
+
continue;
|
|
2510
|
+
const content = ctx.readFile(file);
|
|
2511
|
+
if (!content || (!content.includes('IRequestHandler<') && !content.includes('INotificationHandler<')))
|
|
2512
|
+
continue;
|
|
2513
|
+
const lines = content.split('\n');
|
|
2514
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
2515
|
+
for (const cls of nodesInFile) {
|
|
2516
|
+
if (cls.kind !== 'class')
|
|
2517
|
+
continue;
|
|
2518
|
+
const decl = lines.slice(cls.startLine - 1, cls.startLine - 1 + MEDIATR_HANDLER_DECL_LOOKAHEAD).join('\n');
|
|
2519
|
+
const m = MEDIATR_HANDLER_BASE_RE.exec(decl);
|
|
2520
|
+
if (!m)
|
|
2521
|
+
continue;
|
|
2522
|
+
const type = m[1];
|
|
2523
|
+
const end = cls.endLine ?? cls.startLine;
|
|
2524
|
+
const handle = nodesInFile.find((n) => n.kind === 'method' && n.name === 'Handle' && n.startLine >= cls.startLine && n.startLine <= end);
|
|
2525
|
+
if (!handle)
|
|
2526
|
+
continue;
|
|
2527
|
+
let arr = handlers.get(type);
|
|
2528
|
+
if (!arr) {
|
|
2529
|
+
arr = [];
|
|
2530
|
+
handlers.set(type, arr);
|
|
2531
|
+
}
|
|
2532
|
+
arr.push(handle);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
if (!handlers.size)
|
|
2536
|
+
return [];
|
|
2537
|
+
// Pass 2 — link each mediator-ish .Send(x)/.Publish(x) → the handler of x's type.
|
|
2538
|
+
const edges = [];
|
|
2539
|
+
const seen = new Set();
|
|
2540
|
+
for (const file of ctx.getAllFiles()) {
|
|
2541
|
+
if (!MEDIATR_CS_EXT.test(file))
|
|
2542
|
+
continue;
|
|
2543
|
+
const content = ctx.readFile(file);
|
|
2544
|
+
if (!content || (!content.includes('.Send(') && !content.includes('.Publish(')))
|
|
2545
|
+
continue;
|
|
2546
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'csharp');
|
|
2547
|
+
const safeLines = safe.split('\n');
|
|
2548
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
2549
|
+
MEDIATR_DISPATCH_RE.lastIndex = 0;
|
|
2550
|
+
let m;
|
|
2551
|
+
let added = 0;
|
|
2552
|
+
while ((m = MEDIATR_DISPATCH_RE.exec(safe)) && added < MEDIATR_FANOUT_CAP) {
|
|
2553
|
+
if (!MEDIATR_RECEIVER_RE.test(m[1]))
|
|
2554
|
+
continue; // not a mediator (MessagingCenter, HttpClient, …)
|
|
2555
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
2556
|
+
const disp = enclosingFn(nodesInFile, line);
|
|
2557
|
+
if (!disp)
|
|
2558
|
+
continue;
|
|
2559
|
+
const type = resolveMediatrArgType(m[2], safeLines, disp.startLine, line);
|
|
2560
|
+
if (!type)
|
|
2561
|
+
continue;
|
|
2562
|
+
const targets = handlers.get(type);
|
|
2563
|
+
if (!targets)
|
|
2564
|
+
continue;
|
|
2565
|
+
for (const target of targets) {
|
|
2566
|
+
if (target.id === disp.id)
|
|
2567
|
+
continue;
|
|
2568
|
+
const key = `${disp.id}>${target.id}`;
|
|
2569
|
+
if (seen.has(key))
|
|
2570
|
+
continue;
|
|
2571
|
+
seen.add(key);
|
|
2572
|
+
edges.push({
|
|
2573
|
+
source: disp.id,
|
|
2574
|
+
target: target.id,
|
|
2575
|
+
kind: 'calls',
|
|
2576
|
+
line,
|
|
2577
|
+
provenance: 'heuristic',
|
|
2578
|
+
metadata: { synthesizedBy: 'mediatr-dispatch', via: type, registeredAt: `${file}:${line}` },
|
|
2579
|
+
});
|
|
2580
|
+
added++;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
return edges;
|
|
2585
|
+
}
|
|
2586
|
+
// ── Sidekiq job dispatch (Ruby) ───────────────────────────────────────────────
|
|
2587
|
+
// Sidekiq decouples a job's enqueue site from the worker's `perform`, linked by the WORKER
|
|
2588
|
+
// CLASS NAME:
|
|
2589
|
+
// # app/workers/destroy_user_worker.rb
|
|
2590
|
+
// class DestroyUserWorker
|
|
2591
|
+
// include Sidekiq::Worker # or Sidekiq::Job (the modern alias)
|
|
2592
|
+
// def perform(user_id) … end
|
|
2593
|
+
// # app/services/… — a DIFFERENT file
|
|
2594
|
+
// DestroyUserWorker.perform_async(user.id) # also .perform_in(t, …) / .perform_at(t, …)
|
|
2595
|
+
// Bridge it: link the enclosing method at each `Worker.perform_async/_in/_at(…)` site → that
|
|
2596
|
+
// worker's instance `perform`. Name-keyed (like Celery): the receiver class must be a Sidekiq
|
|
2597
|
+
// worker — gated by reading `include Sidekiq::Job|Worker` from the class body, since that mixin
|
|
2598
|
+
// is an external gem module that forms no resolvable edge. ActiveJob's `perform_later`/`_now` is
|
|
2599
|
+
// a different shape and deliberately not matched, so an ActiveJob-only app yields 0.
|
|
2600
|
+
const SIDEKIQ_DISPATCH_RE = /([A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*)\s*\.\s*perform_(?:async|in|at)\b/g;
|
|
2601
|
+
const SIDEKIQ_WORKER_RE = /\binclude\s+Sidekiq::(?:Job|Worker)\b/;
|
|
2602
|
+
const SIDEKIQ_RB_EXT = /\.rb$/;
|
|
2603
|
+
const SIDEKIQ_FANOUT_CAP = 80;
|
|
2604
|
+
function sidekiqDispatchEdges(ctx) {
|
|
2605
|
+
// class node id → its instance `perform` method (null if the class isn't a Sidekiq worker),
|
|
2606
|
+
// memoized. Reads the class body for the mixin; only consulted for actual dispatch receivers.
|
|
2607
|
+
const performCache = new Map();
|
|
2608
|
+
const performOf = (cls) => {
|
|
2609
|
+
let v = performCache.get(cls.id);
|
|
2610
|
+
if (v !== undefined)
|
|
2611
|
+
return v;
|
|
2612
|
+
v = null;
|
|
2613
|
+
const content = ctx.readFile(cls.filePath);
|
|
2614
|
+
if (content) {
|
|
2615
|
+
const end = cls.endLine ?? cls.startLine;
|
|
2616
|
+
const body = content.split('\n').slice(cls.startLine - 1, end).join('\n');
|
|
2617
|
+
if (SIDEKIQ_WORKER_RE.test(body)) {
|
|
2618
|
+
v = ctx.getNodesInFile(cls.filePath).find((n) => n.kind === 'method' && n.name === 'perform' && n.startLine >= cls.startLine && n.startLine <= end) ?? null;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
performCache.set(cls.id, v);
|
|
2622
|
+
return v;
|
|
2623
|
+
};
|
|
2624
|
+
// Resolve a (possibly namespaced) worker reference to its `perform`. A namespaced ref is
|
|
2625
|
+
// matched by EXACT qualified name first, so same-named workers in different namespaces
|
|
2626
|
+
// (forem has four `SendEmailNotificationWorker`s) resolve to the right one; an unqualified
|
|
2627
|
+
// ref falls back to the simple name and links only when a single worker bears it — an
|
|
2628
|
+
// ambiguous collision bails (precision over recall).
|
|
2629
|
+
const resolve = (ref) => {
|
|
2630
|
+
if (ref.includes('::')) {
|
|
2631
|
+
const q = ctx.getNodesByQualifiedName(ref).find((n) => n.kind === 'class' && performOf(n));
|
|
2632
|
+
if (q)
|
|
2633
|
+
return performOf(q);
|
|
2634
|
+
}
|
|
2635
|
+
const workers = ctx.getNodesByName(ref.split('::').pop()).filter((n) => n.kind === 'class' && performOf(n));
|
|
2636
|
+
return workers.length === 1 ? performOf(workers[0]) : null;
|
|
2637
|
+
};
|
|
2638
|
+
const edges = [];
|
|
2639
|
+
const seen = new Set();
|
|
2640
|
+
for (const file of ctx.getAllFiles()) {
|
|
2641
|
+
if (!SIDEKIQ_RB_EXT.test(file))
|
|
2642
|
+
continue;
|
|
2643
|
+
const content = ctx.readFile(file);
|
|
2644
|
+
if (!content || !/\.perform_(?:async|in|at)\b/.test(content))
|
|
2645
|
+
continue;
|
|
2646
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'ruby');
|
|
2647
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
2648
|
+
SIDEKIQ_DISPATCH_RE.lastIndex = 0;
|
|
2649
|
+
let m;
|
|
2650
|
+
let added = 0;
|
|
2651
|
+
while ((m = SIDEKIQ_DISPATCH_RE.exec(safe)) && added < SIDEKIQ_FANOUT_CAP) {
|
|
2652
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
2653
|
+
const disp = enclosingFn(nodesInFile, line);
|
|
2654
|
+
if (!disp)
|
|
2655
|
+
continue;
|
|
2656
|
+
const target = resolve(m[1]);
|
|
2657
|
+
if (!target || target.id === disp.id)
|
|
2658
|
+
continue;
|
|
2659
|
+
const key = `${disp.id}>${target.id}`;
|
|
2660
|
+
if (seen.has(key))
|
|
2661
|
+
continue;
|
|
2662
|
+
seen.add(key);
|
|
2663
|
+
edges.push({
|
|
2664
|
+
source: disp.id,
|
|
2665
|
+
target: target.id,
|
|
2666
|
+
kind: 'calls',
|
|
2667
|
+
line,
|
|
2668
|
+
provenance: 'heuristic',
|
|
2669
|
+
metadata: { synthesizedBy: 'sidekiq-dispatch', via: m[1], registeredAt: `${file}:${line}` },
|
|
2670
|
+
});
|
|
2671
|
+
added++;
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
return edges;
|
|
2675
|
+
}
|
|
2676
|
+
// ── Laravel events (PHP) ──────────────────────────────────────────────────────
|
|
2677
|
+
// Laravel decouples an event dispatch from its listener(s), linked by the EVENT CLASS:
|
|
2678
|
+
// // app/Events/PlaybackStarted.php + app/Listeners/UpdateLastfmNowPlaying.php
|
|
2679
|
+
// class UpdateLastfmNowPlaying { public function handle(PlaybackStarted $event) { … } }
|
|
2680
|
+
// // a controller / service — a DIFFERENT file
|
|
2681
|
+
// event(new PlaybackStarted($song, $user));
|
|
2682
|
+
// Bridge it: link the enclosing method at each `event(new XEvent(...))` site → every listener's
|
|
2683
|
+
// `handle` for XEvent. Listeners come from TWO registration mechanisms (both real, both needed):
|
|
2684
|
+
// (A) auto-discovery — a `handle(EventType $e)` typed first param (also splits a union A|B);
|
|
2685
|
+
// (B) the `protected $listen = [ XEvent::class => [Listener::class, …] ]` map in an
|
|
2686
|
+
// EventServiceProvider, which also covers a listener whose `handle()` is UNTYPED.
|
|
2687
|
+
// Only `event(new X)` is matched — queued JOBS dispatch via `::dispatch()` and their `handle()`
|
|
2688
|
+
// takes an injected service, never an event type, so jobs are excluded by construction.
|
|
2689
|
+
const LARAVEL_DISPATCH_RE = /\bevent\s*\(\s*new\s+\\?([A-Za-z_][\w\\]*)/g;
|
|
2690
|
+
const LARAVEL_PHP_EXT = /\.php$/;
|
|
2691
|
+
const LARAVEL_FANOUT_CAP = 200;
|
|
2692
|
+
// A `$listen` entry: `Event::class => [Listener::class, …]`, key/values as `::class` or strings.
|
|
2693
|
+
const LISTEN_ENTRY_RE = /(?:([A-Za-z_\\][\w\\]*)::class|'([^']+)'|"([^"]+)")\s*=>\s*\[([^\]]*)\]/g;
|
|
2694
|
+
const LISTEN_CLASS_RE = /(?:([A-Za-z_\\][\w\\]*)::class|'([^']+)'|"([^"]+)")/g;
|
|
2695
|
+
/** Short class name from a PHP reference: `\App\Events\Foo` / `App\Events::Foo` → `Foo`. */
|
|
2696
|
+
function phpSimpleName(s) {
|
|
2697
|
+
return s.replace(/^\\/, '').split('\\').pop().split('::').pop().trim();
|
|
2698
|
+
}
|
|
2699
|
+
/** The first-parameter class type(s) of a `handle(...)` declaration — union-split, short-named,
|
|
2700
|
+
* primitives dropped. `handle(A|B $e)` → [A, B]; `handle(string $x)` / `handle()` → []. */
|
|
2701
|
+
function laravelHandleEventTypes(decl) {
|
|
2702
|
+
const m = /function\s+handle\s*\(\s*(?:\.\.\.\s*)?(\??[A-Za-z_\\][\w\\|]*)\s+&?\s*(?:\.\.\.\s*)?\$/.exec(decl);
|
|
2703
|
+
if (!m)
|
|
2704
|
+
return [];
|
|
2705
|
+
return m[1]
|
|
2706
|
+
.replace(/^\?/, '')
|
|
2707
|
+
.split('|')
|
|
2708
|
+
.map((t) => phpSimpleName(t))
|
|
2709
|
+
.filter((t) => /^[A-Z]\w*$/.test(t));
|
|
2710
|
+
}
|
|
2711
|
+
/** From an opening `[`, the bracket-balanced body up to its matching `]`. */
|
|
2712
|
+
function phpArrayBody(src, openIdx) {
|
|
2713
|
+
let depth = 0;
|
|
2714
|
+
for (let i = openIdx; i < src.length; i++) {
|
|
2715
|
+
if (src[i] === '[')
|
|
2716
|
+
depth++;
|
|
2717
|
+
else if (src[i] === ']' && --depth === 0)
|
|
2718
|
+
return src.slice(openIdx + 1, i);
|
|
2719
|
+
}
|
|
2720
|
+
return null;
|
|
2721
|
+
}
|
|
2722
|
+
function laravelEventEdges(ctx) {
|
|
2723
|
+
// event short name → its listener `handle` methods (deduped by node id).
|
|
2724
|
+
const listeners = new Map();
|
|
2725
|
+
const add = (event, handle) => {
|
|
2726
|
+
let m = listeners.get(event);
|
|
2727
|
+
if (!m) {
|
|
2728
|
+
m = new Map();
|
|
2729
|
+
listeners.set(event, m);
|
|
2730
|
+
}
|
|
2731
|
+
m.set(handle.id, handle);
|
|
2732
|
+
};
|
|
2733
|
+
const handleOf = (cls) => ctx
|
|
2734
|
+
.getNodesInFile(cls.filePath)
|
|
2735
|
+
.find((n) => n.kind === 'method' && n.name === 'handle'
|
|
2736
|
+
&& n.startLine >= cls.startLine && n.startLine <= (cls.endLine ?? cls.startLine)) ?? null;
|
|
2737
|
+
// Pass 1 — build the event→handle map from both registration mechanisms.
|
|
2738
|
+
for (const file of ctx.getAllFiles()) {
|
|
2739
|
+
if (!LARAVEL_PHP_EXT.test(file))
|
|
2740
|
+
continue;
|
|
2741
|
+
const content = ctx.readFile(file);
|
|
2742
|
+
if (!content)
|
|
2743
|
+
continue;
|
|
2744
|
+
// (A) typed listener handles — node-driven, so a commented-out method can't leak in.
|
|
2745
|
+
if (content.includes('function handle')) {
|
|
2746
|
+
const lines = content.split('\n');
|
|
2747
|
+
for (const node of ctx.getNodesInFile(file)) {
|
|
2748
|
+
if (node.kind !== 'method' || node.name !== 'handle')
|
|
2749
|
+
continue;
|
|
2750
|
+
const decl = lines.slice(node.startLine - 1, node.startLine + 2).join('\n');
|
|
2751
|
+
for (const ev of laravelHandleEventTypes(decl))
|
|
2752
|
+
add(ev, node);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
// (B) the EventServiceProvider `$listen` map — parsed from comment-stripped source so a
|
|
2756
|
+
// fully-commented map (firefly's, on auto-discovery) contributes nothing.
|
|
2757
|
+
if (content.includes('$listen')) {
|
|
2758
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'php');
|
|
2759
|
+
const decl = safe.search(/\$listen\s*=\s*\[/);
|
|
2760
|
+
const body = decl >= 0 ? phpArrayBody(safe, safe.indexOf('[', decl)) : null;
|
|
2761
|
+
if (body) {
|
|
2762
|
+
LISTEN_ENTRY_RE.lastIndex = 0;
|
|
2763
|
+
let em;
|
|
2764
|
+
while ((em = LISTEN_ENTRY_RE.exec(body))) {
|
|
2765
|
+
const event = phpSimpleName(em[1] ?? em[2] ?? em[3] ?? '');
|
|
2766
|
+
LISTEN_CLASS_RE.lastIndex = 0;
|
|
2767
|
+
let lm;
|
|
2768
|
+
while ((lm = LISTEN_CLASS_RE.exec(em[4]))) {
|
|
2769
|
+
const ln = phpSimpleName(lm[1] ?? lm[2] ?? lm[3] ?? '');
|
|
2770
|
+
const cls = ctx.getNodesByName(ln).find((n) => n.kind === 'class' && handleOf(n));
|
|
2771
|
+
if (cls)
|
|
2772
|
+
add(event, handleOf(cls));
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
if (!listeners.size)
|
|
2779
|
+
return [];
|
|
2780
|
+
// Pass 2 — link each event(new X(...)) site → every listener of X.
|
|
2781
|
+
const edges = [];
|
|
2782
|
+
const seen = new Set();
|
|
2783
|
+
for (const file of ctx.getAllFiles()) {
|
|
2784
|
+
if (!LARAVEL_PHP_EXT.test(file))
|
|
2785
|
+
continue;
|
|
2786
|
+
const content = ctx.readFile(file);
|
|
2787
|
+
if (!content || !content.includes('event('))
|
|
2788
|
+
continue;
|
|
2789
|
+
const safe = (0, strip_comments_1.stripCommentsForRegex)(content, 'php');
|
|
2790
|
+
const nodesInFile = ctx.getNodesInFile(file);
|
|
2791
|
+
LARAVEL_DISPATCH_RE.lastIndex = 0;
|
|
2792
|
+
let m;
|
|
2793
|
+
let added = 0;
|
|
2794
|
+
while ((m = LARAVEL_DISPATCH_RE.exec(safe)) && added < LARAVEL_FANOUT_CAP) {
|
|
2795
|
+
const targets = listeners.get(phpSimpleName(m[1]));
|
|
2796
|
+
if (!targets)
|
|
2797
|
+
continue;
|
|
2798
|
+
const line = safe.slice(0, m.index).split('\n').length;
|
|
2799
|
+
const disp = enclosingFn(nodesInFile, line);
|
|
2800
|
+
if (!disp)
|
|
2801
|
+
continue;
|
|
2802
|
+
for (const target of targets.values()) {
|
|
2803
|
+
if (target.id === disp.id)
|
|
2804
|
+
continue;
|
|
2805
|
+
const key = `${disp.id}>${target.id}`;
|
|
2806
|
+
if (seen.has(key))
|
|
2807
|
+
continue;
|
|
2808
|
+
seen.add(key);
|
|
2809
|
+
edges.push({
|
|
2810
|
+
source: disp.id,
|
|
2811
|
+
target: target.id,
|
|
2812
|
+
kind: 'calls',
|
|
2813
|
+
line,
|
|
2814
|
+
provenance: 'heuristic',
|
|
2815
|
+
metadata: { synthesizedBy: 'laravel-event', via: phpSimpleName(m[1]), registeredAt: `${file}:${line}` },
|
|
2816
|
+
});
|
|
2817
|
+
added++;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
return edges;
|
|
2822
|
+
}
|
|
1744
2823
|
/**
|
|
1745
2824
|
* Synthesize dispatcher→callback edges (field observers + EventEmitters +
|
|
1746
2825
|
* React re-render + JSX children + Vue templates + SvelteKit load + RN event
|
|
1747
|
-
* channel + Fabric native-impl + MyBatis Java↔XML + Gin middleware chain
|
|
2826
|
+
* channel + Fabric native-impl + MyBatis Java↔XML + Gin middleware chain +
|
|
2827
|
+
* Redux-thunk dispatch chain + object-literal registry dispatch + RTK Query
|
|
2828
|
+
* generated-hook → endpoint + Pinia useStore().action() + Vuex string dispatch +
|
|
2829
|
+
* Celery task .delay()/.apply_async() → task body + Spring publishEvent → @EventListener +
|
|
2830
|
+
* MediatR Send/Publish → IRequestHandler/INotificationHandler +
|
|
2831
|
+
* Sidekiq Worker.perform_async → #perform + Laravel event(new X) → listener handle).
|
|
1748
2832
|
* Returns the count added. Never throws into indexing — callers wrap in try/catch.
|
|
1749
2833
|
*/
|
|
1750
2834
|
function synthesizeCallbackEdges(queries, ctx) {
|
|
@@ -1782,6 +2866,18 @@ function synthesizeCallbackEdges(queries, ctx) {
|
|
|
1782
2866
|
const rnXPlatEdges = rnCrossPlatformEdges(queries);
|
|
1783
2867
|
const mybatisEdges = mybatisJavaXmlEdges(queries);
|
|
1784
2868
|
const ginEdges = ginMiddlewareChainEdges(queries, ctx);
|
|
2869
|
+
const thunkEdges = reduxThunkEdges(queries, ctx);
|
|
2870
|
+
const registryEdges = objectRegistryEdges(ctx);
|
|
2871
|
+
const rtkEdges = rtkQueryEdges(queries, ctx);
|
|
2872
|
+
const piniaEdges = piniaStoreEdges(ctx);
|
|
2873
|
+
const vuexEdges = vuexDispatchEdges(ctx);
|
|
2874
|
+
const celeryEdges = celeryDispatchEdges(ctx);
|
|
2875
|
+
const springEdges = springEventEdges(ctx);
|
|
2876
|
+
const mediatrEdges = mediatrDispatchEdges(ctx);
|
|
2877
|
+
const sidekiqEdges = sidekiqDispatchEdges(ctx);
|
|
2878
|
+
const laravelEdges = laravelEventEdges(ctx);
|
|
2879
|
+
const cFnPtrEdges = (0, c_fnptr_synthesizer_1.cFnPointerDispatchEdges)(queries, ctx);
|
|
2880
|
+
const goframeEdges = (0, goframe_synthesizer_1.goframeRouteEdges)(ctx);
|
|
1785
2881
|
const merged = [];
|
|
1786
2882
|
const seen = new Set();
|
|
1787
2883
|
for (const e of [
|
|
@@ -1804,6 +2900,18 @@ function synthesizeCallbackEdges(queries, ctx) {
|
|
|
1804
2900
|
...rnXPlatEdges,
|
|
1805
2901
|
...mybatisEdges,
|
|
1806
2902
|
...ginEdges,
|
|
2903
|
+
...thunkEdges,
|
|
2904
|
+
...registryEdges,
|
|
2905
|
+
...rtkEdges,
|
|
2906
|
+
...piniaEdges,
|
|
2907
|
+
...vuexEdges,
|
|
2908
|
+
...celeryEdges,
|
|
2909
|
+
...springEdges,
|
|
2910
|
+
...mediatrEdges,
|
|
2911
|
+
...sidekiqEdges,
|
|
2912
|
+
...laravelEdges,
|
|
2913
|
+
...cFnPtrEdges,
|
|
2914
|
+
...goframeEdges,
|
|
1807
2915
|
]) {
|
|
1808
2916
|
const key = `${e.source}>${e.target}`;
|
|
1809
2917
|
if (seen.has(key))
|