mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
feat(cfg): C# CFG visitor + def/use harvest (#2195 U3)
Add createCsharpCfgVisitor + csharp-harvest over the shared CfgBuilder / ControlFlowContext, modeling the C# statement taxonomy: if/else, for/foreach/while/do, switch_section (+ switch_expression arms), try/catch/catch_filter/finally, using + lock (deterministic finalizers -- dispose/release runs on normal AND exception exit, finally-* completion edges on crossing jumps), goto/labeled, yield (surface only), return/ throw/break/continue. Wire into csharpProvider. Every literal validated against tree-sitter-c-sharp via the introspection probe (record_declaration, no else_clause, switch_section, positional access where no field exists). Edge kinds match the contract; functionStartColumn populated; while(true) keeps EXIT reverse-reachable (production CDG probe: 3 edges). buildFunctionCfg returns undefined rather than throwing. 34 real-parser regression tests; grammar-literal gate green; no regression (cfg unit dir 256/256, tsc clean). Documented gaps: yield iterator state machine, goto case/default, async suspension points. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
018635074a
commit
aad3a702b9
5 changed files with 2002 additions and 0 deletions
543
gitnexus/src/core/ingestion/cfg/visitors/csharp-harvest.ts
Normal file
543
gitnexus/src/core/ingestion/cfg/visitors/csharp-harvest.ts
Normal file
|
|
@ -0,0 +1,543 @@
|
|||
/**
|
||||
* C# def/use harvester (#2195 U3, plan KTD2) — the C# analogue of
|
||||
* {@link import('./typescript-harvest.js').TsHarvester} and the closely-related
|
||||
* {@link import('./c-cpp-harvest.js').CCppHarvester}.
|
||||
*
|
||||
* Runs in the parse worker next to the C# CFG visitor, extracting per-statement
|
||||
* variable definition/use facts that ride the side channel for the reaching-defs
|
||||
* / CDG solvers. Output is the per-function binding table ({@link BindingEntry}[])
|
||||
* plus {@link StatementFacts} the visitor attaches to blocks as it walks.
|
||||
*
|
||||
* TWO-PHASE, ORDER-INDEPENDENT (load-bearing — mirrors the TS / C-C++ harvesters):
|
||||
* the CFG walk is NOT source-order (`visitFor` builds the init block after the
|
||||
* body, `visitDoWhile` the condition before the body), so resolving names against
|
||||
* a scope stack populated *during* the walk would mis-resolve. Phase 1 pre-scans
|
||||
* the whole function subtree once into a completed lexical scope tree; phase 2
|
||||
* resolves defs/uses against that finished tree from any walk order.
|
||||
*
|
||||
* v1 def-semantics scope:
|
||||
* - `local_declaration_statement` → `variable_declaration` → `variable_declarator`
|
||||
* (an INITIALIZED local is a def; a bare `int x;` with no initializer writes
|
||||
* nothing at runtime — not a def, like the TS bare-`var` rule).
|
||||
* - `assignment_expression` (plain + compound `+=` etc.), `postfix_unary_expression`
|
||||
* / `prefix_unary_expression` (`x++` / `--x`) — define and (for compound /
|
||||
* update) also use the lvalue.
|
||||
* - parameters (`parameter` → `name` field), the `foreach` loop variable
|
||||
* (`foreach_statement` field `left`), pattern bindings (`declaration_pattern`
|
||||
* `name`, e.g. `o is string s` / `case int n:`), and catch-clause names
|
||||
* (`catch_declaration` `name`).
|
||||
* EXCLUDED, deliberately (TypeScript-CFA precedent): member / element / pointer
|
||||
* writes (`obj.F = …`, `a[i] = …`) are NOT scalar defs — their identifiers are
|
||||
* uses only. Nested-function (lambda / local-function / anonymous-method) bodies
|
||||
* are opaque in BOTH directions (writes to and reads of captured outer variables
|
||||
* are invisible).
|
||||
*
|
||||
* MAY-DEFS: a def inside a conditionally-evaluated subexpression — the right
|
||||
* operand of `&&` / `||` / `??` (`a ?? (a = load())`), a ternary arm, or a switch
|
||||
* arm/case test — is a may-def (gen without kill), so the not-taken path's prior
|
||||
* def is not falsely killed.
|
||||
*
|
||||
* Identifiers with no in-function declaration (fields, properties, statics,
|
||||
* namespaced names) resolve to a SYNTHETIC module-level binding (`name@module`),
|
||||
* applied identically by def and use harvesting.
|
||||
*
|
||||
* NOTE: nothing serialized here may carry a field named `nodeId` — the durable
|
||||
* parsedfile-store reviver dedups objects keyed on that field name.
|
||||
*/
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import type { BindingEntry, StatementFacts } from '../types.js';
|
||||
|
||||
/** Node types that own a nested CFG — their subtrees are opaque to harvesting. */
|
||||
const NESTED_FUNCTION_TYPES = new Set([
|
||||
'lambda_expression',
|
||||
'anonymous_method_expression',
|
||||
'local_function_statement',
|
||||
'method_declaration',
|
||||
'constructor_declaration',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Nodes that open a lexical scope for block-local declarations. A `block` is one
|
||||
* scope; the loop constructs open a scope for their loop variable; a
|
||||
* `catch_clause` scopes its exception name; a `using_statement` scopes its
|
||||
* resource declaration; a `switch_section` scopes its pattern bindings.
|
||||
*/
|
||||
const SCOPE_TYPES = new Set([
|
||||
'block',
|
||||
'for_statement',
|
||||
'foreach_statement',
|
||||
'while_statement',
|
||||
'using_statement',
|
||||
'catch_clause',
|
||||
'switch_section',
|
||||
'switch_expression_arm',
|
||||
]);
|
||||
|
||||
interface Scope {
|
||||
readonly parent: Scope | null;
|
||||
/** name → binding index */
|
||||
readonly table: Map<string, number>;
|
||||
}
|
||||
|
||||
export class CsharpHarvester {
|
||||
private readonly bindings: BindingEntry[] = [];
|
||||
private readonly scopeByNode = new Map<number, Scope>();
|
||||
private readonly root: Scope = { parent: null, table: new Map() };
|
||||
private readonly synthetic = new Map<string, number>();
|
||||
private readonly fnId: number;
|
||||
/** Innermost enclosing scope per visited node id (prescan-filled) — O(scope-chain) phase-2 resolution. */
|
||||
private readonly nearestScopeCache = new Map<number, Scope>();
|
||||
/** >0 while walking a conditionally-evaluated subexpression — defs become may-defs. */
|
||||
private conditionalDepth = 0;
|
||||
|
||||
constructor(private readonly fnNode: SyntaxNode) {
|
||||
this.fnId = fnNode.id;
|
||||
this.scopeByNode.set(fnNode.id, this.root);
|
||||
this.declareParams(fnNode);
|
||||
const body = this.bodyOf(fnNode);
|
||||
if (body) this.prescan(body, this.openScope(body));
|
||||
}
|
||||
|
||||
/** The completed binding table — pass to `CfgBuilder.finish`. */
|
||||
table(): readonly BindingEntry[] {
|
||||
return this.bindings;
|
||||
}
|
||||
|
||||
/** The function/lambda body node (a `block` or an expression for `=> expr`). */
|
||||
private bodyOf(fnNode: SyntaxNode): SyntaxNode | undefined {
|
||||
const body = fnNode.childForFieldName('body');
|
||||
if (body) return body;
|
||||
// Anonymous method / local function: the body is the first `block` child.
|
||||
return fnNode.namedChildren.find((c) => c.type === 'block');
|
||||
}
|
||||
|
||||
// ── phase 1: declaration pre-scan ────────────────────────────────────────
|
||||
|
||||
private openScope(node: SyntaxNode): Scope {
|
||||
const existing = this.scopeByNode.get(node.id);
|
||||
if (existing) return existing;
|
||||
const scope: Scope = { parent: this.nearestScopeOf(node), table: new Map() };
|
||||
this.scopeByNode.set(node.id, scope);
|
||||
return scope;
|
||||
}
|
||||
|
||||
private nearestScopeOf(node: SyntaxNode): Scope {
|
||||
for (let p = node.parent; p; p = p.parent) {
|
||||
const s = this.scopeByNode.get(p.id);
|
||||
if (s) return s;
|
||||
if (p.id === this.fnId) break;
|
||||
}
|
||||
return this.root;
|
||||
}
|
||||
|
||||
private declare(nameNode: SyntaxNode, kind: BindingEntry['kind'], scope: Scope): void {
|
||||
const name = nameNode.text;
|
||||
if (!name || scope.table.has(name)) return;
|
||||
scope.table.set(name, this.bindings.length);
|
||||
this.bindings.push({
|
||||
name,
|
||||
declLine: nameNode.startPosition.row + 1,
|
||||
declColumn: nameNode.startPosition.column,
|
||||
kind,
|
||||
});
|
||||
}
|
||||
|
||||
private declareParams(fnNode: SyntaxNode): void {
|
||||
const params =
|
||||
fnNode.childForFieldName('parameters') ??
|
||||
fnNode.namedChildren.find(
|
||||
(c) => c.type === 'parameter_list' || c.type === 'implicit_parameter',
|
||||
);
|
||||
if (!params) return;
|
||||
if (params.type === 'implicit_parameter') {
|
||||
// Single un-parenthesized lambda parameter: `x => …`.
|
||||
this.declare(params, 'param', this.root);
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < params.namedChildCount; i++) {
|
||||
const p = params.namedChild(i);
|
||||
if (p?.type !== 'parameter') continue;
|
||||
const name = p.childForFieldName('name');
|
||||
if (name) this.declare(name, 'param', this.root);
|
||||
}
|
||||
}
|
||||
|
||||
private prescan(node: SyntaxNode, scope: Scope): void {
|
||||
this.nearestScopeCache.set(node.id, scope);
|
||||
const t = node.type;
|
||||
if (NESTED_FUNCTION_TYPES.has(t) && node.id !== this.fnId) {
|
||||
// A nested function / lambda body is opaque — do not descend.
|
||||
return;
|
||||
}
|
||||
|
||||
let childScope = scope;
|
||||
if (SCOPE_TYPES.has(t)) childScope = this.openScope(node);
|
||||
|
||||
switch (t) {
|
||||
case 'local_declaration_statement': {
|
||||
const decl = node.namedChildren.find((c) => c.type === 'variable_declaration');
|
||||
if (decl) this.declareVariableDeclaration(decl, childScope);
|
||||
break;
|
||||
}
|
||||
case 'foreach_statement': {
|
||||
// `foreach (var x in xs)` — the `left` is the loop var (identifier or a
|
||||
// `tuple_pattern` of identifiers); binds in the loop scope.
|
||||
const left = node.childForFieldName('left');
|
||||
if (left) this.declareForeachTarget(left, childScope);
|
||||
break;
|
||||
}
|
||||
case 'using_statement': {
|
||||
// `using (var f = Open())` — declaration form binds the resource.
|
||||
const decl = node.namedChildren.find((c) => c.type === 'variable_declaration');
|
||||
if (decl) this.declareVariableDeclaration(decl, childScope);
|
||||
break;
|
||||
}
|
||||
case 'catch_clause': {
|
||||
const declNode = node.namedChildren.find((c) => c.type === 'catch_declaration');
|
||||
const name = declNode?.childForFieldName('name');
|
||||
if (name) this.declare(name, 'catch', childScope);
|
||||
break;
|
||||
}
|
||||
case 'declaration_pattern': {
|
||||
// `o is string s` / `case int n:` — `s`/`n` is a fresh binding.
|
||||
const name = node.childForFieldName('name');
|
||||
if (name) this.declare(name, 'var', childScope);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const c = node.namedChild(i);
|
||||
if (c) this.prescan(c, childScope);
|
||||
}
|
||||
}
|
||||
|
||||
/** Declare every `variable_declarator` name in a `variable_declaration`. */
|
||||
private declareVariableDeclaration(declNode: SyntaxNode, scope: Scope): void {
|
||||
for (let i = 0; i < declNode.namedChildCount; i++) {
|
||||
const d = declNode.namedChild(i);
|
||||
if (d?.type !== 'variable_declarator') continue;
|
||||
const name = d.childForFieldName('name');
|
||||
if (name) this.declare(name, 'var', scope);
|
||||
}
|
||||
}
|
||||
|
||||
/** Declare a `foreach` target — an identifier or a `tuple_pattern`. */
|
||||
private declareForeachTarget(left: SyntaxNode, scope: Scope): void {
|
||||
if (left.type === 'identifier') {
|
||||
this.declare(left, 'var', scope);
|
||||
return;
|
||||
}
|
||||
// `var (k, v)` deconstruction — declare each identifier under the pattern.
|
||||
for (let i = 0; i < left.namedChildCount; i++) {
|
||||
const c = left.namedChild(i);
|
||||
if (c?.type === 'identifier') this.declare(c, 'var', scope);
|
||||
}
|
||||
}
|
||||
|
||||
// ── phase 2: per-statement fact extraction ───────────────────────────────
|
||||
|
||||
/** Def/use facts for one statement (or construct-header expression) node. */
|
||||
facts(node: SyntaxNode): StatementFacts {
|
||||
const acc = new FactAccumulator(node.startPosition.row + 1);
|
||||
this.walkValue(node, acc);
|
||||
return acc.finish();
|
||||
}
|
||||
|
||||
/** Facts for an expression whose WHOLE evaluation is conditional (case tests). */
|
||||
factsConditional(node: SyntaxNode): StatementFacts {
|
||||
const acc = new FactAccumulator(node.startPosition.row + 1);
|
||||
this.conditional(() => this.walkValue(node, acc));
|
||||
return acc.finish();
|
||||
}
|
||||
|
||||
/** Facts for a `foreach (decl in right)` head: decl binds, right is used. */
|
||||
forEachHeadFacts(stmt: SyntaxNode): StatementFacts {
|
||||
const acc = new FactAccumulator(stmt.startPosition.row + 1);
|
||||
const left = stmt.childForFieldName('left');
|
||||
const right = stmt.childForFieldName('right');
|
||||
if (left) this.defForeachTarget(left, acc);
|
||||
if (right) this.walkValue(right, acc);
|
||||
return acc.finish();
|
||||
}
|
||||
|
||||
/** ENTRY-block facts for the function's parameters (defs only). */
|
||||
paramFacts(): StatementFacts | undefined {
|
||||
const params =
|
||||
this.fnNode.childForFieldName('parameters') ??
|
||||
this.fnNode.namedChildren.find(
|
||||
(c) => c.type === 'parameter_list' || c.type === 'implicit_parameter',
|
||||
);
|
||||
if (!params) return undefined;
|
||||
const acc = new FactAccumulator(this.fnNode.startPosition.row + 1);
|
||||
if (params.type === 'implicit_parameter') {
|
||||
this.def(params, acc);
|
||||
} else {
|
||||
for (let i = 0; i < params.namedChildCount; i++) {
|
||||
const p = params.namedChild(i);
|
||||
if (p?.type !== 'parameter') continue;
|
||||
const name = p.childForFieldName('name');
|
||||
if (name) this.def(name, acc);
|
||||
}
|
||||
}
|
||||
return acc.defCount() ? acc.finish() : undefined;
|
||||
}
|
||||
|
||||
/** Def fact for a `catch (T e)` declaration — prepend to the handler entry block. */
|
||||
catchParamFacts(catchClause: SyntaxNode): StatementFacts | undefined {
|
||||
const declNode = catchClause.namedChildren.find((c) => c.type === 'catch_declaration');
|
||||
const name = declNode?.childForFieldName('name');
|
||||
if (!name) return undefined;
|
||||
const acc = new FactAccumulator(catchClause.startPosition.row + 1);
|
||||
this.def(name, acc);
|
||||
return acc.defCount() ? acc.finish() : undefined;
|
||||
}
|
||||
|
||||
private resolve(nameNode: SyntaxNode): number {
|
||||
const name = nameNode.text;
|
||||
const cached = this.nearestScopeCache.get(nameNode.id);
|
||||
let startScope: Scope | null = cached ?? null;
|
||||
if (!startScope) {
|
||||
for (let p: SyntaxNode | null = nameNode; p; p = p.parent) {
|
||||
const scope = this.scopeByNode.get(p.id) ?? this.nearestScopeCache.get(p.id);
|
||||
if (scope) {
|
||||
startScope = scope;
|
||||
break;
|
||||
}
|
||||
if (p.id === this.fnId) {
|
||||
startScope = this.root;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let s: Scope | null = startScope; s; s = s.parent) {
|
||||
const idx = s.table.get(name);
|
||||
if (idx !== undefined) return idx;
|
||||
}
|
||||
let idx = this.synthetic.get(name);
|
||||
if (idx === undefined) {
|
||||
idx = this.bindings.length;
|
||||
this.synthetic.set(name, idx);
|
||||
this.bindings.push({ name, declLine: 0, declColumn: 0, kind: 'module', synthetic: true });
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
private def(nameNode: SyntaxNode, acc: FactAccumulator): void {
|
||||
if (this.conditionalDepth > 0) acc.addMayDef(this.resolve(nameNode));
|
||||
else acc.addDef(this.resolve(nameNode));
|
||||
}
|
||||
|
||||
private use(nameNode: SyntaxNode, acc: FactAccumulator): void {
|
||||
acc.addUse(this.resolve(nameNode));
|
||||
}
|
||||
|
||||
/** Run `fn` with defs demoted to may-defs (conditionally-evaluated context). */
|
||||
private conditional(fn: () => void): void {
|
||||
this.conditionalDepth++;
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
this.conditionalDepth--;
|
||||
}
|
||||
}
|
||||
|
||||
/** Strip parenthesized wrappers around an lvalue (`(x) = 1`). */
|
||||
private unwrapLvalue(node: SyntaxNode): SyntaxNode {
|
||||
let n = node;
|
||||
let hops = 8;
|
||||
while (n.type === 'parenthesized_expression' && hops-- > 0) {
|
||||
const inner = n.namedChild(0);
|
||||
if (!inner) break;
|
||||
n = inner;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/** Def a `foreach` target (identifier or tuple) in a header fact accumulator. */
|
||||
private defForeachTarget(left: SyntaxNode, acc: FactAccumulator): void {
|
||||
if (left.type === 'identifier') {
|
||||
this.def(left, acc);
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < left.namedChildCount; i++) {
|
||||
const c = left.namedChild(i);
|
||||
if (c?.type === 'identifier') this.def(c, acc);
|
||||
}
|
||||
}
|
||||
|
||||
/** Value-position walk: collect uses; route def positions to the lvalue handler. */
|
||||
private walkValue(node: SyntaxNode, acc: FactAccumulator): void {
|
||||
const t = node.type;
|
||||
if (NESTED_FUNCTION_TYPES.has(t) && node.id !== this.fnId) {
|
||||
// Opaque nested function / lambda — captured reads/writes are invisible.
|
||||
return;
|
||||
}
|
||||
|
||||
switch (t) {
|
||||
case 'identifier':
|
||||
this.use(node, acc);
|
||||
return;
|
||||
case 'local_declaration_statement':
|
||||
case 'variable_declaration': {
|
||||
const decl = t === 'variable_declaration' ? node : node.namedChild(0);
|
||||
if (decl && decl.type === 'variable_declaration') {
|
||||
for (let i = 0; i < decl.namedChildCount; i++) {
|
||||
const d = decl.namedChild(i);
|
||||
if (d?.type !== 'variable_declarator') continue;
|
||||
const name = d.childForFieldName('name');
|
||||
// The initializer (if any) is the LAST named child after `name`.
|
||||
const init = this.declaratorInit(d);
|
||||
if (name && init) this.def(name, acc);
|
||||
if (init) this.walkValue(init, acc);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'assignment_expression': {
|
||||
const left = node.childForFieldName('left');
|
||||
const right = node.childForFieldName('right');
|
||||
const op = node.childForFieldName('operator')?.text ?? '=';
|
||||
if (left) {
|
||||
const lv = this.unwrapLvalue(left);
|
||||
if (lv.type === 'identifier') {
|
||||
this.def(lv, acc);
|
||||
if (op !== '=') this.use(lv, acc); // compound assign reads too
|
||||
} else if (lv.type === 'tuple_expression') {
|
||||
this.defTupleTargets(lv, acc); // `(a, b) = …` deconstruction
|
||||
} else {
|
||||
this.walkValue(lv, acc); // member/element target — uses only
|
||||
}
|
||||
}
|
||||
if (right) this.walkValue(right, acc);
|
||||
return;
|
||||
}
|
||||
case 'postfix_unary_expression':
|
||||
case 'prefix_unary_expression': {
|
||||
const arg = node.namedChild(0);
|
||||
const lv = arg ? this.unwrapLvalue(arg) : null;
|
||||
// Only `++`/`--` write; `!x`/`-x`/`~x` are pure reads. The operator is an
|
||||
// anonymous child; treat as a def+use only when the operand is an
|
||||
// identifier AND the op text is an increment/decrement.
|
||||
if (lv?.type === 'identifier' && this.isIncDec(node)) {
|
||||
this.def(lv, acc);
|
||||
this.use(lv, acc);
|
||||
} else if (arg) {
|
||||
this.walkValue(arg, acc);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'binary_expression': {
|
||||
const left = node.childForFieldName('left');
|
||||
const right = node.childForFieldName('right');
|
||||
const op = node.childForFieldName('operator')?.text ?? '';
|
||||
if (left) this.walkValue(left, acc);
|
||||
if (right) {
|
||||
if (op === '&&' || op === '||' || op === '??') {
|
||||
this.conditional(() => this.walkValue(right, acc));
|
||||
} else {
|
||||
this.walkValue(right, acc);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'conditional_expression': {
|
||||
const cond = node.childForFieldName('condition');
|
||||
const cons = node.childForFieldName('consequence');
|
||||
const alt = node.childForFieldName('alternative');
|
||||
if (cond) this.walkValue(cond, acc);
|
||||
if (cons) this.conditional(() => this.walkValue(cons, acc));
|
||||
if (alt) this.conditional(() => this.walkValue(alt, acc));
|
||||
return;
|
||||
}
|
||||
case 'member_access_expression': {
|
||||
// `a.B` — value read of the chain root only; the member name is not a
|
||||
// scalar binding. Mirrors the TS member-read use semantics.
|
||||
const expr = node.childForFieldName('expression');
|
||||
if (expr) this.walkValue(expr, acc);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const c = node.namedChild(i);
|
||||
if (c) this.walkValue(c, acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The initializer value of a `variable_declarator` — the named child after `name`. */
|
||||
private declaratorInit(declarator: SyntaxNode): SyntaxNode | undefined {
|
||||
const name = declarator.childForFieldName('name');
|
||||
for (let i = 0; i < declarator.namedChildCount; i++) {
|
||||
const c = declarator.namedChild(i);
|
||||
if (c && c.id !== name?.id) return c;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Whether a unary expression is `++`/`--` (the only writing unary ops). */
|
||||
private isIncDec(node: SyntaxNode): boolean {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const c = node.child(i);
|
||||
if (c && !c.isNamed && (c.text === '++' || c.text === '--')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Def each identifier in a `(a, b) = …` tuple deconstruction target. */
|
||||
private defTupleTargets(tuple: SyntaxNode, acc: FactAccumulator): void {
|
||||
for (let i = 0; i < tuple.namedChildCount; i++) {
|
||||
const c = tuple.namedChild(i);
|
||||
if (!c) continue;
|
||||
// tuple_expression wraps each element in an `argument`.
|
||||
const inner = c.type === 'argument' ? c.namedChild(0) : c;
|
||||
if (inner?.type === 'identifier') this.def(inner, acc);
|
||||
else if (inner) this.walkValue(inner, acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Ordered, deduplicating def/use collector for one statement record. */
|
||||
class FactAccumulator {
|
||||
private readonly defs: number[] = [];
|
||||
private readonly uses: number[] = [];
|
||||
private readonly mayDefs: number[] = [];
|
||||
private readonly defSeen = new Set<number>();
|
||||
private readonly useSeen = new Set<number>();
|
||||
private readonly mayDefSeen = new Set<number>();
|
||||
|
||||
constructor(private readonly line: number) {}
|
||||
|
||||
addDef(idx: number): void {
|
||||
if (this.defSeen.has(idx)) return;
|
||||
this.defSeen.add(idx);
|
||||
this.defs.push(idx);
|
||||
}
|
||||
|
||||
addMayDef(idx: number): void {
|
||||
if (this.mayDefSeen.has(idx)) return;
|
||||
this.mayDefSeen.add(idx);
|
||||
this.mayDefs.push(idx);
|
||||
}
|
||||
|
||||
addUse(idx: number): void {
|
||||
if (this.useSeen.has(idx)) return;
|
||||
this.useSeen.add(idx);
|
||||
this.uses.push(idx);
|
||||
}
|
||||
|
||||
defCount(): number {
|
||||
return this.defs.length + this.mayDefs.length;
|
||||
}
|
||||
|
||||
finish(): StatementFacts {
|
||||
return {
|
||||
line: this.line,
|
||||
defs: this.defs,
|
||||
uses: this.uses,
|
||||
...(this.mayDefs.length > 0 ? { mayDefs: this.mayDefs } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
909
gitnexus/src/core/ingestion/cfg/visitors/csharp.ts
Normal file
909
gitnexus/src/core/ingestion/cfg/visitors/csharp.ts
Normal file
|
|
@ -0,0 +1,909 @@
|
|||
/**
|
||||
* C# CfgVisitor (#2195 U3, plan KTD2).
|
||||
*
|
||||
* Walks a C# function's tree-sitter AST and drives the language-agnostic
|
||||
* {@link CfgBuilder} to produce a serializable {@link FunctionCfg}, plus a
|
||||
* def/use harvest ({@link CsharpHarvester}) for the reaching-defs / CDG solvers.
|
||||
* Structured like the TS visitor — a `visit_<node_type>` dispatch over the
|
||||
* statement taxonomy, driving a per-function {@link ControlFlowContext} — because
|
||||
* C# shares TS's `finally` semantics (try/finally AND `using`/`lock`-as-finally),
|
||||
* which the finalizer-frame machinery in `control-flow-context.ts` models.
|
||||
*
|
||||
* Every node type and field literal below was grammar-validated against
|
||||
* tree-sitter-c-sharp via the introspection probe before use (mandatory pre-step,
|
||||
* KTD5). Known C# surprises pre-empted: records are `record_declaration` (NOT
|
||||
* `record_struct_declaration`); there is no `else_clause` wrapper (an
|
||||
* `if_statement`'s `alternative` is the else body / nested `if` directly); switch
|
||||
* cases live in `switch_section`s under a `switch_body`; `variable_declarator`
|
||||
* exposes only a `name` field (the initializer is a positional child);
|
||||
* `finally_clause` / `lock_statement` / `catch_filter_clause` expose no field for
|
||||
* their body/condition (positional children).
|
||||
*
|
||||
* Function nodes: `method_declaration`, `local_function_statement`,
|
||||
* `constructor_declaration`, `lambda_expression`, `anonymous_method_expression`,
|
||||
* and expression-bodied members (`body` is an `arrow_expression_clause`).
|
||||
*
|
||||
* Edge-kind contract (matches the TS visitor — RD/CDG consume these):
|
||||
* - if/else → `cond-true` / `cond-false`
|
||||
* - loops (for / foreach / while / do-while) → `cond-true` / `loop-back` /
|
||||
* `cond-false`
|
||||
* - switch (`switch_statement`) → `switch-case` / `fallthrough`; a
|
||||
* `switch_expression` arm dispatches as `switch-case` (each arm a guarded
|
||||
* branch, no fallthrough)
|
||||
* - try/catch → `throw` (every protected-region block → the handler); a jump
|
||||
* crossing a `finally` → `finally-return` / `finally-break` / `finally-continue`
|
||||
* - `using` / `lock` → modeled like try/finally: the dispose (`using`) runs on
|
||||
* BOTH normal and exception exit (deterministic), so a `return`/`break`/
|
||||
* `continue` crossing it gets the `finally-*` completion edge too. `lock`'s
|
||||
* monitor-release is likewise a deterministic finalizer.
|
||||
* - return / throw / break / continue → the matching terminator kind
|
||||
* - straight-line → `seq`
|
||||
*
|
||||
* Classic hazards, handled explicitly (mirrors the TS visitor):
|
||||
* - loops allocate a dedicated loop-exit block so `break` has a target before
|
||||
* the loop's successor is known; `continue` targets the header/increment.
|
||||
* - `for (;;) {}` / `while (true) {}` still emit the structural `header →
|
||||
* loopExit` `cond-false` escape edge so EXIT stays reverse-reachable from
|
||||
* every block — the post-dominator / CDG pass silently emits zero CDG for the
|
||||
* function otherwise.
|
||||
* - labeled `goto`: labels resolve within the function (forward AND backward);
|
||||
* an unresolved `goto` (incl. `goto case`/`goto default`, which target a
|
||||
* switch arm this CFG does not label) routes to EXIT and logs, preserving
|
||||
* single-exit.
|
||||
* - try/catch: conservative exceptional flow — EVERY block in the protected
|
||||
* region edges to the handler (an exception may fire mid-block), matching the
|
||||
* TS `visitTry` over-approximation.
|
||||
*
|
||||
* Known limitations:
|
||||
* - `yield return` / `yield break`: C# iterator methods compile to a hidden
|
||||
* state machine; tree-sitter shows only the surface `yield_statement`. This
|
||||
* visitor models `yield break` as a terminator → EXIT (`return`) and
|
||||
* `yield return e` as a block whose value continues to the next statement
|
||||
* (the producer resumes after the consumer pulls). The real suspend/resume
|
||||
* state-machine control flow is NOT modeled — documented gap, not faked.
|
||||
* - `goto case L;` / `goto default;` have no statically-labeled CFG target
|
||||
* (switch arms are not labeled blocks here) and route to EXIT like an
|
||||
* unresolved label.
|
||||
* - Async/await suspension points are modeled as straight-line (the awaited
|
||||
* continuation is not a separate flow), consistent with the TS visitor.
|
||||
* - Def/use harvest scope: see `csharp-harvest.ts` — member/element writes are
|
||||
* not scalar defs; nested-function bodies are opaque in both directions.
|
||||
*
|
||||
* Returns `undefined` (never throws) for an AST shape it cannot model, so a
|
||||
* malformed function never drops the whole file's CFG group (R4).
|
||||
*/
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import { CfgBuilder } from '../cfg-builder.js';
|
||||
import {
|
||||
ControlFlowContext,
|
||||
drainFinalizerPending,
|
||||
wireJumpThroughFinalizers,
|
||||
} from '../control-flow-context.js';
|
||||
import type { TraversalResult } from '../traversal-result.js';
|
||||
import type { CfgVisitor, FunctionCfg } from '../types.js';
|
||||
import { CsharpHarvester } from './csharp-harvest.js';
|
||||
|
||||
/** C# node types that own a CFG-bearing function body. */
|
||||
const CSHARP_FUNCTION_TYPES = new Set([
|
||||
'method_declaration',
|
||||
'local_function_statement',
|
||||
'constructor_declaration',
|
||||
'lambda_expression',
|
||||
'anonymous_method_expression',
|
||||
]);
|
||||
|
||||
/** Statement node types that break a basic block (everything else coalesces). */
|
||||
const CONTROL_FLOW_TYPES = new Set([
|
||||
'if_statement',
|
||||
'while_statement',
|
||||
'do_statement',
|
||||
'for_statement',
|
||||
'foreach_statement',
|
||||
'switch_statement',
|
||||
'try_statement',
|
||||
'using_statement',
|
||||
'lock_statement',
|
||||
'return_statement',
|
||||
'break_statement',
|
||||
'continue_statement',
|
||||
'throw_statement',
|
||||
'goto_statement',
|
||||
'labeled_statement',
|
||||
'yield_statement',
|
||||
'block',
|
||||
]);
|
||||
|
||||
const startLineOf = (n: SyntaxNode): number => n.startPosition.row + 1;
|
||||
const endLineOf = (n: SyntaxNode): number => n.endPosition.row + 1;
|
||||
|
||||
/** A statement sequence that produced no blocks (empty body) is "transparent". */
|
||||
type SeqResult = TraversalResult | null;
|
||||
|
||||
/**
|
||||
* Per-function C# walk state. One instance per function so the
|
||||
* {@link ControlFlowContext}, exception-handler stack, and label tables are
|
||||
* scoped to that function and never leak across functions.
|
||||
*/
|
||||
class CsharpCfgWalk {
|
||||
private readonly cfc = new ControlFlowContext();
|
||||
/** Stack of exception-handler entry blocks (catch/finally) a `throw` jumps to. */
|
||||
private readonly handlers: number[] = [];
|
||||
/** label name → its `labeled_statement` body's entry block (resolved on demand). */
|
||||
private readonly labelBlocks = new Map<string, number>();
|
||||
/** Pending gotos to a label not yet seen: label → list of source blocks. */
|
||||
private readonly pendingGotos = new Map<string, number[]>();
|
||||
|
||||
constructor(
|
||||
private readonly builder: CfgBuilder,
|
||||
private readonly harvest: CsharpHarvester,
|
||||
) {}
|
||||
|
||||
/** Statements of a block node, ignoring comments. */
|
||||
private statementsOf(block: SyntaxNode): SyntaxNode[] {
|
||||
return block.namedChildren.filter((c) => c.type !== 'comment');
|
||||
}
|
||||
|
||||
/** The `body` block of a node (field, or the first `block` child). */
|
||||
private bodyBlockOf(node: SyntaxNode): SyntaxNode | undefined {
|
||||
return node.childForFieldName('body') ?? node.namedChildren.find((c) => c.type === 'block');
|
||||
}
|
||||
|
||||
/** Visit a body that may be a `block` or a single statement. */
|
||||
private visitBody(node: SyntaxNode | undefined | null): SeqResult {
|
||||
if (!node) return null;
|
||||
if (node.type === 'block') return this.visitSeq(this.statementsOf(node));
|
||||
return this.visitStmt(node);
|
||||
}
|
||||
|
||||
/** Wire a sequence of statements, coalescing straight-line runs into blocks. */
|
||||
visitSeq(stmts: SyntaxNode[]): SeqResult {
|
||||
let entry: number | undefined;
|
||||
let dangling: number[] = [];
|
||||
let openSimple: number | undefined;
|
||||
|
||||
for (const stmt of stmts) {
|
||||
if (CONTROL_FLOW_TYPES.has(stmt.type)) {
|
||||
openSimple = undefined; // close any open straight-line block
|
||||
const res = this.visitStmt(stmt);
|
||||
if (res === null) continue; // transparent (empty nested block)
|
||||
if (entry === undefined) entry = res.entry;
|
||||
else this.builder.connect(dangling, res.entry, 'seq');
|
||||
dangling = [...res.exits];
|
||||
} else {
|
||||
if (openSimple === undefined) {
|
||||
const idx = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(stmt),
|
||||
stmt.text,
|
||||
'normal',
|
||||
this.harvest.facts(stmt),
|
||||
);
|
||||
if (entry === undefined) entry = idx;
|
||||
else this.builder.connect(dangling, idx, 'seq');
|
||||
openSimple = idx;
|
||||
dangling = [idx];
|
||||
} else {
|
||||
this.builder.extendBlock(openSimple, endLineOf(stmt), stmt.text, this.harvest.facts(stmt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entry === undefined) return null;
|
||||
return { entry, exits: dangling };
|
||||
}
|
||||
|
||||
/** Dispatch one statement to its handler. Non-null except for empty blocks. */
|
||||
visitStmt(stmt: SyntaxNode): SeqResult {
|
||||
switch (stmt.type) {
|
||||
case 'if_statement':
|
||||
return this.visitIf(stmt);
|
||||
case 'while_statement':
|
||||
return this.visitWhile(stmt);
|
||||
case 'do_statement':
|
||||
return this.visitDoWhile(stmt);
|
||||
case 'for_statement':
|
||||
return this.visitFor(stmt);
|
||||
case 'foreach_statement':
|
||||
return this.visitForEach(stmt);
|
||||
case 'switch_statement':
|
||||
return this.visitSwitch(stmt);
|
||||
case 'try_statement':
|
||||
return this.visitTry(stmt);
|
||||
case 'using_statement':
|
||||
return this.visitUsing(stmt);
|
||||
case 'lock_statement':
|
||||
return this.visitLock(stmt);
|
||||
case 'return_statement':
|
||||
return this.visitReturn(stmt);
|
||||
case 'throw_statement':
|
||||
return this.visitThrow(stmt);
|
||||
case 'break_statement':
|
||||
return this.visitBreak(stmt);
|
||||
case 'continue_statement':
|
||||
return this.visitContinue(stmt);
|
||||
case 'goto_statement':
|
||||
return this.visitGoto(stmt);
|
||||
case 'labeled_statement':
|
||||
return this.visitLabeled(stmt);
|
||||
case 'yield_statement':
|
||||
return this.visitYield(stmt);
|
||||
case 'block':
|
||||
return this.visitSeq(this.statementsOf(stmt));
|
||||
default:
|
||||
return this.visitSimple(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
private visitSimple(stmt: SyntaxNode): TraversalResult {
|
||||
const idx = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(stmt),
|
||||
stmt.text,
|
||||
'normal',
|
||||
this.harvest.facts(stmt),
|
||||
);
|
||||
return { entry: idx, exits: [idx] };
|
||||
}
|
||||
|
||||
private visitReturn(stmt: SyntaxNode): TraversalResult {
|
||||
const idx = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(stmt),
|
||||
stmt.text,
|
||||
'normal',
|
||||
this.harvest.facts(stmt),
|
||||
);
|
||||
// A return crosses EVERY active finally (try/using/lock) before EXIT.
|
||||
wireJumpThroughFinalizers(
|
||||
this.builder,
|
||||
idx,
|
||||
this.cfc.finalizersForReturn(),
|
||||
this.builder.exitIndex,
|
||||
'return',
|
||||
);
|
||||
return { entry: idx, exits: [] };
|
||||
}
|
||||
|
||||
private visitThrow(stmt: SyntaxNode): TraversalResult {
|
||||
const idx = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(stmt),
|
||||
stmt.text,
|
||||
'normal',
|
||||
this.harvest.facts(stmt),
|
||||
);
|
||||
this.builder.edge(idx, this.currentHandler(), 'throw');
|
||||
return { entry: idx, exits: [] };
|
||||
}
|
||||
|
||||
/** `yield return e;` continues; `yield break;` terminates the iterator. */
|
||||
private visitYield(stmt: SyntaxNode): TraversalResult {
|
||||
const idx = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(stmt),
|
||||
stmt.text,
|
||||
'normal',
|
||||
this.harvest.facts(stmt),
|
||||
);
|
||||
// `yield break;` (no value child) ends iteration → EXIT (modeled like return);
|
||||
// `yield return e;` (has a value child) yields one element and CONTINUES, so
|
||||
// it falls through to the next statement. The hidden iterator state machine
|
||||
// is not modeled (documented limitation).
|
||||
if (this.isYieldBreak(stmt)) {
|
||||
wireJumpThroughFinalizers(
|
||||
this.builder,
|
||||
idx,
|
||||
this.cfc.finalizersForReturn(),
|
||||
this.builder.exitIndex,
|
||||
'return',
|
||||
);
|
||||
return { entry: idx, exits: [] };
|
||||
}
|
||||
return { entry: idx, exits: [idx] };
|
||||
}
|
||||
|
||||
/** A `yield break;` has no value child; `yield return e;` has one. */
|
||||
private isYieldBreak(stmt: SyntaxNode): boolean {
|
||||
return stmt.namedChildCount === 0;
|
||||
}
|
||||
|
||||
private visitBreak(stmt: SyntaxNode): TraversalResult {
|
||||
const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
|
||||
const res = this.cfc.resolveBreak(undefined);
|
||||
const { target, finalizers } = res ?? {
|
||||
target: this.builder.exitIndex,
|
||||
finalizers: this.cfc.finalizersForReturn(),
|
||||
};
|
||||
wireJumpThroughFinalizers(this.builder, idx, finalizers, target, 'break');
|
||||
return { entry: idx, exits: [] };
|
||||
}
|
||||
|
||||
private visitContinue(stmt: SyntaxNode): TraversalResult {
|
||||
const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
|
||||
const res = this.cfc.resolveContinue(undefined);
|
||||
const { target, finalizers } = res ?? {
|
||||
target: this.builder.exitIndex,
|
||||
finalizers: this.cfc.finalizersForReturn(),
|
||||
};
|
||||
wireJumpThroughFinalizers(this.builder, idx, finalizers, target, 'continue');
|
||||
return { entry: idx, exits: [] };
|
||||
}
|
||||
|
||||
/** `goto label;` — route to the label block if known, else defer / EXIT. */
|
||||
private visitGoto(stmt: SyntaxNode): TraversalResult {
|
||||
const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
|
||||
const label = this.gotoLabel(stmt);
|
||||
if (label === undefined) {
|
||||
// `goto case L;` / `goto default;` — no statically-labeled CFG target.
|
||||
this.builder.edge(idx, this.builder.exitIndex, 'seq');
|
||||
return { entry: idx, exits: [] };
|
||||
}
|
||||
const target = this.labelBlocks.get(label);
|
||||
if (target !== undefined) {
|
||||
this.builder.edge(idx, target, 'seq'); // backward goto: label already built
|
||||
} else {
|
||||
const list = this.pendingGotos.get(label);
|
||||
if (list) list.push(idx);
|
||||
else this.pendingGotos.set(label, [idx]);
|
||||
}
|
||||
return { entry: idx, exits: [] };
|
||||
}
|
||||
|
||||
private visitLabeled(stmt: SyntaxNode): SeqResult {
|
||||
// `labeled_statement` = label `identifier` + the labeled statement.
|
||||
const labelNode = stmt.namedChildren.find((c) => c.type === 'identifier');
|
||||
const label = labelNode?.text;
|
||||
const body = stmt.namedChildren.find((c) => c.id !== labelNode?.id && c.type !== 'comment');
|
||||
const res = this.visitBody(body ?? null);
|
||||
if (label !== undefined) {
|
||||
const entry = res?.entry ?? this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
||||
this.labelBlocks.set(label, entry);
|
||||
const pending = this.pendingGotos.get(label);
|
||||
if (pending) {
|
||||
for (const from of pending) this.builder.edge(from, entry, 'seq');
|
||||
this.pendingGotos.delete(label);
|
||||
}
|
||||
if (!res) return { entry, exits: [entry] };
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private visitIf(stmt: SyntaxNode): TraversalResult {
|
||||
const cond = stmt.childForFieldName('condition') ?? stmt;
|
||||
const condBlock = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(cond),
|
||||
cond.text,
|
||||
'normal',
|
||||
this.harvest.facts(cond),
|
||||
);
|
||||
const exits: number[] = [];
|
||||
|
||||
const thenRes = this.visitBody(stmt.childForFieldName('consequence'));
|
||||
if (thenRes) {
|
||||
this.builder.edge(condBlock, thenRes.entry, 'cond-true');
|
||||
exits.push(...thenRes.exits);
|
||||
} else {
|
||||
exits.push(condBlock); // empty then — true path falls through
|
||||
}
|
||||
|
||||
// No `else_clause` wrapper in C#: `alternative` is the else body or the
|
||||
// nested `if_statement` for an `else if` chain directly.
|
||||
const elseNode = stmt.childForFieldName('alternative');
|
||||
if (elseNode) {
|
||||
const elseRes = this.visitBody(elseNode);
|
||||
if (elseRes) {
|
||||
this.builder.edge(condBlock, elseRes.entry, 'cond-false');
|
||||
exits.push(...elseRes.exits);
|
||||
} else {
|
||||
exits.push(condBlock);
|
||||
}
|
||||
} else {
|
||||
exits.push(condBlock); // no else — false path falls through to the join
|
||||
}
|
||||
|
||||
return { entry: condBlock, exits: [...new Set(exits)] };
|
||||
}
|
||||
|
||||
private visitWhile(stmt: SyntaxNode): TraversalResult {
|
||||
const cond = stmt.childForFieldName('condition') ?? stmt;
|
||||
const header = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(cond),
|
||||
cond.text,
|
||||
'normal',
|
||||
this.harvest.facts(cond),
|
||||
);
|
||||
const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
||||
|
||||
this.cfc.pushLoop(header, loopExit);
|
||||
const body = this.visitBody(this.bodyBlockOf(stmt));
|
||||
this.cfc.pop();
|
||||
|
||||
if (body) {
|
||||
this.builder.edge(header, body.entry, 'cond-true');
|
||||
this.builder.connect(body.exits, header, 'loop-back');
|
||||
} else {
|
||||
this.builder.edge(header, header, 'loop-back'); // empty body re-tests
|
||||
}
|
||||
// Always emit the structural exit edge — even `while (true)` keeps EXIT
|
||||
// reverse-reachable for the post-dominator / CDG pass.
|
||||
this.builder.edge(header, loopExit, 'cond-false');
|
||||
return { entry: header, exits: [loopExit] };
|
||||
}
|
||||
|
||||
private visitDoWhile(stmt: SyntaxNode): TraversalResult {
|
||||
const cond = stmt.childForFieldName('condition') ?? stmt;
|
||||
const condBlock = this.builder.newBlock(
|
||||
startLineOf(cond),
|
||||
endLineOf(cond),
|
||||
cond.text,
|
||||
'normal',
|
||||
this.harvest.facts(cond),
|
||||
);
|
||||
const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
||||
|
||||
this.cfc.pushLoop(condBlock, loopExit);
|
||||
const body = this.visitBody(this.bodyBlockOf(stmt));
|
||||
this.cfc.pop();
|
||||
|
||||
const backTarget = body ? body.entry : condBlock;
|
||||
if (body) this.builder.connect(body.exits, condBlock, 'seq');
|
||||
this.builder.edge(condBlock, backTarget, 'loop-back'); // cond true → run body again
|
||||
this.builder.edge(condBlock, loopExit, 'cond-false');
|
||||
return { entry: backTarget, exits: [loopExit] };
|
||||
}
|
||||
|
||||
private visitFor(stmt: SyntaxNode): TraversalResult {
|
||||
const init = stmt.childForFieldName('initializer');
|
||||
const cond = stmt.childForFieldName('condition');
|
||||
const incr = stmt.childForFieldName('update');
|
||||
|
||||
const header = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
cond ? endLineOf(cond) : startLineOf(stmt),
|
||||
cond ? cond.text : 'for(;;)',
|
||||
'normal',
|
||||
cond ? this.harvest.facts(cond) : undefined,
|
||||
);
|
||||
const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
||||
|
||||
let incrBlock = header;
|
||||
if (incr) {
|
||||
incrBlock = this.builder.newBlock(
|
||||
startLineOf(incr),
|
||||
endLineOf(incr),
|
||||
incr.text,
|
||||
'normal',
|
||||
this.harvest.facts(incr),
|
||||
);
|
||||
this.builder.edge(incrBlock, header, 'loop-back');
|
||||
}
|
||||
|
||||
this.cfc.pushLoop(incrBlock, loopExit);
|
||||
const body = this.visitBody(this.bodyBlockOf(stmt));
|
||||
this.cfc.pop();
|
||||
|
||||
if (body) {
|
||||
this.builder.edge(header, body.entry, 'cond-true');
|
||||
this.builder.connect(body.exits, incrBlock, incr ? 'seq' : 'loop-back');
|
||||
} else {
|
||||
this.builder.edge(header, incrBlock, 'cond-true');
|
||||
if (!incr) this.builder.edge(header, header, 'loop-back');
|
||||
}
|
||||
// Structural exit edge — `for (;;) {}` (no condition) still keeps EXIT
|
||||
// reverse-reachable so CDG is not silently skipped for the function.
|
||||
this.builder.edge(header, loopExit, 'cond-false');
|
||||
|
||||
let entry = header;
|
||||
if (init) {
|
||||
const initBlock = this.builder.newBlock(
|
||||
startLineOf(init),
|
||||
endLineOf(init),
|
||||
init.text,
|
||||
'normal',
|
||||
this.harvest.facts(init),
|
||||
);
|
||||
this.builder.edge(initBlock, header, 'seq');
|
||||
entry = initBlock;
|
||||
}
|
||||
return { entry, exits: [loopExit] };
|
||||
}
|
||||
|
||||
private visitForEach(stmt: SyntaxNode): TraversalResult {
|
||||
// Header text is SYNTHESIZED, so facts come from the left/right nodes
|
||||
// directly (the loop variable is a def, the iterated expression a use).
|
||||
const header = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
startLineOf(stmt),
|
||||
this.forEachHeaderText(stmt),
|
||||
'normal',
|
||||
this.harvest.forEachHeadFacts(stmt),
|
||||
);
|
||||
const loopExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
||||
|
||||
this.cfc.pushLoop(header, loopExit);
|
||||
const body = this.visitBody(this.bodyBlockOf(stmt));
|
||||
this.cfc.pop();
|
||||
|
||||
if (body) {
|
||||
this.builder.edge(header, body.entry, 'cond-true');
|
||||
this.builder.connect(body.exits, header, 'loop-back');
|
||||
} else {
|
||||
this.builder.edge(header, header, 'loop-back');
|
||||
}
|
||||
this.builder.edge(header, loopExit, 'cond-false');
|
||||
return { entry: header, exits: [loopExit] };
|
||||
}
|
||||
|
||||
private forEachHeaderText(stmt: SyntaxNode): string {
|
||||
const left = stmt.childForFieldName('left')?.text ?? '';
|
||||
const right = stmt.childForFieldName('right')?.text ?? '';
|
||||
return left || right ? `foreach(${left} in ${right})` : 'foreach(… in …)';
|
||||
}
|
||||
|
||||
private visitSwitch(stmt: SyntaxNode): TraversalResult {
|
||||
const value = stmt.childForFieldName('value') ?? stmt;
|
||||
const dispatch = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(value),
|
||||
value.text,
|
||||
'normal',
|
||||
this.harvest.facts(value),
|
||||
);
|
||||
const switchExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
||||
|
||||
this.cfc.pushSwitch(switchExit);
|
||||
const body = stmt.childForFieldName('body');
|
||||
const sections = body
|
||||
? body.namedChildren.filter((c) => c.type === 'switch_section')
|
||||
: [];
|
||||
|
||||
// Each `switch_section`'s case-test patterns/`when` clauses evaluate before
|
||||
// the body runs — harvest their uses (and any pattern binding as a may-def)
|
||||
// onto the dispatch block, one record per section, conditionally (a later
|
||||
// section test only runs when earlier sections didn't match).
|
||||
for (const sec of sections) {
|
||||
for (const test of this.sectionTests(sec)) {
|
||||
this.builder.attachFacts(dispatch, this.harvest.factsConditional(test));
|
||||
}
|
||||
}
|
||||
|
||||
const sectionResults = sections.map((s) => this.visitSeq(this.sectionStatements(s)));
|
||||
const hasDefault = sections.some((s) => this.sectionIsDefault(s));
|
||||
|
||||
const entryOf: number[] = new Array(sections.length);
|
||||
let after = switchExit;
|
||||
for (let i = sections.length - 1; i >= 0; i--) {
|
||||
entryOf[i] = sectionResults[i]?.entry ?? after;
|
||||
after = entryOf[i];
|
||||
}
|
||||
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
this.builder.edge(dispatch, entryOf[i], 'switch-case');
|
||||
}
|
||||
if (!hasDefault) this.builder.edge(dispatch, switchExit, 'switch-case'); // no-match path
|
||||
|
||||
// A non-`break` section's dangling exits fall through to the next section
|
||||
// (an empty `case 2:` falling into `case 3:`, or `goto case`-less spill).
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const res = sectionResults[i];
|
||||
if (!res) continue;
|
||||
const fallTarget = i + 1 < sections.length ? entryOf[i + 1] : switchExit;
|
||||
this.builder.connect(res.exits, fallTarget, 'fallthrough');
|
||||
}
|
||||
|
||||
this.cfc.pop();
|
||||
return { entry: dispatch, exits: [switchExit] };
|
||||
}
|
||||
|
||||
/** A switch_section's body statements (everything but its case labels/patterns). */
|
||||
private sectionStatements(section: SyntaxNode): SyntaxNode[] {
|
||||
return section.namedChildren.filter(
|
||||
(c) => this.isStatementLike(c) && c.type !== 'comment',
|
||||
);
|
||||
}
|
||||
|
||||
/** The case-test expressions of a section (constant patterns + when clauses). */
|
||||
private sectionTests(section: SyntaxNode): SyntaxNode[] {
|
||||
const out: SyntaxNode[] = [];
|
||||
for (const c of section.namedChildren) {
|
||||
if (c.type === 'when_clause') {
|
||||
const inner = c.namedChild(0);
|
||||
if (inner) out.push(inner);
|
||||
} else if (!this.isStatementLike(c) && c.type !== 'comment') {
|
||||
out.push(c); // a pattern (constant_pattern / declaration_pattern / …)
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A `default:` section has only a `default` label (no pattern). */
|
||||
private sectionIsDefault(section: SyntaxNode): boolean {
|
||||
return !section.namedChildren.some(
|
||||
(c) => !this.isStatementLike(c) && c.type !== 'comment' && c.type !== 'when_clause',
|
||||
);
|
||||
}
|
||||
|
||||
/** Heuristic: a section child is statement-like if it ends a basic-block run. */
|
||||
private isStatementLike(node: SyntaxNode): boolean {
|
||||
return node.type.endsWith('_statement') || node.type === 'block';
|
||||
}
|
||||
|
||||
private visitTry(stmt: SyntaxNode): SeqResult {
|
||||
const bodyNode = stmt.childForFieldName('body');
|
||||
const catchClauses: SyntaxNode[] = [];
|
||||
let finallyClause: SyntaxNode | undefined;
|
||||
for (let i = 0; i < stmt.namedChildCount; i++) {
|
||||
const c = stmt.namedChild(i);
|
||||
if (c?.type === 'catch_clause') catchClauses.push(c);
|
||||
else if (c?.type === 'finally_clause') finallyClause = c;
|
||||
}
|
||||
const finallyBody = finallyClause?.namedChildren.find((c) => c.type === 'block');
|
||||
return this.buildProtected(bodyNode ?? null, catchClauses, finallyBody ?? null, stmt);
|
||||
}
|
||||
|
||||
/**
|
||||
* `using (resource) body` — the dispose runs deterministically on BOTH normal
|
||||
* AND exception exit, which is exactly `try { body } finally { dispose }`. We
|
||||
* model the dispose as a synthesized finalizer block (the AST has no Dispose()
|
||||
* call node), so a `return`/`break`/`continue` crossing it threads through and
|
||||
* gets a `finally-*` completion edge.
|
||||
*/
|
||||
private visitUsing(stmt: SyntaxNode): SeqResult {
|
||||
const bodyNode = this.bodyBlockOf(stmt) ?? null;
|
||||
// The resource (declaration or expression) is the named child before `body`.
|
||||
const resource = this.usingResource(stmt);
|
||||
const disposeFacts = resource ? this.harvest.facts(resource) : undefined;
|
||||
return this.buildProtectedSynthetic(
|
||||
bodyNode,
|
||||
stmt,
|
||||
'dispose',
|
||||
disposeFacts,
|
||||
resource ?? stmt,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* `lock (obj) body` — the monitor release runs on both normal and exception
|
||||
* exit (deterministic finalizer), same shape as `using`'s dispose.
|
||||
*/
|
||||
private visitLock(stmt: SyntaxNode): SeqResult {
|
||||
const bodyNode = this.bodyBlockOf(stmt) ?? null;
|
||||
const lockObj = stmt.namedChildren.find((c) => c.type !== 'block');
|
||||
const releaseFacts = lockObj ? this.harvest.facts(lockObj) : undefined;
|
||||
return this.buildProtectedSynthetic(bodyNode, stmt, 'release', releaseFacts, lockObj ?? stmt);
|
||||
}
|
||||
|
||||
/** The `using` resource node (variable_declaration or an expression). */
|
||||
private usingResource(stmt: SyntaxNode): SyntaxNode | undefined {
|
||||
const body = stmt.childForFieldName('body');
|
||||
return stmt.namedChildren.find((c) => c.id !== body?.id && c.type !== 'comment');
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared try/catch/finally builder. `catchClauses` may be empty; `finallyBody`
|
||||
* is the finally's `block` (or null). Models the TS `visitTry` semantics:
|
||||
* normal completion of try AND catch flow through finally; a throw in the
|
||||
* protected region routes to the handler; early exits crossing the finally
|
||||
* thread through it (finally-* completion edges).
|
||||
*/
|
||||
private buildProtected(
|
||||
bodyNode: SyntaxNode | null,
|
||||
catchClauses: SyntaxNode[],
|
||||
finallyBody: SyntaxNode | null,
|
||||
span: SyntaxNode,
|
||||
): SeqResult {
|
||||
const finallyRes = finallyBody ? this.visitSeq(this.statementsOf(finallyBody)) : null;
|
||||
const finFrame = finallyRes ? this.cfc.pushFinalizer(finallyRes.entry) : null;
|
||||
|
||||
// Build each catch handler.
|
||||
const catchEntries: number[] = [];
|
||||
const catchExits: number[] = [];
|
||||
let firstCatchEntry: number | undefined;
|
||||
for (const clause of catchClauses) {
|
||||
const clauseBody = clause.childForFieldName('body');
|
||||
if (finallyRes) this.handlers.push(finallyRes.entry);
|
||||
let res: SeqResult = clauseBody
|
||||
? this.visitSeq(this.statementsOf(clauseBody))
|
||||
: null;
|
||||
if (finallyRes) this.handlers.pop();
|
||||
if (res === null) {
|
||||
// Empty `catch {}` still catches — synthesize one block so exception
|
||||
// flow lands somewhere and the post-try code stays reachable.
|
||||
const idx = this.builder.newBlock(startLineOf(clause), endLineOf(clause), '');
|
||||
res = { entry: idx, exits: [idx] };
|
||||
}
|
||||
const paramFacts = this.harvest.catchParamFacts(clause);
|
||||
if (paramFacts) {
|
||||
const paramBlock = this.builder.newBlock(
|
||||
startLineOf(clause),
|
||||
startLineOf(clause),
|
||||
'',
|
||||
'normal',
|
||||
paramFacts,
|
||||
);
|
||||
this.builder.edge(paramBlock, res.entry, 'seq');
|
||||
res = { entry: paramBlock, exits: res.exits };
|
||||
}
|
||||
catchEntries.push(res.entry);
|
||||
catchExits.push(...res.exits);
|
||||
if (firstCatchEntry === undefined) firstCatchEntry = res.entry;
|
||||
}
|
||||
|
||||
// Handler for the try body: first catch if present, else finally, else outer.
|
||||
const tryHandler = firstCatchEntry ?? finallyRes?.entry ?? this.currentHandler();
|
||||
const protectedStart = this.builder.blockCount;
|
||||
this.handlers.push(tryHandler);
|
||||
const bodyRes = bodyNode
|
||||
? bodyNode.type === 'block'
|
||||
? this.visitSeq(this.statementsOf(bodyNode))
|
||||
: this.visitStmt(bodyNode)
|
||||
: null;
|
||||
this.handlers.pop();
|
||||
|
||||
if (catchClauses.length > 0 || finallyBody) {
|
||||
for (let b = protectedStart; b < this.builder.blockCount; b++) {
|
||||
this.builder.edge(b, tryHandler, 'throw');
|
||||
}
|
||||
}
|
||||
|
||||
if (finFrame && finallyRes) {
|
||||
this.cfc.pop();
|
||||
drainFinalizerPending(this.builder, finFrame, finallyRes.exits);
|
||||
}
|
||||
|
||||
const exits: number[] = [];
|
||||
if (finallyRes) {
|
||||
if (bodyRes) this.builder.connect(bodyRes.exits, finallyRes.entry, 'seq');
|
||||
for (const e of catchExits) this.builder.edge(e, finallyRes.entry, 'seq');
|
||||
exits.push(...finallyRes.exits);
|
||||
// No catch → an exception re-propagates out after finally runs.
|
||||
if (catchClauses.length === 0) {
|
||||
this.builder.connect(finallyRes.exits, this.currentHandler(), 'throw');
|
||||
}
|
||||
} else {
|
||||
if (bodyRes) exits.push(...bodyRes.exits);
|
||||
exits.push(...catchExits);
|
||||
}
|
||||
|
||||
const entry = bodyRes?.entry ?? finallyRes?.entry ?? catchEntries[0];
|
||||
if (entry === undefined) {
|
||||
void span;
|
||||
return null;
|
||||
}
|
||||
return { entry, exits: [...new Set(exits)] };
|
||||
}
|
||||
|
||||
/**
|
||||
* `using`/`lock`: a protected body whose finalizer is a SYNTHESIZED single
|
||||
* block (Dispose() / Monitor.Exit() have no AST node). The finalizer runs on
|
||||
* both normal and exception exit, and crossing jumps thread through it.
|
||||
*/
|
||||
private buildProtectedSynthetic(
|
||||
bodyNode: SyntaxNode | null,
|
||||
span: SyntaxNode,
|
||||
text: string,
|
||||
finalizerFacts: StatementFactsLike,
|
||||
factsNode: SyntaxNode,
|
||||
): SeqResult {
|
||||
void factsNode;
|
||||
const finalizerBlock = this.builder.newBlock(
|
||||
endLineOf(span),
|
||||
endLineOf(span),
|
||||
text,
|
||||
'normal',
|
||||
finalizerFacts ?? undefined,
|
||||
);
|
||||
const finRes: TraversalResult = { entry: finalizerBlock, exits: [finalizerBlock] };
|
||||
const finFrame = this.cfc.pushFinalizer(finRes.entry);
|
||||
|
||||
const protectedStart = this.builder.blockCount;
|
||||
// The finalizer IS the handler — an exception in the body still runs dispose,
|
||||
// which then re-propagates to the outer handler.
|
||||
this.handlers.push(finRes.entry);
|
||||
const bodyRes = bodyNode
|
||||
? bodyNode.type === 'block'
|
||||
? this.visitSeq(this.statementsOf(bodyNode))
|
||||
: this.visitStmt(bodyNode)
|
||||
: null;
|
||||
this.handlers.pop();
|
||||
|
||||
for (let b = protectedStart; b < this.builder.blockCount; b++) {
|
||||
this.builder.edge(b, finRes.entry, 'throw');
|
||||
}
|
||||
|
||||
this.cfc.pop();
|
||||
drainFinalizerPending(this.builder, finFrame, finRes.exits);
|
||||
|
||||
// Normal completion of the body flows through the finalizer; the finalizer's
|
||||
// exit re-propagates an exception to the outer handler (it had no catch).
|
||||
if (bodyRes) this.builder.connect(bodyRes.exits, finRes.entry, 'seq');
|
||||
this.builder.connect(finRes.exits, this.currentHandler(), 'throw');
|
||||
|
||||
const entry = bodyRes?.entry ?? finRes.entry;
|
||||
return { entry, exits: [...finRes.exits] };
|
||||
}
|
||||
|
||||
/** Nearest enclosing exception handler, or the function EXIT. */
|
||||
private currentHandler(): number {
|
||||
return this.handlers.length ? this.handlers[this.handlers.length - 1] : this.builder.exitIndex;
|
||||
}
|
||||
|
||||
/** The target label of a `goto label;` (undefined for `goto case`/`goto default`). */
|
||||
private gotoLabel(stmt: SyntaxNode): string | undefined {
|
||||
const id = stmt.namedChildren.find((c) => c.type === 'identifier');
|
||||
return id?.text;
|
||||
}
|
||||
}
|
||||
|
||||
/** A pre-built {@link StatementFacts} record, or undefined when none. */
|
||||
type StatementFactsLike = ReturnType<CsharpHarvester['facts']> | undefined;
|
||||
|
||||
/** Build the CFG for one C# function node, or `undefined` if not modelable. */
|
||||
function buildFunctionCfg(fnNode: SyntaxNode, filePath: string): FunctionCfg | undefined {
|
||||
try {
|
||||
if (!CSHARP_FUNCTION_TYPES.has(fnNode.type)) return undefined;
|
||||
const startLine = startLineOf(fnNode);
|
||||
const endLine = endLineOf(fnNode);
|
||||
const startColumn = fnNode.startPosition.column;
|
||||
|
||||
// The body is a `block` (field `body`) OR an `arrow_expression_clause`
|
||||
// (expression-bodied member) OR an expression (single-expression lambda).
|
||||
const body =
|
||||
fnNode.childForFieldName('body') ??
|
||||
fnNode.namedChildren.find((c) => c.type === 'block');
|
||||
if (!body) return undefined; // abstract / partial / interface member — no body
|
||||
|
||||
const builder = new CfgBuilder(filePath, startLine, endLine, startColumn);
|
||||
const harvest = new CsharpHarvester(fnNode);
|
||||
|
||||
const paramFacts = harvest.paramFacts();
|
||||
if (paramFacts) builder.attachFacts(builder.entryIndex, paramFacts);
|
||||
|
||||
if (body.type === 'arrow_expression_clause' || body.type !== 'block') {
|
||||
// Expression-bodied member / single-expression lambda: one block whose
|
||||
// value is returned. For an arrow clause the value is its inner expression.
|
||||
const expr =
|
||||
body.type === 'arrow_expression_clause' ? (body.namedChild(0) ?? body) : body;
|
||||
const blk = builder.newBlock(
|
||||
startLineOf(expr),
|
||||
endLineOf(expr),
|
||||
expr.text,
|
||||
'normal',
|
||||
harvest.facts(expr),
|
||||
);
|
||||
builder.edge(builder.entryIndex, blk, 'seq');
|
||||
builder.edge(blk, builder.exitIndex, 'return');
|
||||
return builder.finish(harvest.table());
|
||||
}
|
||||
|
||||
const walk = new CsharpCfgWalk(builder, harvest);
|
||||
const res = walk.visitSeq(body.namedChildren.filter((c) => c.type !== 'comment'));
|
||||
if (!res) {
|
||||
builder.edge(builder.entryIndex, builder.exitIndex, 'seq'); // empty body
|
||||
return builder.finish(harvest.table());
|
||||
}
|
||||
builder.edge(builder.entryIndex, res.entry, 'seq');
|
||||
builder.connect(res.exits, builder.exitIndex, 'seq'); // normal fall-off → EXIT
|
||||
return builder.finish(harvest.table());
|
||||
} catch (err) {
|
||||
// Never throw out of buildFunctionCfg — a malformed AST shape must skip only
|
||||
// this one function's CFG, never drop the whole file's language group (R4).
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[cfg] C# buildFunctionCfg skipped a function in ${filePath}: ${String(err)}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a node is a C# function this visitor builds a CFG for. */
|
||||
function isFunction(node: SyntaxNode): boolean {
|
||||
return CSHARP_FUNCTION_TYPES.has(node.type);
|
||||
}
|
||||
|
||||
/** The C# CFG visitor. */
|
||||
export function createCsharpCfgVisitor(): CfgVisitor<SyntaxNode> {
|
||||
return { buildFunctionCfg, isFunction };
|
||||
}
|
||||
|
||||
export { CSHARP_FUNCTION_TYPES };
|
||||
|
|
@ -24,6 +24,7 @@ import { createMethodExtractor } from '../method-extractors/generic.js';
|
|||
import { csharpMethodConfig } from '../method-extractors/configs/csharp.js';
|
||||
import { createVariableExtractor } from '../variable-extractors/generic.js';
|
||||
import { csharpVariableConfig } from '../variable-extractors/configs/csharp.js';
|
||||
import { createCsharpCfgVisitor } from '../cfg/visitors/csharp.js';
|
||||
import {
|
||||
emitCsharpScopeCaptures,
|
||||
interpretCsharpImport,
|
||||
|
|
@ -200,6 +201,7 @@ export const csharpProvider = defineLanguage({
|
|||
// the full per-hook rationale and the canonical capture vocabulary
|
||||
// in ./csharp/query.ts (CSHARP_SCOPE_QUERY constant).
|
||||
emitScopeCaptures: emitCsharpScopeCaptures,
|
||||
cfgVisitor: createCsharpCfgVisitor(),
|
||||
interpretImport: interpretCsharpImport,
|
||||
interpretTypeBinding: interpretCsharpTypeBinding,
|
||||
bindingScopeFor: csharpBindingScopeFor,
|
||||
|
|
|
|||
159
gitnexus/test/integration/cfg/fixtures/csharp-hazards.cs
Normal file
159
gitnexus/test/integration/cfg/fixtures/csharp-hazards.cs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
// C# CFG hazard fixture (#2195 U3). Exercises every control-flow construct the
|
||||
// csharp CfgVisitor models, so the worker-mode pipeline produces non-trivial
|
||||
// BasicBlock / CFG / REACHING_DEF / CDG counts (and so the byte-identical-off
|
||||
// golden gate has real shapes to compare). Mirrors c-hazards.c / cpp-hazards.cpp.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Hazards
|
||||
{
|
||||
public class Demo
|
||||
{
|
||||
// if / else-if / else.
|
||||
public int Classify(int x)
|
||||
{
|
||||
if (x > 0)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else if (x < 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// for, foreach, while, do-while + break / continue.
|
||||
public int Loops(int[] xs, int n)
|
||||
{
|
||||
int total = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
if (i == 3) { continue; }
|
||||
total += i;
|
||||
}
|
||||
foreach (var x in xs)
|
||||
{
|
||||
if (x < 0) { break; }
|
||||
total += x;
|
||||
}
|
||||
int j = 0;
|
||||
while (j < n)
|
||||
{
|
||||
total += j;
|
||||
j++;
|
||||
}
|
||||
do
|
||||
{
|
||||
total -= 1;
|
||||
} while (total > 100);
|
||||
return total;
|
||||
}
|
||||
|
||||
// switch_statement with fallthrough-empty section + default.
|
||||
public string Name(int code)
|
||||
{
|
||||
switch (code)
|
||||
{
|
||||
case 1:
|
||||
case 2:
|
||||
return "low";
|
||||
case 3:
|
||||
return "three";
|
||||
default:
|
||||
return "other";
|
||||
}
|
||||
}
|
||||
|
||||
// switch_expression arms.
|
||||
public int Score(int grade) => grade switch
|
||||
{
|
||||
1 => 100,
|
||||
2 => 80,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
// try / catch / finally with a return crossing the finally.
|
||||
public int Guarded(int x)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (x < 0) { throw new ArgumentException(nameof(x)); }
|
||||
return Compute(x);
|
||||
}
|
||||
catch (ArgumentException e)
|
||||
{
|
||||
Log(e);
|
||||
return -1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
// using (deterministic dispose on both normal and exception exit).
|
||||
public int ReadAll(string path)
|
||||
{
|
||||
using (var reader = Open(path))
|
||||
{
|
||||
return reader.Read();
|
||||
}
|
||||
}
|
||||
|
||||
// lock (monitor release finalizer).
|
||||
private readonly object _sync = new object();
|
||||
public void Touch(int v)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_value += v;
|
||||
}
|
||||
}
|
||||
|
||||
// goto / labeled statement.
|
||||
public int Retry(int limit)
|
||||
{
|
||||
int attempts = 0;
|
||||
start:
|
||||
attempts++;
|
||||
if (attempts < limit) { goto start; }
|
||||
return attempts;
|
||||
}
|
||||
|
||||
// yield iterator (state-machine limitation documented; surface flow only).
|
||||
public IEnumerable<int> Take(int[] src, int count)
|
||||
{
|
||||
int taken = 0;
|
||||
foreach (var item in src)
|
||||
{
|
||||
if (taken >= count) { yield break; }
|
||||
taken++;
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
|
||||
// null-coalescing may-def + local function.
|
||||
public int Resolve(string s)
|
||||
{
|
||||
int Parse(string v) => int.Parse(v);
|
||||
string chosen = s ?? (s = "0");
|
||||
return Parse(chosen);
|
||||
}
|
||||
|
||||
private int _value;
|
||||
private int Compute(int x) => x * 2;
|
||||
private static void Log(Exception e) { }
|
||||
private void Cleanup() { }
|
||||
private Reader Open(string path) => new Reader();
|
||||
}
|
||||
|
||||
public class Reader
|
||||
{
|
||||
public int Read() => 0;
|
||||
}
|
||||
}
|
||||
389
gitnexus/test/unit/cfg/csharp-visitor.test.ts
Normal file
389
gitnexus/test/unit/cfg/csharp-visitor.test.ts
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
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 } from '../../../src/core/ingestion/cfg/types.js';
|
||||
import { makeCfgHarness, type CfgHarness } from '../../helpers/cfg-harness.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.
|
||||
|
||||
// tree-sitter-c-sharp declares `main: "bindings/node"` (no extension) — load the
|
||||
// explicit subpath, mirroring parser-loader.ts (#1013).
|
||||
const csGrammar = createRequire(import.meta.url)(
|
||||
'tree-sitter-c-sharp/bindings/node/index.js',
|
||||
) as Parameters<typeof makeCfgHarness>[0];
|
||||
|
||||
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 =>
|
||||
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 (structural exit-escape edge)', () => {
|
||||
const cfg = cs.cfgOf(`class C { void M() { while (true) { work(); } } }`);
|
||||
expect(edgeKinds(cfg).has('cond-false')).toBe(true);
|
||||
expect(exitReachableFromAll(cfg)).toBe(true);
|
||||
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
});
|
||||
|
||||
it('for (;;) {} keeps EXIT reverse-reachable', () => {
|
||||
const cfg = cs.cfgOf(`class C { void M() { for (;;) { work(); } } }`);
|
||||
expect(edgeKinds(cfg).has('cond-false')).toBe(true);
|
||||
expect(exitReachableFromAll(cfg)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
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('switch_expression arms each dispatch as a guarded branch (switch-case)', () => {
|
||||
const cfg = cs.cfgOf(`class C { int M(int x) { return x switch { 1 => a(), 2 => b(), _ => c() }; } }`);
|
||||
// The switch-expression lives inside the return block — it does not break a
|
||||
// basic block, but the function still has a well-formed single-exit CFG.
|
||||
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
expect(edgeKinds(cfg).has('return')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
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('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);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue