diff --git a/gitnexus/test/helpers/cfg-harness.ts b/gitnexus/test/helpers/cfg-harness.ts index 2c907d609..e5d787836 100644 --- a/gitnexus/test/helpers/cfg-harness.ts +++ b/gitnexus/test/helpers/cfg-harness.ts @@ -10,7 +10,7 @@ */ import Parser from 'tree-sitter'; import type { SyntaxNode } from '../../src/core/ingestion/utils/ast-helpers.js'; -import type { CfgVisitor, FunctionCfg } from '../../src/core/ingestion/cfg/types.js'; +import type { CfgVisitor, FunctionCfg, SiteRecord } from '../../src/core/ingestion/cfg/types.js'; export interface CfgHarness { /** Parse `code` with the configured grammar; returns the root node. */ @@ -67,3 +67,55 @@ export function makeCfgHarness( return { parse, collectFunctions, cfgOf, cfgsOf }; } + +// ── shared CFG-shape assertions (#2195 U8) ─────────────────────────────────── +// Byte-identical helpers the per-language `*-visitor.test.ts` files each copied. +// CFG-shape only — EXIT-reachability soundness asserts against the production +// `isExitReachableFromAllBlocks` (post-dominators.ts) directly, not a re-impl. + +/** The distinct edge kinds present in `cfg`. */ +export const edgeKinds = (cfg: FunctionCfg): Set => new Set(cfg.edges.map((e) => e.kind)); + +/** Index of the (first) block whose text contains `substr`. Throws if none. */ +export const block = (cfg: FunctionCfg, substr: string): number => { + const b = cfg.blocks.find((bl) => bl.text.includes(substr)); + if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`); + return b.index; +}; + +/** Is `to` forward-reachable from `from` over `cfg`'s edges? */ +export function reaches(cfg: FunctionCfg, from: number, to: number): boolean { + const adj = new Map(); + for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to); + const seen = new Set([from]); + const stack = [from]; + while (stack.length) { + const n = stack.pop() as number; + if (n === to) return true; + for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx)); + } + return seen.has(to); +} + +/** Is `idx` reachable from ENTRY? */ +export const reachable = (cfg: FunctionCfg, idx: number): boolean => + reaches(cfg, cfg.entryIndex, idx); + +/** Resolve a binding by name → its index in the function's binding table. */ +export function bindingIdx(cfg: FunctionCfg, name: string): number { + const i = (cfg.bindings ?? []).findIndex((b) => b.name === name); + if (i < 0) throw new Error(`no binding ${name}`); + return i; +} + +/** Every taint `SiteRecord` harvested across the function's statements. */ +export function allSites(cfg: FunctionCfg): SiteRecord[] { + const out: SiteRecord[] = []; + for (const b of cfg.blocks) for (const s of b.statements ?? []) out.push(...(s.sites ?? [])); + return out; +} + +/** True iff at least one statement carries a (non-empty) `sites` array. */ +export function hasAnySites(cfg: FunctionCfg): boolean { + return cfg.blocks.some((b) => (b.statements ?? []).some((s) => (s.sites ?? []).length > 0)); +} diff --git a/gitnexus/test/unit/cfg/c-cpp-visitor.test.ts b/gitnexus/test/unit/cfg/c-cpp-visitor.test.ts index f72966703..510df9da3 100644 --- a/gitnexus/test/unit/cfg/c-cpp-visitor.test.ts +++ b/gitnexus/test/unit/cfg/c-cpp-visitor.test.ts @@ -6,7 +6,17 @@ import { createCppCfgVisitor, } from '../../../src/core/ingestion/cfg/visitors/c-cpp.js'; import type { FunctionCfg, SiteRecord } from '../../../src/core/ingestion/cfg/types.js'; -import { makeCfgHarness, type CfgHarness } from '../../helpers/cfg-harness.js'; +import { + makeCfgHarness, + type CfgHarness, + block, + edgeKinds, + reaches, + reachable, + bindingIdx, + allSites, + hasAnySites, +} from '../../helpers/cfg-harness.js'; import { isExitReachableFromAllBlocks } from '../../../src/core/ingestion/cfg/post-dominators.js'; import { augmentForPostDom } from '../../../src/core/ingestion/cfg/synthetic-escape.js'; import { computeControlDependence } from '../../../src/core/ingestion/cfg/control-dependence.js'; @@ -24,57 +34,6 @@ const cppGrammar = createRequire(import.meta.url)('tree-sitter-cpp') as Paramete const c: CfgHarness = makeCfgHarness(cGrammar, createCCfgVisitor(), 'fixture.c'); const cpp: CfgHarness = makeCfgHarness(cppGrammar, createCppCfgVisitor(), 'fixture.cpp'); -const block = (cfg: FunctionCfg, substr: string): number => { - const b = cfg.blocks.find((bl) => bl.text.includes(substr)); - if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`); - return b.index; -}; - -const edgeKinds = (cfg: FunctionCfg): Set => new Set(cfg.edges.map((e) => e.kind)); - -function reaches(cfg: FunctionCfg, from: number, to: number): boolean { - const adj = new Map(); - for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to); - const seen = new Set([from]); - const stack = [from]; - while (stack.length) { - const n = stack.pop() as number; - if (n === to) return true; - for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx)); - } - return seen.has(to); -} -const reachable = (cfg: FunctionCfg, idx: number): boolean => reaches(cfg, cfg.entryIndex, idx); - -/** Is EXIT reverse-reachable from every reachable block? (CDG soundness gate.) */ -function exitReachableFromAll(cfg: FunctionCfg): boolean { - for (const b of cfg.blocks) { - if (b.index === cfg.exitIndex) continue; - if (!reachable(cfg, b.index)) continue; // unreachable blocks exempt - if (!reaches(cfg, b.index, cfg.exitIndex)) return false; - } - return true; -} - -/** Resolve a binding by name → its index in the function's binding table. */ -function bindingIdx(cfg: FunctionCfg, name: string): number { - const i = (cfg.bindings ?? []).findIndex((b) => b.name === name); - if (i < 0) throw new Error(`no binding ${name}`); - return i; -} - -/** Every taint `SiteRecord` harvested across the function's statements. */ -function allSites(cfg: FunctionCfg): SiteRecord[] { - const out: SiteRecord[] = []; - for (const b of cfg.blocks) for (const s of b.statements ?? []) out.push(...(s.sites ?? [])); - return out; -} - -/** True iff at least one statement carries a (non-empty) `sites` array. */ -function hasAnySites(cfg: FunctionCfg): boolean { - return cfg.blocks.some((b) => (b.statements ?? []).some((s) => (s.sites ?? []).length > 0)); -} - describe('C CfgVisitor — structure', () => { it('straight-line body: ENTRY → block → EXIT (seq)', () => { const cfg = c.cfgOf(`void f() { a(); b(); c(); }`); @@ -322,7 +281,7 @@ describe('C++ CfgVisitor — range-for', () => { const header = cfg.edges.find((e) => e.kind === 'loop-back' && e.from === body)?.to; expect(header).toBeDefined(); expect(reachable(cfg, block(cfg, 'done();'))).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); }); it('range-for declarator defines the loop variable; iterated expr is a use', () => { diff --git a/gitnexus/test/unit/cfg/csharp-visitor.test.ts b/gitnexus/test/unit/cfg/csharp-visitor.test.ts index c34e71a6f..0851e8e87 100644 --- a/gitnexus/test/unit/cfg/csharp-visitor.test.ts +++ b/gitnexus/test/unit/cfg/csharp-visitor.test.ts @@ -2,7 +2,17 @@ import { describe, it, expect, vi } from 'vitest'; import { createRequire } from 'node:module'; import { createCsharpCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/csharp.js'; import type { FunctionCfg, SiteRecord } from '../../../src/core/ingestion/cfg/types.js'; -import { makeCfgHarness, type CfgHarness } from '../../helpers/cfg-harness.js'; +import { + makeCfgHarness, + type CfgHarness, + block, + edgeKinds, + reaches, + reachable, + bindingIdx, + allSites, + hasAnySites, +} from '../../helpers/cfg-harness.js'; import { isExitReachableFromAllBlocks } from '../../../src/core/ingestion/cfg/post-dominators.js'; import { augmentForPostDom } from '../../../src/core/ingestion/cfg/synthetic-escape.js'; import { computeControlDependence } from '../../../src/core/ingestion/cfg/control-dependence.js'; @@ -20,45 +30,6 @@ const csGrammar = createRequire(import.meta.url)( const cs: CfgHarness = makeCfgHarness(csGrammar, createCsharpCfgVisitor(), 'fixture.cs'); -const block = (cfg: FunctionCfg, substr: string): number => { - const b = cfg.blocks.find((bl) => bl.text.includes(substr)); - if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`); - return b.index; -}; - -const edgeKinds = (cfg: FunctionCfg): Set => new Set(cfg.edges.map((e) => e.kind)); - -function reaches(cfg: FunctionCfg, from: number, to: number): boolean { - const adj = new Map(); - for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to); - const seen = new Set([from]); - const stack = [from]; - while (stack.length) { - const n = stack.pop() as number; - if (n === to) return true; - for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx)); - } - return seen.has(to); -} -const reachable = (cfg: FunctionCfg, idx: number): boolean => reaches(cfg, cfg.entryIndex, idx); - -/** Is EXIT reverse-reachable from every reachable block? (CDG soundness gate.) */ -function exitReachableFromAll(cfg: FunctionCfg): boolean { - for (const b of cfg.blocks) { - if (b.index === cfg.exitIndex) continue; - if (!reachable(cfg, b.index)) continue; // unreachable blocks exempt - if (!reaches(cfg, b.index, cfg.exitIndex)) return false; - } - return true; -} - -/** Resolve a binding by name → its index in the function's binding table. */ -function bindingIdx(cfg: FunctionCfg, name: string): number { - const i = (cfg.bindings ?? []).findIndex((b) => b.name === name); - if (i < 0) throw new Error(`no binding ${name}`); - return i; -} - const hasDef = (cfg: FunctionCfg, idx: number): boolean => cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(idx))); const hasUse = (cfg: FunctionCfg, idx: number): boolean => @@ -66,15 +37,6 @@ const hasUse = (cfg: FunctionCfg, idx: number): boolean => const hasMayDef = (cfg: FunctionCfg, idx: number): boolean => cfg.blocks.some((bl) => bl.statements?.some((s) => (s.mayDefs ?? []).includes(idx))); -/** Every taint `SiteRecord` harvested across the function's statements. */ -function allSites(cfg: FunctionCfg): SiteRecord[] { - const out: SiteRecord[] = []; - for (const b of cfg.blocks) for (const s of b.statements ?? []) out.push(...(s.sites ?? [])); - return out; -} -const hasAnySites = (cfg: FunctionCfg): boolean => - cfg.blocks.some((b) => (b.statements ?? []).some((s) => (s.sites ?? []).length > 0)); - const wrap = (body: string): string => `class C { void M(${''}) { ${body} } }`; describe('C# CfgVisitor — structure', () => { diff --git a/gitnexus/test/unit/cfg/dart-visitor.test.ts b/gitnexus/test/unit/cfg/dart-visitor.test.ts index 1906c28e1..413b0650a 100644 --- a/gitnexus/test/unit/cfg/dart-visitor.test.ts +++ b/gitnexus/test/unit/cfg/dart-visitor.test.ts @@ -2,7 +2,15 @@ import { describe, it, expect } from 'vitest'; import { requireVendoredGrammar } from '../../../src/core/tree-sitter/vendored-grammars.js'; import { createDartCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/dart.js'; import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js'; -import { makeCfgHarness, type CfgHarness } from '../../helpers/cfg-harness.js'; +import { + makeCfgHarness, + type CfgHarness, + block, + edgeKinds, + reaches, + reachable, + bindingIdx, +} from '../../helpers/cfg-harness.js'; import { isExitReachableFromAllBlocks } from '../../../src/core/ingestion/cfg/post-dominators.js'; import { computeControlDependence } from '../../../src/core/ingestion/cfg/control-dependence.js'; @@ -21,45 +29,6 @@ const dartGrammar = requireVendoredGrammar('tree-sitter-dart') as Parameters< const dart: CfgHarness = makeCfgHarness(dartGrammar, createDartCfgVisitor(), 'fixture.dart'); -const block = (cfg: FunctionCfg, substr: string): number => { - const b = cfg.blocks.find((bl) => bl.text.includes(substr)); - if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`); - return b.index; -}; - -const edgeKinds = (cfg: FunctionCfg): Set => new Set(cfg.edges.map((e) => e.kind)); - -function reaches(cfg: FunctionCfg, from: number, to: number): boolean { - const adj = new Map(); - for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to); - const seen = new Set([from]); - const stack = [from]; - while (stack.length) { - const n = stack.pop() as number; - if (n === to) return true; - for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx)); - } - return seen.has(to); -} -const reachable = (cfg: FunctionCfg, idx: number): boolean => reaches(cfg, cfg.entryIndex, idx); - -/** Is EXIT reverse-reachable from every reachable block? (CDG soundness gate.) */ -function exitReachableFromAll(cfg: FunctionCfg): boolean { - for (const b of cfg.blocks) { - if (b.index === cfg.exitIndex) continue; - if (!reachable(cfg, b.index)) continue; // unreachable blocks exempt - if (!reaches(cfg, b.index, cfg.exitIndex)) return false; - } - return true; -} - -/** Resolve a binding by name → its index in the function's binding table. */ -function bindingIdx(cfg: FunctionCfg, name: string): number { - const i = (cfg.bindings ?? []).findIndex((b) => b.name === name); - if (i < 0) throw new Error(`no binding ${name}`); - return i; -} - const definesBinding = (cfg: FunctionCfg, idx: number): boolean => cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(idx))); const usesBinding = (cfg: FunctionCfg, idx: number): boolean => diff --git a/gitnexus/test/unit/cfg/go-visitor.test.ts b/gitnexus/test/unit/cfg/go-visitor.test.ts index 2795b5ba1..5f62e2900 100644 --- a/gitnexus/test/unit/cfg/go-visitor.test.ts +++ b/gitnexus/test/unit/cfg/go-visitor.test.ts @@ -2,7 +2,17 @@ import { describe, it, expect, vi } from 'vitest'; import { createRequire } from 'node:module'; import { createGoCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/go.js'; import type { FunctionCfg, SiteRecord } from '../../../src/core/ingestion/cfg/types.js'; -import { makeCfgHarness, type CfgHarness } from '../../helpers/cfg-harness.js'; +import { + makeCfgHarness, + type CfgHarness, + block, + edgeKinds, + reaches, + reachable, + bindingIdx, + allSites, + hasAnySites, +} from '../../helpers/cfg-harness.js'; import { isExitReachableFromAllBlocks } from '../../../src/core/ingestion/cfg/post-dominators.js'; import { computeControlDependence } from '../../../src/core/ingestion/cfg/control-dependence.js'; import { augmentForPostDom } from '../../../src/core/ingestion/cfg/synthetic-escape.js'; @@ -19,35 +29,6 @@ const goGrammar = createRequire(import.meta.url)('tree-sitter-go') as Parameters const go: CfgHarness = makeCfgHarness(goGrammar, createGoCfgVisitor(), 'fixture.go'); -const block = (cfg: FunctionCfg, substr: string): number => { - const b = cfg.blocks.find((bl) => bl.text.includes(substr)); - if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`); - return b.index; -}; - -const edgeKinds = (cfg: FunctionCfg): Set => new Set(cfg.edges.map((e) => e.kind)); - -function reaches(cfg: FunctionCfg, from: number, to: number): boolean { - const adj = new Map(); - for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to); - const seen = new Set([from]); - const stack = [from]; - while (stack.length) { - const n = stack.pop() as number; - if (n === to) return true; - for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx)); - } - return seen.has(to); -} -const reachable = (cfg: FunctionCfg, idx: number): boolean => reaches(cfg, cfg.entryIndex, idx); - -/** Resolve a binding by name → its index in the function's binding table. */ -function bindingIdx(cfg: FunctionCfg, name: string): number { - const i = (cfg.bindings ?? []).findIndex((b) => b.name === name); - if (i < 0) throw new Error(`no binding ${name}`); - return i; -} - const hasDef = (cfg: FunctionCfg, idx: number): boolean => cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(idx))); const hasUse = (cfg: FunctionCfg, idx: number): boolean => @@ -55,15 +36,6 @@ const hasUse = (cfg: FunctionCfg, idx: number): boolean => const hasMayDef = (cfg: FunctionCfg, idx: number): boolean => cfg.blocks.some((bl) => bl.statements?.some((s) => (s.mayDefs ?? []).includes(idx))); -/** Every taint `SiteRecord` harvested across the function's statements. */ -function allSites(cfg: FunctionCfg): SiteRecord[] { - const out: SiteRecord[] = []; - for (const b of cfg.blocks) for (const s of b.statements ?? []) out.push(...(s.sites ?? [])); - return out; -} -const hasAnySites = (cfg: FunctionCfg): boolean => - cfg.blocks.some((b) => (b.statements ?? []).some((s) => (s.sites ?? []).length > 0)); - /** Wrap a Go function body in a minimal compilable package. */ const pkg = (src: string): string => `package main\n${src}\n`; diff --git a/gitnexus/test/unit/cfg/java-visitor.test.ts b/gitnexus/test/unit/cfg/java-visitor.test.ts index 93c039514..b250f05a1 100644 --- a/gitnexus/test/unit/cfg/java-visitor.test.ts +++ b/gitnexus/test/unit/cfg/java-visitor.test.ts @@ -2,7 +2,17 @@ import { describe, it, expect, vi } from 'vitest'; import { createRequire } from 'node:module'; import { createJavaCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/java.js'; import type { FunctionCfg, SiteRecord } from '../../../src/core/ingestion/cfg/types.js'; -import { makeCfgHarness, type CfgHarness } from '../../helpers/cfg-harness.js'; +import { + makeCfgHarness, + type CfgHarness, + block, + edgeKinds, + reaches, + reachable, + bindingIdx, + allSites, + hasAnySites, +} from '../../helpers/cfg-harness.js'; import { isExitReachableFromAllBlocks } from '../../../src/core/ingestion/cfg/post-dominators.js'; import { computeControlDependence } from '../../../src/core/ingestion/cfg/control-dependence.js'; @@ -17,54 +27,6 @@ const javaGrammar = createRequire(import.meta.url)('tree-sitter-java') as Parame const java: CfgHarness = makeCfgHarness(javaGrammar, createJavaCfgVisitor(), 'fixture.java'); -const block = (cfg: FunctionCfg, substr: string): number => { - const b = cfg.blocks.find((bl) => bl.text.includes(substr)); - if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`); - return b.index; -}; - -const edgeKinds = (cfg: FunctionCfg): Set => new Set(cfg.edges.map((e) => e.kind)); - -function reaches(cfg: FunctionCfg, from: number, to: number): boolean { - const adj = new Map(); - for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to); - const seen = new Set([from]); - const stack = [from]; - while (stack.length) { - const n = stack.pop() as number; - if (n === to) return true; - for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx)); - } - return seen.has(to); -} -const reachable = (cfg: FunctionCfg, idx: number): boolean => reaches(cfg, cfg.entryIndex, idx); - -/** Is EXIT reverse-reachable from every reachable block? (CDG soundness gate.) */ -function exitReachableFromAll(cfg: FunctionCfg): boolean { - for (const b of cfg.blocks) { - if (b.index === cfg.exitIndex) continue; - if (!reachable(cfg, b.index)) continue; // unreachable blocks exempt - if (!reaches(cfg, b.index, cfg.exitIndex)) return false; - } - return true; -} - -/** Resolve a binding by name → its index in the function's binding table. */ -function bindingIdx(cfg: FunctionCfg, name: string): number { - const i = (cfg.bindings ?? []).findIndex((b) => b.name === name); - if (i < 0) throw new Error(`no binding ${name}`); - return i; -} - -/** Every taint `SiteRecord` harvested across the function's statements. */ -function allSites(cfg: FunctionCfg): SiteRecord[] { - const out: SiteRecord[] = []; - for (const b of cfg.blocks) for (const s of b.statements ?? []) out.push(...(s.sites ?? [])); - return out; -} -const hasAnySites = (cfg: FunctionCfg): boolean => - cfg.blocks.some((b) => (b.statements ?? []).some((s) => (s.sites ?? []).length > 0)); - const hasDef = (cfg: FunctionCfg, idx: number): boolean => cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(idx))); const hasUse = (cfg: FunctionCfg, idx: number): boolean => diff --git a/gitnexus/test/unit/cfg/kotlin-visitor.test.ts b/gitnexus/test/unit/cfg/kotlin-visitor.test.ts index e9623a6c1..f41e1b299 100644 --- a/gitnexus/test/unit/cfg/kotlin-visitor.test.ts +++ b/gitnexus/test/unit/cfg/kotlin-visitor.test.ts @@ -2,7 +2,15 @@ import { describe, it, expect } from 'vitest'; import { requireVendoredGrammar } from '../../../src/core/tree-sitter/vendored-grammars.js'; import { createKotlinCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/kotlin.js'; import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js'; -import { makeCfgHarness, type CfgHarness } from '../../helpers/cfg-harness.js'; +import { + makeCfgHarness, + type CfgHarness, + block, + edgeKinds, + reaches, + reachable, + bindingIdx, +} from '../../helpers/cfg-harness.js'; import { isExitReachableFromAllBlocks } from '../../../src/core/ingestion/cfg/post-dominators.js'; import { computeControlDependence } from '../../../src/core/ingestion/cfg/control-dependence.js'; @@ -19,45 +27,6 @@ const kotlinGrammar = requireVendoredGrammar('tree-sitter-kotlin') as Parameters const kotlin: CfgHarness = makeCfgHarness(kotlinGrammar, createKotlinCfgVisitor(), 'fixture.kt'); -const block = (cfg: FunctionCfg, substr: string): number => { - const b = cfg.blocks.find((bl) => bl.text.includes(substr)); - if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`); - return b.index; -}; - -const edgeKinds = (cfg: FunctionCfg): Set => new Set(cfg.edges.map((e) => e.kind)); - -function reaches(cfg: FunctionCfg, from: number, to: number): boolean { - const adj = new Map(); - for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to); - const seen = new Set([from]); - const stack = [from]; - while (stack.length) { - const n = stack.pop() as number; - if (n === to) return true; - for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx)); - } - return seen.has(to); -} -const reachable = (cfg: FunctionCfg, idx: number): boolean => reaches(cfg, cfg.entryIndex, idx); - -/** Is EXIT reverse-reachable from every reachable block? (CDG soundness gate.) */ -function exitReachableFromAll(cfg: FunctionCfg): boolean { - for (const b of cfg.blocks) { - if (b.index === cfg.exitIndex) continue; - if (!reachable(cfg, b.index)) continue; // unreachable blocks exempt - if (!reaches(cfg, b.index, cfg.exitIndex)) return false; - } - return true; -} - -/** Resolve a binding by name → its index in the function's binding table. */ -function bindingIdx(cfg: FunctionCfg, name: string): number { - const i = (cfg.bindings ?? []).findIndex((b) => b.name === name); - if (i < 0) throw new Error(`no binding ${name}`); - return i; -} - const definesBinding = (cfg: FunctionCfg, idx: number): boolean => cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(idx))); const usesBinding = (cfg: FunctionCfg, idx: number): boolean => @@ -226,7 +195,7 @@ describe('Kotlin CfgVisitor — loops', () => { it('do {} while (true) keeps EXIT reverse-reachable', () => { const cfg = kotlin.cfgOf(`fun f() { do { work() } while (true) }`); expect(edgeKinds(cfg).has('cond-false')).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true); }); }); diff --git a/gitnexus/test/unit/cfg/php-visitor.test.ts b/gitnexus/test/unit/cfg/php-visitor.test.ts index d914a227f..802440322 100644 --- a/gitnexus/test/unit/cfg/php-visitor.test.ts +++ b/gitnexus/test/unit/cfg/php-visitor.test.ts @@ -2,7 +2,16 @@ import { describe, it, expect } from 'vitest'; import { createRequire } from 'node:module'; import { createPhpCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/php.js'; import type { FunctionCfg, SiteRecord } from '../../../src/core/ingestion/cfg/types.js'; -import { makeCfgHarness, type CfgHarness } from '../../helpers/cfg-harness.js'; +import { + makeCfgHarness, + type CfgHarness, + block, + edgeKinds, + reaches, + reachable, + bindingIdx, + allSites, +} from '../../helpers/cfg-harness.js'; import { isExitReachableFromAllBlocks } from '../../../src/core/ingestion/cfg/post-dominators.js'; import { computeControlDependence } from '../../../src/core/ingestion/cfg/control-dependence.js'; @@ -19,45 +28,6 @@ const php: CfgHarness = makeCfgHarness(phpGrammar, createPhpCfgVisitor(), 'fixtu const wrap = (body: string): string => ` { - const b = cfg.blocks.find((bl) => bl.text.includes(substr)); - if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`); - return b.index; -}; - -const edgeKinds = (cfg: FunctionCfg): Set => new Set(cfg.edges.map((e) => e.kind)); - -function reaches(cfg: FunctionCfg, from: number, to: number): boolean { - const adj = new Map(); - for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to); - const seen = new Set([from]); - const stack = [from]; - while (stack.length) { - const n = stack.pop() as number; - if (n === to) return true; - for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx)); - } - return seen.has(to); -} -const reachable = (cfg: FunctionCfg, idx: number): boolean => reaches(cfg, cfg.entryIndex, idx); - -/** Is EXIT reverse-reachable from every reachable block? (CDG soundness gate.) */ -function exitReachableFromAll(cfg: FunctionCfg): boolean { - for (const b of cfg.blocks) { - if (b.index === cfg.exitIndex) continue; - if (!reachable(cfg, b.index)) continue; // unreachable blocks exempt - if (!reaches(cfg, b.index, cfg.exitIndex)) return false; - } - return true; -} - -/** Resolve a binding by name → its index in the function's binding table. */ -function bindingIdx(cfg: FunctionCfg, name: string): number { - const i = (cfg.bindings ?? []).findIndex((b) => b.name === name); - if (i < 0) throw new Error(`no binding ${name}`); - return i; -} - const hasDef = (cfg: FunctionCfg, idx: number): boolean => cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(idx))); const hasUse = (cfg: FunctionCfg, idx: number): boolean => @@ -65,13 +35,6 @@ const hasUse = (cfg: FunctionCfg, idx: number): boolean => const hasMayDef = (cfg: FunctionCfg, idx: number): boolean => cfg.blocks.some((bl) => bl.statements?.some((s) => (s.mayDefs ?? []).includes(idx))); -/** Every taint `SiteRecord` harvested across the function's statements. */ -function allSites(cfg: FunctionCfg): SiteRecord[] { - const out: SiteRecord[] = []; - for (const b of cfg.blocks) for (const s of b.statements ?? []) out.push(...(s.sites ?? [])); - return out; -} - describe('PHP CfgVisitor — structure', () => { it('straight-line body: ENTRY → block → EXIT (seq)', () => { const cfg = php.cfgOf(` { expect(reaches(cfg, block(cfg, 'pos();'), after)).toBe(true); expect(reaches(cfg, block(cfg, 'neg();'), after)).toBe(true); expect(reaches(cfg, block(cfg, 'zero();'), after)).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); }); it('alternative colon if/elseif/else (endif) is modeled like the brace form', () => { @@ -140,7 +103,7 @@ describe('PHP CfgVisitor — branching', () => { const after = block(cfg, 'after();'); expect(reaches(cfg, block(cfg, 'pos();'), after)).toBe(true); expect(reaches(cfg, block(cfg, 'zero();'), after)).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); }); }); @@ -152,7 +115,7 @@ describe('PHP CfgVisitor — loops', () => { expect(kinds.has('loop-back')).toBe(true); expect(kinds.has('cond-false')).toBe(true); expect(reaches(cfg, block(cfg, 'body();'), block(cfg, 'after();'))).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); }); it('foreach ($it as $v): loops with cond-true / loop-back / cond-false', () => { @@ -161,7 +124,7 @@ describe('PHP CfgVisitor — loops', () => { expect(kinds.has('cond-true')).toBe(true); expect(kinds.has('loop-back')).toBe(true); expect(kinds.has('cond-false')).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); }); it('while: cond-true / loop-back / cond-false', () => { @@ -170,7 +133,7 @@ describe('PHP CfgVisitor — loops', () => { expect(kinds.has('cond-true')).toBe(true); expect(kinds.has('loop-back')).toBe(true); expect(kinds.has('cond-false')).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); }); it('do-while: body runs before the test (loop-back from the condition)', () => { @@ -182,7 +145,7 @@ describe('PHP CfgVisitor — loops', () => { const body = block(cfg, 'tick();'); expect(reachable(cfg, body)).toBe(true); expect(reaches(cfg, body, block(cfg, 'after();'))).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); }); it('while (true) {} keeps EXIT reverse-reachable AND emits CDG > 0', () => { @@ -199,7 +162,7 @@ describe('PHP CfgVisitor — loops', () => { it('for (;;) {} (no condition) keeps EXIT reverse-reachable', () => { const cfg = php.cfgOf(wrap(`for (;;) { step(); }`)); expect(edgeKinds(cfg).has('cond-false')).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); }); }); @@ -217,7 +180,7 @@ describe('PHP CfgVisitor — switch / match', () => { expect(cfg.edges).toContainEqual({ from: c2, to: c3, kind: 'fallthrough' }); // case 1's break skips case 2's body, but the switch join is reachable. expect(reaches(cfg, block(cfg, 'a();'), block(cfg, 'e();'))).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); }); it('switch without break: no switch-case edge is mislabeled fallthrough at the dispatch', () => { @@ -225,7 +188,7 @@ describe('PHP CfgVisitor — switch / match', () => { // No default → the no-match path reaches the join directly. expect(edgeKinds(cfg).has('switch-case')).toBe(true); expect(reaches(cfg, block(cfg, 'a();'), block(cfg, 'after();'))).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); }); it('match is a value expression (no fallthrough), kept inline — value flows to the assign', () => { @@ -235,7 +198,7 @@ describe('PHP CfgVisitor — switch / match', () => { expect(edgeKinds(cfg).has('switch-case')).toBe(false); // The match value and the return both reach EXIT. expect(reaches(cfg, block(cfg, 'match ($x)'), cfg.exitIndex)).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); }); }); @@ -250,7 +213,7 @@ describe('PHP CfgVisitor — try / catch / finally', () => { expect(reaches(cfg, block(cfg, 'risky();'), fin)).toBe(true); expect(reaches(cfg, block(cfg, 'handle($e)'), fin)).toBe(true); expect(reaches(cfg, fin, block(cfg, 'after();'))).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); }); it('multi-catch type list (TypeError | ValueError) catches and reaches the join', () => { @@ -259,7 +222,7 @@ describe('PHP CfgVisitor — try / catch / finally', () => { ); expect(edgeKinds(cfg).has('throw')).toBe(true); expect(reaches(cfg, block(cfg, 'handle($e)'), block(cfg, 'after();'))).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); }); it('return inside try threads through finally (finally-return completion edge)', () => { @@ -271,7 +234,7 @@ describe('PHP CfgVisitor — try / catch / finally', () => { const ret = block(cfg, 'return early()'); const fin = block(cfg, 'release();'); expect(reaches(cfg, ret, fin)).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); }); }); @@ -309,14 +272,14 @@ describe('PHP CfgVisitor — break N / continue N', () => { }`, ); expect(edgeKinds(cfg).has('continue')).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); }); it('bare break targets the nearest loop', () => { const cfg = php.cfgOf(wrap(`while ($x) { if ($x) { break; } step(); } after();`)); expect(edgeKinds(cfg).has('break')).toBe(true); expect(reaches(cfg, block(cfg, 'break;'), block(cfg, 'after();'))).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); }); }); @@ -415,6 +378,6 @@ describe('PHP CfgVisitor — robustness', () => { it('goto / named label are modeled as straight-line blocks (no crash)', () => { const cfg = php.cfgOf(` { - const b = cfg.blocks.find((bl) => bl.text.includes(substr)); - if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`); - return b.index; -}; - -const edgeKinds = (cfg: FunctionCfg): Set => new Set(cfg.edges.map((e) => e.kind)); - -function reaches(cfg: FunctionCfg, from: number, to: number): boolean { - const adj = new Map(); - for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to); - const seen = new Set([from]); - const stack = [from]; - while (stack.length) { - const n = stack.pop() as number; - if (n === to) return true; - for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx)); - } - return seen.has(to); -} -const reachable = (cfg: FunctionCfg, idx: number): boolean => reaches(cfg, cfg.entryIndex, idx); - -/** Resolve a binding by name → its index in the function's binding table. */ -function bindingIdx(cfg: FunctionCfg, name: string): number { - const i = (cfg.bindings ?? []).findIndex((b) => b.name === name); - if (i < 0) throw new Error(`no binding ${name}`); - return i; -} - const hasDef = (cfg: FunctionCfg, idx: number): boolean => cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(idx))); const hasUse = (cfg: FunctionCfg, idx: number): boolean => diff --git a/gitnexus/test/unit/cfg/ruby-visitor.test.ts b/gitnexus/test/unit/cfg/ruby-visitor.test.ts index 217f19da8..005ec411d 100644 --- a/gitnexus/test/unit/cfg/ruby-visitor.test.ts +++ b/gitnexus/test/unit/cfg/ruby-visitor.test.ts @@ -2,7 +2,14 @@ import { describe, it, expect } from 'vitest'; import { createRequire } from 'node:module'; import { createRubyCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/ruby.js'; import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js'; -import { makeCfgHarness, type CfgHarness } from '../../helpers/cfg-harness.js'; +import { + makeCfgHarness, + type CfgHarness, + block, + edgeKinds, + reaches, + bindingIdx, +} from '../../helpers/cfg-harness.js'; import { isExitReachableFromAllBlocks } from '../../../src/core/ingestion/cfg/post-dominators.js'; import { computeControlDependence } from '../../../src/core/ingestion/cfg/control-dependence.js'; @@ -20,35 +27,6 @@ const rubyGrammar = createRequire(import.meta.url)('tree-sitter-ruby') as Parame const rb: CfgHarness = makeCfgHarness(rubyGrammar, createRubyCfgVisitor(), 'fixture.rb'); -const block = (cfg: FunctionCfg, substr: string): number => { - const b = cfg.blocks.find((bl) => bl.text.includes(substr)); - if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`); - return b.index; -}; - -const edgeKinds = (cfg: FunctionCfg): Set => new Set(cfg.edges.map((e) => e.kind)); - -function reaches(cfg: FunctionCfg, from: number, to: number): boolean { - const adj = new Map(); - for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to); - const seen = new Set([from]); - const stack = [from]; - while (stack.length) { - const n = stack.pop() as number; - if (n === to) return true; - for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx)); - } - return seen.has(to); -} -const reachable = (cfg: FunctionCfg, idx: number): boolean => reaches(cfg, cfg.entryIndex, idx); - -/** Resolve a binding by name → its index in the function's binding table. */ -function bindingIdx(cfg: FunctionCfg, name: string): number { - const i = (cfg.bindings ?? []).findIndex((b) => b.name === name); - if (i < 0) throw new Error(`no binding ${name}`); - return i; -} - const hasDef = (cfg: FunctionCfg, idx: number): boolean => cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(idx))); const hasUse = (cfg: FunctionCfg, idx: number): boolean => diff --git a/gitnexus/test/unit/cfg/rust-visitor.test.ts b/gitnexus/test/unit/cfg/rust-visitor.test.ts index fd0f008ff..3f9ac635b 100644 --- a/gitnexus/test/unit/cfg/rust-visitor.test.ts +++ b/gitnexus/test/unit/cfg/rust-visitor.test.ts @@ -2,7 +2,14 @@ import { describe, it, expect } from 'vitest'; import { createRequire } from 'node:module'; import { createRustCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/rust.js'; import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js'; -import { makeCfgHarness, type CfgHarness } from '../../helpers/cfg-harness.js'; +import { + makeCfgHarness, + type CfgHarness, + block, + edgeKinds, + reaches, + bindingIdx, +} from '../../helpers/cfg-harness.js'; import { isExitReachableFromAllBlocks } from '../../../src/core/ingestion/cfg/post-dominators.js'; import { computeControlDependence } from '../../../src/core/ingestion/cfg/control-dependence.js'; @@ -19,35 +26,6 @@ const rustGrammar = createRequire(import.meta.url)('tree-sitter-rust') as Parame const rust: CfgHarness = makeCfgHarness(rustGrammar, createRustCfgVisitor(), 'fixture.rs'); -const block = (cfg: FunctionCfg, substr: string): number => { - const b = cfg.blocks.find((bl) => bl.text.includes(substr)); - if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`); - return b.index; -}; - -const edgeKinds = (cfg: FunctionCfg): Set => new Set(cfg.edges.map((e) => e.kind)); - -function reaches(cfg: FunctionCfg, from: number, to: number): boolean { - const adj = new Map(); - for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to); - const seen = new Set([from]); - const stack = [from]; - while (stack.length) { - const n = stack.pop() as number; - if (n === to) return true; - for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx)); - } - return seen.has(to); -} -const reachable = (cfg: FunctionCfg, idx: number): boolean => reaches(cfg, cfg.entryIndex, idx); - -/** Resolve a binding by name → its index in the function's binding table. */ -function bindingIdx(cfg: FunctionCfg, name: string): number { - const i = (cfg.bindings ?? []).findIndex((b) => b.name === name); - if (i < 0) throw new Error(`no binding ${name}`); - return i; -} - const hasDef = (cfg: FunctionCfg, idx: number): boolean => cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(idx))); const hasUse = (cfg: FunctionCfg, idx: number): boolean => diff --git a/gitnexus/test/unit/cfg/swift-visitor.test.ts b/gitnexus/test/unit/cfg/swift-visitor.test.ts index ed6f22d62..36b1ef40e 100644 --- a/gitnexus/test/unit/cfg/swift-visitor.test.ts +++ b/gitnexus/test/unit/cfg/swift-visitor.test.ts @@ -2,7 +2,15 @@ import { describe, it, expect } from 'vitest'; import { requireVendoredGrammar } from '../../../src/core/tree-sitter/vendored-grammars.js'; import { createSwiftCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/swift.js'; import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js'; -import { makeCfgHarness, type CfgHarness } from '../../helpers/cfg-harness.js'; +import { + makeCfgHarness, + type CfgHarness, + block, + edgeKinds, + reaches, + reachable, + bindingIdx, +} from '../../helpers/cfg-harness.js'; import { isExitReachableFromAllBlocks } from '../../../src/core/ingestion/cfg/post-dominators.js'; import { computeControlDependence } from '../../../src/core/ingestion/cfg/control-dependence.js'; @@ -19,45 +27,6 @@ const swiftGrammar = requireVendoredGrammar('tree-sitter-swift') as Parameters< const swift: CfgHarness = makeCfgHarness(swiftGrammar, createSwiftCfgVisitor(), 'fixture.swift'); -const block = (cfg: FunctionCfg, substr: string): number => { - const b = cfg.blocks.find((bl) => bl.text.includes(substr)); - if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`); - return b.index; -}; - -const edgeKinds = (cfg: FunctionCfg): Set => new Set(cfg.edges.map((e) => e.kind)); - -function reaches(cfg: FunctionCfg, from: number, to: number): boolean { - const adj = new Map(); - for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to); - const seen = new Set([from]); - const stack = [from]; - while (stack.length) { - const n = stack.pop() as number; - if (n === to) return true; - for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx)); - } - return seen.has(to); -} -const reachable = (cfg: FunctionCfg, idx: number): boolean => reaches(cfg, cfg.entryIndex, idx); - -/** Is EXIT reverse-reachable from every reachable block? (CDG soundness gate.) */ -function exitReachableFromAll(cfg: FunctionCfg): boolean { - for (const b of cfg.blocks) { - if (b.index === cfg.exitIndex) continue; - if (!reachable(cfg, b.index)) continue; // unreachable blocks exempt - if (!reaches(cfg, b.index, cfg.exitIndex)) return false; - } - return true; -} - -/** Resolve a binding by name → its index in the function's binding table. */ -function bindingIdx(cfg: FunctionCfg, name: string): number { - const i = (cfg.bindings ?? []).findIndex((b) => b.name === name); - if (i < 0) throw new Error(`no binding ${name}`); - return i; -} - describe('Swift CfgVisitor — structure', () => { it('straight-line body: ENTRY → block → EXIT (seq)', () => { const cfg = swift.cfgOf(`func f() { a(); b(); c() }`); @@ -187,7 +156,7 @@ describe('Swift CfgVisitor — loops', () => { it('repeat {} while true keeps EXIT reverse-reachable', () => { const cfg = swift.cfgOf(`func f() { repeat { work() } while true }`); expect(edgeKinds(cfg).has('cond-false')).toBe(true); - expect(exitReachableFromAll(cfg)).toBe(true); + expect(isExitReachableFromAllBlocks(cfg)).toBe(true); expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true); }); }); diff --git a/gitnexus/test/unit/cfg/typescript-visitor.test.ts b/gitnexus/test/unit/cfg/typescript-visitor.test.ts index 7dc7711a5..ef77f0556 100644 --- a/gitnexus/test/unit/cfg/typescript-visitor.test.ts +++ b/gitnexus/test/unit/cfg/typescript-visitor.test.ts @@ -7,6 +7,7 @@ import { TS_FUNCTION_TYPES, } from '../../../src/core/ingestion/cfg/visitors/typescript.js'; import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js'; +import { block, edgeKinds, reaches, reachable } from '../../helpers/cfg-harness.js'; // U2 — the TS/JS CfgVisitor, one hazard per test. Each fixture's distinctive // statement text (markerWork(), handleErr(), cleanup(), …) lets us find the @@ -46,30 +47,6 @@ function cfgOf(code: string, index = 0): FunctionCfg { return cfg; } -const block = (cfg: FunctionCfg, substr: string): number => { - const b = cfg.blocks.find((bl) => bl.text.includes(substr)); - if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`); - return b.index; -}; - -const edgeKinds = (cfg: FunctionCfg): Set => new Set(cfg.edges.map((e) => e.kind)); - -/** Does control reach `to` from `from` following edges? */ -function reaches(cfg: FunctionCfg, from: number, to: number): boolean { - const adj = new Map(); - for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to); - const seen = new Set([from]); - const stack = [from]; - while (stack.length) { - const n = stack.pop() as number; - if (n === to) return true; - for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx)); - } - return seen.has(to); -} - -const reachable = (cfg: FunctionCfg, idx: number): boolean => reaches(cfg, cfg.entryIndex, idx); - describe('TS/JS CfgVisitor — structure', () => { it('straight-line body: ENTRY → block → EXIT', () => { const cfg = cfgOf(`function f() { a(); b(); c(); }`); diff --git a/gitnexus/test/unit/cfg/vue-visitor.test.ts b/gitnexus/test/unit/cfg/vue-visitor.test.ts index ede313554..9301b7f96 100644 --- a/gitnexus/test/unit/cfg/vue-visitor.test.ts +++ b/gitnexus/test/unit/cfg/vue-visitor.test.ts @@ -3,7 +3,7 @@ import fs from 'fs'; import path from 'path'; import TypeScript from 'tree-sitter-typescript'; import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js'; -import { makeCfgHarness } from '../../helpers/cfg-harness.js'; +import { makeCfgHarness, block, edgeKinds, reaches } from '../../helpers/cfg-harness.js'; import { extractVueScript } from '../../../src/core/ingestion/vue-sfc-extractor.js'; import { getProvider } from '../../../src/core/ingestion/languages/index.js'; import { SupportedLanguages } from '../../../src/config/supported-languages.js'; @@ -34,28 +34,6 @@ function cfgsOfSfc(sfc: string): FunctionCfg[] { const FIXTURE = path.join(__dirname, '../../integration/cfg/fixtures/vue-hazards.vue'); -const block = (cfg: FunctionCfg, substr: string): number => { - const b = cfg.blocks.find((bl) => bl.text.includes(substr)); - if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`); - return b.index; -}; - -const edgeKinds = (cfg: FunctionCfg): Set => new Set(cfg.edges.map((e) => e.kind)); - -/** Does control reach `to` from `from` following edges? */ -function reaches(cfg: FunctionCfg, from: number, to: number): boolean { - const adj = new Map(); - for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to); - const seen = new Set([from]); - const stack = [from]; - while (stack.length) { - const n = stack.pop() as number; - if (n === to) return true; - for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx)); - } - return seen.has(to); -} - describe('Vue CfgVisitor reuse — SFC