gitnexus 1.6.8-rc.12 → 1.6.8-rc.14
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/dist/_shared/scope-resolution/parsed-file.d.ts +21 -0
- package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
- package/dist/cli/analyze-config.js +1 -0
- package/dist/cli/analyze.d.ts +6 -0
- package/dist/cli/analyze.js +2 -0
- package/dist/cli/index.js +2 -0
- package/dist/core/ingestion/cfg/cfg-builder.d.ts +51 -0
- package/dist/core/ingestion/cfg/cfg-builder.js +100 -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 +27 -0
- package/dist/core/ingestion/cfg/control-flow-context.js +49 -0
- package/dist/core/ingestion/cfg/emit.d.ts +64 -0
- package/dist/core/ingestion/cfg/emit.js +95 -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 +66 -0
- package/dist/core/ingestion/cfg/types.js +13 -0
- package/dist/core/ingestion/cfg/visitors/typescript.d.ts +49 -0
- package/dist/core/ingestion/cfg/visitors/typescript.js +492 -0
- package/dist/core/ingestion/language-provider.d.ts +11 -0
- package/dist/core/ingestion/languages/typescript.js +5 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +18 -1
- package/dist/core/ingestion/pipeline.d.ts +23 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +3 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +10 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +62 -0
- package/dist/core/ingestion/workers/parse-worker.js +40 -1
- package/dist/core/ingestion/workers/worker-pool.d.ts +9 -0
- package/dist/core/ingestion/workers/worker-pool.js +5 -2
- package/dist/core/run-analyze.d.ts +32 -0
- package/dist/core/run-analyze.js +75 -1
- 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.d.ts +17 -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/parse-cache.d.ts +22 -1
- package/dist/storage/parse-cache.js +20 -10
- package/dist/storage/repo-manager.d.ts +31 -7
- package/package.json +3 -1
- package/web/assets/{agent-CKPMqImC.js → agent-B0-_Ihgi.js} +1 -1
- package/web/assets/{architectureDiagram-UL44E2DR-D1EXI0zA.js → architectureDiagram-UL44E2DR-PdDPNXTO.js} +1 -1
- package/web/assets/{chunk-LCXTWHL2-Dfmux4m1.js → chunk-LCXTWHL2-u4-0IIru.js} +1 -1
- package/web/assets/{chunk-RG4AUYOV-CAkzcoRj.js → chunk-RG4AUYOV--R3bV0KH.js} +1 -1
- package/web/assets/{classDiagram-KGZ6W3CR-Bv93af_b.js → classDiagram-KGZ6W3CR-CLDcI2Tc.js} +1 -1
- package/web/assets/{classDiagram-v2-72OJOZXJ-CLfEqHUa.js → classDiagram-v2-72OJOZXJ-dwefssU_.js} +1 -1
- package/web/assets/{diagram-3NCE3AQN-BqAtKUpW.js → diagram-3NCE3AQN-DMYhnaiQ.js} +1 -1
- package/web/assets/{diagram-GF46GFSD-CaBG6n6o.js → diagram-GF46GFSD-CgWD3Go0.js} +1 -1
- package/web/assets/{diagram-QXG6HAR7-CZ-O3rcV.js → diagram-QXG6HAR7-Bwu9Wi_f.js} +1 -1
- package/web/assets/{diagram-WEQXMOUZ-P4lSL4GH.js → diagram-WEQXMOUZ-CaxeST-3.js} +1 -1
- package/web/assets/{erDiagram-L5TCEMPS-QN2eEP1e.js → erDiagram-L5TCEMPS-WlblWt40.js} +1 -1
- package/web/assets/{flowDiagram-H6V6AXG4-CXbXImlN.js → flowDiagram-H6V6AXG4-CRrYkD90.js} +1 -1
- package/web/assets/index-COMMmbxW.css +2 -0
- package/web/assets/index-D_6rUFGW.js +626 -0
- package/web/assets/{infoDiagram-3YFTVSEB-DmLICZx1.js → infoDiagram-3YFTVSEB-pvT6ArHz.js} +1 -1
- package/web/assets/{ishikawaDiagram-BNXS4ZKH-6LKOvBfp.js → ishikawaDiagram-BNXS4ZKH-D_0fBKRi.js} +1 -1
- package/web/assets/{kanban-definition-75IXJCU3-DgDi9oJT.js → kanban-definition-75IXJCU3-DGABptDC.js} +1 -1
- package/web/assets/{mindmap-definition-2TDM6QVE-CwR5sBB-.js → mindmap-definition-2TDM6QVE-r-bFdhPw.js} +1 -1
- package/web/assets/{pieDiagram-CU6KROY3-By8g6f6B.js → pieDiagram-CU6KROY3-CS3o1Ug4.js} +1 -1
- package/web/assets/{requirementDiagram-JXO7QTGE-7oDcJ1_J.js → requirementDiagram-JXO7QTGE-BMYGXwjH.js} +1 -1
- package/web/assets/{sequenceDiagram-VS2MUI6T-FySeKCUy.js → sequenceDiagram-VS2MUI6T-BJnTiLwO.js} +1 -1
- package/web/assets/{stateDiagram-7D4R322I-CB2nABwH.js → stateDiagram-7D4R322I-G35bS8CQ.js} +1 -1
- package/web/assets/{stateDiagram-v2-36443NZ5-COBGd2RL.js → stateDiagram-v2-36443NZ5-BUaZpLs6.js} +1 -1
- package/web/assets/{timeline-definition-O6YCAMPW-Ds2CnVZK.js → timeline-definition-O6YCAMPW-B9o1j771.js} +1 -1
- package/web/assets/{vennDiagram-MWXL3ELB-DUIEwXWp.js → vennDiagram-MWXL3ELB-Vz5Sin3H.js} +1 -1
- package/web/assets/{wardleyDiagram-CUQ6CDDI-DEiFPQih.js → wardleyDiagram-CUQ6CDDI-D42Nu4nI.js} +1 -1
- package/web/assets/{xychartDiagram-N2JHSOCM-BUbayhST.js → xychartDiagram-N2JHSOCM-CSImZ1y9.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/index-BKWA-m7o.css +0 -2
- package/web/assets/index-CG6q8eTs.js +0 -626
|
@@ -96,5 +96,26 @@ export interface ParsedFile {
|
|
|
96
96
|
* side effects — the contract default) leave this undefined.
|
|
97
97
|
*/
|
|
98
98
|
readonly captureSideChannel?: unknown;
|
|
99
|
+
/**
|
|
100
|
+
* Per-function control-flow graphs for this file (#2081 M1, PDG/taint
|
|
101
|
+
* substrate). A DISTINCT field from {@link captureSideChannel} — different
|
|
102
|
+
* producer, consumer, and lifecycle: the worker builds it from the
|
|
103
|
+
* tree-sitter AST via `LanguageProvider.cfgVisitor` (only on a `--pdg` run),
|
|
104
|
+
* and scope-resolution emits BasicBlock nodes + CFG edges from it while the
|
|
105
|
+
* disk-backed ParsedFile store is still live (it is NOT a capture-time
|
|
106
|
+
* marker the resolver restores into module maps). Kept separate so a future
|
|
107
|
+
* change to either channel's shape invalidates independently.
|
|
108
|
+
*
|
|
109
|
+
* Shared / ingestion code treats this as opaque (`unknown`) per AGENTS.md.
|
|
110
|
+
* Concretely it is a `readonly FunctionCfg[]` (see
|
|
111
|
+
* `core/ingestion/cfg/types.ts`) — plain JSON-serializable data (no AST
|
|
112
|
+
* refs, no class instances) so it round-trips through the parse cache and
|
|
113
|
+
* the `parsedfile-store` (whose interning reviver keys on `nodeId`, which
|
|
114
|
+
* these blocks/edges deliberately lack).
|
|
115
|
+
*
|
|
116
|
+
* Optional: `undefined` on non-`--pdg` runs and for languages with no
|
|
117
|
+
* `cfgVisitor` — the default for every run today.
|
|
118
|
+
*/
|
|
119
|
+
readonly cfgSideChannel?: unknown;
|
|
99
120
|
}
|
|
100
121
|
//# sourceMappingURL=parsed-file.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parsed-file.d.ts","sourceRoot":"","sources":["../../src/scope-resolution/parsed-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oDAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,SAAS,YAAY,EAAE,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAChD,QAAQ,CAAC,cAAc,EAAE,SAAS,aAAa,EAAE,CAAC;IAClD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"parsed-file.d.ts","sourceRoot":"","sources":["../../src/scope-resolution/parsed-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oDAAoD;IACpD,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,SAAS,YAAY,EAAE,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAChD,QAAQ,CAAC,cAAc,EAAE,SAAS,aAAa,EAAE,CAAC;IAClD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAEtC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,OAAO,CAAC;CACnC"}
|
|
@@ -63,6 +63,7 @@ const KEY_SPECS = {
|
|
|
63
63
|
skipContextFiles: { target: 'skipAgentsMd', kind: 'boolean' },
|
|
64
64
|
skipAiContext: { target: 'skipAgentsMd', kind: 'boolean' },
|
|
65
65
|
skipSkills: { target: 'skipSkills', kind: 'boolean' },
|
|
66
|
+
pdg: { target: 'pdg', kind: 'boolean' },
|
|
66
67
|
indexOnly: { target: 'indexOnly', kind: 'boolean' },
|
|
67
68
|
stats: { target: 'stats', kind: 'boolean' },
|
|
68
69
|
noStats: { target: 'stats', kind: 'boolean-negate' },
|
package/dist/cli/analyze.d.ts
CHANGED
|
@@ -37,6 +37,12 @@ export interface AnalyzeOptions {
|
|
|
37
37
|
verbose?: boolean;
|
|
38
38
|
/** Skip AGENTS.md and CLAUDE.md gitnexus block updates. */
|
|
39
39
|
skipAgentsMd?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Build the control-flow-graph / PDG substrate (#2081 M1). Opt-in; off by
|
|
42
|
+
* default. Threaded to both the worker (CFG build) and scope-resolution
|
|
43
|
+
* (BasicBlock/CFG emit).
|
|
44
|
+
*/
|
|
45
|
+
pdg?: boolean;
|
|
40
46
|
/**
|
|
41
47
|
* Stats inclusion in AGENTS.md and CLAUDE.md.
|
|
42
48
|
*
|
package/dist/cli/analyze.js
CHANGED
|
@@ -841,6 +841,8 @@ const analyzeCommandImpl = async (inputPath, cliOptions) => {
|
|
|
841
841
|
skipGit: options.skipGit,
|
|
842
842
|
skipAgentsMd,
|
|
843
843
|
skipSkills,
|
|
844
|
+
// CFG/PDG substrate opt-in (#2081 M1) — threaded to both sinks downstream.
|
|
845
|
+
pdg: options.pdg === true,
|
|
844
846
|
// Resolved default branch (CLI > .gitnexusrc > auto-detect > "main")
|
|
845
847
|
// threaded into the generated regression-compare example (#243).
|
|
846
848
|
defaultBranch: resolvedDefaultBranch,
|
package/dist/cli/index.js
CHANGED
|
@@ -32,6 +32,8 @@ program
|
|
|
32
32
|
.option('--skills', 'Generate repo-specific skill files from detected communities ' +
|
|
33
33
|
'(no-op when --index-only is also set).')
|
|
34
34
|
.option('--skip-agents-md', 'Skip updating the gitnexus section in AGENTS.md and CLAUDE.md')
|
|
35
|
+
.option('--pdg', 'Build the control-flow-graph / PDG substrate (BasicBlock nodes + CFG edges) ' +
|
|
36
|
+
'for supported languages. Opt-in; off by default. (#2081 M1)')
|
|
35
37
|
.option('--default-branch <branch>', 'Default branch used in the generated regression-compare example (base_ref). ' +
|
|
36
38
|
'Falls back to .gitnexusrc, then auto-detected origin/HEAD, then "main".')
|
|
37
39
|
.option('--branch <name>', 'Index the working tree under a specific branch slot (multi-branch indexing). ' +
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CfgBuilder (issue #2081, M1) — the language-agnostic accumulator.
|
|
3
|
+
*
|
|
4
|
+
* A per-language `CfgVisitor` drives this: it creates blocks as it walks
|
|
5
|
+
* statements, wires edges (including back-edges and break/continue/return/throw
|
|
6
|
+
* targets resolved via {@link ControlFlowContext}), and calls {@link finish} to
|
|
7
|
+
* produce the serializable {@link FunctionCfg}. The builder owns the synthetic
|
|
8
|
+
* ENTRY (index 0) and EXIT blocks and de-duplicates identical edges so repeated
|
|
9
|
+
* `connect` calls (common when wiring a set of dangling exits) stay idempotent.
|
|
10
|
+
*
|
|
11
|
+
* It has no knowledge of any AST — it is exercised directly in unit tests with
|
|
12
|
+
* hand-built block sequences, which is how the classic CFG hazards are pinned
|
|
13
|
+
* before the tree-sitter visitor (U2) drives it.
|
|
14
|
+
*/
|
|
15
|
+
import type { BasicBlockData, CfgEdgeKind, FunctionCfg } from './types.js';
|
|
16
|
+
export declare class CfgBuilder {
|
|
17
|
+
private readonly filePath;
|
|
18
|
+
private readonly functionStartLine;
|
|
19
|
+
private readonly functionEndLine;
|
|
20
|
+
/** Start column of the owning function — disambiguates same-line functions
|
|
21
|
+
* in the BasicBlock ids (see {@link FunctionCfg.functionStartColumn}).
|
|
22
|
+
* Defaults to 0 for hand-built test CFGs that don't model columns. */
|
|
23
|
+
private readonly functionStartColumn;
|
|
24
|
+
private readonly blocks;
|
|
25
|
+
private readonly edges;
|
|
26
|
+
private readonly edgeKeys;
|
|
27
|
+
readonly entryIndex: number;
|
|
28
|
+
readonly exitIndex: number;
|
|
29
|
+
constructor(filePath: string, functionStartLine: number, functionEndLine: number,
|
|
30
|
+
/** Start column of the owning function — disambiguates same-line functions
|
|
31
|
+
* in the BasicBlock ids (see {@link FunctionCfg.functionStartColumn}).
|
|
32
|
+
* Defaults to 0 for hand-built test CFGs that don't model columns. */
|
|
33
|
+
functionStartColumn?: number);
|
|
34
|
+
/** Create a block and return its index. */
|
|
35
|
+
newBlock(startLine: number, endLine: number, text: string, kind?: BasicBlockData['kind']): number;
|
|
36
|
+
/** Add a single edge (idempotent on from+to+kind). */
|
|
37
|
+
edge(from: number, to: number, kind: CfgEdgeKind): void;
|
|
38
|
+
/** Wire a set of dangling exits to a single target block with one kind. */
|
|
39
|
+
connect(exits: readonly number[], to: number, kind?: CfgEdgeKind): void;
|
|
40
|
+
/** Extend a block's end line as more statements accrue to it. */
|
|
41
|
+
extendBlock(index: number, endLine: number, appendText?: string): void;
|
|
42
|
+
get blockCount(): number;
|
|
43
|
+
/** Produce the serializable CFG. Caller is responsible for having wired the
|
|
44
|
+
* function's dangling exits to {@link exitIndex} before calling. */
|
|
45
|
+
finish(): FunctionCfg;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Block indices reachable from `entryIndex` by following edges. Backs the
|
|
49
|
+
* reachability property tests (R9) over hand-built and visitor-produced CFGs.
|
|
50
|
+
*/
|
|
51
|
+
export declare const reachableBlocks: (cfg: FunctionCfg) => Set<number>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export class CfgBuilder {
|
|
2
|
+
filePath;
|
|
3
|
+
functionStartLine;
|
|
4
|
+
functionEndLine;
|
|
5
|
+
functionStartColumn;
|
|
6
|
+
blocks = [];
|
|
7
|
+
edges = [];
|
|
8
|
+
edgeKeys = new Set();
|
|
9
|
+
entryIndex;
|
|
10
|
+
exitIndex;
|
|
11
|
+
constructor(filePath, functionStartLine, functionEndLine,
|
|
12
|
+
/** Start column of the owning function — disambiguates same-line functions
|
|
13
|
+
* in the BasicBlock ids (see {@link FunctionCfg.functionStartColumn}).
|
|
14
|
+
* Defaults to 0 for hand-built test CFGs that don't model columns. */
|
|
15
|
+
functionStartColumn = 0) {
|
|
16
|
+
this.filePath = filePath;
|
|
17
|
+
this.functionStartLine = functionStartLine;
|
|
18
|
+
this.functionEndLine = functionEndLine;
|
|
19
|
+
this.functionStartColumn = functionStartColumn;
|
|
20
|
+
this.entryIndex = this.newBlock(functionStartLine, functionStartLine, '', 'entry');
|
|
21
|
+
this.exitIndex = this.newBlock(functionEndLine, functionEndLine, '', 'exit');
|
|
22
|
+
}
|
|
23
|
+
/** Create a block and return its index. */
|
|
24
|
+
newBlock(startLine, endLine, text, kind = 'normal') {
|
|
25
|
+
this.blocks.push({ startLine, endLine, textParts: text ? [text] : [], kind });
|
|
26
|
+
return this.blocks.length - 1;
|
|
27
|
+
}
|
|
28
|
+
/** Add a single edge (idempotent on from+to+kind). */
|
|
29
|
+
edge(from, to, kind) {
|
|
30
|
+
const key = `${from}->${to}:${kind}`;
|
|
31
|
+
if (this.edgeKeys.has(key))
|
|
32
|
+
return;
|
|
33
|
+
this.edgeKeys.add(key);
|
|
34
|
+
this.edges.push({ from, to, kind });
|
|
35
|
+
}
|
|
36
|
+
/** Wire a set of dangling exits to a single target block with one kind. */
|
|
37
|
+
connect(exits, to, kind = 'seq') {
|
|
38
|
+
for (const from of exits)
|
|
39
|
+
this.edge(from, to, kind);
|
|
40
|
+
}
|
|
41
|
+
/** Extend a block's end line as more statements accrue to it. */
|
|
42
|
+
extendBlock(index, endLine, appendText) {
|
|
43
|
+
const b = this.blocks[index];
|
|
44
|
+
if (!b)
|
|
45
|
+
return;
|
|
46
|
+
if (endLine > b.endLine)
|
|
47
|
+
b.endLine = endLine;
|
|
48
|
+
if (appendText)
|
|
49
|
+
b.textParts.push(appendText);
|
|
50
|
+
}
|
|
51
|
+
get blockCount() {
|
|
52
|
+
return this.blocks.length;
|
|
53
|
+
}
|
|
54
|
+
/** Produce the serializable CFG. Caller is responsible for having wired the
|
|
55
|
+
* function's dangling exits to {@link exitIndex} before calling. */
|
|
56
|
+
finish() {
|
|
57
|
+
return {
|
|
58
|
+
filePath: this.filePath,
|
|
59
|
+
functionStartLine: this.functionStartLine,
|
|
60
|
+
functionEndLine: this.functionEndLine,
|
|
61
|
+
functionStartColumn: this.functionStartColumn,
|
|
62
|
+
entryIndex: this.entryIndex,
|
|
63
|
+
exitIndex: this.exitIndex,
|
|
64
|
+
blocks: this.blocks.map((b, index) => ({
|
|
65
|
+
index,
|
|
66
|
+
startLine: b.startLine,
|
|
67
|
+
endLine: b.endLine,
|
|
68
|
+
text: b.textParts.join('\n'),
|
|
69
|
+
kind: b.kind,
|
|
70
|
+
})),
|
|
71
|
+
edges: [...this.edges],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Block indices reachable from `entryIndex` by following edges. Backs the
|
|
77
|
+
* reachability property tests (R9) over hand-built and visitor-produced CFGs.
|
|
78
|
+
*/
|
|
79
|
+
export const reachableBlocks = (cfg) => {
|
|
80
|
+
const adj = new Map();
|
|
81
|
+
for (const e of cfg.edges) {
|
|
82
|
+
const list = adj.get(e.from);
|
|
83
|
+
if (list)
|
|
84
|
+
list.push(e.to);
|
|
85
|
+
else
|
|
86
|
+
adj.set(e.from, [e.to]);
|
|
87
|
+
}
|
|
88
|
+
const seen = new Set([cfg.entryIndex]);
|
|
89
|
+
const stack = [cfg.entryIndex];
|
|
90
|
+
while (stack.length) {
|
|
91
|
+
const n = stack.pop();
|
|
92
|
+
for (const next of adj.get(n) ?? []) {
|
|
93
|
+
if (!seen.has(next)) {
|
|
94
|
+
seen.add(next);
|
|
95
|
+
stack.push(next);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return seen;
|
|
100
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* collectFunctionCfgs (issue #2081, M1).
|
|
3
|
+
*
|
|
4
|
+
* Walks a parsed file's tree-sitter tree and builds one {@link FunctionCfg} per
|
|
5
|
+
* CFG-bearing function via the language's {@link CfgVisitor}. Runs IN THE PARSE
|
|
6
|
+
* WORKER (where the AST lives — KTD1/KTD7); the result rides on
|
|
7
|
+
* `ParsedFile.cfgSideChannel` across the worker→main boundary.
|
|
8
|
+
*
|
|
9
|
+
* Nested functions are enumerated independently — each gets its own CFG, and
|
|
10
|
+
* appears as an opaque straight-line block in its enclosing function's CFG (the
|
|
11
|
+
* visitor does not descend into nested function bodies). `maxFunctionLines`
|
|
12
|
+
* bounds per-function cost: a function whose source span exceeds the cap is
|
|
13
|
+
* skipped (and counted) rather than walked, so a pathological mega-function
|
|
14
|
+
* cannot blow up worker time/memory. A cap of `0` means no limit.
|
|
15
|
+
*/
|
|
16
|
+
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
|
17
|
+
import type { CfgVisitor, FunctionCfg } from './types.js';
|
|
18
|
+
/**
|
|
19
|
+
* Default per-function source-line cap used by the worker when the `--pdg` run
|
|
20
|
+
* does not specify `pdgMaxFunctionLines`. A function longer than this (almost
|
|
21
|
+
* always minified/generated code) is skipped rather than walked — its CFG is
|
|
22
|
+
* both expensive and low-value. Overridable via `PipelineOptions.pdgMaxFunctionLines`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const DEFAULT_PDG_MAX_FUNCTION_LINES = 2000;
|
|
25
|
+
export interface CollectedCfgs {
|
|
26
|
+
readonly cfgs: readonly FunctionCfg[];
|
|
27
|
+
/** Functions skipped for exceeding `maxFunctionLines` (0 ⇒ none skipped). */
|
|
28
|
+
readonly skipped: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function collectFunctionCfgs(root: SyntaxNode, visitor: CfgVisitor<SyntaxNode>, filePath: string, maxFunctionLines?: number): CollectedCfgs;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default per-function source-line cap used by the worker when the `--pdg` run
|
|
3
|
+
* does not specify `pdgMaxFunctionLines`. A function longer than this (almost
|
|
4
|
+
* always minified/generated code) is skipped rather than walked — its CFG is
|
|
5
|
+
* both expensive and low-value. Overridable via `PipelineOptions.pdgMaxFunctionLines`.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_PDG_MAX_FUNCTION_LINES = 2000;
|
|
8
|
+
export function collectFunctionCfgs(root, visitor, filePath, maxFunctionLines = 0) {
|
|
9
|
+
const cfgs = [];
|
|
10
|
+
let skipped = 0;
|
|
11
|
+
const stack = [root];
|
|
12
|
+
while (stack.length) {
|
|
13
|
+
const node = stack.pop();
|
|
14
|
+
if (visitor.isFunction(node)) {
|
|
15
|
+
const lines = node.endPosition.row - node.startPosition.row + 1;
|
|
16
|
+
if (maxFunctionLines > 0 && lines > maxFunctionLines) {
|
|
17
|
+
skipped++;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
const cfg = visitor.buildFunctionCfg(node, filePath);
|
|
21
|
+
if (cfg)
|
|
22
|
+
cfgs.push(cfg);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Descend regardless (a skipped mega-function may still contain small
|
|
26
|
+
// nested functions that are worth a CFG of their own).
|
|
27
|
+
for (let i = node.namedChildCount - 1; i >= 0; i--) {
|
|
28
|
+
const child = node.namedChild(i);
|
|
29
|
+
if (child)
|
|
30
|
+
stack.push(child);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { cfgs, skipped };
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ControlFlowContext (issue #2081, M1).
|
|
3
|
+
*
|
|
4
|
+
* Resolves the targets of `break`/`continue` (plain and labeled) as the visitor
|
|
5
|
+
* descends through loops and switches. Loops and switches push a target frame
|
|
6
|
+
* on entry and pop it on exit; a labeled statement attaches its label to the
|
|
7
|
+
* frame of the construct it labels, so `break outer` / `continue outer` resolve
|
|
8
|
+
* against the right enclosing loop/switch rather than the nearest one.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ControlFlowContext {
|
|
11
|
+
private readonly stack;
|
|
12
|
+
pushLoop(continueTo: number, breakTo: number, label?: string): void;
|
|
13
|
+
pushSwitch(breakTo: number, label?: string): void;
|
|
14
|
+
pop(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Target block for a `break`. With a label, the nearest enclosing frame
|
|
17
|
+
* carrying that label (loop or switch); without, the nearest frame of any
|
|
18
|
+
* kind. Returns `undefined` if there is no valid target (malformed input).
|
|
19
|
+
*/
|
|
20
|
+
breakTarget(label?: string): number | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Target block for a `continue`. With a label, the nearest enclosing **loop**
|
|
23
|
+
* carrying that label; without, the nearest loop (switches are skipped — you
|
|
24
|
+
* cannot `continue` a switch). Returns `undefined` if there is no valid loop.
|
|
25
|
+
*/
|
|
26
|
+
continueTarget(label?: string): number | undefined;
|
|
27
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ControlFlowContext (issue #2081, M1).
|
|
3
|
+
*
|
|
4
|
+
* Resolves the targets of `break`/`continue` (plain and labeled) as the visitor
|
|
5
|
+
* descends through loops and switches. Loops and switches push a target frame
|
|
6
|
+
* on entry and pop it on exit; a labeled statement attaches its label to the
|
|
7
|
+
* frame of the construct it labels, so `break outer` / `continue outer` resolve
|
|
8
|
+
* against the right enclosing loop/switch rather than the nearest one.
|
|
9
|
+
*/
|
|
10
|
+
export class ControlFlowContext {
|
|
11
|
+
stack = [];
|
|
12
|
+
pushLoop(continueTo, breakTo, label) {
|
|
13
|
+
this.stack.push({ kind: 'loop', continueTo, breakTo, label });
|
|
14
|
+
}
|
|
15
|
+
pushSwitch(breakTo, label) {
|
|
16
|
+
this.stack.push({ kind: 'switch', breakTo, label });
|
|
17
|
+
}
|
|
18
|
+
pop() {
|
|
19
|
+
this.stack.pop();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Target block for a `break`. With a label, the nearest enclosing frame
|
|
23
|
+
* carrying that label (loop or switch); without, the nearest frame of any
|
|
24
|
+
* kind. Returns `undefined` if there is no valid target (malformed input).
|
|
25
|
+
*/
|
|
26
|
+
breakTarget(label) {
|
|
27
|
+
for (let i = this.stack.length - 1; i >= 0; i--) {
|
|
28
|
+
const f = this.stack[i];
|
|
29
|
+
if (label === undefined || f.label === label)
|
|
30
|
+
return f.breakTo;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Target block for a `continue`. With a label, the nearest enclosing **loop**
|
|
36
|
+
* carrying that label; without, the nearest loop (switches are skipped — you
|
|
37
|
+
* cannot `continue` a switch). Returns `undefined` if there is no valid loop.
|
|
38
|
+
*/
|
|
39
|
+
continueTarget(label) {
|
|
40
|
+
for (let i = this.stack.length - 1; i >= 0; i--) {
|
|
41
|
+
const f = this.stack[i];
|
|
42
|
+
if (f.kind !== 'loop')
|
|
43
|
+
continue;
|
|
44
|
+
if (label === undefined || f.label === label)
|
|
45
|
+
return f.continueTo;
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cfg/emit.ts (issue #2081, M1) — serialized side-channel → graph.
|
|
3
|
+
*
|
|
4
|
+
* Pure helper: given a file's per-function CFGs (off `ParsedFile.cfgSideChannel`,
|
|
5
|
+
* produced by the worker in U3), emit one persisted `BasicBlock` node per block
|
|
6
|
+
* and one `CFG` edge per edge into the {@link KnowledgeGraph}. Invoked from
|
|
7
|
+
* scope-resolution (run.ts Phase 4) while the disk-backed ParsedFile store is
|
|
8
|
+
* still live — the only window where the worker-built CFGs are loaded (KTD1/
|
|
9
|
+
* KTD5). Default (`--pdg` off) runs never call this, so the emitted graph stays
|
|
10
|
+
* byte-identical to a pre-#2081 run.
|
|
11
|
+
*
|
|
12
|
+
* BasicBlock id: `BasicBlock:<filePath>:<functionStartLine>:<functionStartColumn>:<blockIndex>`
|
|
13
|
+
* (KTD3). The function start line+column segments disambiguate blocks across
|
|
14
|
+
* multiple functions in one file — including same-line functions — since each
|
|
15
|
+
* function's block indices restart at 0; blocks carry no `name` (the
|
|
16
|
+
* BasicBlock table has no such column). The edge KIND
|
|
17
|
+
* (`seq`/`cond-true`/…) rides in the relationship `reason` — CFG edges are
|
|
18
|
+
* values of the single `CodeRelation` table's `type` column (`'CFG'`), so the
|
|
19
|
+
* kind cannot be its own edge type and is queried via `reason`.
|
|
20
|
+
*/
|
|
21
|
+
import type { KnowledgeGraph } from '../../graph/types.js';
|
|
22
|
+
import type { FunctionCfg } from './types.js';
|
|
23
|
+
/**
|
|
24
|
+
* Default per-function CFG edge cap. A pathological generated function could
|
|
25
|
+
* otherwise emit an unbounded edge set; the cap bounds graph growth and is
|
|
26
|
+
* overridable via `--pdg` options. `0` (in options) means no cap (unlimited
|
|
27
|
+
* — see the `cap` mapping in {@link emitFileCfgs}); `undefined` means this
|
|
28
|
+
* default.
|
|
29
|
+
*/
|
|
30
|
+
export declare const DEFAULT_MAX_CFG_EDGES_PER_FUNCTION = 5000;
|
|
31
|
+
export interface CfgEmitResult {
|
|
32
|
+
blocks: number;
|
|
33
|
+
edges: number;
|
|
34
|
+
/** Edges dropped because a function's edge count exceeded the cap. */
|
|
35
|
+
droppedEdges: number;
|
|
36
|
+
/** Number of functions that hit the cap. */
|
|
37
|
+
cappedFunctions: number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether an untrusted `cfgSideChannel` element is safe to feed to
|
|
41
|
+
* {@link emitFileCfgs}. Deliberately NOT full FunctionCfg validation — it
|
|
42
|
+
* checks exactly the fields whose corruption is SILENT given emit's
|
|
43
|
+
* mechanics: {@link basicBlockId} string-templates every id-anchor value
|
|
44
|
+
* (filePath, function start line/column, block index, edge endpoints) and
|
|
45
|
+
* the graph's addNode/addRelationship are no-throw Map inserts. Unchecked,
|
|
46
|
+
* a missing anchor field cross-wires same-`undefined`-id blocks across
|
|
47
|
+
* functions (addNode is first-writer-wins), and an edge endpoint that
|
|
48
|
+
* matches no block index becomes a dangling `BasicBlock:…:<n>` edge that
|
|
49
|
+
* detonates much later at DB bulk-load instead of throwing here — so
|
|
50
|
+
* endpoints are checked for MEMBERSHIP in the block-index set, not just
|
|
51
|
+
* integer-ness. Lives in this module so the guard evolves with the id
|
|
52
|
+
* templating it defends (#2099 F4; M2 fields that join the id path must
|
|
53
|
+
* join this check).
|
|
54
|
+
*/
|
|
55
|
+
export declare const isEmitSafeCfg: (cfg: FunctionCfg | undefined | null) => cfg is FunctionCfg;
|
|
56
|
+
/**
|
|
57
|
+
* Emit BasicBlock nodes + CFG edges for every function CFG in `cfgs`.
|
|
58
|
+
*
|
|
59
|
+
* `maxEdgesPerFunction` caps edges per function. On overflow we stop emitting
|
|
60
|
+
* that function's remaining edges and call `onWarn` naming the dropped count —
|
|
61
|
+
* no silent truncation (KTD6/R6). Block nodes are always fully emitted (their
|
|
62
|
+
* count is bounded by the function's statement count); only edges are capped.
|
|
63
|
+
*/
|
|
64
|
+
export declare function emitFileCfgs(graph: KnowledgeGraph, cfgs: readonly FunctionCfg[], maxEdgesPerFunction?: number, onWarn?: (message: string) => void): CfgEmitResult;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { generateId } from '../../../lib/utils.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default per-function CFG edge cap. A pathological generated function could
|
|
4
|
+
* otherwise emit an unbounded edge set; the cap bounds graph growth and is
|
|
5
|
+
* overridable via `--pdg` options. `0` (in options) means no cap (unlimited
|
|
6
|
+
* — see the `cap` mapping in {@link emitFileCfgs}); `undefined` means this
|
|
7
|
+
* default.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_MAX_CFG_EDGES_PER_FUNCTION = 5000;
|
|
10
|
+
const basicBlockId = (filePath, functionStartLine, functionStartColumn, blockIndex) => `BasicBlock:${filePath}:${functionStartLine}:${functionStartColumn}:${blockIndex}`;
|
|
11
|
+
/**
|
|
12
|
+
* Whether an untrusted `cfgSideChannel` element is safe to feed to
|
|
13
|
+
* {@link emitFileCfgs}. Deliberately NOT full FunctionCfg validation — it
|
|
14
|
+
* checks exactly the fields whose corruption is SILENT given emit's
|
|
15
|
+
* mechanics: {@link basicBlockId} string-templates every id-anchor value
|
|
16
|
+
* (filePath, function start line/column, block index, edge endpoints) and
|
|
17
|
+
* the graph's addNode/addRelationship are no-throw Map inserts. Unchecked,
|
|
18
|
+
* a missing anchor field cross-wires same-`undefined`-id blocks across
|
|
19
|
+
* functions (addNode is first-writer-wins), and an edge endpoint that
|
|
20
|
+
* matches no block index becomes a dangling `BasicBlock:…:<n>` edge that
|
|
21
|
+
* detonates much later at DB bulk-load instead of throwing here — so
|
|
22
|
+
* endpoints are checked for MEMBERSHIP in the block-index set, not just
|
|
23
|
+
* integer-ness. Lives in this module so the guard evolves with the id
|
|
24
|
+
* templating it defends (#2099 F4; M2 fields that join the id path must
|
|
25
|
+
* join this check).
|
|
26
|
+
*/
|
|
27
|
+
export const isEmitSafeCfg = (cfg) => {
|
|
28
|
+
if (typeof cfg?.filePath !== 'string' ||
|
|
29
|
+
!Number.isInteger(cfg.functionStartLine) ||
|
|
30
|
+
!Number.isInteger(cfg.functionStartColumn) ||
|
|
31
|
+
!Array.isArray(cfg.blocks) ||
|
|
32
|
+
!Array.isArray(cfg.edges)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const blockIndices = new Set();
|
|
36
|
+
for (const b of cfg.blocks) {
|
|
37
|
+
if (!Number.isInteger(b?.index))
|
|
38
|
+
return false;
|
|
39
|
+
blockIndices.add(b.index);
|
|
40
|
+
}
|
|
41
|
+
return cfg.edges.every((e) => blockIndices.has(e?.from) && blockIndices.has(e?.to));
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Emit BasicBlock nodes + CFG edges for every function CFG in `cfgs`.
|
|
45
|
+
*
|
|
46
|
+
* `maxEdgesPerFunction` caps edges per function. On overflow we stop emitting
|
|
47
|
+
* that function's remaining edges and call `onWarn` naming the dropped count —
|
|
48
|
+
* no silent truncation (KTD6/R6). Block nodes are always fully emitted (their
|
|
49
|
+
* count is bounded by the function's statement count); only edges are capped.
|
|
50
|
+
*/
|
|
51
|
+
export function emitFileCfgs(graph, cfgs, maxEdgesPerFunction = DEFAULT_MAX_CFG_EDGES_PER_FUNCTION, onWarn) {
|
|
52
|
+
const result = { blocks: 0, edges: 0, droppedEdges: 0, cappedFunctions: 0 };
|
|
53
|
+
const cap = maxEdgesPerFunction > 0 ? maxEdgesPerFunction : Infinity;
|
|
54
|
+
for (const cfg of cfgs) {
|
|
55
|
+
const { filePath, functionStartLine, functionStartColumn } = cfg;
|
|
56
|
+
for (const b of cfg.blocks) {
|
|
57
|
+
graph.addNode({
|
|
58
|
+
id: basicBlockId(filePath, functionStartLine, functionStartColumn, b.index),
|
|
59
|
+
label: 'BasicBlock',
|
|
60
|
+
properties: {
|
|
61
|
+
name: '', // BasicBlock has no name column; identified by id + span
|
|
62
|
+
filePath,
|
|
63
|
+
startLine: b.startLine,
|
|
64
|
+
endLine: b.endLine,
|
|
65
|
+
text: b.text,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
result.blocks++;
|
|
69
|
+
}
|
|
70
|
+
let emittedForFn = 0;
|
|
71
|
+
for (const e of cfg.edges) {
|
|
72
|
+
if (emittedForFn >= cap) {
|
|
73
|
+
const dropped = cfg.edges.length - emittedForFn;
|
|
74
|
+
result.droppedEdges += dropped;
|
|
75
|
+
result.cappedFunctions++;
|
|
76
|
+
onWarn?.(`[cfg] ${filePath}:${functionStartLine}: per-function CFG edge cap ` +
|
|
77
|
+
`(${maxEdgesPerFunction}) reached — dropped ${dropped} of ${cfg.edges.length} edges`);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
const sourceId = basicBlockId(filePath, functionStartLine, functionStartColumn, e.from);
|
|
81
|
+
const targetId = basicBlockId(filePath, functionStartLine, functionStartColumn, e.to);
|
|
82
|
+
graph.addRelationship({
|
|
83
|
+
id: generateId('CFG', `${sourceId}->${targetId}:${e.kind}`),
|
|
84
|
+
type: 'CFG',
|
|
85
|
+
sourceId,
|
|
86
|
+
targetId,
|
|
87
|
+
confidence: 1.0,
|
|
88
|
+
reason: e.kind, // CfgEdgeKind (seq/cond-true/loop-back/…) — queryable
|
|
89
|
+
});
|
|
90
|
+
result.edges++;
|
|
91
|
+
emittedForFn++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
@@ -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,66 @@
|
|
|
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
|
+
/** A basic block: a maximal straight-line run of statements between leaders. */
|
|
14
|
+
export interface BasicBlockData {
|
|
15
|
+
/** Block index within its function. The synthetic ENTRY is always 0. */
|
|
16
|
+
readonly index: number;
|
|
17
|
+
readonly startLine: number;
|
|
18
|
+
readonly endLine: number;
|
|
19
|
+
/** Source snippet for the block (empty for synthetic ENTRY/EXIT). */
|
|
20
|
+
readonly text: string;
|
|
21
|
+
readonly kind: 'entry' | 'exit' | 'normal';
|
|
22
|
+
}
|
|
23
|
+
/** Why one block flows to another — drives the `reason` on the emitted CFG edge. */
|
|
24
|
+
export type CfgEdgeKind = 'seq' | 'cond-true' | 'cond-false' | 'loop-back' | 'break' | 'continue' | 'return' | 'throw' | 'switch-case' | 'fallthrough';
|
|
25
|
+
export interface CfgEdgeData {
|
|
26
|
+
readonly from: number;
|
|
27
|
+
readonly to: number;
|
|
28
|
+
readonly kind: CfgEdgeKind;
|
|
29
|
+
}
|
|
30
|
+
/** One function's control-flow graph. `cfgSideChannel` is `readonly FunctionCfg[]`. */
|
|
31
|
+
export interface FunctionCfg {
|
|
32
|
+
readonly filePath: string;
|
|
33
|
+
/** Source span of the owning function — anchors the BasicBlock node ids. */
|
|
34
|
+
readonly functionStartLine: number;
|
|
35
|
+
readonly functionEndLine: number;
|
|
36
|
+
/**
|
|
37
|
+
* Start COLUMN of the owning function. Combined with `functionStartLine` it
|
|
38
|
+
* disambiguates the BasicBlock node ids when two functions share a start line
|
|
39
|
+
* — e.g. `{ a: () => x(), b: () => y() }`, where both arrows begin on the same
|
|
40
|
+
* line and each restarts its block indices at 0. Without the column the ids
|
|
41
|
+
* collide and the graph's first-writer-wins `addNode` silently drops the
|
|
42
|
+
* second function's blocks and cross-wires its edges.
|
|
43
|
+
*/
|
|
44
|
+
readonly functionStartColumn: number;
|
|
45
|
+
readonly entryIndex: number;
|
|
46
|
+
readonly exitIndex: number;
|
|
47
|
+
readonly blocks: readonly BasicBlockData[];
|
|
48
|
+
readonly edges: readonly CfgEdgeData[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Per-language CFG strategy. Invoked **in the parse worker** for each function
|
|
52
|
+
* node. `TNode` is the language's AST node type (tree-sitter `SyntaxNode` for
|
|
53
|
+
* TS/JS) — kept generic so this module stays AST-library-agnostic. Returns
|
|
54
|
+
* `undefined` when the node is not a CFG-bearing function (the caller skips it).
|
|
55
|
+
*/
|
|
56
|
+
export interface CfgVisitor<TNode = unknown> {
|
|
57
|
+
buildFunctionCfg(fnNode: TNode, filePath: string): FunctionCfg | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Whether `node` is a CFG-bearing function this visitor handles. Lets the
|
|
60
|
+
* worker enumerate functions (and apply the per-function line budget) by a
|
|
61
|
+
* cheap node-type test, instead of attempting to build a CFG for every AST
|
|
62
|
+
* node. `buildFunctionCfg` still re-checks, so this is purely an optimization
|
|
63
|
+
* + the seam the line-budget hooks into.
|
|
64
|
+
*/
|
|
65
|
+
isFunction(node: TNode): boolean;
|
|
66
|
+
}
|
|
@@ -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 {};
|