GitNexus/gitnexus/test/unit/cfg/csharp-visitor.test.ts
Gergő Magyar 999cc9f913
fix(deps): upgrade native tree-sitter to 0.25.1 (#3531)
* fix(deps): upgrade native tree-sitter to 0.25.1

* fix(tree-sitter): preserve sync loading and update verified capture baselines

Route C# scope queries through the synchronous native grammar loader so the compiled analyze module remains require-compatible on Node 22.18. Refresh only the audited empty-Python-module and renamed-PHP-fixture captures, and pin the runtime assertion to 0.25.1.

Validated 479 targeted tests, all 16 scope-capture benchmark cases, Python capture/import guards, compiled C# worker parsing, build, typecheck and formatting. Performance budgets are unchanged.

* fix(tree-sitter): refresh upstream vendors and preserve parser semantics

* fix(ci): refresh prebuilds and support Windows ARM

Refresh macOS, Linux ARM, and Windows x64 grammar binaries from the successful native CI builds. Pin node-gyp 12.4.0 so the Windows ARM jobs recognize Visual Studio 2026.

Use local Git filtering for E2E to avoid PR-files API timeouts on generated parser diffs.

Validation: 143 focused tests, Actionlint, and all 16 scope-capture benchmark checks passed. Full npm test run started; native database suites still running.

* fix(ci): refresh Windows ARM grammar prebuilds

Import all seven Windows ARM grammar artifacts from successful native build run 37965761760. Artifact filenames and SHA-256 hashes verified; all 42 native build jobs passed.

Validation: 31 prebuild coverage tests passed.

* fix(bench): stabilize scope capture timing guard

Warm each input size seven times and use 21 median samples while retaining isolated timing blocks. Yield between languages so native finalizers release allocations outside the measured sections. Keep all fixture sizes, fingerprints, scaling formulas, and budgets unchanged.

Validation: all 16 language guards passed with a 2 GB heap limit (1.38 deep-chain ratio; 1518 MB peak RSS). Linear and quadratic controls passed their expected guard outcomes. Formatting and graph change analysis passed. Full npm test is running without failures so far.

* fix(parser): reduce TypeScript capture traversal cost

* fix(test): await parser worker readiness before teardown

* fix(parser): load C# synchronously in workers

* fix(grpc): resolve native proto detections against their own file

With the native proto grammar active, sibling .proto files that declare
the same short service name under different packages tied in the
directory-based resolveProtoConflict heuristic and both providers were
dropped. A proto-origin detection already knows its defining file, so
use that file's entry before falling back to the heuristic.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci(packaging): assert native grammars load from the installed tarball

The packaged-install smoke only ran `gitnexus --version`, so a grammar
that failed to load from the published package (bundled runtime, patched
peer manifests, vendored prebuilds, C# sync loader, Protobuf wrapper)
would ship silently; the dev-tree ABI smoke also accepts vendored
grammars that report unavailable. Parse a sample for every registered
grammar plus the native Protobuf plugin from the installed package, on
the main thread and in a worker thread.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci(tree-sitter): track compatibility workarounds instead of 0.25 readiness

The daily readiness report still targeted tree-sitter@0.25.0, so after the
upgrade it would keep reporting "Blocked" and never close its tracking issue.
Repurpose it to track what we carry for the bundled runtime: derive the target
from the package.json pin, list each pack-time peer patch from
prepare-tree-sitter-bundle.cjs with its drop condition (bump, wait for an
upstream release, transitive, or intentionally pinned), and reword the
buckets. The workflow adopts the issue opened under the old title.

Also update the grammar-update-monitor header and auto-PR text, which still
described the 0.21.1 runtime and a tree-sitter-c hold.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(dart): assert both nested labels target the loop; fix Zig call-shape doc

The nested-label CFG test only jumped to `outer` and checked reachability,
which still passes when a jump is retargeted to the inner loop (its exit
reaches the outer header). Jump to both labels and assert the exact
`continue` edges; dropping the inner label now fails the test.

Update the Zig callable-flow comment: call_expression now wraps actuals in a
required `arguments:` field.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:07:59 +03:00

524 lines
24 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import csGrammar from 'tree-sitter-c-sharp/bindings/node/index.js';
import { createCsharpCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/csharp.js';
import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.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';
// U3 — the C# CfgVisitor, one hazard per test (KTD5: real-parser regression,
// NOT snapshot-pinning). Each fixture's distinctive statement text (step(),
// done(), handle(e), …) lets us locate the block for a region by text and assert
// the control-flow topology around it.
const cs: CfgHarness = makeCfgHarness(csGrammar, createCsharpCfgVisitor(), 'fixture.cs');
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 =>
cfg.blocks.some((bl) => bl.statements?.some((s) => s.uses.includes(idx)));
const hasMayDef = (cfg: FunctionCfg, idx: number): boolean =>
cfg.blocks.some((bl) => bl.statements?.some((s) => (s.mayDefs ?? []).includes(idx)));
const wrap = (body: string): string => `class C { void M(${''}) { ${body} } }`;
describe('C# CfgVisitor — structure', () => {
it('straight-line body: ENTRY → block → EXIT (seq)', () => {
const cfg = cs.cfgOf(`class C { void M() { a(); b(); c(); } }`);
expect(cfg.blocks.filter((b) => b.kind === 'normal')).toHaveLength(1);
const body = block(cfg, 'a();');
expect(cfg.edges).toContainEqual({ from: cfg.entryIndex, to: body, kind: 'seq' });
expect(reaches(cfg, body, cfg.exitIndex)).toBe(true);
});
it('empty body: ENTRY → EXIT', () => {
const cfg = cs.cfgOf(`class C { void M() {} }`);
expect(cfg.blocks).toHaveLength(2);
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
});
it('expression-bodied method: single block returns its value', () => {
const cfg = cs.cfgOf(`class C { int M(int n) => n * 2; }`);
const body = block(cfg, 'n * 2');
expect(cfg.edges).toContainEqual({ from: body, to: cfg.exitIndex, kind: 'return' });
});
it('constructor and local function are CFG-bearing functions', () => {
const cfgs = cs.cfgsOf(
`class C { C(int a) { x = a; } void Outer() { int L(int z) { return z; } L(1); } }`,
);
// constructor C, Outer, and the local function L = 3 CFGs.
expect(cfgs.length).toBeGreaterThanOrEqual(3);
for (const cfg of cfgs) expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
});
it('unmodeled member shape (no body) → graceful partial, no throw', () => {
// An abstract method has no body block → buildFunctionCfg returns undefined,
// never throws; a property (not a function) is not collected at all.
const root = cs.parse(`abstract class C { public abstract void M(); int P => 1; }`);
const fns = cs.collectFunctions(root);
for (const fn of fns) {
expect(() => createCsharpCfgVisitor().buildFunctionCfg(fn, 'f.cs')).not.toThrow();
}
});
});
describe('C# CfgVisitor — branching', () => {
it('if/else: cond-true to then, cond-false to else, both reach the join', () => {
const cfg = cs.cfgOf(wrap(`if (x > 0) { a(); } else { b(); } c();`).replace('M()', 'M(int x)'));
const kinds = edgeKinds(cfg);
expect(kinds.has('cond-true')).toBe(true);
expect(kinds.has('cond-false')).toBe(true);
const join = block(cfg, 'c();');
expect(reaches(cfg, block(cfg, 'a();'), join)).toBe(true);
expect(reaches(cfg, block(cfg, 'b();'), join)).toBe(true);
});
it('else if chains through the nested alternative (no else_clause wrapper)', () => {
const cfg = cs.cfgOf(
`class C { void M(int x) { if (x > 0) { a(); } else if (x < 0) { b(); } else { c(); } } }`,
);
// all three arms reach EXIT; the else-if condition is its own block.
expect(reaches(cfg, block(cfg, 'a();'), cfg.exitIndex)).toBe(true);
expect(reaches(cfg, block(cfg, 'b();'), cfg.exitIndex)).toBe(true);
expect(reaches(cfg, block(cfg, 'c();'), cfg.exitIndex)).toBe(true);
});
});
describe('C# CfgVisitor — loops', () => {
it('while loop: header + back-edge + exit', () => {
const cfg = cs.cfgOf(`class C { void M(int x) { while (x > 0) { step(); } done(); } }`);
const header = block(cfg, 'x > 0');
const body = block(cfg, 'step();');
expect(cfg.edges).toContainEqual({ from: body, to: header, kind: 'loop-back' });
expect(edgeKinds(cfg).has('cond-true')).toBe(true);
expect(reaches(cfg, header, block(cfg, 'done();'))).toBe(true);
});
it('do-while runs the body BEFORE testing, then loops back from the bottom', () => {
const cfg = cs.cfgOf(`class C { void M(int x) { do { step(); } while (x > 0); done(); } }`);
const body = block(cfg, 'step();');
const cond = block(cfg, 'x > 0');
expect(reaches(cfg, cfg.entryIndex, body)).toBe(true); // body runs first
expect(reaches(cfg, body, cond)).toBe(true);
expect(cfg.edges).toContainEqual({ from: cond, to: body, kind: 'loop-back' });
expect(reaches(cfg, cond, block(cfg, 'done();'))).toBe(true);
});
it('C-style for: init once, condition header, back-edge through update', () => {
const cfg = cs.cfgOf(
`class C { void M(int n) { for (int i = 0; i < n; i++) { step(); } done(); } }`,
);
const init = block(cfg, 'int i = 0');
const header = block(cfg, 'i < n');
const incr = block(cfg, 'i++');
const body = block(cfg, 'step();');
expect(cfg.edges).toContainEqual({ from: cfg.entryIndex, to: init, kind: 'seq' });
expect(reaches(cfg, body, incr)).toBe(true);
expect(cfg.edges).toContainEqual({ from: incr, to: header, kind: 'loop-back' });
expect(reaches(cfg, header, block(cfg, 'done();'))).toBe(true);
});
it('foreach: header + body + loop-back + exit; loop var is a def, source a use', () => {
const cfg = cs.cfgOf(
`class C { void M(int[] xs) { foreach (var x in xs) { use(x); } done(); } }`,
);
const body = block(cfg, 'use(x);');
expect(edgeKinds(cfg).has('cond-true')).toBe(true);
expect(edgeKinds(cfg).has('loop-back')).toBe(true);
const header = cfg.edges.find((e) => e.kind === 'loop-back' && e.from === body)?.to;
expect(header).toBeDefined();
expect(reaches(cfg, header!, block(cfg, 'done();'))).toBe(true);
const x = bindingIdx(cfg, 'x');
expect(hasDef(cfg, x)).toBe(true);
});
it('while (true) {} keeps EXIT reverse-reachable AND emits CDG > 0', () => {
// The inner `if` is a real control point; assert through the production
// post-dom/CDG passes (matching go/python/ruby/rust/vue) — CDG is only
// computed when EXIT stays reverse-reachable, so a non-empty CDG proves the
// structural exit-escape edge keeps the function CDG-bearing.
const cfg = cs.cfgOf(`class C { void M(bool x) { while (true) { if (x) { g(); } } } }`);
expect(edgeKinds(cfg).has('cond-false')).toBe(true);
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
});
it('for (;;) {} keeps EXIT reverse-reachable AND emits CDG > 0', () => {
const cfg = cs.cfgOf(`class C { void M(bool x) { for (;;) { if (x) { g(); } } } }`);
expect(edgeKinds(cfg).has('cond-false')).toBe(true);
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
});
});
describe('C# CfgVisitor — switch', () => {
it('switch_statement: cases dispatch, break-terminated case rejoins after', () => {
const cfg = cs.cfgOf(`class C { void M(int x) {
switch (x) {
case 1: one(); break;
case 2: two(); break;
default: other(); break;
}
after();
} }`);
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
expect(reaches(cfg, block(cfg, 'one();'), block(cfg, 'after();'))).toBe(true);
expect(reaches(cfg, block(cfg, 'two();'), block(cfg, 'after();'))).toBe(true);
// break-terminated case 1 does not fall into case 2.
expect(reaches(cfg, block(cfg, 'one();'), block(cfg, 'two();'))).toBe(false);
});
it('empty case section falls through to the next section', () => {
const cfg = cs.cfgOf(`class C { void M(int x) {
switch (x) { case 1: case 2: shared(); break; default: d(); break; }
after();
} }`);
// case 1 (empty) reaches the shared body.
expect(reaches(cfg, block(cfg, 'shared();'), block(cfg, 'after();'))).toBe(true);
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
});
it('return switch_expression: each arm dispatches and returns the result (#2207)', () => {
const cfg = cs.cfgOf(
`class C { int M(int x) { return x switch { 1 => a(), 2 => b(), _ => c() }; } }`,
);
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
expect(edgeKinds(cfg).has('return')).toBe(true);
// every arm reaches EXIT (its value IS the returned result).
expect(reaches(cfg, block(cfg, 'a()'), cfg.exitIndex)).toBe(true);
expect(reaches(cfg, block(cfg, 'c()'), cfg.exitIndex)).toBe(true);
// a() does NOT fall into b() (arms never fall through).
expect(reaches(cfg, block(cfg, 'a()'), block(cfg, 'b()'))).toBe(false);
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
});
it('value-position switch declaration is modeled, def bound at the join (#2207)', () => {
const cfg = cs.cfgOf(
`class C { int M(int x) { var y = x switch { 1 => a(), _ => b() }; use(y); return 0; } }`,
);
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
// each arm rejoins and reaches the downstream use of the bound result.
expect(reaches(cfg, block(cfg, 'a()'), block(cfg, 'use(y);'))).toBe(true);
expect(reaches(cfg, block(cfg, 'b()'), block(cfg, 'use(y);'))).toBe(true);
const y = bindingIdx(cfg, 'y');
expect(hasDef(cfg, y)).toBe(true);
expect(hasUse(cfg, y)).toBe(true);
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
});
it('expression-bodied member `=> k switch {…}` models the arms (#2207)', () => {
const cfg = cs.cfgOf(`class C { int G(int x) => x switch { 1 => a(), _ => b() }; }`);
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
expect(edgeKinds(cfg).has('return')).toBe(true);
expect(reaches(cfg, block(cfg, 'a()'), cfg.exitIndex)).toBe(true);
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
});
it('assignment-RHS / single-arm value switch stays inline (documented gap)', () => {
const assign = cs.cfgOf(
`class C { int M(int x) { int y = 0; y = x switch { 1 => 1, _ => 2 }; return y; } }`,
);
expect(edgeKinds(assign).has('switch-case')).toBe(false);
expect(reaches(assign, assign.entryIndex, assign.exitIndex)).toBe(true);
const oneArm = cs.cfgOf(`class C { int M(int x) { var y = x switch { _ => 0 }; return y; } }`);
expect(edgeKinds(oneArm).has('switch-case')).toBe(false);
});
it('non-exhaustive switch expression (no `_` arm) keeps a no-match edge (EXIT reachable) (#2211)', () => {
const cfg = cs.cfgOf(
`class C { int M(int x) { var y = x switch { 1 => a(), 2 => b() }; return y; } }`,
);
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
// 2 arms + the conservative no-match path = 3 switch-case successors from the dispatch.
const dispatchIdx = block(cfg, 'x');
expect(cfg.edges.filter((e) => e.from === dispatchIdx && e.kind === 'switch-case').length).toBe(
3,
);
});
});
describe('C# CfgVisitor — using / lock (deterministic finalizer)', () => {
it('using runs the body then dispose on the NORMAL exit path', () => {
const cfg = cs.cfgOf(`class C { void M() { using (var f = Open()) { read(f); } after(); } }`);
const body = block(cfg, 'read(f);');
const dispose = block(cfg, 'dispose');
// body → dispose → after() (normal completion threads through the dispose).
expect(reaches(cfg, body, dispose)).toBe(true);
expect(reaches(cfg, dispose, block(cfg, 'after();'))).toBe(true);
});
it('using disposes on the EXCEPTION path too (throw routes through dispose)', () => {
const cfg = cs.cfgOf(`class C { void M() { using (var f = Open()) { risky(f); } } }`);
const body = block(cfg, 'risky(f);');
const dispose = block(cfg, 'dispose');
// a throw in the protected body reaches the dispose finalizer.
expect(edgeKinds(cfg).has('throw')).toBe(true);
expect(reaches(cfg, body, dispose)).toBe(true);
});
it('a return inside using crosses the dispose (finally-return completion edge)', () => {
const cfg = cs.cfgOf(`class C { int M() { using (var f = Open()) { return read(f); } } }`);
expect(edgeKinds(cfg).has('finally-return')).toBe(true);
});
it('using var (C# 8 declaration form) disposes at enclosing-scope exit (#2206)', () => {
const cfg = cs.cfgOf(`class C { void M() { using var f = Open(); read(f); } }`);
const dispose = block(cfg, 'dispose');
// the rest of the scope (read(f)) is protected; dispose runs at the end and on
// the exception path.
expect(reaches(cfg, block(cfg, 'read(f);'), dispose)).toBe(true);
expect(edgeKinds(cfg).has('throw')).toBe(true);
});
it('a return after a using var declaration crosses the dispose (finally-return, #2206)', () => {
const cfg = cs.cfgOf(`class C { int M() { using var f = Open(); return read(f); } }`);
expect(block(cfg, 'dispose')).toBeGreaterThanOrEqual(0);
expect(edgeKinds(cfg).has('finally-return')).toBe(true);
});
it('lock body runs then releases the monitor (finalizer semantics)', () => {
const cfg = cs.cfgOf(`class C { void M(object sync) { lock (sync) { touch(); } after(); } }`);
const body = block(cfg, 'touch();');
const release = block(cfg, 'release');
expect(reaches(cfg, body, release)).toBe(true);
expect(reaches(cfg, release, block(cfg, 'after();'))).toBe(true);
});
});
describe('C# CfgVisitor — try/catch/finally completion edges', () => {
it('try/catch: a throw edge runs from each protected block to the handler', () => {
const cfg = cs.cfgOf(`class C { void M() {
try { risky(); deeper(); } catch (System.Exception e) { handle(e); }
after();
} }`);
expect(edgeKinds(cfg).has('throw')).toBe(true);
const handler = block(cfg, 'handle(e);');
expect(reaches(cfg, block(cfg, 'risky();'), handler)).toBe(true);
expect(reachable(cfg, block(cfg, 'after();'))).toBe(true);
});
it('finally runs on normal completion of the try', () => {
const cfg = cs.cfgOf(`class C { void M() {
try { work(); } finally { cleanup(); }
after();
} }`);
const body = block(cfg, 'work();');
const fin = block(cfg, 'cleanup();');
expect(reaches(cfg, body, fin)).toBe(true);
expect(reaches(cfg, fin, block(cfg, 'after();'))).toBe(true);
});
it('return crossing a finally emits a finally-return completion edge', () => {
const cfg = cs.cfgOf(`class C { int M() {
try { return compute(); } finally { cleanup(); }
} }`);
expect(edgeKinds(cfg).has('finally-return')).toBe(true);
});
it('break crossing a finally emits a finally-break completion edge', () => {
const cfg = cs.cfgOf(`class C { void M(int n) {
for (int i = 0; i < n; i++) {
try { if (done()) break; } finally { tick(); }
}
after();
} }`);
expect(edgeKinds(cfg).has('finally-break')).toBe(true);
});
});
describe('C# CfgVisitor — goto / labels', () => {
it('backward goto wires to an already-seen label block', () => {
const cfg = cs.cfgOf(
`class C { void M() { int i = 0; top: work(); i++; if (i < 10) goto top; done(); } }`,
);
const gotoB = block(cfg, 'goto top;');
const label = block(cfg, 'work();');
expect(reaches(cfg, gotoB, label)).toBe(true);
expect(cfg.edges.some((e) => e.from === gotoB && e.to === label)).toBe(true);
});
it('forward goto wires to a label that appears later', () => {
const cfg = cs.cfgOf(`class C { void M(int x) { if (x > 0) goto end; work(); end: done(); } }`);
const gotoB = block(cfg, 'goto end;');
const label = block(cfg, 'done();');
expect(reaches(cfg, gotoB, label)).toBe(true);
expect(reachable(cfg, block(cfg, 'work();'))).toBe(true);
});
// #2197 U1 — an UNCONDITIONAL goto-cycle traps EXIT (the `goto start` has no
// exit path); the synthetic-escape pass bridges it so CDG is emitted instead
// of withheld. The conditional goto tests above already had an exit path.
it('unconditional goto-cycle: bridged → EXIT reachable AND CDG emitted', () => {
const cfg = cs.cfgOf(`class K { void handler(int a){ start: if(a>0){work();} goto start; } }`);
expect(isExitReachableFromAllBlocks(cfg)).toBe(false); // trapped without the pass
const view = augmentForPostDom(cfg);
expect(isExitReachableFromAllBlocks(view)).toBe(true);
expect(computeControlDependence(view).edges.length).toBeGreaterThan(0);
});
});
describe('C# CfgVisitor — yield', () => {
it('yield break terminates the iterator (routes to EXIT)', () => {
const cfg = cs.cfgOf(`class C { System.Collections.Generic.IEnumerable<int> G(int x) {
if (x < 0) { yield break; }
yield return x;
} }`);
const yb = block(cfg, 'yield break;');
expect(reaches(cfg, yb, cfg.exitIndex)).toBe(true);
// yield break terminates its block — does not fall into yield return.
expect(reaches(cfg, yb, block(cfg, 'yield return x;'))).toBe(false);
});
it('yield return continues to the next statement', () => {
const cfg = cs.cfgOf(`class C { System.Collections.Generic.IEnumerable<int> G() {
yield return 1;
done();
} }`);
const yr = block(cfg, 'yield return 1;');
expect(reaches(cfg, yr, block(cfg, 'done();'))).toBe(true);
});
});
describe('C# CfgVisitor — def/use harvest', () => {
it('local declaration: int x = a + b; use(x); → def of x + use of x', () => {
const cfg = cs.cfgOf(`class C { void M(int a, int b) { int x = a + b; use(x); } }`);
const x = bindingIdx(cfg, 'x');
expect(hasDef(cfg, x)).toBe(true);
expect(hasUse(cfg, x)).toBe(true);
});
it('c ?? (c = load()) records c as a MAY-def, not a must-kill', () => {
const cfg = cs.cfgOf(`class C { void M(string c) { var v = c ?? (c = load()); use(v); } }`);
const c = bindingIdx(cfg, 'c');
expect(hasMayDef(cfg, c)).toBe(true);
});
it('a && (x = f()) records x as a may-def inside the short-circuit', () => {
const cfg = cs.cfgOf(`class C { void M(bool a) { int x = 0; if (a && (x = g()) > 0) h(x); } }`);
const x = bindingIdx(cfg, 'x');
expect(hasMayDef(cfg, x)).toBe(true);
});
it('compound assignment reads AND writes the lvalue', () => {
const cfg = cs.cfgOf(`class C { void M() { int z = 1; z += 3; } }`);
const z = bindingIdx(cfg, 'z');
expect(hasDef(cfg, z)).toBe(true);
expect(hasUse(cfg, z)).toBe(true);
});
it('out var n records n as a callee-written def (#2195 P1)', () => {
const cfg = cs.cfgOf(`class C { void M(string s) { int.TryParse(s, out var n); use(n); } }`);
const n = bindingIdx(cfg, 'n');
expect(hasDef(cfg, n)).toBe(true);
expect(hasUse(cfg, n)).toBe(true);
});
it('var (a, b) = T() deconstruction declaration defines BOTH a and b (#2195 P1)', () => {
const cfg = cs.cfgOf(`class C { void M() { var (a, b) = T(); use(a); use(b); } }`);
expect(hasDef(cfg, bindingIdx(cfg, 'a'))).toBe(true);
expect(hasDef(cfg, bindingIdx(cfg, 'b'))).toBe(true);
});
});
describe('C# CfgVisitor — functionStartColumn', () => {
it('two same-line methods get distinct functionStartColumn', () => {
const cfgs = cs.cfgsOf(`class C { int A() { return 1; } int B() { return 2; } }`);
expect(cfgs).toHaveLength(2);
expect(cfgs[0].functionStartLine).toBe(cfgs[1].functionStartLine); // same line
expect(cfgs[0].functionStartColumn).not.toBe(cfgs[1].functionStartColumn); // distinct column
});
});
describe('C# CfgVisitor — does not throw on exotic shapes', () => {
it('lambda / anonymous method bodies build their own CFGs', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
try {
const cfgs = cs.cfgsOf(`class C { void M() {
System.Func<int, int> f = x => { if (x > 0) { return x; } return 0; };
System.Action h = delegate () { act(); };
f(1); h();
} }`);
expect(cfgs.length).toBeGreaterThanOrEqual(3); // M, lambda, anon method
for (const cfg of cfgs) expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
} finally {
warn.mockRestore();
}
});
it('a truncated value-position switch never throws out of the carrier path (R4) (#2211)', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
try {
const root = cs.parse(`class C { int M(int x) { var y = x switch { 1 => a(`);
for (const fn of cs.collectFunctions(root)) {
expect(() => createCsharpCfgVisitor().buildFunctionCfg(fn, 'f.cs')).not.toThrow();
}
} finally {
warn.mockRestore();
}
});
});
// U6 — call-site `sites[]` taint substrate. INERT BY DESIGN: no C# taint model
// is registered, so these sites produce zero TAINTED edges; they only give the
// deferred per-language source/sink model something to match against.
describe('C# CfgVisitor — call-site sites[] substrate', () => {
const cfgOf = (body: string): FunctionCfg =>
cs.cfgOf(`class C { void f(int cmd, int x, int a) { ${body} } }`);
it('a bare invocation records a `call` site with callee name + arg occurrence', () => {
const cfg = cfgOf(`Exec(cmd);`);
const site = allSites(cfg).find((s) => s.kind === 'call' && s.callee === 'Exec');
expect(site).toBeDefined();
expect(site?.args?.[0]).toContainEqual(bindingIdx(cfg, 'cmd'));
});
it('a member invocation (`db.Query(x)`) records the receiver + dotted callee', () => {
const cfg = cfgOf(`db.Query(x);`);
const site = allSites(cfg).find((s) => s.kind === 'call' && s.callee === 'db.Query');
expect(site).toBeDefined();
expect(site?.receiver).toBe(bindingIdx(cfg, 'db'));
expect(site?.args?.[0]).toContainEqual(bindingIdx(cfg, 'x'));
});
it('`new Foo(x)` records a `new` site with the type as callee', () => {
const cfg = cfgOf(`var p = new Foo(x);`);
const site = allSites(cfg).find((s) => s.kind === 'new');
expect(site?.callee).toBe('Foo');
expect(site?.args?.[0]).toContainEqual(bindingIdx(cfg, 'x'));
expect(site?.resultDefs).toContain(bindingIdx(cfg, 'p'));
});
it('a nested invocation via-tags the inner site (sanitizer substrate)', () => {
const cfg = cfgOf(`Exec(Escape(x));`);
const sites = allSites(cfg);
const exec = sites.findIndex((s) => s.callee === 'Exec');
const escape = sites.findIndex((s) => s.callee === 'Escape');
expect(exec).toBeGreaterThanOrEqual(0);
expect(escape).toBeGreaterThanOrEqual(0);
const x = bindingIdx(cfg, 'x');
expect(sites[escape].args?.[0]).toContainEqual(x);
expect(sites[exec].args?.[0]).toContainEqual([x, escape]);
});
it('a CFG-only function (no calls) emits NO sites key (omit-when-empty)', () => {
const cfg = cs.cfgOf(`class C { void f(int a, int b) { int x = a + b; } }`);
expect(hasAnySites(cfg)).toBe(false);
});
});