gitnexus 1.6.8-rc.16 → 1.6.8-rc.17

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.
@@ -12,7 +12,7 @@
12
12
  * hand-built block sequences, which is how the classic CFG hazards are pinned
13
13
  * before the tree-sitter visitor (U2) drives it.
14
14
  */
15
- import type { BasicBlockData, CfgEdgeKind, FunctionCfg } from './types.js';
15
+ import type { BasicBlockData, BindingEntry, CfgEdgeKind, FunctionCfg, StatementFacts } from './types.js';
16
16
  export declare class CfgBuilder {
17
17
  private readonly filePath;
18
18
  private readonly functionStartLine;
@@ -32,17 +32,30 @@ export declare class CfgBuilder {
32
32
  * Defaults to 0 for hand-built test CFGs that don't model columns. */
33
33
  functionStartColumn?: number);
34
34
  /** Create a block and return its index. */
35
- newBlock(startLine: number, endLine: number, text: string, kind?: BasicBlockData['kind']): number;
35
+ newBlock(startLine: number, endLine: number, text: string, kind?: BasicBlockData['kind'], facts?: StatementFacts): number;
36
36
  /** Add a single edge (idempotent on from+to+kind). */
37
37
  edge(from: number, to: number, kind: CfgEdgeKind): void;
38
38
  /** Wire a set of dangling exits to a single target block with one kind. */
39
39
  connect(exits: readonly number[], to: number, kind?: CfgEdgeKind): void;
40
40
  /** Extend a block's end line as more statements accrue to it. */
41
- extendBlock(index: number, endLine: number, appendText?: string): void;
41
+ extendBlock(index: number, endLine: number, appendText?: string, facts?: StatementFacts): void;
42
+ /**
43
+ * Attach a facts-only statement record to a block WITHOUT touching its text
44
+ * or line span (#2082 M2 U1) — bench fingerprints and CFG snapshots include
45
+ * block text, so harvesting must never perturb it (ENTRY-block param defs
46
+ * are the canonical use; records that must precede a walked body get their
47
+ * own facts-only block instead, see the catch-param handling in visitTry).
48
+ */
49
+ attachFacts(index: number, facts: StatementFacts): void;
42
50
  get blockCount(): number;
43
51
  /** Produce the serializable CFG. Caller is responsible for having wired the
44
- * function's dangling exits to {@link exitIndex} before calling. */
45
- finish(): FunctionCfg;
52
+ * function's dangling exits to {@link exitIndex} before calling.
53
+ *
54
+ * Pass `bindings` (the function's binding table, possibly empty) to emit
55
+ * statement facts (#2082 M2 U1) — every block then carries a `statements`
56
+ * array. Omit it (hand-built test CFGs, pre-M2 producers) and both fields
57
+ * are absent, which the reaching-defs solver reports as `no-facts`. */
58
+ finish(bindings?: readonly BindingEntry[]): FunctionCfg;
46
59
  }
47
60
  /**
48
61
  * Block indices reachable from `entryIndex` by following edges. Backs the
@@ -21,8 +21,14 @@ export class CfgBuilder {
21
21
  this.exitIndex = this.newBlock(functionEndLine, functionEndLine, '', 'exit');
22
22
  }
23
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 });
24
+ newBlock(startLine, endLine, text, kind = 'normal', facts) {
25
+ this.blocks.push({
26
+ startLine,
27
+ endLine,
28
+ textParts: text ? [text] : [],
29
+ kind,
30
+ statements: facts ? [facts] : [],
31
+ });
26
32
  return this.blocks.length - 1;
27
33
  }
28
34
  /** Add a single edge (idempotent on from+to+kind). */
@@ -39,7 +45,7 @@ export class CfgBuilder {
39
45
  this.edge(from, to, kind);
40
46
  }
41
47
  /** Extend a block's end line as more statements accrue to it. */
42
- extendBlock(index, endLine, appendText) {
48
+ extendBlock(index, endLine, appendText, facts) {
43
49
  const b = this.blocks[index];
44
50
  if (!b)
45
51
  return;
@@ -47,13 +53,34 @@ export class CfgBuilder {
47
53
  b.endLine = endLine;
48
54
  if (appendText)
49
55
  b.textParts.push(appendText);
56
+ if (facts)
57
+ b.statements.push(facts);
58
+ }
59
+ /**
60
+ * Attach a facts-only statement record to a block WITHOUT touching its text
61
+ * or line span (#2082 M2 U1) — bench fingerprints and CFG snapshots include
62
+ * block text, so harvesting must never perturb it (ENTRY-block param defs
63
+ * are the canonical use; records that must precede a walked body get their
64
+ * own facts-only block instead, see the catch-param handling in visitTry).
65
+ */
66
+ attachFacts(index, facts) {
67
+ const b = this.blocks[index];
68
+ if (!b)
69
+ return;
70
+ b.statements.push(facts);
50
71
  }
51
72
  get blockCount() {
52
73
  return this.blocks.length;
53
74
  }
54
75
  /** Produce the serializable CFG. Caller is responsible for having wired the
55
- * function's dangling exits to {@link exitIndex} before calling. */
56
- finish() {
76
+ * function's dangling exits to {@link exitIndex} before calling.
77
+ *
78
+ * Pass `bindings` (the function's binding table, possibly empty) to emit
79
+ * statement facts (#2082 M2 U1) — every block then carries a `statements`
80
+ * array. Omit it (hand-built test CFGs, pre-M2 producers) and both fields
81
+ * are absent, which the reaching-defs solver reports as `no-facts`. */
82
+ finish(bindings) {
83
+ const withFacts = bindings !== undefined;
57
84
  return {
58
85
  filePath: this.filePath,
59
86
  functionStartLine: this.functionStartLine,
@@ -67,8 +94,10 @@ export class CfgBuilder {
67
94
  endLine: b.endLine,
68
95
  text: b.textParts.join('\n'),
69
96
  kind: b.kind,
97
+ ...(withFacts ? { statements: b.statements } : {}),
70
98
  })),
71
99
  edges: [...this.edges],
100
+ ...(withFacts ? { bindings } : {}),
72
101
  };
73
102
  }
74
103
  }
@@ -1,27 +1,97 @@
1
1
  /**
2
- * ControlFlowContext (issue #2081, M1).
2
+ * ControlFlowContext (issue #2081 M1; finalizer frames added by #2082 M2 U2).
3
3
  *
4
4
  * Resolves the targets of `break`/`continue` (plain and labeled) as the visitor
5
5
  * descends through loops and switches. Loops and switches push a target frame
6
6
  * on entry and pop it on exit; a labeled statement attaches its label to the
7
7
  * frame of the construct it labels, so `break outer` / `continue outer` resolve
8
8
  * against the right enclosing loop/switch rather than the nearest one.
9
+ *
10
+ * M2 adds FINALIZER frames, interleaved on the SAME stack as loop/switch frames
11
+ * — interleaving is load-bearing: a jump must route through exactly the
12
+ * `finally` bodies lexically BETWEEN it and its target (target-relative
13
+ * threading). A `break` whose loop lives entirely inside the `try` crosses no
14
+ * finally and must keep its direct edge; re-routing it anyway would force the
15
+ * only path to the in-try continuation through the finally, letting a finally
16
+ * redefinition falsely KILL in-loop definitions for the downstream
17
+ * reaching-defs pass (a taint false negative). A parallel stack cannot express
18
+ * that between-ness, which is why the frames live here.
9
19
  */
20
+ import type { CfgBuilder } from './cfg-builder.js';
21
+ import type { CfgEdgeKind } from './types.js';
22
+ /** A `finally` whose body any crossing jump must route through. */
23
+ export interface FinalizerFrame {
24
+ readonly kind: 'finalizer';
25
+ /** Entry block of the finally body. */
26
+ readonly entry: number;
27
+ /**
28
+ * Completion legs registered by jumps that crossed this finally: once the
29
+ * owning try pops the frame, it wires `finally-exits → to` with `kind` for
30
+ * each entry. Mutated by the jump handlers via {@link ControlFlowContext}.
31
+ */
32
+ readonly pending: {
33
+ to: number;
34
+ kind: CfgEdgeKind;
35
+ }[];
36
+ }
37
+ /** A resolved jump: its ultimate target + the finallys it crosses (inner→outer). */
38
+ export interface JumpResolution {
39
+ readonly target: number;
40
+ readonly finalizers: readonly FinalizerFrame[];
41
+ }
10
42
  export declare class ControlFlowContext {
11
43
  private readonly stack;
12
- pushLoop(continueTo: number, breakTo: number, label?: string): void;
13
- pushSwitch(breakTo: number, label?: string): void;
44
+ pushLoop(continueTo: number, breakTo: number, labels?: readonly string[]): void;
45
+ pushSwitch(breakTo: number, labels?: readonly string[]): void;
46
+ /** Push a labeled non-loop statement's break-target frame. */
47
+ pushLabeledBlock(breakTo: number, labels: readonly string[]): void;
48
+ /**
49
+ * Push a finalizer frame and return it — the owning `visitTry` keeps the
50
+ * reference to wire {@link FinalizerFrame.pending} after popping it.
51
+ */
52
+ pushFinalizer(entry: number): FinalizerFrame;
14
53
  pop(): void;
15
54
  /**
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).
55
+ * Resolve a `break`: the nearest enclosing loop/switch frame (or, with a
56
+ * label, the nearest frame carrying that label) plus every finalizer frame
57
+ * stacked ABOVE it i.e. exactly the finallys the jump crosses, innermost
58
+ * first. Returns `undefined` if there is no valid target (malformed input or
59
+ * an unmodeled label) — the caller falls back to its conservative routing and
60
+ * threads nothing.
19
61
  */
20
- breakTarget(label?: string): number | undefined;
62
+ resolveBreak(label?: string): JumpResolution | undefined;
63
+ /** Resolve a `continue`: like {@link resolveBreak} but only loop frames match. */
64
+ resolveContinue(label?: string): JumpResolution | undefined;
65
+ /** Every active finalizer, innermost first — what a `return` must cross. */
66
+ finalizersForReturn(): readonly FinalizerFrame[];
21
67
  /**
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.
68
+ * Target block for a `break` (no finalizer info) see {@link resolveBreak}.
69
+ * Prefer `resolveBreak` + {@link wireJumpThroughFinalizers} in visitors: a
70
+ * target-only lookup silently loses finalizer threading (the M2 soundness
71
+ * fix). Kept for target-shape assertions in tests.
25
72
  */
73
+ breakTarget(label?: string): number | undefined;
74
+ /** Target block for a `continue` — same caveat as {@link breakTarget}. */
26
75
  continueTarget(label?: string): number | undefined;
76
+ private resolve;
27
77
  }
78
+ /**
79
+ * Wire a jump from `from` to `target`, routing through the finallys it
80
+ * crosses (innermost first). The first leg keeps the bare jump `kind`
81
+ * (preserving the "kind ⟹ source-block terminator" invariant in types.ts);
82
+ * each finally's completion leg is registered as pending on its frame with the
83
+ * matching `finally-*` kind and wired by the owning try via
84
+ * {@link drainFinalizerPending} once the finally's exits are known.
85
+ *
86
+ * Language-agnostic on purpose (#2082 M2): the threading protocol encodes
87
+ * three subtle invariants every future language visitor needs identically —
88
+ * keeping it here means a new visitor cannot drift on any of them.
89
+ */
90
+ export declare function wireJumpThroughFinalizers(builder: CfgBuilder, from: number, finalizers: readonly FinalizerFrame[], target: number, kind: 'return' | 'break' | 'continue'): void;
91
+ /**
92
+ * Wire a popped finalizer frame's pending completion legs from the finally's
93
+ * exit blocks. A finally that itself always jumps (`finally { return 2; }`)
94
+ * has no exits — its pending legs wire nowhere, matching JS's
95
+ * finally-override semantics.
96
+ */
97
+ export declare function drainFinalizerPending(builder: CfgBuilder, frame: FinalizerFrame, finallyExits: readonly number[]): void;
@@ -1,49 +1,113 @@
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
1
  export class ControlFlowContext {
11
2
  stack = [];
12
- pushLoop(continueTo, breakTo, label) {
13
- this.stack.push({ kind: 'loop', continueTo, breakTo, label });
3
+ pushLoop(continueTo, breakTo, labels = []) {
4
+ this.stack.push({ kind: 'loop', continueTo, breakTo, labels });
14
5
  }
15
- pushSwitch(breakTo, label) {
16
- this.stack.push({ kind: 'switch', breakTo, label });
6
+ pushSwitch(breakTo, labels = []) {
7
+ this.stack.push({ kind: 'switch', breakTo, labels });
8
+ }
9
+ /** Push a labeled non-loop statement's break-target frame. */
10
+ pushLabeledBlock(breakTo, labels) {
11
+ this.stack.push({ kind: 'block', breakTo, labels });
12
+ }
13
+ /**
14
+ * Push a finalizer frame and return it — the owning `visitTry` keeps the
15
+ * reference to wire {@link FinalizerFrame.pending} after popping it.
16
+ */
17
+ pushFinalizer(entry) {
18
+ const frame = { kind: 'finalizer', entry, pending: [] };
19
+ this.stack.push(frame);
20
+ return frame;
17
21
  }
18
22
  pop() {
19
23
  this.stack.pop();
20
24
  }
21
25
  /**
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).
26
+ * Resolve a `break`: the nearest enclosing loop/switch frame (or, with a
27
+ * label, the nearest frame carrying that label) plus every finalizer frame
28
+ * stacked ABOVE it i.e. exactly the finallys the jump crosses, innermost
29
+ * first. Returns `undefined` if there is no valid target (malformed input or
30
+ * an unmodeled label) — the caller falls back to its conservative routing and
31
+ * threads nothing.
25
32
  */
26
- breakTarget(label) {
33
+ resolveBreak(label) {
34
+ return this.resolve((f) => label === undefined
35
+ ? f.kind !== 'block' // an unlabeled break never targets a labeled block
36
+ : f.labels.includes(label));
37
+ }
38
+ /** Resolve a `continue`: like {@link resolveBreak} but only loop frames match. */
39
+ resolveContinue(label) {
40
+ return this.resolve((f) => f.kind === 'loop' && (label === undefined || f.labels.includes(label)), (f) => f.continueTo);
41
+ }
42
+ /** Every active finalizer, innermost first — what a `return` must cross. */
43
+ finalizersForReturn() {
44
+ const fins = [];
27
45
  for (let i = this.stack.length - 1; i >= 0; i--) {
28
46
  const f = this.stack[i];
29
- if (label === undefined || f.label === label)
30
- return f.breakTo;
47
+ if (f.kind === 'finalizer')
48
+ fins.push(f);
31
49
  }
32
- return undefined;
50
+ return fins;
33
51
  }
34
52
  /**
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.
53
+ * Target block for a `break` (no finalizer info) see {@link resolveBreak}.
54
+ * Prefer `resolveBreak` + {@link wireJumpThroughFinalizers} in visitors: a
55
+ * target-only lookup silently loses finalizer threading (the M2 soundness
56
+ * fix). Kept for target-shape assertions in tests.
38
57
  */
58
+ breakTarget(label) {
59
+ return this.resolveBreak(label)?.target;
60
+ }
61
+ /** Target block for a `continue` — same caveat as {@link breakTarget}. */
39
62
  continueTarget(label) {
63
+ return this.resolveContinue(label)?.target;
64
+ }
65
+ resolve(matches, targetOf = (f) => f.breakTo) {
66
+ const crossed = [];
40
67
  for (let i = this.stack.length - 1; i >= 0; i--) {
41
68
  const f = this.stack[i];
42
- if (f.kind !== 'loop')
69
+ if (f.kind === 'finalizer') {
70
+ crossed.push(f);
43
71
  continue;
44
- if (label === undefined || f.label === label)
45
- return f.continueTo;
72
+ }
73
+ if (matches(f))
74
+ return { target: targetOf(f), finalizers: crossed };
46
75
  }
47
76
  return undefined;
48
77
  }
49
78
  }
79
+ /**
80
+ * Wire a jump from `from` to `target`, routing through the finallys it
81
+ * crosses (innermost first). The first leg keeps the bare jump `kind`
82
+ * (preserving the "kind ⟹ source-block terminator" invariant in types.ts);
83
+ * each finally's completion leg is registered as pending on its frame with the
84
+ * matching `finally-*` kind and wired by the owning try via
85
+ * {@link drainFinalizerPending} once the finally's exits are known.
86
+ *
87
+ * Language-agnostic on purpose (#2082 M2): the threading protocol encodes
88
+ * three subtle invariants every future language visitor needs identically —
89
+ * keeping it here means a new visitor cannot drift on any of them.
90
+ */
91
+ export function wireJumpThroughFinalizers(builder, from, finalizers, target, kind) {
92
+ if (finalizers.length === 0) {
93
+ builder.edge(from, target, kind);
94
+ return;
95
+ }
96
+ const completionKind = `finally-${kind}`;
97
+ builder.edge(from, finalizers[0].entry, kind);
98
+ for (let i = 0; i < finalizers.length; i++) {
99
+ const to = i + 1 < finalizers.length ? finalizers[i + 1].entry : target;
100
+ finalizers[i].pending.push({ to, kind: completionKind });
101
+ }
102
+ }
103
+ /**
104
+ * Wire a popped finalizer frame's pending completion legs from the finally's
105
+ * exit blocks. A finally that itself always jumps (`finally { return 2; }`)
106
+ * has no exits — its pending legs wire nowhere, matching JS's
107
+ * finally-override semantics.
108
+ */
109
+ export function drainFinalizerPending(builder, frame, finallyExits) {
110
+ for (const p of frame.pending) {
111
+ builder.connect(finallyExits, p.to, p.kind);
112
+ }
113
+ }
@@ -28,6 +28,26 @@ import type { FunctionCfg } from './types.js';
28
28
  * default.
29
29
  */
30
30
  export declare const DEFAULT_MAX_CFG_EDGES_PER_FUNCTION = 5000;
31
+ /**
32
+ * Default per-function REACHING_DEF edge cap (#2082 M2 KTD9). 4000 mirrors
33
+ * Joern's per-method `maxNumberOfDefinitions` — the closest production prior
34
+ * art — but truncates-and-warns instead of silently skipping the function.
35
+ * Counts (defBlock, useBlock, binding) DEDUPED edges, not statement-level
36
+ * facts. `0` ⇒ unlimited; `undefined` ⇒ this default.
37
+ */
38
+ export declare const DEFAULT_PDG_MAX_REACHING_DEF_EDGES_PER_FUNCTION = 4000;
39
+ /**
40
+ * Fact-materialization headroom over the edge cap (#2082 M2 U3/F3): facts are
41
+ * O(defs×uses) BY SPEC in merge-heavy code, and the edge cap alone bounds the
42
+ * GRAPH, not the per-function memory spike of materializing facts before
43
+ * dedup. {@link emitFileReachingDefs} hands `edgeCap × this` to
44
+ * `computeReachingDefs` as `maxFacts` (unlimited when the edge cap is 0) —
45
+ * single source of truth; the DEFAULT constant below is derived, never the
46
+ * mechanism.
47
+ */
48
+ export declare const REACHING_DEF_FACTS_PER_EDGE_CAP = 4;
49
+ /** Derived emit-path fact limit at the default edge cap (bench/doc anchor). */
50
+ export declare const DEFAULT_PDG_MAX_REACHING_DEF_FACTS_PER_FUNCTION: number;
31
51
  export interface CfgEmitResult {
32
52
  blocks: number;
33
53
  edges: number;
@@ -53,6 +73,17 @@ export interface CfgEmitResult {
53
73
  * join this check).
54
74
  */
55
75
  export declare const isEmitSafeCfg: (cfg: FunctionCfg | undefined | null) => cfg is FunctionCfg;
76
+ /**
77
+ * Whether a structurally-valid CFG's M2 statement facts are safe to feed to
78
+ * the reaching-defs solver + REACHING_DEF id templating (#2082 U1/U4): the
79
+ * binding table's name/declLine/declColumn template into edge ids, and
80
+ * statement def/use indices must stay IN RANGE of the table (an escaping
81
+ * index would fabricate `undefined`-keyed ids). Deliberately SEPARATE from
82
+ * {@link isEmitSafeCfg}: malformed facts must cost only the function's
83
+ * REACHING_DEF projection — degrading to M1 behavior (CFG emitted, no facts)
84
+ * — never the BasicBlock/CFG layer itself.
85
+ */
86
+ export declare const hasEmitSafeFacts: (cfg: FunctionCfg) => boolean;
56
87
  /**
57
88
  * Emit BasicBlock nodes + CFG edges for every function CFG in `cfgs`.
58
89
  *
@@ -62,3 +93,37 @@ export declare const isEmitSafeCfg: (cfg: FunctionCfg | undefined | null) => cfg
62
93
  * count is bounded by the function's statement count); only edges are capped.
63
94
  */
64
95
  export declare function emitFileCfgs(graph: KnowledgeGraph, cfgs: readonly FunctionCfg[], maxEdgesPerFunction?: number, onWarn?: (message: string) => void): CfgEmitResult;
96
+ export interface ReachingDefEmitResult {
97
+ /** Deduped (defBlock, useBlock, binding) edges persisted. */
98
+ edges: number;
99
+ /** Deduped edges dropped by the per-function edge cap. */
100
+ droppedEdges: number;
101
+ cappedFunctions: number;
102
+ /** Functions whose FACT materialization hit the solver's maxFacts limit. */
103
+ truncatedFunctions: number;
104
+ /** Functions whose facts failed {@link hasEmitSafeFacts} (CFG kept, facts skipped). */
105
+ malformedFactFunctions: number;
106
+ /** Total statement-level facts the solver produced (pre-dedup telemetry). */
107
+ facts: number;
108
+ }
109
+ /**
110
+ * Compute reaching definitions per function and persist the bounded
111
+ * REACHING_DEF projection (#2082 M2 U4).
112
+ *
113
+ * Facts are DEDUPED to (defBlock, useBlock, binding) before budgeting — the
114
+ * persisted columns (`from,to,type,confidence,reason,step`; relationship ids
115
+ * are in-memory-only, the CodeRelation table has no id column) cannot
116
+ * distinguish finer rows, so statement-indexed ids would only manufacture
117
+ * byte-identical duplicate rows that burn budget. Statement granularity lives
118
+ * in the in-memory {@link computeReachingDefs} result, which the M3 taint
119
+ * engine recomputes on demand — the budget here governs only this projection
120
+ * and can never drop a taint fact.
121
+ *
122
+ * R7 (no silent truncation) covers BOTH layers: the per-function edge cap AND
123
+ * the solver's fact-materialization limit (which can fire without the edge
124
+ * cap ever being reached, since dedup is many-to-one) each produce one
125
+ * unconditional `onWarn`. The edge-cap warn names the top bindings by fact
126
+ * count — overflow is almost always one variable, which is exactly the datum
127
+ * M3 tuning wants.
128
+ */
129
+ export declare function emitFileReachingDefs(graph: KnowledgeGraph, cfgs: readonly FunctionCfg[], maxEdgesPerFunction?: number, onWarn?: (message: string) => void): ReachingDefEmitResult;