mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
Unconditional goto-cycles (C/C++/C#/Go) wire a backward seq edge with no
structural exit-escape edge, so EXIT becomes non-reverse-reachable and
emitFileCdg silently skipped ALL control-dependence for the function.
New cfg/synthetic-escape.ts: a pure deterministic SCC routine (iterative
Tarjan, sorted adjacency) + augmentForPostDom(cfg). No-op when EXIT is
already reverse-reachable (terminating fns + visitor-escaped loops are
byte-identical — returns the same object). Otherwise it batch-bridges
every exit-less SCC by adding an ANALYSIS-ONLY escape edge from the SCC's
controlling block (highest out-degree branch; lowest-index tie-break) to
EXIT, on a shallow-cloned FunctionCfg — never mutating persisted
cfg.edges. emitFileCdg threads that augmented view through BOTH
isExitReachableFromAllBlocks AND computeControlDependence (the Ferrante
walk re-reads cfg.edges, so a tree-only augmentation would be wrong).
Precision (anti-masking): only a trapped region containing a control
point (>=2-successor block) is bridged — a branch-less trapped region
carries no recoverable control-dependence and is indistinguishable from a
genuine construction anomaly, so it stays on the skip path (the existing
disconnected-block skip test still skips, skippedUnsoundFunctions===1). A
residual non-cycle dangling block is never bridged.
repro `void handler(int a){ start: if(a>0){work();} goto start; }`:
before exitReachable=false/CDG=0 → after one synthetic 2->1 edge,
exitReachable=true, exact CDG = {2->2:T,2->2:F,2->3:T,2->4:T,2->4:F}
(pinned exactly, not CDG>0 — catches a wrong representative). AC2 property
test extended to the augmented graph; per-language goto-cycle regressions
(C/C++/C#/Go). 199 cfg tests green; bench --check fingerprints unchanged
(analysis-only, zero persisted drift).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
458 lines
18 KiB
TypeScript
458 lines
18 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
computeControlDependence,
|
|
type ControlDepEdge,
|
|
type CdgLabel,
|
|
} from '../../../src/core/ingestion/cfg/control-dependence.js';
|
|
import {
|
|
computePostDominators,
|
|
isExitReachableFromAllBlocks,
|
|
postDominates,
|
|
} from '../../../src/core/ingestion/cfg/post-dominators.js';
|
|
import { augmentForPostDom } from '../../../src/core/ingestion/cfg/synthetic-escape.js';
|
|
import type {
|
|
BasicBlockData,
|
|
CfgEdgeData,
|
|
CfgEdgeKind,
|
|
FunctionCfg,
|
|
} from '../../../src/core/ingestion/cfg/types.js';
|
|
import Parser from 'tree-sitter';
|
|
import TypeScript from 'tree-sitter-typescript';
|
|
import { collectFunctionCfgs } from '../../../src/core/ingestion/cfg/collect.js';
|
|
import { getProvider } from '../../../src/core/ingestion/languages/index.js';
|
|
import { SupportedLanguages } from '../../../src/config/supported-languages.js';
|
|
|
|
// U3 (#2085 M5) — Ferrante §3.1.1 control dependence over the post-dom tree.
|
|
// Hand-built CFG literals plus real-parser regression tests. The labelled
|
|
// expected edge sets ARE the spec; the property test (AC2) cross-checks the
|
|
// tree-walk against a reference that computes post-dominance INDEPENDENTLY (by
|
|
// node-removal reachability, sharing NO code with post-dominators.ts), so a
|
|
// post-dominator *direction* bug cannot pass both (#2188 F4).
|
|
|
|
// ── hand-built CFG helper (edges carry a kind so labels can be asserted) ─────
|
|
|
|
function mkCfg(
|
|
blockCount: number,
|
|
edges: [number, number, CfgEdgeKind][],
|
|
opts: { entry?: number; exit?: number } = {},
|
|
): FunctionCfg {
|
|
const entry = opts.entry ?? 0;
|
|
const exit = opts.exit ?? blockCount - 1;
|
|
const blocks: BasicBlockData[] = Array.from({ length: blockCount }, (_, i) => ({
|
|
index: i,
|
|
startLine: i + 1,
|
|
endLine: i + 1,
|
|
text: '',
|
|
kind: i === entry ? 'entry' : i === exit ? 'exit' : 'normal',
|
|
}));
|
|
const cfgEdges: CfgEdgeData[] = edges.map(([from, to, kind]) => ({ from, to, kind }));
|
|
return {
|
|
filePath: 't.ts',
|
|
functionStartLine: 1,
|
|
functionStartColumn: 0,
|
|
entryIndex: entry,
|
|
exitIndex: exit,
|
|
blocks,
|
|
edges: cfgEdges,
|
|
};
|
|
}
|
|
|
|
const ser = (e: ControlDepEdge): string => `${e.controllerBlock}->${e.dependentBlock}:${e.label}`;
|
|
const serAll = (edges: readonly ControlDepEdge[]): string[] => edges.map(ser);
|
|
|
|
/**
|
|
* Build a successor adjacency list for a CFG (in-range edges only).
|
|
*/
|
|
function succsOf(cfg: FunctionCfg): number[][] {
|
|
const n = cfg.blocks.length;
|
|
const succs: number[][] = Array.from({ length: n }, () => []);
|
|
for (const e of cfg.edges)
|
|
if (e.from >= 0 && e.from < n && e.to >= 0 && e.to < n) succs[e.from].push(e.to);
|
|
return succs;
|
|
}
|
|
|
|
/**
|
|
* INDEPENDENT post-dominance via node-removal reachability — shares NO code with
|
|
* post-dominators.ts (that is the whole point: it must catch a CHK *direction*
|
|
* bug that a shared-substrate reference would mirror, #2188 F4). `p`
|
|
* post-dominates `b` iff every path from `b` to EXIT passes through `p`:
|
|
* reflexive (`p === b`), else true exactly when EXIT is unreachable from `b`
|
|
* once `p` is removed (AND `b` can reach EXIT at all). Defined only for the
|
|
* exit-reachable fixtures used below. A raw exit-unreachable cycle (#2188 F2)
|
|
* would be unsound, so the AC2 set now feeds the synthetic-escape pass's
|
|
* AUGMENTED view of every goto-cycle fixture (#2197 U1) — once bridged it IS
|
|
* exit-reachable and the Ferrante walk must equal this independent reference.
|
|
*/
|
|
function independentPostDom(cfg: FunctionCfg, succs: number[][], p: number, b: number): boolean {
|
|
if (p === b) return true;
|
|
const exit = cfg.exitIndex;
|
|
const reach = (avoid: number): boolean => {
|
|
if (b === avoid) return false;
|
|
const seen = new Set<number>([b]);
|
|
const stack = [b];
|
|
while (stack.length) {
|
|
const x = stack.pop()!;
|
|
if (x === exit) return true;
|
|
for (const y of succs[x]) {
|
|
if (y === avoid || seen.has(y)) continue;
|
|
seen.add(y);
|
|
stack.push(y);
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
// p post-dominates b ⇔ b reaches EXIT, but cannot reach it with p removed.
|
|
return reach(-1) && !reach(p);
|
|
}
|
|
|
|
/**
|
|
* Reference control-dependence pairs from the Ferrante definition, using the
|
|
* INDEPENDENT post-dominance above: N is control-dependent on A iff some CFG
|
|
* edge A→B has N post-dominating B while N does NOT strictly post-dominate A.
|
|
* Label-agnostic (distinct "A->N" pairs) — the pure definition has no sense.
|
|
*/
|
|
function referencePairs(cfg: FunctionCfg): Set<string> {
|
|
const n = cfg.blocks.length;
|
|
const succs = succsOf(cfg);
|
|
const pd = (x: number, y: number): boolean => independentPostDom(cfg, succs, x, y);
|
|
const pairs = new Set<string>();
|
|
for (let a = 0; a < n; a++) {
|
|
for (const b of succs[a]) {
|
|
if (pd(b, a)) continue; // edge is not a control point
|
|
for (let nn = 0; nn < n; nn++) {
|
|
const nPostDomB = pd(nn, b);
|
|
const nStrictlyPostDomA = nn !== a && pd(nn, a);
|
|
if (nPostDomB && !nStrictlyPostDomA) pairs.add(`${a}->${nn}`);
|
|
}
|
|
}
|
|
}
|
|
return pairs;
|
|
}
|
|
|
|
describe('computeControlDependence — Ferrante §3.1.1', () => {
|
|
it('diamond: each arm is control-dependent on the branch with its own T/F label', () => {
|
|
// 0(branch) → 1(then, T), 2(else, F); 1,2 → 3(join) → 4(exit)
|
|
const cfg = mkCfg(5, [
|
|
[0, 1, 'cond-true'],
|
|
[0, 2, 'cond-false'],
|
|
[1, 3, 'seq'],
|
|
[2, 3, 'seq'],
|
|
[3, 4, 'seq'],
|
|
]);
|
|
const { edges } = computeControlDependence(cfg);
|
|
expect(serAll(edges).sort()).toEqual(['0->1:T', '0->2:F']);
|
|
// the join (3) post-dominates the branch, so it depends on nothing
|
|
expect(edges.some((e) => e.dependentBlock === 3)).toBe(false);
|
|
});
|
|
|
|
it('guard clause: the post-guard body is control-dependent on the guard (the #559/#2086 case)', () => {
|
|
// function f(x){ if(!ok(x)) return; use(x); }
|
|
// 0(entry) → 1(guard); 1 → 2(return, T) , 1 → 3(use, F); 2,3 → 4(exit)
|
|
const cfg = mkCfg(5, [
|
|
[0, 1, 'seq'],
|
|
[1, 2, 'cond-true'], // !ok(x) → return
|
|
[1, 3, 'cond-false'], // else → use(x)
|
|
[2, 4, 'return'],
|
|
[3, 4, 'seq'],
|
|
]);
|
|
const { edges } = computeControlDependence(cfg);
|
|
// use(x) (block 3) runs only when the guard condition is false → label 'F'
|
|
expect(serAll(edges).sort()).toEqual(['1->2:T', '1->3:F']);
|
|
});
|
|
|
|
it('straight-line function (no branches) has no control dependence', () => {
|
|
const cfg = mkCfg(3, [
|
|
[0, 1, 'seq'],
|
|
[1, 2, 'seq'],
|
|
]);
|
|
expect(computeControlDependence(cfg).edges).toEqual([]);
|
|
});
|
|
|
|
it('while loop: the body depends on the header, and the header is control-dependent on itself', () => {
|
|
// 0(entry) → 1(header); 1 → 2(body, T) , 1 → 3(exit, F); 2 → 1 (back-edge)
|
|
const cfg = mkCfg(4, [
|
|
[0, 1, 'seq'],
|
|
[1, 2, 'cond-true'],
|
|
[2, 1, 'loop-back'],
|
|
[1, 3, 'cond-false'],
|
|
]);
|
|
const { edges } = computeControlDependence(cfg);
|
|
// body(2) control-dep on header(1); header(1) control-dep on itself (the
|
|
// loop predicate gates its own re-execution — standard PDG behavior).
|
|
expect(serAll(edges).sort()).toEqual(['1->1:T', '1->2:T']);
|
|
});
|
|
|
|
it('switch: every case body is control-dependent on the dispatch (all T in M5)', () => {
|
|
// 0(entry) → 1(dispatch); 1 → 2,3,4 (cases); 2,3,4 → 5(exit)
|
|
const cfg = mkCfg(6, [
|
|
[0, 1, 'seq'],
|
|
[1, 2, 'switch-case'],
|
|
[1, 3, 'switch-case'],
|
|
[1, 4, 'switch-case'],
|
|
[2, 5, 'break'],
|
|
[3, 5, 'break'],
|
|
[4, 5, 'break'],
|
|
]);
|
|
const { edges } = computeControlDependence(cfg);
|
|
expect(serAll(edges).sort()).toEqual(['1->2:T', '1->3:T', '1->4:T']);
|
|
});
|
|
|
|
it('exit-less loop (KTD5): terminates and stays in-range, but the result is KNOWN-UNSOUND (#2188 F2)', () => {
|
|
// No block can reach EXIT (block 3), so every ipdom is NO_IPDOM and the
|
|
// Ferrante walk degenerates to one edge per control point. The termination /
|
|
// in-range invariants MUST hold (the walk hits NO_IPDOM immediately). The
|
|
// emitted dependence SET, however, is NOT a sound over-approximation: it both
|
|
// drops real dependences and invents spurious ones in exit-unreachable
|
|
// regions (#2188 F2). This test pins the degenerate output to document that
|
|
// behavior, NOT to bless it; the labels here are likewise indeterminate
|
|
// (no controller carries an explicit cond-true/cond-false arm, so the
|
|
// fall-through complement resolves to 'F'). The current TS visitor never
|
|
// produces such a region (every loop gets a structural header→loopExit edge).
|
|
const cfg = mkCfg(4, [
|
|
[0, 1, 'seq'],
|
|
[1, 2, 'seq'],
|
|
[2, 1, 'loop-back'],
|
|
]);
|
|
const { edges } = computeControlDependence(cfg);
|
|
expect(serAll(edges).sort()).toEqual(['0->1:F', '1->2:F', '2->1:F']);
|
|
for (const e of edges) {
|
|
expect(e.controllerBlock).toBeGreaterThanOrEqual(0);
|
|
expect(e.controllerBlock).toBeLessThan(cfg.blocks.length);
|
|
expect(e.dependentBlock).toBeGreaterThanOrEqual(0);
|
|
expect(e.dependentBlock).toBeLessThan(cfg.blocks.length);
|
|
}
|
|
});
|
|
|
|
it('is deterministic (stable sorted order across runs)', () => {
|
|
const make = (): FunctionCfg =>
|
|
mkCfg(5, [
|
|
[0, 1, 'cond-true'],
|
|
[0, 2, 'cond-false'],
|
|
[1, 3, 'seq'],
|
|
[2, 3, 'seq'],
|
|
[3, 4, 'seq'],
|
|
]);
|
|
expect(serAll(computeControlDependence(make()).edges)).toEqual(
|
|
serAll(computeControlDependence(make()).edges),
|
|
);
|
|
});
|
|
|
|
describe('AC2 — a control dependence exists iff post-dominance fails for the branch', () => {
|
|
const fixtures: Record<string, FunctionCfg> = {
|
|
diamond: mkCfg(5, [
|
|
[0, 1, 'cond-true'],
|
|
[0, 2, 'cond-false'],
|
|
[1, 3, 'seq'],
|
|
[2, 3, 'seq'],
|
|
[3, 4, 'seq'],
|
|
]),
|
|
guard: mkCfg(5, [
|
|
[0, 1, 'seq'],
|
|
[1, 2, 'cond-true'],
|
|
[1, 3, 'cond-false'],
|
|
[2, 4, 'return'],
|
|
[3, 4, 'seq'],
|
|
]),
|
|
loop: mkCfg(4, [
|
|
[0, 1, 'seq'],
|
|
[1, 2, 'cond-true'],
|
|
[2, 1, 'loop-back'],
|
|
[1, 3, 'cond-false'],
|
|
]),
|
|
// Escaped `goto`-cycle (#2197 U1): after the synthetic-escape pass the
|
|
// exit-unreachable cycle is bridged and the dependence set becomes a SOUND
|
|
// over-approximation, so it now joins the AC2 set (the obsolete
|
|
// exit-unreachable exclusion is lifted — see the AUGMENTED-view note below).
|
|
// Repro shape: ENTRY=0, EXIT=1, b2=`(a>0)` predicate, b3=`work()`,
|
|
// b4=`goto start`; the `if` predicate (b2) is the only control point.
|
|
gotoCycle: augmentForPostDom(
|
|
mkCfg(
|
|
5,
|
|
[
|
|
[0, 2, 'seq'],
|
|
[2, 3, 'cond-true'],
|
|
[2, 4, 'seq'],
|
|
[3, 4, 'seq'],
|
|
[4, 2, 'seq'],
|
|
],
|
|
{ entry: 0, exit: 1 },
|
|
),
|
|
),
|
|
// Spine before the goto label — ENTRY + straight-line stmts are in the
|
|
// exit-unreachable closure but must reach EXIT after the bridge.
|
|
gotoCycleSpine: augmentForPostDom(
|
|
mkCfg(
|
|
7,
|
|
[
|
|
[0, 2, 'seq'],
|
|
[2, 3, 'seq'],
|
|
[3, 4, 'seq'],
|
|
[4, 5, 'cond-true'],
|
|
[4, 6, 'seq'],
|
|
[5, 6, 'seq'],
|
|
[6, 4, 'seq'],
|
|
],
|
|
{ entry: 0, exit: 1 },
|
|
),
|
|
),
|
|
// nested if: outer branch (0) → inner branch (1) or outer-else (5);
|
|
// inner branch → 2/3 → inner join (4); 4 and 5 → outer join (6, exit).
|
|
nestedIf: mkCfg(
|
|
7,
|
|
[
|
|
[0, 1, 'cond-true'],
|
|
[0, 5, 'cond-false'],
|
|
[1, 2, 'cond-true'],
|
|
[1, 3, 'cond-false'],
|
|
[2, 4, 'seq'],
|
|
[3, 4, 'seq'],
|
|
[4, 6, 'seq'],
|
|
[5, 6, 'seq'],
|
|
],
|
|
{ entry: 0, exit: 6 },
|
|
),
|
|
switchStmt: mkCfg(6, [
|
|
[0, 1, 'seq'],
|
|
[1, 2, 'switch-case'],
|
|
[1, 3, 'switch-case'],
|
|
[1, 4, 'switch-case'],
|
|
[2, 5, 'break'],
|
|
[3, 5, 'break'],
|
|
[4, 5, 'break'],
|
|
]),
|
|
};
|
|
|
|
it.each(Object.keys(fixtures))(
|
|
'%s: is exit-reachable from all blocks (the AC2 reference is well-defined)',
|
|
(name) => {
|
|
// Every AC2 fixture — including the AUGMENTED goto-cycle ones (#2197 U1)
|
|
// — must be exit-reachable, else the node-removal reference is undefined.
|
|
expect(isExitReachableFromAllBlocks(fixtures[name])).toBe(true);
|
|
},
|
|
);
|
|
|
|
it.each(Object.keys(fixtures))(
|
|
'%s: tree-walk pair set equals the brute-force reference',
|
|
(name) => {
|
|
const cfg = fixtures[name];
|
|
const { edges } = computeControlDependence(cfg);
|
|
const walkPairs = new Set(edges.map((e) => `${e.controllerBlock}->${e.dependentBlock}`));
|
|
expect(walkPairs).toEqual(referencePairs(cfg));
|
|
},
|
|
);
|
|
|
|
it.each(Object.keys(fixtures))(
|
|
'%s: for every CFG edge, it yields a dependent IFF the target does not post-dominate the source',
|
|
(name) => {
|
|
const cfg = fixtures[name];
|
|
const tree = computePostDominators(cfg);
|
|
const { edges } = computeControlDependence(cfg);
|
|
for (const e of cfg.edges) {
|
|
const failsPostDom = !postDominates(tree, e.to, e.from);
|
|
// does THIS edge's source appear as a controller with at least one
|
|
// dependent reachable from its target? Equivalent statement of AC2:
|
|
// post-dominance failing for (from→to) ⇔ `from` is a control point.
|
|
const fromIsControlPoint = edges.some((c) => c.controllerBlock === e.from);
|
|
if (failsPostDom) {
|
|
expect(
|
|
fromIsControlPoint,
|
|
`${name}: edge ${e.from}->${e.to} should make ${e.from} a control point`,
|
|
).toBe(true);
|
|
}
|
|
// and a self-post-dominating edge (to post-dominates from) can never
|
|
// be the SOLE reason a block is a control point: if from has only
|
|
// post-dominating successors it controls nothing.
|
|
}
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('computeControlDependence — maxEdges materialization ceiling (#2188)', () => {
|
|
// A switch dispatch yields three deduped CDG edges (1->2/3/4, all 'T').
|
|
const switchCfg = (): FunctionCfg =>
|
|
mkCfg(6, [
|
|
[0, 1, 'seq'],
|
|
[1, 2, 'switch-case'],
|
|
[1, 3, 'switch-case'],
|
|
[1, 4, 'switch-case'],
|
|
[2, 5, 'break'],
|
|
[3, 5, 'break'],
|
|
[4, 5, 'break'],
|
|
]);
|
|
|
|
it('stops at the ceiling and reports truncated (deterministic prefix)', () => {
|
|
const r = computeControlDependence(switchCfg(), undefined, 2);
|
|
expect(r.truncated).toBe(true);
|
|
expect(r.edges).toHaveLength(2);
|
|
// the prefix is still sorted/deduped, a valid subset of the full result
|
|
for (const e of r.edges) expect(['T', 'F']).toContain(e.label);
|
|
});
|
|
|
|
it('maxEdges of 0 means unbounded (full result, not truncated)', () => {
|
|
const r = computeControlDependence(switchCfg(), undefined, 0);
|
|
expect(r.truncated).toBe(false);
|
|
expect(serAll(r.edges).sort()).toEqual(['1->2:T', '1->3:T', '1->4:T']);
|
|
});
|
|
|
|
it('a ceiling at/above the true count is not truncated', () => {
|
|
const r = computeControlDependence(switchCfg(), undefined, 3);
|
|
expect(r.truncated).toBe(false);
|
|
expect(r.edges).toHaveLength(3);
|
|
});
|
|
});
|
|
|
|
describe('#2188 F1 — branch-label correctness on the REAL TS visitor (regression)', () => {
|
|
// The label is the AC3 "under what condition does X run?" answer. The bug:
|
|
// branchSense inferred it from the edge KIND alone, but the M1 visitor wires a
|
|
// condition's fall-through FALSE arm as `seq`/`loop-back` (not `cond-false`),
|
|
// so guard clauses / loop break got 'T' instead of 'F'. These tests run the
|
|
// REAL parser+visitor (the hand-built tests above used a fictional `cond-false`
|
|
// edge and could not catch the regression).
|
|
const tsVisitor = getProvider(SupportedLanguages.TypeScript).cfgVisitor;
|
|
const parser = new Parser();
|
|
if (tsVisitor) parser.setLanguage(TypeScript.typescript);
|
|
|
|
function cdgOf(code: string): { cfg: FunctionCfg; edges: readonly ControlDepEdge[] } {
|
|
if (!tsVisitor) throw new Error('no cfgVisitor');
|
|
const cfgs = collectFunctionCfgs(parser.parse(code).rootNode, tsVisitor, 't.ts').cfgs;
|
|
expect(cfgs.length).toBe(1);
|
|
return { cfg: cfgs[0], edges: computeControlDependence(cfgs[0]).edges };
|
|
}
|
|
const labelOf = (
|
|
edges: readonly ControlDepEdge[],
|
|
controller: number,
|
|
dependent: number,
|
|
): CdgLabel | undefined =>
|
|
edges.find((e) => e.controllerBlock === controller && e.dependentBlock === dependent)?.label;
|
|
|
|
it("guard clause: post-guard body runs on the guard's FALSE (seq) arm → 'F'", () => {
|
|
const { cfg, edges } = cdgOf(`function f(x){ if (!ok(x)) return; use(x); }`);
|
|
const guard = cfg.blocks.find((b) => b.text.includes('ok(x)'))!;
|
|
const use = cfg.blocks.find((b) => b.text.includes('use(x)'))!;
|
|
expect(labelOf(edges, guard.index, use.index)).toBe('F');
|
|
});
|
|
|
|
it("do/while: body runs on the bottom-test's TRUE (loop-back) arm → 'T'", () => {
|
|
const { cfg, edges } = cdgOf(`function f(){ do { body(); } while (c()); }`);
|
|
const test = cfg.blocks.find((b) => b.text.includes('c()'))!;
|
|
const body = cfg.blocks.find((b) => b.text.includes('body()'))!;
|
|
expect(labelOf(edges, test.index, body.index)).toBe('T');
|
|
});
|
|
|
|
it("while+break: post-break tail runs on the if's FALSE (seq) arm → 'F'", () => {
|
|
const { cfg, edges } = cdgOf(`function f(o,i){ while (o) { if (i) break; tail(); } }`);
|
|
const ifCond = cfg.blocks.find((b) => b.text === '(i)')!;
|
|
const tail = cfg.blocks.find((b) => b.text.includes('tail()'))!;
|
|
expect(labelOf(edges, ifCond.index, tail.index)).toBe('F');
|
|
});
|
|
|
|
it("if/else still labels both arms correctly (no regression) → then 'T', else 'F'", () => {
|
|
const { cfg, edges } = cdgOf(`function f(x){ if (x) { a(); } else { b(); } }`);
|
|
const cond = cfg.blocks.find((b) => b.text === '(x)')!;
|
|
const thenB = cfg.blocks.find((b) => b.text.includes('a()'))!;
|
|
const elseB = cfg.blocks.find((b) => b.text.includes('b()'))!;
|
|
expect(labelOf(edges, cond.index, thenB.index)).toBe('T');
|
|
expect(labelOf(edges, cond.index, elseB.index)).toBe('F');
|
|
});
|
|
});
|