mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
feat(cfg): Java CFG visitor + def/use harvest (#2195 U4)
Add createJavaCfgVisitor + java-harvest over the shared CfgBuilder / ControlFlowContext: if/else, classic for, enhanced-for, while, do-while, classic-vs-arrow switch (switch_block_statement_group fallthrough vs switch_rule no-fallthrough), try/catch/finally + try-with-resources (auto-close synthesized as a finalizer, closes on normal AND exception exit) + synchronized (monitor-release finalizer), labeled break/continue to the labeled frame, yield, return/throw/break/continue. Wire into javaProvider. Every literal validated against tree-sitter-java via the probe (switch_expression covers both switch forms, generic_type, line_comment, for init field). Edge kinds match the contract; functionStartColumn populated; while(true)/for(;;) keep EXIT reverse-reachable (production CDG probe: 3 edges; hazard fixture: 34 CDG edges). buildFunctionCfg returns undefined rather than throwing. 43 real-parser regression tests; grammar-literal gate green; no regression (cfg unit suite 304, tsc clean). Documented gaps: switch-as- expression-value inline, yield state machine, async/field-write defs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aad3a702b9
commit
6dbe7373ff
5 changed files with 2175 additions and 0 deletions
512
gitnexus/src/core/ingestion/cfg/visitors/java-harvest.ts
Normal file
512
gitnexus/src/core/ingestion/cfg/visitors/java-harvest.ts
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
/**
|
||||
* Java def/use harvester (#2195 U4, plan KTD2) — the Java analogue of
|
||||
* {@link import('./typescript-harvest.js').TsHarvester} and the closely-related
|
||||
* {@link import('./csharp-harvest.js').CsharpHarvester}.
|
||||
*
|
||||
* Runs in the parse worker next to the Java 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# 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_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.) and `update_expression`
|
||||
* (`x++` / `--x`) — define and (for compound / update) also use the lvalue.
|
||||
* - parameters (`formal_parameter` / `spread_parameter` → `name`), the
|
||||
* enhanced-for loop variable (`enhanced_for_statement` field `name`), and
|
||||
* catch parameters (`catch_formal_parameter` → `name`), incl. multi-catch.
|
||||
* EXCLUDED, deliberately (TypeScript-CFA precedent): field / array writes
|
||||
* (`obj.f = …`, `a[i] = …`) are NOT scalar defs — their identifiers are uses
|
||||
* only. Nested-function (lambda) 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 && (x = f())`), a ternary arm, or a switch case test
|
||||
* — is a may-def (gen without kill), so the not-taken path's prior def is not
|
||||
* falsely killed. (Java has no `??`.)
|
||||
*
|
||||
* Identifiers with no in-function declaration (fields, statics, imported 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',
|
||||
'method_declaration',
|
||||
'constructor_declaration',
|
||||
'compact_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 `try_with_resources_statement`
|
||||
* scopes its resource declarations; a switch group/rule scopes its statements.
|
||||
*/
|
||||
const SCOPE_TYPES = new Set([
|
||||
'block',
|
||||
'for_statement',
|
||||
'enhanced_for_statement',
|
||||
'while_statement',
|
||||
'do_statement',
|
||||
'catch_clause',
|
||||
'try_with_resources_statement',
|
||||
'switch_block_statement_group',
|
||||
'switch_rule',
|
||||
]);
|
||||
|
||||
/** Comment node types tree-sitter-java surfaces (NOT `comment`). */
|
||||
const COMMENT_TYPES = new Set(['line_comment', 'block_comment']);
|
||||
|
||||
interface Scope {
|
||||
readonly parent: Scope | null;
|
||||
/** name → binding index */
|
||||
readonly table: Map<string, number>;
|
||||
}
|
||||
|
||||
export class JavaHarvester {
|
||||
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 && body.type === 'block') 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 `x -> expr`). */
|
||||
private bodyOf(fnNode: SyntaxNode): SyntaxNode | undefined {
|
||||
const body = fnNode.childForFieldName('body');
|
||||
if (body) return body;
|
||||
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');
|
||||
if (!params) {
|
||||
// Lambda single un-parenthesized parameter: `x -> …` (a bare identifier).
|
||||
const lambdaParam = fnNode.namedChildren.find((c) => c.type === 'identifier');
|
||||
if (fnNode.type === 'lambda_expression' && lambdaParam) {
|
||||
this.declare(lambdaParam, 'param', this.root);
|
||||
}
|
||||
// Lambda inferred parameters: `(x, y) -> …`.
|
||||
const inferred = fnNode.namedChildren.find((c) => c.type === 'inferred_parameters');
|
||||
if (inferred) {
|
||||
for (let i = 0; i < inferred.namedChildCount; i++) {
|
||||
const p = inferred.namedChild(i);
|
||||
if (p?.type === 'identifier') this.declare(p, 'param', this.root);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < params.namedChildCount; i++) {
|
||||
const p = params.namedChild(i);
|
||||
if (p?.type !== 'formal_parameter' && p?.type !== 'spread_parameter') continue;
|
||||
const name = this.paramName(p);
|
||||
if (name) this.declare(name, 'param', this.root);
|
||||
}
|
||||
}
|
||||
|
||||
/** The `name` identifier of a `formal_parameter` / `spread_parameter`. */
|
||||
private paramName(param: SyntaxNode): SyntaxNode | undefined {
|
||||
const named = param.childForFieldName('name');
|
||||
if (named) return named;
|
||||
// `spread_parameter` (`int... xs`) exposes its name through a nested
|
||||
// variable_declarator rather than a `name` field.
|
||||
const declarator = param.namedChildren.find((c) => c.type === 'variable_declarator');
|
||||
return declarator?.childForFieldName('name');
|
||||
}
|
||||
|
||||
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_variable_declaration':
|
||||
this.declareVariableDeclaration(node, childScope);
|
||||
break;
|
||||
case 'enhanced_for_statement': {
|
||||
const name = node.childForFieldName('name');
|
||||
if (name) this.declare(name, 'var', childScope);
|
||||
break;
|
||||
}
|
||||
case 'resource': {
|
||||
// `try (var f = open())` — the resource binds in the try scope.
|
||||
const name = node.childForFieldName('name');
|
||||
if (name) this.declare(name, 'var', childScope);
|
||||
break;
|
||||
}
|
||||
case 'catch_clause': {
|
||||
const param = node.namedChildren.find((c) => c.type === 'catch_formal_parameter');
|
||||
const name = param?.childForFieldName('name');
|
||||
if (name) this.declare(name, 'catch', 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 `local_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);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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 `for (T name : value)` head: name binds, value is used. */
|
||||
forEachHeadFacts(stmt: SyntaxNode): StatementFacts {
|
||||
const acc = new FactAccumulator(stmt.startPosition.row + 1);
|
||||
const name = stmt.childForFieldName('name');
|
||||
const value = stmt.childForFieldName('value');
|
||||
if (name) this.def(name, acc);
|
||||
if (value) this.walkValue(value, acc);
|
||||
return acc.finish();
|
||||
}
|
||||
|
||||
/** Facts for the resource-close finalizer: each resource is USED on close. */
|
||||
resourceCloseFacts(resources: readonly SyntaxNode[]): StatementFacts | undefined {
|
||||
const first = resources[0];
|
||||
if (!first) return undefined;
|
||||
const acc = new FactAccumulator(first.startPosition.row + 1);
|
||||
for (const r of resources) {
|
||||
const name = r.childForFieldName('name');
|
||||
if (name) this.use(name, acc);
|
||||
}
|
||||
return acc.useCount() ? acc.finish() : undefined;
|
||||
}
|
||||
|
||||
/** ENTRY-block facts for the function's parameters (defs only). */
|
||||
paramFacts(): StatementFacts | undefined {
|
||||
const acc = new FactAccumulator(this.fnNode.startPosition.row + 1);
|
||||
const params = this.fnNode.childForFieldName('parameters');
|
||||
if (params) {
|
||||
for (let i = 0; i < params.namedChildCount; i++) {
|
||||
const p = params.namedChild(i);
|
||||
if (p?.type !== 'formal_parameter' && p?.type !== 'spread_parameter') continue;
|
||||
const name = this.paramName(p);
|
||||
if (name) this.def(name, acc);
|
||||
}
|
||||
} else if (this.fnNode.type === 'lambda_expression') {
|
||||
const lambdaParam = this.fnNode.namedChildren.find((c) => c.type === 'identifier');
|
||||
if (lambdaParam) this.def(lambdaParam, acc);
|
||||
const inferred = this.fnNode.namedChildren.find((c) => c.type === 'inferred_parameters');
|
||||
if (inferred) {
|
||||
for (let i = 0; i < inferred.namedChildCount; i++) {
|
||||
const p = inferred.namedChild(i);
|
||||
if (p?.type === 'identifier') this.def(p, acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
return acc.defCount() ? acc.finish() : undefined;
|
||||
}
|
||||
|
||||
/** Def fact for a `catch (T e)` parameter — prepend to the handler entry block. */
|
||||
catchParamFacts(catchClause: SyntaxNode): StatementFacts | undefined {
|
||||
const param = catchClause.namedChildren.find((c) => c.type === 'catch_formal_parameter');
|
||||
const name = param?.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.namedChildren.find((c) => !COMMENT_TYPES.has(c.type));
|
||||
if (!inner) break;
|
||||
n = inner;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/** 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_variable_declaration': {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const d = node.namedChild(i);
|
||||
if (d?.type !== 'variable_declarator') continue;
|
||||
const name = d.childForFieldName('name');
|
||||
const value = d.childForFieldName('value');
|
||||
// Only an INITIALIZED declarator writes (`int x = e;`). A bare
|
||||
// `int x;` is not a def (it writes nothing at runtime), matching the
|
||||
// TS bare-`var` rule.
|
||||
if (name && value) this.def(name, acc);
|
||||
if (value) this.walkValue(value, 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 {
|
||||
this.walkValue(lv, acc); // field/array target — uses only
|
||||
}
|
||||
}
|
||||
if (right) this.walkValue(right, acc);
|
||||
return;
|
||||
}
|
||||
case 'update_expression': {
|
||||
// `x++` / `++x` / `x--` / `--x` — the only writing unary ops. The operand
|
||||
// is an anonymous (non-field) child; treat as def+use when it's an
|
||||
// identifier.
|
||||
const operand = this.updateOperand(node);
|
||||
const lv = operand ? this.unwrapLvalue(operand) : null;
|
||||
if (lv?.type === 'identifier') {
|
||||
this.def(lv, acc);
|
||||
this.use(lv, acc);
|
||||
} else if (operand) {
|
||||
this.walkValue(operand, 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 === '||') this.conditional(() => this.walkValue(right, acc));
|
||||
else this.walkValue(right, acc);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 'ternary_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 'field_access': {
|
||||
// `a.b` — value read of the object root only; the field name is not a
|
||||
// scalar binding. Mirrors the TS member-read use semantics.
|
||||
const obj = node.childForFieldName('object');
|
||||
if (obj) this.walkValue(obj, acc);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const c = node.namedChild(i);
|
||||
if (c) this.walkValue(c, acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The operand identifier of an `update_expression` (`x++` / `--x`). */
|
||||
private updateOperand(node: SyntaxNode): SyntaxNode | undefined {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const c = node.namedChild(i);
|
||||
if (c && !COMMENT_TYPES.has(c.type)) return c;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
useCount(): number {
|
||||
return this.uses.length;
|
||||
}
|
||||
|
||||
finish(): StatementFacts {
|
||||
return {
|
||||
line: this.line,
|
||||
defs: this.defs,
|
||||
uses: this.uses,
|
||||
...(this.mayDefs.length > 0 ? { mayDefs: this.mayDefs } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
995
gitnexus/src/core/ingestion/cfg/visitors/java.ts
Normal file
995
gitnexus/src/core/ingestion/cfg/visitors/java.ts
Normal file
|
|
@ -0,0 +1,995 @@
|
|||
/**
|
||||
* Java CfgVisitor (#2195 U4, plan KTD2).
|
||||
*
|
||||
* Walks a Java method/constructor/lambda's tree-sitter AST and drives the
|
||||
* language-agnostic {@link CfgBuilder} to produce a serializable
|
||||
* {@link FunctionCfg}, plus a def/use harvest ({@link JavaHarvester}) for the
|
||||
* reaching-defs / CDG solvers. Structured like the C# visitor — a
|
||||
* `visit_<node_type>` dispatch over the statement taxonomy, driving a
|
||||
* per-function {@link ControlFlowContext} — because Java shares C#'s `finally`
|
||||
* semantics (try/finally, try-with-resources auto-close = finally, labeled
|
||||
* break/continue), which the finalizer-frame + labeled-frame machinery in
|
||||
* `control-flow-context.ts` models.
|
||||
*
|
||||
* Every node type and field literal below was grammar-validated against
|
||||
* tree-sitter-java via the introspection probe before use (mandatory pre-step,
|
||||
* KTD5). Known Java surprises pre-empted (verified by a real parse):
|
||||
* - Generics are `generic_type` (NOT `parameterized_type`, which this grammar
|
||||
* does not have).
|
||||
* - BOTH the classic colon `switch` and the arrow `switch` parse under
|
||||
* `switch_expression` (there is no `switch_statement` node). Its body is a
|
||||
* `switch_block` (field `body`). A classic group is a
|
||||
* `switch_block_statement_group` (`switch_label` + statements, FALLS THROUGH);
|
||||
* an arrow rule is a `switch_rule` (`switch_label` `->` one body, does NOT
|
||||
* fall through). Case tests live in a `switch_label`.
|
||||
* - Comments are `line_comment` / `block_comment` (NOT `comment`).
|
||||
* - An `if`/`while`/`do`/`synchronized` condition is wrapped in a
|
||||
* `parenthesized_expression`; `consequence`/`alternative`/`body` are
|
||||
* statements directly (no `else_clause` wrapper — an `else if` is the nested
|
||||
* `if_statement` in the `alternative` field).
|
||||
* - `for_statement` uses the `init` field (NOT `initializer`), plus `condition`,
|
||||
* `update`, `body`. `enhanced_for_statement` uses `type`/`name`/`value`/`body`.
|
||||
* - `try_with_resources_statement` carries a `resources` field
|
||||
* (`resource_specification` of `resource` nodes); a plain `try_statement` has
|
||||
* `body` + `catch_clause`s + an optional `finally_clause`.
|
||||
* - `labeled_statement` = a leading `identifier` (the label) then the labeled
|
||||
* statement (no field). `break`/`continue` carry an optional trailing
|
||||
* `identifier` label.
|
||||
*
|
||||
* Function nodes: `method_declaration`, `constructor_declaration`,
|
||||
* `compact_constructor_declaration` (a record's canonical-constructor body), and
|
||||
* `lambda_expression`.
|
||||
*
|
||||
* Edge-kind contract (matches the TS / C# visitors — RD/CDG consume these):
|
||||
* - if/else → `cond-true` / `cond-false`
|
||||
* - loops (for / enhanced-for / while / do-while) → `cond-true` / `loop-back` /
|
||||
* `cond-false`
|
||||
* - switch → `switch-case` / `fallthrough`; a `switch_block_statement_group`
|
||||
* (classic colon form) falls through to the next group when it does not
|
||||
* `break`/`return`/`yield`; a `switch_rule` (arrow form) does NOT fall
|
||||
* through (its single body always rejoins after the switch).
|
||||
* - try/catch → `throw` (every protected-region block → the handler); a
|
||||
* `try_with_resources_statement`'s auto-close runs on BOTH normal and
|
||||
* exception exit (finally semantics), so a `return`/`break`/`continue`
|
||||
* crossing it gets a `finally-*` completion edge too. `synchronized`'s
|
||||
* monitor-release is likewise a deterministic finalizer.
|
||||
* - return / throw / break / continue → the matching terminator kind; a labeled
|
||||
* `break outer;` / `continue outer;` targets the labeled frame, not the
|
||||
* nearest one.
|
||||
* - straight-line → `seq`
|
||||
*
|
||||
* Classic hazards, handled explicitly (mirrors the C# / TS visitors):
|
||||
* - 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 `break outer;` / `continue outer;`: the label resolves against the
|
||||
* frame of the construct it names (a labeled loop/switch, or a labeled block),
|
||||
* NOT the nearest enclosing frame. An UNLABELED break never targets a labeled
|
||||
* block frame (control-flow-context.ts enforces this).
|
||||
* - 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:
|
||||
* - `switch` as an EXPRESSION value (`int r = switch (x) { … };`) is left INLINE
|
||||
* inside its owning statement's block — its arms are not modeled as separate
|
||||
* CFG blocks (the value flows to the assignment). Only a `switch` used as a
|
||||
* STATEMENT (a direct statement child) is modeled as a dispatch construct.
|
||||
* This mirrors the C# `switch_expression`-in-return handling — documented gap.
|
||||
* - `yield` (in a switch expression) continues to the next statement (it yields
|
||||
* one value to the enclosing switch and the arm ends); the switch-expression
|
||||
* state machine is not modeled, consistent with the inline-value-switch gap.
|
||||
* - Exceptions thrown by a method call mid-statement are over-approximated by
|
||||
* the conservative per-block throw edge inside a `try`; outside any `try` they
|
||||
* are not modeled (no edge), matching TS.
|
||||
* - Def/use harvest scope: see `java-harvest.ts` — field/array writes are not
|
||||
* scalar defs; nested-function (lambda) 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, StatementFacts } from '../types.js';
|
||||
import { JavaHarvester } from './java-harvest.js';
|
||||
|
||||
/** Java node types that own a CFG-bearing function body. */
|
||||
const JAVA_FUNCTION_TYPES = new Set([
|
||||
'method_declaration',
|
||||
'constructor_declaration',
|
||||
'compact_constructor_declaration',
|
||||
'lambda_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',
|
||||
'enhanced_for_statement',
|
||||
'switch_expression',
|
||||
'try_statement',
|
||||
'try_with_resources_statement',
|
||||
'synchronized_statement',
|
||||
'return_statement',
|
||||
'break_statement',
|
||||
'continue_statement',
|
||||
'throw_statement',
|
||||
'labeled_statement',
|
||||
'yield_statement',
|
||||
'block',
|
||||
]);
|
||||
|
||||
/** Comment node types tree-sitter-java surfaces (NOT `comment`). */
|
||||
const COMMENT_TYPES = new Set(['line_comment', 'block_comment']);
|
||||
|
||||
const startLineOf = (n: SyntaxNode): number => n.startPosition.row + 1;
|
||||
const endLineOf = (n: SyntaxNode): number => n.endPosition.row + 1;
|
||||
|
||||
const isComment = (n: SyntaxNode): boolean => COMMENT_TYPES.has(n.type);
|
||||
|
||||
/** A statement sequence that produced no blocks (empty body) is "transparent". */
|
||||
type SeqResult = TraversalResult | null;
|
||||
|
||||
/** A pre-built {@link StatementFacts} record, or undefined when none. */
|
||||
type StatementFactsLike = StatementFacts | undefined;
|
||||
|
||||
/**
|
||||
* Per-function Java walk state. One instance per function so the
|
||||
* {@link ControlFlowContext}, exception-handler stack, and labeled-block frame
|
||||
* bookkeeping are scoped to that function and never leak across functions.
|
||||
*/
|
||||
class JavaCfgWalk {
|
||||
private readonly cfc = new ControlFlowContext();
|
||||
/** Stack of exception-handler entry blocks (catch/finally) a `throw` jumps to. */
|
||||
private readonly handlers: number[] = [];
|
||||
/** Label(s) pending attachment to the NEXT pushed loop/switch frame. */
|
||||
private pendingLabels: string[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly builder: CfgBuilder,
|
||||
private readonly harvest: JavaHarvester,
|
||||
) {}
|
||||
|
||||
/** Statements of a block node, ignoring comments. */
|
||||
private statementsOf(block: SyntaxNode): SyntaxNode[] {
|
||||
return block.namedChildren.filter((c) => !isComment(c));
|
||||
}
|
||||
|
||||
/** 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');
|
||||
}
|
||||
|
||||
/** Strip a `parenthesized_expression` wrapper (Java `if`/`while` conditions). */
|
||||
private unwrapParen(node: SyntaxNode): SyntaxNode {
|
||||
if (node.type === 'parenthesized_expression') {
|
||||
const inner = node.namedChildren.find((c) => !isComment(c));
|
||||
if (inner) return inner;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
// A `switch_expression` only breaks a block when it is a STATEMENT switch.
|
||||
// Used as a value (inside a declaration / return) it coalesces normally.
|
||||
const breaks =
|
||||
CONTROL_FLOW_TYPES.has(stmt.type) &&
|
||||
(stmt.type !== 'switch_expression' || this.isStatementSwitch(stmt));
|
||||
if (breaks) {
|
||||
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 'enhanced_for_statement':
|
||||
return this.visitForEach(stmt);
|
||||
case 'switch_expression':
|
||||
return this.visitSwitch(stmt);
|
||||
case 'try_statement':
|
||||
case 'try_with_resources_statement':
|
||||
return this.visitTry(stmt);
|
||||
case 'synchronized_statement':
|
||||
return this.visitSynchronized(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 '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 finalizer (finally / try-with-resources /
|
||||
// synchronized) 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 e;` (switch-expression arm value) — yields one value to the enclosing
|
||||
* switch and the arm ends; modeled as a block that continues to whatever
|
||||
* follows (the switch-expression state machine is not modeled, see the visitor
|
||||
* limitations). It carries the yielded value's def/use facts.
|
||||
*/
|
||||
private visitYield(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 visitBreak(stmt: SyntaxNode): TraversalResult {
|
||||
const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text);
|
||||
const label = this.jumpLabel(stmt);
|
||||
const res = this.cfc.resolveBreak(label);
|
||||
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 label = this.jumpLabel(stmt);
|
||||
const res = this.cfc.resolveContinue(label);
|
||||
const { target, finalizers } = res ?? {
|
||||
target: this.builder.exitIndex,
|
||||
finalizers: this.cfc.finalizersForReturn(),
|
||||
};
|
||||
wireJumpThroughFinalizers(this.builder, idx, finalizers, target, 'continue');
|
||||
return { entry: idx, exits: [] };
|
||||
}
|
||||
|
||||
/** The trailing label `identifier` of a `break`/`continue`, if any. */
|
||||
private jumpLabel(stmt: SyntaxNode): string | undefined {
|
||||
const id = stmt.namedChildren.find((c) => c.type === 'identifier');
|
||||
return id?.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* `label: <statement>` — the label names the construct it directly wraps. For a
|
||||
* loop/switch we forward the label so its pushed frame carries it (`break
|
||||
* outer;` then resolves to it); for any other labeled statement we push a
|
||||
* labeled-block frame around its body so `break label;` reaches the join after.
|
||||
*/
|
||||
private visitLabeled(stmt: SyntaxNode): SeqResult {
|
||||
const labelNode = stmt.namedChildren.find((c) => c.type === 'identifier');
|
||||
const label = labelNode?.text;
|
||||
const body = stmt.namedChildren.find((c) => c.id !== labelNode?.id && !isComment(c)) ?? null;
|
||||
if (!body || label === undefined) return this.visitBody(body);
|
||||
|
||||
if (this.isLoopOrSwitchStatement(body)) {
|
||||
// Forward the label to the loop/switch frame this statement pushes.
|
||||
this.pendingLabels = [...this.pendingLabels, label];
|
||||
return this.visitStmt(body);
|
||||
}
|
||||
|
||||
// Labeled non-loop (`blk: { … break blk; … }`) — a break-to-label join after
|
||||
// the body; an unlabeled break never matches it.
|
||||
const joinBlock = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
|
||||
this.cfc.pushLabeledBlock(joinBlock, [label]);
|
||||
const res = this.visitBody(body);
|
||||
this.cfc.pop();
|
||||
if (res) this.builder.connect(res.exits, joinBlock, 'seq');
|
||||
const entry = res?.entry ?? joinBlock;
|
||||
return { entry, exits: [joinBlock] };
|
||||
}
|
||||
|
||||
private isLoopOrSwitchStatement(node: SyntaxNode): boolean {
|
||||
return (
|
||||
node.type === 'for_statement' ||
|
||||
node.type === 'enhanced_for_statement' ||
|
||||
node.type === 'while_statement' ||
|
||||
node.type === 'do_statement' ||
|
||||
(node.type === 'switch_expression' && this.isStatementSwitch(node))
|
||||
);
|
||||
}
|
||||
|
||||
/** Take and clear the labels queued by an enclosing `labeled_statement`. */
|
||||
private takeLabels(): string[] {
|
||||
const labels = this.pendingLabels;
|
||||
this.pendingLabels = [];
|
||||
return labels;
|
||||
}
|
||||
|
||||
private visitIf(stmt: SyntaxNode): TraversalResult {
|
||||
const cond = this.condOf(stmt) ?? 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 Java: `alternative` is the else body or the
|
||||
// nested `if_statement` of 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)] };
|
||||
}
|
||||
|
||||
/** The (paren-unwrapped) condition expression of an if/while/do/synchronized. */
|
||||
private condOf(stmt: SyntaxNode): SyntaxNode | undefined {
|
||||
const cond = stmt.childForFieldName('condition');
|
||||
return cond ? this.unwrapParen(cond) : undefined;
|
||||
}
|
||||
|
||||
private visitWhile(stmt: SyntaxNode): TraversalResult {
|
||||
const labels = this.takeLabels();
|
||||
const cond = this.condOf(stmt) ?? 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, labels);
|
||||
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 labels = this.takeLabels();
|
||||
const cond = this.condOf(stmt) ?? 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, labels);
|
||||
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 labels = this.takeLabels();
|
||||
const init = stmt.childForFieldName('init');
|
||||
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, labels);
|
||||
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 {
|
||||
const labels = this.takeLabels();
|
||||
// Header text is SYNTHESIZED, so facts come from the name/value 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, labels);
|
||||
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 name = stmt.childForFieldName('name')?.text ?? '';
|
||||
const value = stmt.childForFieldName('value')?.text ?? '';
|
||||
return name || value ? `for(${name} : ${value})` : 'for(… : …)';
|
||||
}
|
||||
|
||||
/**
|
||||
* `synchronized (obj) body` — the monitor release runs on BOTH normal AND
|
||||
* exception exit (deterministic finalizer), exactly like a `finally`. Modeled
|
||||
* as a synthesized finalizer block so a `return`/`break`/`continue` crossing it
|
||||
* threads through and gets a `finally-*` completion edge.
|
||||
*/
|
||||
private visitSynchronized(stmt: SyntaxNode): SeqResult {
|
||||
const bodyNode = this.bodyBlockOf(stmt) ?? null;
|
||||
const lockExpr = this.synchronizedLock(stmt);
|
||||
const releaseFacts = lockExpr ? this.harvest.facts(lockExpr) : undefined;
|
||||
return this.buildProtectedSynthetic(bodyNode, stmt, 'release', releaseFacts);
|
||||
}
|
||||
|
||||
/** The locked expression of a `synchronized (expr) {…}` (paren-unwrapped). */
|
||||
private synchronizedLock(stmt: SyntaxNode): SyntaxNode | undefined {
|
||||
const body = stmt.childForFieldName('body');
|
||||
const expr = stmt.namedChildren.find((c) => c.id !== body?.id && !isComment(c));
|
||||
return expr ? this.unwrapParen(expr) : undefined;
|
||||
}
|
||||
|
||||
/** Whether a `switch_expression` is used as a STATEMENT (vs an expression value). */
|
||||
private isStatementSwitch(node: SyntaxNode): boolean {
|
||||
const p = node.parent;
|
||||
if (!p) return false;
|
||||
// A statement-position switch is a direct child of a `block`, a classic
|
||||
// group, or an arrow rule body; an expression-value switch is nested under a
|
||||
// declaration / return / assignment / argument and is NOT one of these.
|
||||
return (
|
||||
p.type === 'block' ||
|
||||
p.type === 'switch_block_statement_group' ||
|
||||
p.type === 'switch_rule' ||
|
||||
p.type === 'labeled_statement'
|
||||
);
|
||||
}
|
||||
|
||||
private visitSwitch(stmt: SyntaxNode): TraversalResult {
|
||||
const labels = this.takeLabels();
|
||||
const value = this.condOf(stmt) ?? 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, labels);
|
||||
const body = stmt.childForFieldName('body');
|
||||
// A `switch_block` holds `switch_block_statement_group`s (classic, fall
|
||||
// through) OR `switch_rule`s (arrow, no fallthrough); never both.
|
||||
const groups = body
|
||||
? body.namedChildren.filter(
|
||||
(c) => c.type === 'switch_block_statement_group' || c.type === 'switch_rule',
|
||||
)
|
||||
: [];
|
||||
|
||||
// Each group's case-test expression(s) evaluate before the body runs —
|
||||
// harvest their uses (and any pattern binding as a may-def) onto the dispatch
|
||||
// block, one record per test, CONDITIONALLY (a later case test only runs when
|
||||
// earlier cases didn't match).
|
||||
for (const g of groups) {
|
||||
for (const test of this.caseTests(g)) {
|
||||
this.builder.attachFacts(dispatch, this.harvest.factsConditional(test));
|
||||
}
|
||||
}
|
||||
|
||||
const groupResults = groups.map((g) => this.visitSeq(this.caseStatements(g)));
|
||||
const hasDefault = groups.some((g) => this.caseIsDefault(g));
|
||||
const arrowForm = groups.some((g) => g.type === 'switch_rule');
|
||||
|
||||
const entryOf: number[] = new Array(groups.length);
|
||||
let after = switchExit;
|
||||
for (let i = groups.length - 1; i >= 0; i--) {
|
||||
entryOf[i] = groupResults[i]?.entry ?? after;
|
||||
after = entryOf[i];
|
||||
}
|
||||
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
this.builder.edge(dispatch, entryOf[i], 'switch-case');
|
||||
}
|
||||
if (!hasDefault) this.builder.edge(dispatch, switchExit, 'switch-case'); // no-match path
|
||||
|
||||
// Classic colon groups FALL THROUGH to the next group when not break/return-
|
||||
// terminated; arrow rules NEVER fall through (each body rejoins after).
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
const res = groupResults[i];
|
||||
if (!res) continue;
|
||||
if (groups[i].type === 'switch_rule') {
|
||||
this.builder.connect(res.exits, switchExit, 'seq');
|
||||
} else {
|
||||
const fallTarget = i + 1 < groups.length ? entryOf[i + 1] : switchExit;
|
||||
this.builder.connect(res.exits, fallTarget, 'fallthrough');
|
||||
}
|
||||
}
|
||||
void arrowForm;
|
||||
|
||||
this.cfc.pop();
|
||||
return { entry: dispatch, exits: [switchExit] };
|
||||
}
|
||||
|
||||
/** A switch group/rule's body statements (everything but its `switch_label`). */
|
||||
private caseStatements(group: SyntaxNode): SyntaxNode[] {
|
||||
return group.namedChildren.filter((c) => c.type !== 'switch_label' && !isComment(c));
|
||||
}
|
||||
|
||||
/** The case-test value expressions of a group (a `switch_label`'s values). */
|
||||
private caseTests(group: SyntaxNode): SyntaxNode[] {
|
||||
const label = group.namedChildren.find((c) => c.type === 'switch_label');
|
||||
if (!label) return [];
|
||||
return label.namedChildren.filter((c) => !isComment(c));
|
||||
}
|
||||
|
||||
/** A `default:` group has a `switch_label` with no value children. */
|
||||
private caseIsDefault(group: SyntaxNode): boolean {
|
||||
const label = group.namedChildren.find((c) => c.type === 'switch_label');
|
||||
if (!label) return false;
|
||||
return label.namedChildren.filter((c) => !isComment(c)).length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* try / catch / finally / try-with-resources. The `resources` of a
|
||||
* try-with-resources auto-close on BOTH normal and exception exit — exactly
|
||||
* `finally`-shaped — so we synthesize a close finalizer that wraps an explicit
|
||||
* `finally` (if any).
|
||||
*/
|
||||
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');
|
||||
|
||||
// try-with-resources: the resource close is a deterministic finalizer that
|
||||
// runs after the body and (if present) before the explicit finally. Model it
|
||||
// as a synthesized close block prepended to the finalizer chain.
|
||||
const resources = this.tryResources(stmt);
|
||||
return this.buildProtected(
|
||||
bodyNode ?? null,
|
||||
catchClauses,
|
||||
finallyBody ?? null,
|
||||
resources,
|
||||
stmt,
|
||||
);
|
||||
}
|
||||
|
||||
/** The `resource` nodes of a try-with-resources, or [] for a plain try. */
|
||||
private tryResources(stmt: SyntaxNode): SyntaxNode[] {
|
||||
const spec = stmt.childForFieldName('resources');
|
||||
if (!spec) return [];
|
||||
return spec.namedChildren.filter((c) => c.type === 'resource');
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared try/catch/finally builder. `catchClauses` may be empty; `finallyBody`
|
||||
* is the explicit finally's `block` (or null); `resources` are try-with-
|
||||
* resources resources (auto-close = an implicit finally that runs first).
|
||||
*
|
||||
* Models the TS/C# `visitTry` semantics: normal completion of try AND catch
|
||||
* flow through the finalizers; a throw in the protected region routes to the
|
||||
* handler; early exits crossing a finalizer thread through it (`finally-*`
|
||||
* completion edges). The resource close finalizer (synthesized) and the
|
||||
* explicit finally are chained so a crossing jump threads BOTH.
|
||||
*/
|
||||
private buildProtected(
|
||||
bodyNode: SyntaxNode | null,
|
||||
catchClauses: SyntaxNode[],
|
||||
finallyBody: SyntaxNode | null,
|
||||
resources: SyntaxNode[],
|
||||
span: SyntaxNode,
|
||||
): SeqResult {
|
||||
// Innermost finalizer first on the active stack = outermost lexically. The
|
||||
// resource close runs BEFORE the explicit finally, so push the explicit
|
||||
// finally first (deeper) then the close (shallower / nearer the jump).
|
||||
const explicitFinRes = finallyBody ? this.visitSeq(this.statementsOf(finallyBody)) : null;
|
||||
const explicitFrame = explicitFinRes ? this.cfc.pushFinalizer(explicitFinRes.entry) : null;
|
||||
|
||||
// Synthesized resource-close finalizer (a single block; close()/AutoCloseable
|
||||
// has no AST node), carrying the resource bindings' facts as uses.
|
||||
let closeRes: TraversalResult | null = null;
|
||||
let closeFrame: ReturnType<ControlFlowContext['pushFinalizer']> | null = null;
|
||||
if (resources.length > 0) {
|
||||
const closeFacts = this.harvest.resourceCloseFacts(resources);
|
||||
const closeBlock = this.builder.newBlock(
|
||||
endLineOf(span),
|
||||
endLineOf(span),
|
||||
'close',
|
||||
'normal',
|
||||
closeFacts,
|
||||
);
|
||||
closeRes = { entry: closeBlock, exits: [closeBlock] };
|
||||
// The close's normal exit flows into the explicit finally (if any).
|
||||
if (explicitFinRes) this.builder.edge(closeBlock, explicitFinRes.entry, 'seq');
|
||||
closeFrame = this.cfc.pushFinalizer(closeRes.entry);
|
||||
}
|
||||
|
||||
// The entry of the finalizer chain (what normal/exception completion runs
|
||||
// first): close (if any) else explicit finally.
|
||||
const finalizerEntry = closeRes?.entry ?? explicitFinRes?.entry;
|
||||
// The set of normal exits AFTER the whole finalizer chain runs.
|
||||
const finalizerExits = explicitFinRes?.exits ?? closeRes?.exits ?? 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 (finalizerEntry !== undefined) this.handlers.push(finalizerEntry);
|
||||
let res: SeqResult = clauseBody ? this.visitSeq(this.statementsOf(clauseBody)) : null;
|
||||
if (finalizerEntry !== undefined) 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 the finalizer
|
||||
// chain, else the outer handler.
|
||||
const tryHandler = firstCatchEntry ?? finalizerEntry ?? 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 || finalizerEntry !== undefined) {
|
||||
for (let b = protectedStart; b < this.builder.blockCount; b++) {
|
||||
this.builder.edge(b, tryHandler, 'throw');
|
||||
}
|
||||
}
|
||||
|
||||
// Pop the finalizer frames (inner→outer) and drain their pending legs.
|
||||
if (closeFrame && closeRes) {
|
||||
this.cfc.pop();
|
||||
drainFinalizerPending(this.builder, closeFrame, closeRes.exits);
|
||||
}
|
||||
if (explicitFrame && explicitFinRes) {
|
||||
this.cfc.pop();
|
||||
drainFinalizerPending(this.builder, explicitFrame, explicitFinRes.exits);
|
||||
}
|
||||
|
||||
const exits: number[] = [];
|
||||
if (finalizerEntry !== undefined) {
|
||||
if (bodyRes) this.builder.connect(bodyRes.exits, finalizerEntry, 'seq');
|
||||
for (const e of catchExits) this.builder.edge(e, finalizerEntry, 'seq');
|
||||
if (finalizerExits) exits.push(...finalizerExits);
|
||||
// No catch → an exception re-propagates out after the finalizers run.
|
||||
if (catchClauses.length === 0 && finalizerExits) {
|
||||
this.builder.connect(finalizerExits, this.currentHandler(), 'throw');
|
||||
}
|
||||
} else {
|
||||
if (bodyRes) exits.push(...bodyRes.exits);
|
||||
exits.push(...catchExits);
|
||||
}
|
||||
|
||||
const entry = bodyRes?.entry ?? finalizerEntry ?? catchEntries[0];
|
||||
if (entry === undefined) {
|
||||
void span;
|
||||
return null;
|
||||
}
|
||||
return { entry, exits: [...new Set(exits)] };
|
||||
}
|
||||
|
||||
/**
|
||||
* `synchronized`: a protected body whose finalizer is a SYNTHESIZED single
|
||||
* block (Monitor exit has 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,
|
||||
): SeqResult {
|
||||
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 the
|
||||
// release, 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;
|
||||
}
|
||||
}
|
||||
|
||||
/** Build the CFG for one Java function node, or `undefined` if not modelable. */
|
||||
function buildFunctionCfg(fnNode: SyntaxNode, filePath: string): FunctionCfg | undefined {
|
||||
try {
|
||||
if (!JAVA_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 expression (single-expression
|
||||
// lambda `x -> expr`).
|
||||
const body =
|
||||
fnNode.childForFieldName('body') ?? fnNode.namedChildren.find((c) => c.type === 'block');
|
||||
if (!body) return undefined; // abstract / interface method — no body
|
||||
|
||||
const builder = new CfgBuilder(filePath, startLine, endLine, startColumn);
|
||||
const harvest = new JavaHarvester(fnNode);
|
||||
|
||||
const paramFacts = harvest.paramFacts();
|
||||
if (paramFacts) builder.attachFacts(builder.entryIndex, paramFacts);
|
||||
|
||||
if (body.type !== 'block') {
|
||||
// Single-expression lambda (`x -> expr`): one block whose value is returned.
|
||||
const blk = builder.newBlock(
|
||||
startLineOf(body),
|
||||
endLineOf(body),
|
||||
body.text,
|
||||
'normal',
|
||||
harvest.facts(body),
|
||||
);
|
||||
builder.edge(builder.entryIndex, blk, 'seq');
|
||||
builder.edge(blk, builder.exitIndex, 'return');
|
||||
return builder.finish(harvest.table());
|
||||
}
|
||||
|
||||
const walk = new JavaCfgWalk(builder, harvest);
|
||||
const res = walk.visitSeq(body.namedChildren.filter((c) => !isComment(c)));
|
||||
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] Java buildFunctionCfg skipped a function in ${filePath}: ${String(err)}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a node is a Java function this visitor builds a CFG for. */
|
||||
function isFunction(node: SyntaxNode): boolean {
|
||||
return JAVA_FUNCTION_TYPES.has(node.type);
|
||||
}
|
||||
|
||||
/** The Java CFG visitor. */
|
||||
export function createJavaCfgVisitor(): CfgVisitor<SyntaxNode> {
|
||||
return { buildFunctionCfg, isFunction };
|
||||
}
|
||||
|
||||
export { JAVA_FUNCTION_TYPES };
|
||||
|
|
@ -26,6 +26,7 @@ import { createMethodExtractor } from '../method-extractors/generic.js';
|
|||
import { javaMethodConfig } from '../method-extractors/configs/jvm.js';
|
||||
import { createVariableExtractor } from '../variable-extractors/generic.js';
|
||||
import { javaVariableConfig } from '../variable-extractors/configs/jvm.js';
|
||||
import { createJavaCfgVisitor } from '../cfg/visitors/java.js';
|
||||
import type { SymbolDefinition } from 'gitnexus-shared';
|
||||
import {
|
||||
emitJavaScopeCaptures,
|
||||
|
|
@ -118,6 +119,9 @@ export const javaProvider = defineLanguage({
|
|||
|
||||
// ── RFC #909 Ring 3: scope-based resolution hooks ──
|
||||
emitScopeCaptures: emitJavaScopeCaptures,
|
||||
|
||||
// ── PDG: per-function CFG + def/use harvest (#2195 U4) ──
|
||||
cfgVisitor: createJavaCfgVisitor(),
|
||||
interpretImport: interpretJavaImport,
|
||||
interpretTypeBinding: interpretJavaTypeBinding,
|
||||
bindingScopeFor: javaBindingScopeFor,
|
||||
|
|
|
|||
149
gitnexus/test/integration/cfg/fixtures/java-hazards.java
Normal file
149
gitnexus/test/integration/cfg/fixtures/java-hazards.java
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
// Java CFG hazard fixture (#2195 U4). Each method exercises one control-flow
|
||||
// construct the Java CfgVisitor models; the worker-mode pipeline + snapshot
|
||||
// tests assert non-trivial BasicBlock / CFG / REACHING_DEF / CDG output here.
|
||||
package fixtures;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
class Hazards {
|
||||
|
||||
// if / else — cond-true / cond-false to both arms, joining after.
|
||||
int branch(int x) {
|
||||
int r;
|
||||
if (x > 0) {
|
||||
r = 1;
|
||||
} else {
|
||||
r = 2;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// classic C-style for — init once, condition header, back-edge through update.
|
||||
int classicFor(int n) {
|
||||
int sum = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
sum += i;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
// enhanced for (for-each) — loop var def, iterated value use.
|
||||
int forEach(int[] xs) {
|
||||
int total = 0;
|
||||
for (int v : xs) {
|
||||
total += v;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// while — bottom-of-header re-test, back-edge.
|
||||
int whileLoop(int x) {
|
||||
while (x > 0) {
|
||||
x = step(x);
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
// do-while — body runs before the test.
|
||||
int doWhile(int x) {
|
||||
do {
|
||||
x = step(x);
|
||||
} while (x > 0);
|
||||
return x;
|
||||
}
|
||||
|
||||
// try-with-resources — the resource closes on BOTH normal and exception exit
|
||||
// (finally semantics); a return inside crosses the close (finally-return).
|
||||
int withResources(String path) throws IOException {
|
||||
try (var r = open(path)) {
|
||||
return read(r);
|
||||
} catch (IOException e) {
|
||||
return handle(e);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
// labeled break — `break outer;` from a nested loop targets the labeled frame.
|
||||
int labeledBreak(int[][] grid, int needle) {
|
||||
int found = -1;
|
||||
outer:
|
||||
for (int i = 0; i < grid.length; i++) {
|
||||
for (int j = 0; j < grid[i].length; j++) {
|
||||
if (grid[i][j] == needle) {
|
||||
found = i;
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
// classic colon switch — fallthrough between break-less cases; the case test
|
||||
// is recorded as a may-def on the dispatch block.
|
||||
int classicSwitch(int x) {
|
||||
int r = 0;
|
||||
switch (x) {
|
||||
case 1:
|
||||
case 2:
|
||||
r = 10;
|
||||
break;
|
||||
case 3:
|
||||
r = 30;
|
||||
default:
|
||||
r = -1;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// arrow switch — each rule rejoins after the switch (no fallthrough).
|
||||
int arrowSwitch(int x) {
|
||||
int r = 0;
|
||||
switch (x) {
|
||||
case 1 -> r = 1;
|
||||
case 2, 3 -> r = 2;
|
||||
default -> r = -1;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// switch expression with yield — yields a value to the enclosing switch.
|
||||
int yieldSwitch(int x) {
|
||||
int r = switch (x) {
|
||||
case 1 -> 10;
|
||||
default -> {
|
||||
yield 20;
|
||||
}
|
||||
};
|
||||
return r;
|
||||
}
|
||||
|
||||
// synchronized — the monitor release is a deterministic finalizer.
|
||||
void sync(Object lock) {
|
||||
synchronized (lock) {
|
||||
touch();
|
||||
}
|
||||
after();
|
||||
}
|
||||
|
||||
// a server-style non-terminating loop — EXIT must stay reverse-reachable so
|
||||
// CDG is not silently skipped for the method.
|
||||
void serve() {
|
||||
while (true) {
|
||||
if (ready()) {
|
||||
handle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helpers (no bodies of interest)
|
||||
int step(int x) { return x - 1; }
|
||||
Object open(String p) { return null; }
|
||||
int read(Object r) { return 0; }
|
||||
int handle(IOException e) { return 0; }
|
||||
void cleanup() {}
|
||||
void touch() {}
|
||||
void after() {}
|
||||
boolean ready() { return false; }
|
||||
void handle() {}
|
||||
}
|
||||
515
gitnexus/test/unit/cfg/java-visitor.test.ts
Normal file
515
gitnexus/test/unit/cfg/java-visitor.test.ts
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { createRequire } from 'node:module';
|
||||
import { createJavaCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/java.js';
|
||||
import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js';
|
||||
import { makeCfgHarness, type CfgHarness } from '../../helpers/cfg-harness.js';
|
||||
|
||||
// U4 — the Java 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 javaGrammar = createRequire(import.meta.url)('tree-sitter-java') as Parameters<
|
||||
typeof makeCfgHarness
|
||||
>[0];
|
||||
|
||||
const java: CfgHarness = makeCfgHarness(javaGrammar, createJavaCfgVisitor(), 'fixture.java');
|
||||
|
||||
const block = (cfg: FunctionCfg, substr: string): number => {
|
||||
const b = cfg.blocks.find((bl) => bl.text.includes(substr));
|
||||
if (!b) throw new Error(`no block containing ${JSON.stringify(substr)}`);
|
||||
return b.index;
|
||||
};
|
||||
|
||||
const edgeKinds = (cfg: FunctionCfg): Set<string> => new Set(cfg.edges.map((e) => e.kind));
|
||||
|
||||
function reaches(cfg: FunctionCfg, from: number, to: number): boolean {
|
||||
const adj = new Map<number, number[]>();
|
||||
for (const e of cfg.edges) (adj.get(e.from) ?? adj.set(e.from, []).get(e.from)!).push(e.to);
|
||||
const seen = new Set([from]);
|
||||
const stack = [from];
|
||||
while (stack.length) {
|
||||
const n = stack.pop() as number;
|
||||
if (n === to) return true;
|
||||
for (const nx of adj.get(n) ?? []) if (!seen.has(nx)) (seen.add(nx), stack.push(nx));
|
||||
}
|
||||
return seen.has(to);
|
||||
}
|
||||
const reachable = (cfg: FunctionCfg, idx: number): boolean => reaches(cfg, cfg.entryIndex, idx);
|
||||
|
||||
/** Is EXIT reverse-reachable from every reachable block? (CDG soundness gate.) */
|
||||
function exitReachableFromAll(cfg: FunctionCfg): boolean {
|
||||
for (const b of cfg.blocks) {
|
||||
if (b.index === cfg.exitIndex) continue;
|
||||
if (!reachable(cfg, b.index)) continue; // unreachable blocks exempt
|
||||
if (!reaches(cfg, b.index, cfg.exitIndex)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Resolve a binding by name → its index in the function's binding table. */
|
||||
function bindingIdx(cfg: FunctionCfg, name: string): number {
|
||||
const i = (cfg.bindings ?? []).findIndex((b) => b.name === name);
|
||||
if (i < 0) throw new Error(`no binding ${name}`);
|
||||
return i;
|
||||
}
|
||||
|
||||
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(int x) { ${body} } }`;
|
||||
|
||||
describe('Java CfgVisitor — structure', () => {
|
||||
it('straight-line body: ENTRY → block → EXIT (seq)', () => {
|
||||
const cfg = java.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 = java.cfgOf(`class C { void m() {} }`);
|
||||
expect(cfg.blocks).toHaveLength(2);
|
||||
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
});
|
||||
|
||||
it('constructor, compact constructor, and lambda are CFG-bearing functions', () => {
|
||||
const cfgs = java.cfgsOf(
|
||||
`class C { C(int a) { this.x = a; } void outer() { Runnable r = () -> { go(); }; r.run(); } }`,
|
||||
);
|
||||
// constructor C, outer, and the lambda = 3 CFGs.
|
||||
expect(cfgs.length).toBeGreaterThanOrEqual(3);
|
||||
for (const cfg of cfgs) expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
});
|
||||
|
||||
it('record compact constructor is CFG-bearing', () => {
|
||||
const cfgs = java.cfgsOf(`record R(int a) { R { if (a < 0) throw new RuntimeException(); } }`);
|
||||
expect(cfgs.length).toBeGreaterThanOrEqual(1);
|
||||
for (const cfg of cfgs) expect(reachable(cfg, cfg.exitIndex)).toBe(true);
|
||||
});
|
||||
|
||||
it('single-expression lambda: one block returns its value', () => {
|
||||
const cfgs = java.cfgsOf(
|
||||
`class C { void m() { java.util.function.Function<Integer,Integer> f = z -> z * 2; f.apply(1); } }`,
|
||||
);
|
||||
// The lambda's OWN cfg has a single body block whose text IS exactly the
|
||||
// expression (the enclosing method's block contains the whole lambda source).
|
||||
const lambda = cfgs.find((c) => c.blocks.some((b) => b.text === 'z * 2'));
|
||||
expect(lambda).toBeDefined();
|
||||
const body = lambda!.blocks.find((b) => b.text === 'z * 2')!.index;
|
||||
expect(lambda!.edges).toContainEqual({ from: body, to: lambda!.exitIndex, kind: 'return' });
|
||||
});
|
||||
|
||||
it('abstract method (no body) → graceful undefined, no throw', () => {
|
||||
const root = java.parse(`abstract class C { abstract void m(); }`);
|
||||
const fns = java.collectFunctions(root);
|
||||
for (const fn of fns) {
|
||||
expect(() => createJavaCfgVisitor().buildFunctionCfg(fn, 'f.java')).not.toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Java CfgVisitor — branching', () => {
|
||||
it('if/else: cond-true to then, cond-false to else, both reach the join', () => {
|
||||
const cfg = java.cfgOf(wrap(`if (x > 0) { a(); } else { b(); } c();`));
|
||||
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 = java.cfgOf(
|
||||
`class C { void m(int x) { if (x > 0) { a(); } else if (x < 0) { b(); } else { c(); } } }`,
|
||||
);
|
||||
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('Java CfgVisitor — loops', () => {
|
||||
it('while loop: header + back-edge + exit', () => {
|
||||
const cfg = java.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 = java.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('classic for: init once, condition header, back-edge through update', () => {
|
||||
const cfg = java.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('enhanced for (for-each): header + body + loop-back + exit; loop var is a def', () => {
|
||||
const cfg = java.cfgOf(
|
||||
`class C { void m(int[] xs) { for (int v : xs) { use(v); } done(); } }`,
|
||||
);
|
||||
const body = block(cfg, 'use(v);');
|
||||
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 v = bindingIdx(cfg, 'v');
|
||||
expect(hasDef(cfg, v)).toBe(true);
|
||||
});
|
||||
|
||||
it('while (true) {} keeps EXIT reverse-reachable (structural exit-escape edge)', () => {
|
||||
const cfg = java.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 = java.cfgOf(`class C { void m() { for (;;) { work(); } } }`);
|
||||
expect(edgeKinds(cfg).has('cond-false')).toBe(true);
|
||||
expect(exitReachableFromAll(cfg)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Java CfgVisitor — labeled break/continue', () => {
|
||||
it('break outer; from a nested loop targets the labeled (outer) frame', () => {
|
||||
const cfg = java.cfgOf(`class C { void m() {
|
||||
outer:
|
||||
for (int i = 0; i < 3; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
if (hit()) break outer;
|
||||
inner();
|
||||
}
|
||||
afterInner();
|
||||
}
|
||||
done();
|
||||
} }`);
|
||||
expect(edgeKinds(cfg).has('break')).toBe(true);
|
||||
const brk = block(cfg, 'break outer;');
|
||||
// The labeled break skips past BOTH loops to the post-outer-loop continuation
|
||||
// (done()), not just the inner loop's exit (afterInner()).
|
||||
expect(reaches(cfg, brk, block(cfg, 'done();'))).toBe(true);
|
||||
// It must NOT route back into the outer loop body's afterInner().
|
||||
expect(reaches(cfg, brk, block(cfg, 'afterInner();'))).toBe(false);
|
||||
});
|
||||
|
||||
it('continue outer; from a nested loop continues the labeled (outer) loop', () => {
|
||||
const cfg = java.cfgOf(`class C { void m() {
|
||||
outer:
|
||||
for (int i = 0; i < 3; i++) {
|
||||
for (int j = 0; j < 3; j++) {
|
||||
if (skip()) continue outer;
|
||||
inner();
|
||||
}
|
||||
}
|
||||
done();
|
||||
} }`);
|
||||
expect(edgeKinds(cfg).has('continue')).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'continue outer;'), block(cfg, 'i < 3'))).toBe(true);
|
||||
});
|
||||
|
||||
it('unlabeled break does NOT match a labeled BLOCK frame', () => {
|
||||
// `blk:` labels a plain block; an UNLABELED break here is a compile error in
|
||||
// real Java, but the CFG must still route it to EXIT (no labeled-block match),
|
||||
// and a labeled `break blk;` must reach the post-block join.
|
||||
const cfg = java.cfgOf(`class C { void m() {
|
||||
blk: {
|
||||
if (cond()) break blk;
|
||||
work();
|
||||
}
|
||||
done();
|
||||
} }`);
|
||||
const brk = block(cfg, 'break blk;');
|
||||
// labeled break reaches the join after the labeled block (done()).
|
||||
expect(reaches(cfg, brk, block(cfg, 'done();'))).toBe(true);
|
||||
// work() after the break is still reachable (false arm of the if).
|
||||
expect(reachable(cfg, block(cfg, 'work();'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Java CfgVisitor — switch', () => {
|
||||
it('classic colon switch: break-terminated case rejoins, fallthrough on break-less', () => {
|
||||
const cfg = java.cfgOf(`class C { void m(int x) {
|
||||
switch (x) {
|
||||
case 1: one(); break;
|
||||
case 2: two(); break;
|
||||
default: other();
|
||||
}
|
||||
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 colon case falls through to the next case (fallthrough edge)', () => {
|
||||
const cfg = java.cfgOf(`class C { void m(int x) {
|
||||
switch (x) { case 1: case 2: shared(); break; default: d(); }
|
||||
after();
|
||||
} }`);
|
||||
expect(reaches(cfg, block(cfg, 'shared();'), block(cfg, 'after();'))).toBe(true);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
// a break-less default flows to after().
|
||||
expect(reaches(cfg, block(cfg, 'd();'), block(cfg, 'after();'))).toBe(true);
|
||||
});
|
||||
|
||||
it('a non-break case FALLS THROUGH to the next case (classic semantics)', () => {
|
||||
const cfg = java.cfgOf(`class C { void m(int x) {
|
||||
switch (x) { case 1: one(); case 2: two(); break; default: d(); }
|
||||
after();
|
||||
} }`);
|
||||
// case 1 has no break → it falls into case 2's body.
|
||||
expect(reaches(cfg, block(cfg, 'one();'), block(cfg, 'two();'))).toBe(true);
|
||||
expect(edgeKinds(cfg).has('fallthrough')).toBe(true);
|
||||
});
|
||||
|
||||
it('arrow switch: each rule rejoins after the switch, no fallthrough', () => {
|
||||
const cfg = java.cfgOf(`class C { void m(int x) {
|
||||
switch (x) {
|
||||
case 1 -> a();
|
||||
case 2, 3 -> b();
|
||||
default -> c();
|
||||
}
|
||||
after();
|
||||
} }`);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'a();'), block(cfg, 'after();'))).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'b();'), block(cfg, 'after();'))).toBe(true);
|
||||
// arrow rule a() does NOT fall into b().
|
||||
expect(reaches(cfg, block(cfg, 'a();'), block(cfg, 'b();'))).toBe(false);
|
||||
expect(edgeKinds(cfg).has('fallthrough')).toBe(false);
|
||||
});
|
||||
|
||||
it('switch case-test is recorded as a may-def/use on the dispatch block', () => {
|
||||
const cfg = java.cfgOf(`class C { void m(int x, int k) {
|
||||
switch (x) { case 1: a(); break; default: b(); }
|
||||
} }`);
|
||||
// The dispatch block (switch value `x`) uses x; the case-test constants carry
|
||||
// no bindings, but a value-bearing dispatch records the discriminant use.
|
||||
const x = bindingIdx(cfg, 'x');
|
||||
expect(hasUse(cfg, x)).toBe(true);
|
||||
});
|
||||
|
||||
it('switch EXPRESSION value with yield stays inline; method has a single-exit CFG', () => {
|
||||
const cfg = java.cfgOf(`class C { int m(int x) {
|
||||
int r = switch (x) { case 1 -> 10; default -> { yield 20; } };
|
||||
return r;
|
||||
} }`);
|
||||
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
expect(edgeKinds(cfg).has('return')).toBe(true);
|
||||
});
|
||||
|
||||
it('statement switch with a yield arm builds a dispatch with a yield block', () => {
|
||||
const cfg = java.cfgOf(`class C { int m(int x) {
|
||||
switch (x) {
|
||||
case 1: yield 10;
|
||||
default: yield 20;
|
||||
}
|
||||
} }`);
|
||||
// statement-position switch breaks a block → switch-case dispatch edges.
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Java CfgVisitor — try / catch / finally / try-with-resources', () => {
|
||||
it('try/catch: a throw edge runs from each protected block to the handler', () => {
|
||||
const cfg = java.cfgOf(`class C { void m() {
|
||||
try { risky(); deeper(); } catch (java.lang.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 = java.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 = java.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 = java.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);
|
||||
});
|
||||
|
||||
it('try-with-resources closes resources on the NORMAL exit path', () => {
|
||||
const cfg = java.cfgOf(`class C { void m() {
|
||||
try (var r = open()) { read(r); }
|
||||
after();
|
||||
} }`);
|
||||
const body = block(cfg, 'read(r);');
|
||||
const close = block(cfg, 'close');
|
||||
// body → close → after() (normal completion threads through the close).
|
||||
expect(reaches(cfg, body, close)).toBe(true);
|
||||
expect(reaches(cfg, close, block(cfg, 'after();'))).toBe(true);
|
||||
});
|
||||
|
||||
it('try-with-resources closes resources on the EXCEPTION path too', () => {
|
||||
const cfg = java.cfgOf(`class C { void m() {
|
||||
try (var r = open()) { risky(r); }
|
||||
} }`);
|
||||
const body = block(cfg, 'risky(r);');
|
||||
const close = block(cfg, 'close');
|
||||
expect(edgeKinds(cfg).has('throw')).toBe(true);
|
||||
expect(reaches(cfg, body, close)).toBe(true);
|
||||
});
|
||||
|
||||
it('a return inside try-with-resources crosses the close (finally-return)', () => {
|
||||
const cfg = java.cfgOf(`class C { int m() {
|
||||
try (var r = open()) { return read(r); }
|
||||
} }`);
|
||||
expect(edgeKinds(cfg).has('finally-return')).toBe(true);
|
||||
});
|
||||
|
||||
it('multi-catch (A | B) binds the exception name as a def in the handler', () => {
|
||||
const cfg = java.cfgOf(`class C { void m() {
|
||||
try { x(); } catch (java.io.IOException | java.lang.RuntimeException e) { handle(e); }
|
||||
} }`);
|
||||
expect(edgeKinds(cfg).has('throw')).toBe(true);
|
||||
const e = bindingIdx(cfg, 'e');
|
||||
expect(hasDef(cfg, e)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Java CfgVisitor — synchronized (deterministic finalizer)', () => {
|
||||
it('synchronized body runs then releases the monitor on the normal path', () => {
|
||||
const cfg = java.cfgOf(`class C { void m(Object lock) {
|
||||
synchronized (lock) { 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);
|
||||
});
|
||||
|
||||
it('synchronized releases the monitor on the exception path (throw → release)', () => {
|
||||
const cfg = java.cfgOf(`class C { void m(Object lock) { synchronized (lock) { risky(); } } }`);
|
||||
expect(edgeKinds(cfg).has('throw')).toBe(true);
|
||||
const body = block(cfg, 'risky();');
|
||||
const release = block(cfg, 'release');
|
||||
expect(reaches(cfg, body, release)).toBe(true);
|
||||
});
|
||||
|
||||
it('a return inside synchronized crosses the release (finally-return)', () => {
|
||||
const cfg = java.cfgOf(`class C { int m(Object lock) { synchronized (lock) { return get(); } } }`);
|
||||
expect(edgeKinds(cfg).has('finally-return')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Java CfgVisitor — def/use harvest', () => {
|
||||
it('local declaration: int x = a + b; use(x); → def of x + use of x', () => {
|
||||
const cfg = java.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('bare uninitialized local is NOT a def until assigned', () => {
|
||||
const cfg = java.cfgOf(`class C { void m() { int x; x = 5; use(x); } }`);
|
||||
const x = bindingIdx(cfg, 'x');
|
||||
// the assignment defines x; the declaration alone does not.
|
||||
expect(hasDef(cfg, x)).toBe(true);
|
||||
expect(hasUse(cfg, x)).toBe(true);
|
||||
});
|
||||
|
||||
it('a && (x = g()) records x as a may-def inside the short-circuit', () => {
|
||||
const cfg = java.cfgOf(`class C { void m(boolean a) { int x = 0; if (a && (x = g()) > 0) h(x); } }`);
|
||||
const x = bindingIdx(cfg, 'x');
|
||||
expect(hasMayDef(cfg, x)).toBe(true);
|
||||
});
|
||||
|
||||
it('ternary arms record their writes as may-defs', () => {
|
||||
const cfg = java.cfgOf(`class C { void m(int a) { int x = 0; int y = a > 0 ? (x = 1) : (x = 2); use(y); } }`);
|
||||
const x = bindingIdx(cfg, 'x');
|
||||
expect(hasMayDef(cfg, x)).toBe(true);
|
||||
});
|
||||
|
||||
it('compound assignment reads AND writes the lvalue', () => {
|
||||
const cfg = java.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('field/array writes are NOT scalar defs (their roots are uses)', () => {
|
||||
const cfg = java.cfgOf(`class C { void m(int[] a) { a[0] = 1; this.f = 2; } }`);
|
||||
// `a` is a parameter; the array write `a[0] = 1` reads a (subscript root),
|
||||
// it does not define `a`. No `this.f` scalar binding is created.
|
||||
const a = bindingIdx(cfg, 'a');
|
||||
expect(hasUse(cfg, a)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Java CfgVisitor — functionStartColumn', () => {
|
||||
it('two same-line methods get distinct functionStartColumn', () => {
|
||||
const cfgs = java.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('Java CfgVisitor — does not throw on exotic shapes', () => {
|
||||
it('nested lambdas / anonymous-class methods each build their own CFGs', () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
try {
|
||||
const cfgs = java.cfgsOf(`class C { void m() {
|
||||
Runnable r = () -> { if (cond()) { go(); } };
|
||||
java.util.function.Function<Integer,Integer> f = z -> z + 1;
|
||||
r.run();
|
||||
} }`);
|
||||
expect(cfgs.length).toBeGreaterThanOrEqual(3); // m, lambda block, lambda expr
|
||||
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