gitnexus 1.6.8-rc.2 → 1.6.8-rc.21
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/README.md +20 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts +21 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
- package/dist/_shared/scope-resolution/symbol-definition.d.ts +4 -0
- package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
- package/dist/cli/ai-context.js +1 -0
- package/dist/cli/analyze-config.js +40 -0
- package/dist/cli/analyze.d.ts +22 -0
- package/dist/cli/analyze.js +36 -6
- package/dist/cli/clean.d.ts +1 -0
- package/dist/cli/clean.js +43 -1
- package/dist/cli/eval-server.js +43 -0
- package/dist/cli/help-i18n.js +11 -0
- package/dist/cli/i18n/en.d.ts +13 -0
- package/dist/cli/i18n/en.js +13 -0
- package/dist/cli/i18n/resources.d.ts +26 -0
- package/dist/cli/i18n/zh-CN.d.ts +13 -0
- package/dist/cli/i18n/zh-CN.js +13 -0
- package/dist/cli/index.js +19 -0
- package/dist/cli/list.js +12 -0
- package/dist/cli/optional-grammars.d.ts +6 -8
- package/dist/cli/optional-grammars.js +8 -11
- package/dist/cli/skill-gen.js +1 -0
- package/dist/cli/status.js +26 -5
- package/dist/cli/tool.d.ts +12 -1
- package/dist/cli/tool.js +41 -1
- package/dist/core/embeddings/embedder.js +4 -0
- package/dist/core/embeddings/embedding-pipeline.js +27 -16
- package/dist/core/embeddings/onnxruntime-common-resolver.d.ts +6 -0
- package/dist/core/embeddings/onnxruntime-common-resolver.js +130 -0
- package/dist/core/graph/import-cycles.d.ts +10 -0
- package/dist/core/graph/import-cycles.js +103 -0
- package/dist/core/group/extractors/grpc-patterns/proto.js +10 -6
- package/dist/core/group/extractors/http-patterns/java.js +3 -50
- package/dist/core/group/extractors/http-patterns/kotlin.js +7 -7
- package/dist/core/group/extractors/include-extractor.js +7 -7
- package/dist/core/ingestion/cfg/cfg-builder.d.ts +64 -0
- package/dist/core/ingestion/cfg/cfg-builder.js +129 -0
- package/dist/core/ingestion/cfg/collect.d.ts +30 -0
- package/dist/core/ingestion/cfg/collect.js +34 -0
- package/dist/core/ingestion/cfg/control-flow-context.d.ts +97 -0
- package/dist/core/ingestion/cfg/control-flow-context.js +113 -0
- package/dist/core/ingestion/cfg/emit.d.ts +144 -0
- package/dist/core/ingestion/cfg/emit.js +315 -0
- package/dist/core/ingestion/cfg/reaching-defs.d.ts +90 -0
- package/dist/core/ingestion/cfg/reaching-defs.js +364 -0
- package/dist/core/ingestion/cfg/traversal-result.d.ts +20 -0
- package/dist/core/ingestion/cfg/traversal-result.js +2 -0
- package/dist/core/ingestion/cfg/types.d.ts +231 -0
- package/dist/core/ingestion/cfg/types.js +13 -0
- package/dist/core/ingestion/cfg/visitors/typescript-harvest.d.ts +144 -0
- package/dist/core/ingestion/cfg/visitors/typescript-harvest.js +1032 -0
- package/dist/core/ingestion/cfg/visitors/typescript.d.ts +66 -0
- package/dist/core/ingestion/cfg/visitors/typescript.js +584 -0
- package/dist/core/ingestion/language-provider.d.ts +36 -2
- package/dist/core/ingestion/languages/c-cpp.js +11 -4
- package/dist/core/ingestion/languages/cpp/arity-metadata.js +6 -2
- package/dist/core/ingestion/languages/cpp/captures.js +24 -1
- package/dist/core/ingestion/languages/cpp/query.js +23 -0
- package/dist/core/ingestion/languages/java.js +3 -0
- package/dist/core/ingestion/languages/kotlin/query.js +3 -2
- package/dist/core/ingestion/languages/kotlin.js +5 -1
- package/dist/core/ingestion/languages/typescript.js +5 -0
- package/dist/core/ingestion/method-extractors/configs/c-cpp.js +6 -13
- package/dist/core/ingestion/method-extractors/generic.js +1 -0
- package/dist/core/ingestion/method-types.d.ts +2 -0
- package/dist/core/ingestion/model/symbol-table.d.ts +1 -0
- package/dist/core/ingestion/model/symbol-table.js +1 -0
- package/dist/core/ingestion/parsing-processor.js +22 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +18 -1
- package/dist/core/ingestion/pipeline-phases/routes.js +64 -14
- package/dist/core/ingestion/pipeline.d.ts +57 -0
- package/dist/core/ingestion/route-extractors/spring-shared.d.ts +50 -0
- package/dist/core/ingestion/route-extractors/spring-shared.js +80 -0
- package/dist/core/ingestion/route-extractors/spring.d.ts +35 -0
- package/dist/core/ingestion/route-extractors/spring.js +136 -0
- package/dist/core/ingestion/scope-extractor.js +3 -0
- package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +12 -0
- package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +145 -24
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +6 -0
- package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +43 -3
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +21 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +207 -0
- package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
- package/dist/core/ingestion/taint/emit.d.ts +124 -0
- package/dist/core/ingestion/taint/emit.js +204 -0
- package/dist/core/ingestion/taint/match.d.ts +153 -0
- package/dist/core/ingestion/taint/match.js +278 -0
- package/dist/core/ingestion/taint/path-codec.d.ts +134 -0
- package/dist/core/ingestion/taint/path-codec.js +190 -0
- package/dist/core/ingestion/taint/propagate.d.ts +216 -0
- package/dist/core/ingestion/taint/propagate.js +664 -0
- package/dist/core/ingestion/taint/site-safety.d.ts +29 -0
- package/dist/core/ingestion/taint/site-safety.js +98 -0
- package/dist/core/ingestion/taint/source-sink-config.d.ts +94 -23
- package/dist/core/ingestion/taint/source-sink-config.js +11 -11
- package/dist/core/ingestion/taint/source-sink-registry.d.ts +6 -4
- package/dist/core/ingestion/taint/source-sink-registry.js +6 -4
- package/dist/core/ingestion/taint/typescript-model.d.ts +38 -0
- package/dist/core/ingestion/taint/typescript-model.js +102 -0
- package/dist/core/ingestion/utils/method-props.js +1 -0
- package/dist/core/ingestion/workers/clone-safety.d.ts +109 -0
- package/dist/core/ingestion/workers/clone-safety.js +465 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +11 -0
- package/dist/core/ingestion/workers/parse-worker.js +70 -53
- package/dist/core/ingestion/workers/post-result.d.ts +22 -0
- package/dist/core/ingestion/workers/post-result.js +87 -0
- package/dist/core/ingestion/workers/result-merge.d.ts +20 -0
- package/dist/core/ingestion/workers/result-merge.js +43 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +15 -0
- package/dist/core/ingestion/workers/worker-pool.js +33 -16
- package/dist/core/lbug/lbug-adapter.d.ts +19 -0
- package/dist/core/lbug/lbug-adapter.js +56 -1
- package/dist/core/run-analyze.d.ts +87 -0
- package/dist/core/run-analyze.js +280 -25
- package/dist/core/tree-sitter/parser-loader.js +5 -4
- package/dist/core/tree-sitter/vendored-grammars.d.ts +39 -0
- package/dist/core/tree-sitter/vendored-grammars.js +57 -0
- package/dist/mcp/core/embedder.js +4 -0
- package/dist/mcp/local/local-backend.d.ts +86 -2
- package/dist/mcp/local/local-backend.js +657 -27
- package/dist/mcp/resources.js +1 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.js +109 -0
- package/dist/server/analyze-launch.d.ts +29 -0
- package/dist/server/analyze-launch.js +137 -0
- package/dist/server/analyze-upload.d.ts +33 -0
- package/dist/server/analyze-upload.js +123 -0
- package/dist/server/analyze-worker-ipc.d.ts +58 -0
- package/dist/server/analyze-worker-ipc.js +16 -0
- package/dist/server/analyze-worker.d.ts +17 -1
- package/dist/server/analyze-worker.js +7 -1
- package/dist/server/api.js +46 -140
- package/dist/server/git-clone.d.ts +1 -0
- package/dist/server/git-clone.js +1 -1
- package/dist/server/middleware.d.ts +11 -0
- package/dist/server/middleware.js +27 -0
- package/dist/server/upload-ingest.d.ts +56 -0
- package/dist/server/upload-ingest.js +276 -0
- package/dist/server/upload-paths.d.ts +31 -0
- package/dist/server/upload-paths.js +51 -0
- package/dist/server/upload-sweep.d.ts +20 -0
- package/dist/server/upload-sweep.js +57 -0
- package/dist/storage/branch-index.d.ts +52 -0
- package/dist/storage/branch-index.js +65 -0
- package/dist/storage/git.d.ts +11 -0
- package/dist/storage/git.js +28 -0
- package/dist/storage/parse-cache.d.ts +22 -1
- package/dist/storage/parse-cache.js +32 -10
- package/dist/storage/repo-manager.d.ts +122 -10
- package/dist/storage/repo-manager.js +162 -21
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +42 -8
- package/hooks/claude/gitnexus-hook.cjs +36 -8
- package/hooks/claude/hook-db-lock-probe.cjs +123 -2
- package/package.json +4 -2
- package/scripts/assert-publish-grammar-coverage.cjs +31 -0
- package/scripts/build-tree-sitter-grammars.cjs +16 -10
- package/skills/gitnexus-guide.md +11 -0
- package/vendor/tree-sitter-c/package.json +1 -1
- package/vendor/tree-sitter-dart/package.json +1 -1
- package/vendor/tree-sitter-kotlin/package.json +1 -1
- package/vendor/tree-sitter-proto/package.json +1 -1
- package/vendor/tree-sitter-swift/package.json +1 -1
- package/web/assets/{agent-CKPMqImC.js → agent-ay4LD70X.js} +1 -1
- package/web/assets/{architectureDiagram-UL44E2DR-D1EXI0zA.js → architectureDiagram-UL44E2DR-Dc-viYhd.js} +1 -1
- package/web/assets/{chunk-LCXTWHL2-Dfmux4m1.js → chunk-LCXTWHL2-4rpojOyj.js} +1 -1
- package/web/assets/{chunk-RG4AUYOV-CAkzcoRj.js → chunk-RG4AUYOV-BnOy944n.js} +1 -1
- package/web/assets/{classDiagram-KGZ6W3CR-Bv93af_b.js → classDiagram-KGZ6W3CR-Bsgpy98Q.js} +1 -1
- package/web/assets/{classDiagram-v2-72OJOZXJ-CLfEqHUa.js → classDiagram-v2-72OJOZXJ-D5atDGjc.js} +1 -1
- package/web/assets/{diagram-3NCE3AQN-BqAtKUpW.js → diagram-3NCE3AQN-Cz1OEMVi.js} +1 -1
- package/web/assets/{diagram-GF46GFSD-CaBG6n6o.js → diagram-GF46GFSD-CWYwzfP8.js} +1 -1
- package/web/assets/{diagram-QXG6HAR7-CZ-O3rcV.js → diagram-QXG6HAR7-DtBObr8L.js} +1 -1
- package/web/assets/{diagram-WEQXMOUZ-P4lSL4GH.js → diagram-WEQXMOUZ-BUV44Ov_.js} +1 -1
- package/web/assets/{erDiagram-L5TCEMPS-QN2eEP1e.js → erDiagram-L5TCEMPS-Ds9s-sRF.js} +1 -1
- package/web/assets/{flowDiagram-H6V6AXG4-CXbXImlN.js → flowDiagram-H6V6AXG4-CupStHQb.js} +1 -1
- package/web/assets/index-COMMmbxW.css +2 -0
- package/web/assets/{index-CG6q8eTs.js → index-Do8AE5yF.js} +85 -85
- package/web/assets/{infoDiagram-3YFTVSEB-DmLICZx1.js → infoDiagram-3YFTVSEB-DPTXSbgu.js} +1 -1
- package/web/assets/{ishikawaDiagram-BNXS4ZKH-6LKOvBfp.js → ishikawaDiagram-BNXS4ZKH-CeK-Qhet.js} +1 -1
- package/web/assets/{kanban-definition-75IXJCU3-DgDi9oJT.js → kanban-definition-75IXJCU3-D4tx6QQ-.js} +1 -1
- package/web/assets/{mindmap-definition-2TDM6QVE-CwR5sBB-.js → mindmap-definition-2TDM6QVE-Bcto_kGa.js} +1 -1
- package/web/assets/{pieDiagram-CU6KROY3-By8g6f6B.js → pieDiagram-CU6KROY3-DPIeBTG6.js} +1 -1
- package/web/assets/{requirementDiagram-JXO7QTGE-7oDcJ1_J.js → requirementDiagram-JXO7QTGE-DDGXjVKO.js} +1 -1
- package/web/assets/{sequenceDiagram-VS2MUI6T-FySeKCUy.js → sequenceDiagram-VS2MUI6T-BzDqpcVW.js} +1 -1
- package/web/assets/{stateDiagram-7D4R322I-CB2nABwH.js → stateDiagram-7D4R322I-Bx__als3.js} +1 -1
- package/web/assets/{stateDiagram-v2-36443NZ5-COBGd2RL.js → stateDiagram-v2-36443NZ5-QqOh2yO-.js} +1 -1
- package/web/assets/{timeline-definition-O6YCAMPW-Ds2CnVZK.js → timeline-definition-O6YCAMPW-C1eogTOG.js} +1 -1
- package/web/assets/{vennDiagram-MWXL3ELB-DUIEwXWp.js → vennDiagram-MWXL3ELB-D20F4rSW.js} +1 -1
- package/web/assets/{wardleyDiagram-CUQ6CDDI-DEiFPQih.js → wardleyDiagram-CUQ6CDDI-BeBHg7ST.js} +1 -1
- package/web/assets/{xychartDiagram-N2JHSOCM-BUbayhST.js → xychartDiagram-N2JHSOCM-CdOWeoNI.js} +1 -1
- package/web/index.html +2 -2
- package/scripts/materialize-vendor-grammars.cjs +0 -97
- package/web/assets/index-BKWA-m7o.css +0 -2
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical `block:stmt` string key for a program point. Colon-separated to
|
|
3
|
+
* match the codebase's `blockIndex:stmtIndex` id conventions. Shared by the
|
|
4
|
+
* taint propagation engine (dedup/state keys) and the taint emit path
|
|
5
|
+
* (persisted edge-id material) so the two never drift.
|
|
6
|
+
*/
|
|
7
|
+
export function pointKey(p) {
|
|
8
|
+
return `${p.blockIndex}:${p.stmtIndex}`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* def-site key: packs (blockIndex, stmtIndex) into one number. The stride is
|
|
12
|
+
* a per-BLOCK statement bound, and `maxFunctionLines` caps LINES, not
|
|
13
|
+
* statements — a minified one-line function coalesces arbitrarily many
|
|
14
|
+
* statements into one block, so an overflow would silently alias
|
|
15
|
+
* (block b, stmt STRIDE+k) with (block b+1, stmt k) and fabricate wrong-block
|
|
16
|
+
* facts. computeReachingDefs therefore range-checks up front and bails to a
|
|
17
|
+
* sound empty `truncated` result instead of ever letting a key alias.
|
|
18
|
+
* 2^21 statements per block × blocks ≤ 2^32 stays inside Number's 2^53.
|
|
19
|
+
*/
|
|
20
|
+
const STMT_STRIDE = 1 << 21;
|
|
21
|
+
const defKey = (blockIndex, stmtIndex) => blockIndex * STMT_STRIDE + stmtIndex;
|
|
22
|
+
const EMPTY_LATTICE = new Map();
|
|
23
|
+
/**
|
|
24
|
+
* Compute reaching definitions for one function. See the module doc for the
|
|
25
|
+
* purity/determinism/sharing contract.
|
|
26
|
+
*/
|
|
27
|
+
export function computeReachingDefs(cfg, limits) {
|
|
28
|
+
if (!cfg.bindings) {
|
|
29
|
+
return { status: 'no-facts', bindings: [], facts: [], defCount: 0, useCount: 0 };
|
|
30
|
+
}
|
|
31
|
+
const blocks = cfg.blocks;
|
|
32
|
+
const n = blocks.length;
|
|
33
|
+
// Key-aliasing guard (see STMT_STRIDE): a block with ≥ STRIDE statements
|
|
34
|
+
// cannot be keyed without aliasing into the next block's def sites, which
|
|
35
|
+
// would fabricate wrong-block facts — strictly worse than producing none.
|
|
36
|
+
// Bail to a sound empty `overflow` result (the emit path warns distinctly).
|
|
37
|
+
for (const b of blocks) {
|
|
38
|
+
if ((b.statements?.length ?? 0) >= STMT_STRIDE) {
|
|
39
|
+
return { status: 'overflow', bindings: cfg.bindings, facts: [], defCount: 0, useCount: 0 };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// ── adjacency (sorted for deterministic merges) ─────────────────────────
|
|
43
|
+
// A `throw` edge contributes IN(from) ∪ allDefs(from) to its handler, not
|
|
44
|
+
// OUT: an exception can fire BEFORE the block's defs complete (the seed def
|
|
45
|
+
// in `let x = seed(); try { x = risky(); } catch { sink(x) }` must reach the
|
|
46
|
+
// sink) AND between any two defs of a multi-def coalesced block (the parse
|
|
47
|
+
// def in `x = parse(a); x = normalize(x);` is live exactly when normalize
|
|
48
|
+
// throws — OUT's last-def-wins misses it). Sound over-approximation;
|
|
49
|
+
// monotone, so the fixpoint absorbs it. See mergePreds.
|
|
50
|
+
const preds = Array.from({ length: n }, () => []);
|
|
51
|
+
const succs = Array.from({ length: n }, () => []);
|
|
52
|
+
// Handlers whose IN depends on this block's IN (throw edges) — requeued on
|
|
53
|
+
// IN change, since a genned binding can absorb IN growth without changing
|
|
54
|
+
// OUT, which would otherwise leave the handler stale.
|
|
55
|
+
const throwSuccs = Array.from({ length: n }, () => []);
|
|
56
|
+
for (const e of cfg.edges) {
|
|
57
|
+
// Optional-chained pushes drop out-of-range endpoints defensively — the
|
|
58
|
+
// emit path validates via isEmitSafeCfg, but this pure function also runs
|
|
59
|
+
// on hand-built CFGs.
|
|
60
|
+
succs[e.from]?.push(e.to);
|
|
61
|
+
preds[e.to]?.push({ from: e.from, viaThrow: e.kind === 'throw' });
|
|
62
|
+
if (e.kind === 'throw')
|
|
63
|
+
throwSuccs[e.from]?.push(e.to);
|
|
64
|
+
}
|
|
65
|
+
for (const list of preds) {
|
|
66
|
+
list.sort((a, b) => a.from - b.from || Number(a.viaThrow) - Number(b.viaThrow));
|
|
67
|
+
// duplicate (from, throw+non-throw) pairs both survive — the throw leg
|
|
68
|
+
// adds IN(from); the merge dedups set-wise.
|
|
69
|
+
}
|
|
70
|
+
for (const list of succs)
|
|
71
|
+
list.sort((a, b) => a - b);
|
|
72
|
+
const gen = new Array(n).fill(null);
|
|
73
|
+
// allDefsGen[b]: bindingIdx → EVERY def-site key in the block (must + may).
|
|
74
|
+
// This is what a throw edge delivers to its handler: an exception can fire
|
|
75
|
+
// between any two statements, so every intermediate def may be the live one
|
|
76
|
+
// at the handler — IN∪OUT alone misses defs overwritten later in the same
|
|
77
|
+
// coalesced block (`try { x = parse(a); x = normalize(x); } catch { sink(x) }`
|
|
78
|
+
// — parse's value is exactly what sink sees when normalize throws).
|
|
79
|
+
const allDefsGen = new Array(n).fill(null);
|
|
80
|
+
const defLine = new Map(); // defKey → source line
|
|
81
|
+
let defCount = 0;
|
|
82
|
+
let useCount = 0;
|
|
83
|
+
for (const b of blocks) {
|
|
84
|
+
const stmts = b.statements;
|
|
85
|
+
if (!stmts || stmts.length === 0)
|
|
86
|
+
continue;
|
|
87
|
+
let g = null;
|
|
88
|
+
let all = null;
|
|
89
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
90
|
+
const s = stmts[i];
|
|
91
|
+
useCount += s.uses.length;
|
|
92
|
+
const key = defKey(b.index, i);
|
|
93
|
+
const record = (d, kills) => {
|
|
94
|
+
defCount += 1;
|
|
95
|
+
defLine.set(key, s.line);
|
|
96
|
+
if (!g)
|
|
97
|
+
g = new Map();
|
|
98
|
+
const entry = g.get(d);
|
|
99
|
+
if (kills || !entry) {
|
|
100
|
+
g.set(d, { set: new Set([key]), kills: kills || (entry?.kills ?? false) });
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
entry.set.add(key); // may-def accumulates; never clears
|
|
104
|
+
}
|
|
105
|
+
if (!all)
|
|
106
|
+
all = new Map();
|
|
107
|
+
const allSet = all.get(d);
|
|
108
|
+
if (allSet)
|
|
109
|
+
allSet.add(key);
|
|
110
|
+
else
|
|
111
|
+
all.set(d, new Set([key]));
|
|
112
|
+
};
|
|
113
|
+
if (s.mayDefs)
|
|
114
|
+
for (const d of s.mayDefs)
|
|
115
|
+
record(d, false);
|
|
116
|
+
for (const d of s.defs)
|
|
117
|
+
record(d, true);
|
|
118
|
+
}
|
|
119
|
+
gen[b.index] = g;
|
|
120
|
+
allDefsGen[b.index] = all;
|
|
121
|
+
}
|
|
122
|
+
// ── iteration order: RPO over reachable blocks, then the rest by index ──
|
|
123
|
+
const order = reversePostOrder(cfg.entryIndex, succs, n);
|
|
124
|
+
// ── fixpoint ────────────────────────────────────────────────────────────
|
|
125
|
+
const inSets = new Array(n).fill(EMPTY_LATTICE);
|
|
126
|
+
const outSets = new Array(n).fill(EMPTY_LATTICE);
|
|
127
|
+
const inWorklist = new Array(n).fill(true);
|
|
128
|
+
let pending = n;
|
|
129
|
+
while (pending > 0) {
|
|
130
|
+
for (const b of order) {
|
|
131
|
+
if (!inWorklist[b])
|
|
132
|
+
continue;
|
|
133
|
+
inWorklist[b] = false;
|
|
134
|
+
pending -= 1;
|
|
135
|
+
const p = preds[b];
|
|
136
|
+
const inB = p.length === 0
|
|
137
|
+
? EMPTY_LATTICE
|
|
138
|
+
: p.length === 1 && !p[0].viaThrow
|
|
139
|
+
? outSets[p[0].from] // alias — zero allocation on straight-line chains
|
|
140
|
+
: mergePreds(p, inSets, outSets, allDefsGen);
|
|
141
|
+
const inChanged = !latticeEquals(inSets[b], inB);
|
|
142
|
+
inSets[b] = inB;
|
|
143
|
+
const g = gen[b];
|
|
144
|
+
// OUT = overlay(IN): a KILLING gen entry replaces the binding's set; a
|
|
145
|
+
// may-def-only entry unions with the incoming set (never kills). When
|
|
146
|
+
// nothing is genned, OUT aliases IN outright.
|
|
147
|
+
let outB;
|
|
148
|
+
if (!g) {
|
|
149
|
+
outB = inB;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
outB = new Map(inB); // copies REFERENCES, never set contents
|
|
153
|
+
for (const [bindingIdx, entry] of g) {
|
|
154
|
+
if (entry.kills) {
|
|
155
|
+
outB.set(bindingIdx, entry.set);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
const incoming = inB.get(bindingIdx);
|
|
159
|
+
outB.set(bindingIdx, incoming ? unionSets(incoming, entry.set) : entry.set);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const requeue = (s) => {
|
|
164
|
+
if (!inWorklist[s]) {
|
|
165
|
+
inWorklist[s] = true;
|
|
166
|
+
pending += 1;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
if (!latticeEquals(outSets[b], outB)) {
|
|
170
|
+
outSets[b] = outB;
|
|
171
|
+
for (const s of succs[b])
|
|
172
|
+
requeue(s);
|
|
173
|
+
}
|
|
174
|
+
if (inChanged)
|
|
175
|
+
for (const s of throwSuccs[b])
|
|
176
|
+
requeue(s);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// ── statement sweep: recover statement-granular def→use facts ───────────
|
|
180
|
+
const maxFacts = limits?.maxFacts && limits.maxFacts > 0 ? limits.maxFacts : Infinity;
|
|
181
|
+
const facts = [];
|
|
182
|
+
let truncated = false;
|
|
183
|
+
outer: for (const b of blocks) {
|
|
184
|
+
const stmts = b.statements;
|
|
185
|
+
if (!stmts || stmts.length === 0)
|
|
186
|
+
continue;
|
|
187
|
+
// Lazy overlay of IN — entries are replaced (never mutated) on def, so the
|
|
188
|
+
// shared sets stay intact.
|
|
189
|
+
let reach = null;
|
|
190
|
+
for (let i = 0; i < stmts.length; i++) {
|
|
191
|
+
const s = stmts[i];
|
|
192
|
+
// A use's binding that the SAME statement also defines could be a
|
|
193
|
+
// read-then-write (`x += 1` — sees prior defs) OR a write-then-read
|
|
194
|
+
// (`if ((m = re.exec(s)) && m[1])` — sees the same-statement def).
|
|
195
|
+
// StatementFacts carries no intra-statement order, so emit BOTH: prior
|
|
196
|
+
// defs ∪ the same-statement def. Sound over-approximation — the extra
|
|
197
|
+
// self-fact on compound assignments is harmless; missing the
|
|
198
|
+
// assign-and-test def→use (the most common JS idiom) would be a taint
|
|
199
|
+
// false negative. May-defs join the self-key set the same way.
|
|
200
|
+
const sameStmtDefs = s.defs.length > 0 || s.mayDefs?.length ? new Set([...s.defs, ...(s.mayDefs ?? [])]) : null;
|
|
201
|
+
for (const u of s.uses) {
|
|
202
|
+
const reaching = (reach ?? inSets[b.index]).get(u);
|
|
203
|
+
const selfKey = sameStmtDefs?.has(u) ? defKey(b.index, i) : undefined;
|
|
204
|
+
if (!reaching && selfKey === undefined)
|
|
205
|
+
continue;
|
|
206
|
+
const keys = selfKey !== undefined && !reaching?.has(selfKey)
|
|
207
|
+
? [...(reaching ?? []), selfKey]
|
|
208
|
+
: [...(reaching ?? [])];
|
|
209
|
+
for (const key of keys) {
|
|
210
|
+
if (facts.length >= maxFacts) {
|
|
211
|
+
truncated = true;
|
|
212
|
+
break outer;
|
|
213
|
+
}
|
|
214
|
+
const defBlock = Math.floor(key / STMT_STRIDE);
|
|
215
|
+
const defStmt = key % STMT_STRIDE;
|
|
216
|
+
facts.push({
|
|
217
|
+
bindingIdx: u,
|
|
218
|
+
def: { blockIndex: defBlock, stmtIndex: defStmt, line: defLine.get(key) ?? s.line },
|
|
219
|
+
use: { blockIndex: b.index, stmtIndex: i, line: s.line },
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (s.mayDefs?.length) {
|
|
224
|
+
// Gen WITHOUT kill: the conditional def joins the binding's set.
|
|
225
|
+
if (!reach)
|
|
226
|
+
reach = new Map(inSets[b.index]);
|
|
227
|
+
const key = defKey(b.index, i);
|
|
228
|
+
for (const d of s.mayDefs) {
|
|
229
|
+
const prior = reach.get(d);
|
|
230
|
+
reach.set(d, prior ? unionSets(prior, new Set([key])) : new Set([key]));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (s.defs.length > 0) {
|
|
234
|
+
if (!reach)
|
|
235
|
+
reach = new Map(inSets[b.index]);
|
|
236
|
+
for (const d of s.defs)
|
|
237
|
+
reach.set(d, new Set([defKey(b.index, i)])); // kill + gen
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
facts.sort((a, b) => a.def.blockIndex - b.def.blockIndex ||
|
|
242
|
+
a.def.stmtIndex - b.def.stmtIndex ||
|
|
243
|
+
a.use.blockIndex - b.use.blockIndex ||
|
|
244
|
+
a.use.stmtIndex - b.use.stmtIndex ||
|
|
245
|
+
a.bindingIdx - b.bindingIdx);
|
|
246
|
+
return {
|
|
247
|
+
status: truncated ? 'truncated' : 'computed',
|
|
248
|
+
bindings: cfg.bindings,
|
|
249
|
+
facts,
|
|
250
|
+
defCount,
|
|
251
|
+
useCount,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/** RPO over blocks reachable from `entry`; unreachable blocks appended by index. */
|
|
255
|
+
function reversePostOrder(entry, succs, n) {
|
|
256
|
+
const visited = new Array(n).fill(false);
|
|
257
|
+
const post = [];
|
|
258
|
+
// Iterative DFS with an explicit phase stack (children pushed in reverse so
|
|
259
|
+
// they pop in sorted order — determinism).
|
|
260
|
+
const stack = [{ node: entry, childIdx: 0 }];
|
|
261
|
+
visited[entry] = true;
|
|
262
|
+
while (stack.length) {
|
|
263
|
+
const top = stack[stack.length - 1];
|
|
264
|
+
const children = succs[top.node];
|
|
265
|
+
if (top.childIdx < children.length) {
|
|
266
|
+
const next = children[top.childIdx];
|
|
267
|
+
top.childIdx += 1;
|
|
268
|
+
if (!visited[next]) {
|
|
269
|
+
visited[next] = true;
|
|
270
|
+
stack.push({ node: next, childIdx: 0 });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
post.push(top.node);
|
|
275
|
+
stack.pop();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const order = post.reverse();
|
|
279
|
+
for (let b = 0; b < n; b++)
|
|
280
|
+
if (!visited[b])
|
|
281
|
+
order.push(b);
|
|
282
|
+
return order;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Union predecessor lattices, sharing sets where possible. A normal edge
|
|
286
|
+
* contributes OUT(from). A THROW edge contributes IN(from) ∪ allDefs(from):
|
|
287
|
+
* an exception may fire before, between, or after any of the block's defs, so
|
|
288
|
+
* the handler can observe the incoming state OR any intermediate def — OUT
|
|
289
|
+
* alone (last-def-wins) misses defs overwritten later in the same block.
|
|
290
|
+
* IN ∪ allDefs ⊇ OUT, so the throw contribution subsumes it.
|
|
291
|
+
*/
|
|
292
|
+
function mergePreds(preds, inSets, outSets, allDefsGen) {
|
|
293
|
+
const merged = new Map();
|
|
294
|
+
const mergeOne = (source) => {
|
|
295
|
+
for (const [bindingIdx, set] of source) {
|
|
296
|
+
const existing = merged.get(bindingIdx);
|
|
297
|
+
if (!existing) {
|
|
298
|
+
merged.set(bindingIdx, set); // share the first contributor's set
|
|
299
|
+
}
|
|
300
|
+
else if (existing !== set) {
|
|
301
|
+
// Union only when the references differ. Copy-on-extend: `existing`
|
|
302
|
+
// may be a shared set from another block — never mutate it.
|
|
303
|
+
let target = existing;
|
|
304
|
+
let copied = false;
|
|
305
|
+
for (const key of set) {
|
|
306
|
+
if (!target.has(key)) {
|
|
307
|
+
if (!copied) {
|
|
308
|
+
target = new Set(existing);
|
|
309
|
+
copied = true;
|
|
310
|
+
}
|
|
311
|
+
target.add(key);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (copied)
|
|
315
|
+
merged.set(bindingIdx, target);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
for (const p of preds) {
|
|
320
|
+
if (p.viaThrow) {
|
|
321
|
+
mergeOne(inSets[p.from]); // exception may fire pre-defs…
|
|
322
|
+
const all = allDefsGen[p.from];
|
|
323
|
+
if (all)
|
|
324
|
+
mergeOne(all); // …or after ANY of the block's defs
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
mergeOne(outSets[p.from]);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return merged;
|
|
331
|
+
}
|
|
332
|
+
/** Order-stable union of two def-sets (shares `a` when `b` adds nothing). */
|
|
333
|
+
function unionSets(a, b) {
|
|
334
|
+
let target = a;
|
|
335
|
+
let copied = false;
|
|
336
|
+
for (const key of b) {
|
|
337
|
+
if (!target.has(key)) {
|
|
338
|
+
if (!copied) {
|
|
339
|
+
target = new Set(a);
|
|
340
|
+
copied = true;
|
|
341
|
+
}
|
|
342
|
+
target.add(key);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return target;
|
|
346
|
+
}
|
|
347
|
+
/** Per-binding equality with a reference fast path (sets only ever grow). */
|
|
348
|
+
function latticeEquals(a, b) {
|
|
349
|
+
if (a === b)
|
|
350
|
+
return true;
|
|
351
|
+
if (a.size !== b.size)
|
|
352
|
+
return false;
|
|
353
|
+
for (const [k, bSet] of b) {
|
|
354
|
+
const aSet = a.get(k);
|
|
355
|
+
if (aSet === bSet)
|
|
356
|
+
continue;
|
|
357
|
+
if (!aSet || aSet.size !== bSet.size)
|
|
358
|
+
return false;
|
|
359
|
+
for (const v of bSet)
|
|
360
|
+
if (!aSet.has(v))
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TraversalResult (issue #2081, M1).
|
|
3
|
+
*
|
|
4
|
+
* Visiting a statement (or a statement sequence) returns the block its control
|
|
5
|
+
* flow ENTERS through, plus the set of blocks whose **normal** control flows
|
|
6
|
+
* out the bottom (the "dangling exits") — to be wired to the entry of whatever
|
|
7
|
+
* comes next. Abnormal exits (return/break/continue/throw) are wired directly
|
|
8
|
+
* to their targets during the walk and are NOT part of `exits`.
|
|
9
|
+
*
|
|
10
|
+
* A statement that cannot fall through (e.g. ends in `return`/`throw`, or both
|
|
11
|
+
* branches of an `if` return) yields an empty `exits` array.
|
|
12
|
+
*/
|
|
13
|
+
export interface TraversalResult {
|
|
14
|
+
/** Block index control enters this statement/sequence through. */
|
|
15
|
+
readonly entry: number;
|
|
16
|
+
/** Block indices whose normal control falls out the bottom (may be empty). */
|
|
17
|
+
readonly exits: readonly number[];
|
|
18
|
+
}
|
|
19
|
+
/** A sequence of statements that produced no blocks (e.g. an empty body). */
|
|
20
|
+
export declare const emptyTraversal: (entry: number) => TraversalResult;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CFG data model — plain, JSON-serializable types (issue #2081, M1).
|
|
3
|
+
*
|
|
4
|
+
* These cross the worker→main boundary and the disk-backed/durable ParsedFile
|
|
5
|
+
* store, so they must contain NO tree-sitter AST references, class instances,
|
|
6
|
+
* or anything that does not survive `JSON.stringify` → `JSON.parse`. Block and
|
|
7
|
+
* edge endpoints are referenced by integer index within a function's CFG.
|
|
8
|
+
*
|
|
9
|
+
* The per-language `CfgVisitor` (built in the parse worker, where the AST
|
|
10
|
+
* lives — see the M1 plan KTD1/KTD7) produces a `FunctionCfg` per function; the
|
|
11
|
+
* array of them is what rides on `ParsedFile.cfgSideChannel`.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* One distinct declared variable (binding) within a function (#2082 M2 U1).
|
|
15
|
+
*
|
|
16
|
+
* Statement facts reference bindings by integer index into
|
|
17
|
+
* {@link FunctionCfg.bindings} — names appear once per binding instead of once
|
|
18
|
+
* per occurrence (measured ~4× smaller serialized payload than named records).
|
|
19
|
+
* Distinct bindings of the same name (shadowing) get distinct entries, which is
|
|
20
|
+
* what keeps an inner `let x` from falsely killing the outer `x`'s definitions
|
|
21
|
+
* in the reaching-defs solver. NOTE: no field here may be named `nodeId` — the
|
|
22
|
+
* durable parsedfile-store reviver dedups objects keyed on that field name.
|
|
23
|
+
*/
|
|
24
|
+
export interface BindingEntry {
|
|
25
|
+
/** Source-level variable name (what the persisted edge's `reason` carries). */
|
|
26
|
+
readonly name: string;
|
|
27
|
+
/**
|
|
28
|
+
* 1-based line/0-based column of the canonical declaration site — `var`
|
|
29
|
+
* multi-declarations canonicalize to the FIRST declaration in source order.
|
|
30
|
+
* Both 0 for synthetic bindings.
|
|
31
|
+
*/
|
|
32
|
+
readonly declLine: number;
|
|
33
|
+
readonly declColumn: number;
|
|
34
|
+
/** How the binding was introduced (param/catch matter to the M3 taint pass). */
|
|
35
|
+
readonly kind: 'var' | 'let' | 'const' | 'param' | 'catch' | 'function' | 'class' | 'module';
|
|
36
|
+
/**
|
|
37
|
+
* True when the name has no in-function declaration site (implicit global,
|
|
38
|
+
* import, or a variable captured from an enclosing function) — keyed
|
|
39
|
+
* `name@module` in edge ids instead of `name:line:col`.
|
|
40
|
+
*/
|
|
41
|
+
readonly synthetic?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* One occurrence of a binding inside a call/new site's argument position
|
|
45
|
+
* (#2083 M3 U1). A bare `number` is a DIRECT occurrence (binding index into
|
|
46
|
+
* {@link FunctionCfg.bindings}); a `[bindingIdx, viaSiteIdx]` tuple marks an
|
|
47
|
+
* occurrence that reaches this argument THROUGH the nested site at
|
|
48
|
+
* `viaSiteIdx` (an index into the SAME statement's {@link StatementFacts.sites}
|
|
49
|
+
* array). The tag is load-bearing for sanitizer interposition (plan KTD4a):
|
|
50
|
+
* a flat per-arg binding set cannot distinguish `exec(escape(x))` (kill) from
|
|
51
|
+
* `exec(x)` (finding) — the single most common safe pattern would
|
|
52
|
+
* false-positive without it.
|
|
53
|
+
*/
|
|
54
|
+
export type SiteArgOccurrence = number | readonly [number, number];
|
|
55
|
+
/**
|
|
56
|
+
* One call site, constructor call, or value-position member read harvested
|
|
57
|
+
* from a statement (#2083 M3 U1, plan KTD2). Worker-side substrate for the M3
|
|
58
|
+
* taint pass: the M2 facts carry no expression structure, and the main thread
|
|
59
|
+
* cannot re-parse (the #1983 OOM shape). Spec-AGNOSTIC — records structure
|
|
60
|
+
* only, never source/sink/sanitizer-ness (matching is a main-thread concern).
|
|
61
|
+
*
|
|
62
|
+
* Integer indices: binding fields (`receiver`/`object`/`resultDefs`/arg
|
|
63
|
+
* occurrences) index {@link FunctionCfg.bindings}; site references (`parent`,
|
|
64
|
+
* via-tags) index the OWNING statement's `sites` array. JSON-plain; NO field
|
|
65
|
+
* here may be named `nodeId` (durable parsedfile-store reviver hazard — see
|
|
66
|
+
* {@link BindingEntry}).
|
|
67
|
+
*/
|
|
68
|
+
export interface SiteRecord {
|
|
69
|
+
readonly kind: 'call' | 'new' | 'member-read';
|
|
70
|
+
/**
|
|
71
|
+
* Dotted callee path for call/new sites whose callee chain is rooted at an
|
|
72
|
+
* identifier/`this`/`super` (`child_process.exec`, `req.body.toString`).
|
|
73
|
+
* Optional chaining is normalized (`a?.b()` ⇒ `a.b`); string-literal
|
|
74
|
+
* subscripts fold into the path (`cp["exec"]` ⇒ `cp.exec`). Absent when the
|
|
75
|
+
* chain is not statically resolvable (dynamic key, call-rooted chain).
|
|
76
|
+
*/
|
|
77
|
+
readonly callee?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Binding index of the callee chain's ROOT identifier when the callee is a
|
|
80
|
+
* member chain (`userInput.trim()` ⇒ `userInput`). Method calls launder
|
|
81
|
+
* taint without it (plan KTD5 receiver-position TITO). Absent for bare
|
|
82
|
+
* calls (`exec(x)`) and non-identifier roots.
|
|
83
|
+
*/
|
|
84
|
+
readonly receiver?: number;
|
|
85
|
+
/**
|
|
86
|
+
* Per-argument-position occurrence entries (trailing empty positions are
|
|
87
|
+
* trimmed; absent when no argument carries a binding occurrence). For
|
|
88
|
+
* `template: true` sites every substitution occurrence aggregates at
|
|
89
|
+
* position 0 (tagged templates have no positional argument list).
|
|
90
|
+
*/
|
|
91
|
+
readonly args?: ReadonlyArray<readonly SiteArgOccurrence[]>;
|
|
92
|
+
/**
|
|
93
|
+
* Bindings defined by a declarator/assignment whose ENTIRE value (after
|
|
94
|
+
* unwrapping parens/`await`/`as`/`!`) is this call — `const b = escape(t)`
|
|
95
|
+
* ⇒ `[b]`. Per-declarator: `const a = t, b = escape(t)` attaches `[b]`
|
|
96
|
+
* only. Kill placement (KTD4b) keys on this: a sanitizer kills exactly the
|
|
97
|
+
* defs that receive its result directly.
|
|
98
|
+
*/
|
|
99
|
+
readonly resultDefs?: readonly number[];
|
|
100
|
+
/**
|
|
101
|
+
* `[siteIdx, argIdx]` of the innermost enclosing call/new site argument
|
|
102
|
+
* position this site occurs in (`exec(escape(x))` ⇒ escape's parent is
|
|
103
|
+
* `[execSiteIdx, 0]`). Absent for top-level sites.
|
|
104
|
+
*/
|
|
105
|
+
readonly parent?: readonly [number, number];
|
|
106
|
+
/**
|
|
107
|
+
* Index of the FIRST spread argument (`exec(...args)` ⇒ 0). Presence means
|
|
108
|
+
* position matching must degrade soundly (any sink position ≥ this index —
|
|
109
|
+
* plan KTD2/U2). A number (not boolean) because the matcher needs the index.
|
|
110
|
+
*/
|
|
111
|
+
readonly spread?: number;
|
|
112
|
+
/** Tagged-template call (`sql\`…${id}\``) — argument positions are not positional. */
|
|
113
|
+
readonly template?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* String-literal first argument when the callee is bare `require` —
|
|
116
|
+
* CommonJS aliases resolve like ESM imports on the main thread (KTD7).
|
|
117
|
+
*/
|
|
118
|
+
readonly requireArg?: string;
|
|
119
|
+
/** Member read: binding index of the object root (`req.body` ⇒ `req`). */
|
|
120
|
+
readonly object?: number;
|
|
121
|
+
/**
|
|
122
|
+
* Member read: property name (`req.body` ⇒ `'body'`; `req["body"]`
|
|
123
|
+
* included; dynamic `req[key]` is never recorded — documented KTD10 FN).
|
|
124
|
+
*/
|
|
125
|
+
readonly property?: string;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Def/use facts for one harvested statement (or construct header), in
|
|
129
|
+
* execution order within its block (#2082 M2 U1). `defs`/`uses` are indices
|
|
130
|
+
* into {@link FunctionCfg.bindings}. A compound assignment / update expression
|
|
131
|
+
* lists its binding in BOTH. Self-describing — `line` is carried here, never
|
|
132
|
+
* inferred from the block's text fragments (facts-only records exist, e.g.
|
|
133
|
+
* params on ENTRY and catch params).
|
|
134
|
+
*
|
|
135
|
+
* `mayDefs` (tri-review P1): defs harvested inside CONDITIONALLY-EVALUATED
|
|
136
|
+
* subexpressions — short-circuit right operands (`a && (x = v)`,
|
|
137
|
+
* `c ?? (c = load())`), ternary arms, logical-assignment operators, and
|
|
138
|
+
* switch case-test expressions. The solver treats them as GEN WITHOUT KILL:
|
|
139
|
+
* treating them as must-defs would falsely kill the prior def on the
|
|
140
|
+
* not-taken path (a taint false negative on core JS idioms). Optional —
|
|
141
|
+
* absent means none.
|
|
142
|
+
*
|
|
143
|
+
* `sites` (#2083 M3 U1): call/member-read structure for the taint pass —
|
|
144
|
+
* see {@link SiteRecord}. Optional and omit-when-empty; absent on pre-M3
|
|
145
|
+
* channels and on statements with no calls or member reads. Sites inside
|
|
146
|
+
* nested functions are NOT recorded (consistent with def/use invisibility —
|
|
147
|
+
* the enclosing `arr.forEach(...)` call IS, with receiver `arr`).
|
|
148
|
+
*/
|
|
149
|
+
export interface StatementFacts {
|
|
150
|
+
readonly line: number;
|
|
151
|
+
readonly defs: readonly number[];
|
|
152
|
+
readonly uses: readonly number[];
|
|
153
|
+
readonly mayDefs?: readonly number[];
|
|
154
|
+
readonly sites?: readonly SiteRecord[];
|
|
155
|
+
}
|
|
156
|
+
/** A basic block: a maximal straight-line run of statements between leaders. */
|
|
157
|
+
export interface BasicBlockData {
|
|
158
|
+
/** Block index within its function. The synthetic ENTRY is always 0. */
|
|
159
|
+
readonly index: number;
|
|
160
|
+
readonly startLine: number;
|
|
161
|
+
readonly endLine: number;
|
|
162
|
+
/** Source snippet for the block (empty for synthetic ENTRY/EXIT). */
|
|
163
|
+
readonly text: string;
|
|
164
|
+
readonly kind: 'entry' | 'exit' | 'normal';
|
|
165
|
+
/**
|
|
166
|
+
* Per-statement def/use facts in execution order (#2082 M2 U1). Present only
|
|
167
|
+
* when the producing visitor harvests (TS/JS under `--pdg`); absent on
|
|
168
|
+
* hand-built or pre-M2 CFGs — the reaching-defs solver reports `no-facts`.
|
|
169
|
+
*/
|
|
170
|
+
readonly statements?: readonly StatementFacts[];
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Why one block flows to another — drives the `reason` on the emitted CFG edge.
|
|
174
|
+
*
|
|
175
|
+
* Kind invariant (M2): a bare jump kind (`return`/`break`/`continue`) means the
|
|
176
|
+
* SOURCE block's terminator is that jump statement. A `finally-*` kind marks a
|
|
177
|
+
* COMPLETION edge out of a `finally` body's exit — the leg that resumes a jump
|
|
178
|
+
* which was re-routed through the finally (issue #2082 U2). Reusing the bare
|
|
179
|
+
* kinds on completion edges would silently break consumers that infer the
|
|
180
|
+
* source block's terminator from the kind, and a single generic kind would lose
|
|
181
|
+
* WHICH jump each completion edge completes when a shared finally has several
|
|
182
|
+
* pending targets.
|
|
183
|
+
*/
|
|
184
|
+
export type CfgEdgeKind = 'seq' | 'cond-true' | 'cond-false' | 'loop-back' | 'break' | 'continue' | 'return' | 'throw' | 'switch-case' | 'fallthrough' | 'finally-return' | 'finally-break' | 'finally-continue';
|
|
185
|
+
export interface CfgEdgeData {
|
|
186
|
+
readonly from: number;
|
|
187
|
+
readonly to: number;
|
|
188
|
+
readonly kind: CfgEdgeKind;
|
|
189
|
+
}
|
|
190
|
+
/** One function's control-flow graph. `cfgSideChannel` is `readonly FunctionCfg[]`. */
|
|
191
|
+
export interface FunctionCfg {
|
|
192
|
+
readonly filePath: string;
|
|
193
|
+
/** Source span of the owning function — anchors the BasicBlock node ids. */
|
|
194
|
+
readonly functionStartLine: number;
|
|
195
|
+
readonly functionEndLine: number;
|
|
196
|
+
/**
|
|
197
|
+
* Start COLUMN of the owning function. Combined with `functionStartLine` it
|
|
198
|
+
* disambiguates the BasicBlock node ids when two functions share a start line
|
|
199
|
+
* — e.g. `{ a: () => x(), b: () => y() }`, where both arrows begin on the same
|
|
200
|
+
* line and each restarts its block indices at 0. Without the column the ids
|
|
201
|
+
* collide and the graph's first-writer-wins `addNode` silently drops the
|
|
202
|
+
* second function's blocks and cross-wires its edges.
|
|
203
|
+
*/
|
|
204
|
+
readonly functionStartColumn: number;
|
|
205
|
+
readonly entryIndex: number;
|
|
206
|
+
readonly exitIndex: number;
|
|
207
|
+
readonly blocks: readonly BasicBlockData[];
|
|
208
|
+
readonly edges: readonly CfgEdgeData[];
|
|
209
|
+
/**
|
|
210
|
+
* The function's binding table (#2082 M2 U1) — referenced by index from
|
|
211
|
+
* {@link BasicBlockData.statements}. Present iff statement facts are.
|
|
212
|
+
*/
|
|
213
|
+
readonly bindings?: readonly BindingEntry[];
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Per-language CFG strategy. Invoked **in the parse worker** for each function
|
|
217
|
+
* node. `TNode` is the language's AST node type (tree-sitter `SyntaxNode` for
|
|
218
|
+
* TS/JS) — kept generic so this module stays AST-library-agnostic. Returns
|
|
219
|
+
* `undefined` when the node is not a CFG-bearing function (the caller skips it).
|
|
220
|
+
*/
|
|
221
|
+
export interface CfgVisitor<TNode = unknown> {
|
|
222
|
+
buildFunctionCfg(fnNode: TNode, filePath: string): FunctionCfg | undefined;
|
|
223
|
+
/**
|
|
224
|
+
* Whether `node` is a CFG-bearing function this visitor handles. Lets the
|
|
225
|
+
* worker enumerate functions (and apply the per-function line budget) by a
|
|
226
|
+
* cheap node-type test, instead of attempting to build a CFG for every AST
|
|
227
|
+
* node. `buildFunctionCfg` still re-checks, so this is purely an optimization
|
|
228
|
+
* + the seam the line-budget hooks into.
|
|
229
|
+
*/
|
|
230
|
+
isFunction(node: TNode): boolean;
|
|
231
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CFG data model — plain, JSON-serializable types (issue #2081, M1).
|
|
3
|
+
*
|
|
4
|
+
* These cross the worker→main boundary and the disk-backed/durable ParsedFile
|
|
5
|
+
* store, so they must contain NO tree-sitter AST references, class instances,
|
|
6
|
+
* or anything that does not survive `JSON.stringify` → `JSON.parse`. Block and
|
|
7
|
+
* edge endpoints are referenced by integer index within a function's CFG.
|
|
8
|
+
*
|
|
9
|
+
* The per-language `CfgVisitor` (built in the parse worker, where the AST
|
|
10
|
+
* lives — see the M1 plan KTD1/KTD7) produces a `FunctionCfg` per function; the
|
|
11
|
+
* array of them is what rides on `ParsedFile.cfgSideChannel`.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|