test(cfg): consolidate copied visitor-test helpers into cfg-harness (#2195 U8)

The 13 *-visitor.test.ts files each copied a byte-identical set of CFG-shape
helpers (edgeKinds/block/reaches/reachable/bindingIdx/allSites/hasAnySites,
~380 lines total). Export them once from test/helpers/cfg-harness.ts and import
per file (only the subset each references). Also drop each file's local
exitReachableFromAll — a re-implementation of the production
isExitReachableFromAllBlocks (semantically identical: false iff some
entry-reachable non-EXIT block can't reach EXIT) — and point its live call
sites at the already-imported production function. Pure test-only mechanical
move, behavior-preserving: tsc clean, test/unit/cfg/ 613 passed unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-06-14 16:54:45 +00:00
parent ccbfef2eb6
commit f47e952ba1
14 changed files with 180 additions and 513 deletions

View file

@ -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<string> => 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<number, number[]>();
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));
}

View file

@ -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<string> => new Set(cfg.edges.map((e) => e.kind));
function reaches(cfg: FunctionCfg, from: number, to: number): boolean {
const adj = new Map<number, number[]>();
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', () => {

View file

@ -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<string> => new Set(cfg.edges.map((e) => e.kind));
function reaches(cfg: FunctionCfg, from: number, to: number): boolean {
const adj = new Map<number, number[]>();
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', () => {

View file

@ -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<string> => new Set(cfg.edges.map((e) => e.kind));
function reaches(cfg: FunctionCfg, from: number, to: number): boolean {
const adj = new Map<number, number[]>();
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 =>

View file

@ -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<string> => new Set(cfg.edges.map((e) => e.kind));
function reaches(cfg: FunctionCfg, from: number, to: number): boolean {
const adj = new Map<number, number[]>();
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`;

View file

@ -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<string> => new Set(cfg.edges.map((e) => e.kind));
function reaches(cfg: FunctionCfg, from: number, to: number): boolean {
const adj = new Map<number, number[]>();
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 =>

View file

@ -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<string> => new Set(cfg.edges.map((e) => e.kind));
function reaches(cfg: FunctionCfg, from: number, to: number): boolean {
const adj = new Map<number, number[]>();
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);
});
});

View file

@ -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 => `<?php function f($x) { ${body} }`;
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<string> => new Set(cfg.edges.map((e) => e.kind));
function reaches(cfg: FunctionCfg, from: number, to: number): boolean {
const adj = new Map<number, number[]>();
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(`<?php function f() { a(); b(); c(); }`);
@ -127,7 +90,7 @@ describe('PHP CfgVisitor — branching', () => {
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(`<?php function f() { start(); goto end; end: done(); }`);
expect(reachable(cfg, block(cfg, 'done()'))).toBe(true);
expect(exitReachableFromAll(cfg)).toBe(true);
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
});
});

View file

@ -2,7 +2,15 @@ import { describe, it, expect } from 'vitest';
import { createRequire } from 'node:module';
import { createPythonCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/python.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';
@ -20,35 +28,6 @@ const pyGrammar = createRequire(import.meta.url)('tree-sitter-python') as Parame
const py: CfgHarness = makeCfgHarness(pyGrammar, createPythonCfgVisitor(), 'fixture.py');
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<string> => new Set(cfg.edges.map((e) => e.kind));
function reaches(cfg: FunctionCfg, from: number, to: number): boolean {
const adj = new Map<number, number[]>();
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 =>

View file

@ -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<string> => new Set(cfg.edges.map((e) => e.kind));
function reaches(cfg: FunctionCfg, from: number, to: number): boolean {
const adj = new Map<number, number[]>();
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 =>

View file

@ -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<string> => new Set(cfg.edges.map((e) => e.kind));
function reaches(cfg: FunctionCfg, from: number, to: number): boolean {
const adj = new Map<number, number[]>();
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 =>

View file

@ -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<string> => new Set(cfg.edges.map((e) => e.kind));
function reaches(cfg: FunctionCfg, from: number, to: number): boolean {
const adj = new Map<number, number[]>();
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);
});
});

View file

@ -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<string> => 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<number, number[]>();
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(); }`);

View file

@ -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<string> => 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<number, number[]>();
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 <script> → TypeScript CFG', () => {
it('extracts the <script setup> block and builds one CFG per script function', () => {
const sfc = fs.readFileSync(FIXTURE, 'utf8');