mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* feat(cfg): model Java value-position switch as control flow (#2207) A value-position `switch` expression with ≥2 arms is now modeled as a CFG dispatch in the two highest-value carriers, instead of collapsing the owning statement to a single inline block: - `var x = switch (k) { … }` — the arms become real blocks reached by `switch-case` edges and rejoin at a binding continuation that carries the declared name's def (uses stay on the arm blocks). - `return switch (k) { … }` — each arm returns the function result, threading every active finalizer. This makes the arms control-dependent on the dispatch (the point of #2207 — they previously produced zero CDG), mirroring the Kotlin / Rust value-position binding pattern. `breaksBlock` routes a value-switch declaration out of `visitSeq` coalescing; `visitReturn` and `visitStmt` gain the carrier handling; `java-harvest` gains `bindingDefFacts`. An assignment RHS (`x = switch …`), a call argument, and a multi- declarator decl remain inline (documented gap). Java has no value- position `if` (the ternary is excluded, like Kotlin's elvis). Verified: 51 java-visitor tests (4 new), full CFG unit+integration suites (661) green, CDG snapshot byte-identical, bench --check PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cfg): model C# value-position switch expression as control flow (#2207) A C# `switch_expression` (`k switch { p => v, … }`) with ≥2 arms is now modeled as a CFG `switch-case` dispatch — a discriminant block, each arm value a block reached by a dispatch edge, all arms rejoining at one exit — in the three value-position carriers, instead of collapsing the owning construct to a single inline block: - `var x = k switch { … }` — arms rejoin at a binding continuation that carries the declared name's def (discriminant + arm uses on the arms). - `return k switch { … }` — each arm returns the function result, threading every active finalizer. - `=> k switch { … }` expression-bodied member — each arm returns. The arms are now control-dependent on the discriminant (the point of #2207 — they previously produced zero CDG). Arm patterns / `when` guards are harvested as conditional uses on the dispatch; an unguarded `_`/`var` arm is the exhaustive catch-all (a non-exhaustive switch keeps EXIT reachable via a no-match edge). `switch_expression` is distinct from `switch_statement`, so this adds a dedicated `visitSwitchExpr`. An assignment RHS (`x = k switch …`), a call argument, and a multi- declarator decl remain inline (documented gap). `csharp-harvest` gains `bindingDefFacts`. Verified: 47 csharp-visitor tests (4 new), full CFG unit+integration suites (664) green, CDG snapshot byte-identical, bench --check PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cfg): model PHP value-position match expression as control flow (#2207) A PHP `match($v) { c => v, default => v }` with ≥2 arms is now modeled as a CFG `switch-case` dispatch — a discriminant block, each arm value a block reached by a dispatch edge, all arms rejoining at one exit (no fallthrough) — in the two value-position carriers, instead of collapsing the owning statement to one inline block: - `$x = match($v) { … }` — the dominant PHP idiom (no typed local decl): arms rejoin at a binding continuation carrying the assignment target's def (condition + arm uses on the arms). - `return match($v) { … }` — each arm returns the function result, threading every active finally. The arms are now control-dependent on the discriminant (the point of #2207). Arm `match_condition_list`s are harvested as conditional uses on the dispatch; a `default` arm is the catch-all (a defaultless `match` throws UnhandledMatchError, kept EXIT-reachable via a no-match edge). `php-harvest` gains `assignmentDefFacts`. A `match` in a call argument / nested subexpression stays inline; the ternary `?:` is excluded by design (a micro-branch, like elvis). Verified: 37 php-visitor tests (3 new), CDG snapshot byte-identical, bench --check PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cfg): model Dart value-position switch expression as control flow (#2207) A Dart 3 value-position `switch (v) { p => e, _ => e }` with ≥2 arms is now modeled as a CFG `switch-case` dispatch — a discriminant block, each arm value a block reached by a dispatch edge, all arms rejoining at one exit (no fallthrough) — in the two value-position carriers, instead of collapsing the owning statement to one inline block: - `var x = switch (v) { … }` — single-binding decl; arms rejoin at a binding continuation carrying the declared name's def. - `return switch (v) { … }` — each arm returns the function result, threading every active finalizer. The arms are now control-dependent on the discriminant (the point of #2207). A Dart call value parses as `identifier` + `selector` (multiple children, not one node), so the arm-value facts come from a dedicated `switchExprArmValueFacts`; arm patterns harvest conditionally onto the dispatch; a `_` arm is the catch-all (a non-exhaustive switch keeps EXIT reachable via a no-match edge). `dart-harvest` gains `bindingDefFacts` + the arm value/pattern fact helpers. A `switch_expression` in a call argument / multi-binding decl stays inline (its conditional arm sub-evaluation remains the #2206 harvest may-def path, re-pointed in the regression test). `?:`/`??`/`?.` excluded by design. Verified: 39 dart-visitor tests (4 new), CDG snapshot byte-identical, bench --check PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cfg): model Swift value-position if/switch as control flow (#2207) A Swift 5.9 value-position `if`/`switch` is now modeled as control flow in the two value-position carriers, instead of collapsing the owning statement to one inline block: - `let x = if … else … / switch v { … }` — arms rejoin at a binding continuation carrying the declared name's def (condition + arm uses on the branch blocks). - `return if … / switch …` — each arm returns the function result, threading every active finalizer. The arms are now control-dependent on the branch (the point of #2207). tree-sitter-swift reuses `if_statement` / `switch_statement` for the value form (no separate `if_expression`/`switch_expression`), so the existing `visitIf`/`visitSwitch` are reused — this mirrors the Kotlin carrier exactly. `swift-harvest` gains `bindingDefFacts`. A value-position `if` requires an `else`; a value `switch` needs ≥2 entries. A value branch in a call argument / interpolation stays inline; `?:`/`??` are excluded by design. Verified: 31 swift-visitor tests (4 new), CDG snapshot byte-identical, bench --check PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cfg): model Kotlin assignment-RHS and value-position try as control flow (#2205) Completes the value-position branch carriers #2205 left deferred after the initial val/var-binding + return + expr-body work: - `x = when (k) { … }` / `x = if (c) a else b` / `x = try { … }` — a plain `=` assignment whose RHS is a modelable branch now models the arms as control flow and binds the LHS target at the rejoin (a compound `+=` and a plain-call RHS stay inline). - `val x = try { … } catch { … }` — a value-position `try` is now a modelable value branch (reusing visitTry), so the binding/assignment carriers route it through control flow too. The arms are now control-dependent on the branch (the point of #2205). `isModelableValueBranch` gains `try_expression`; `visitBranchExpr` routes it to `visitTry`; `isControlFlow`/`visitStmt` gain the `assignment` carrier; `kotlin-harvest` gains `assignmentDefFacts`. A branch nested in a call argument (`f(when …)`) stays inline (the direct value is the call); `?:`/`?.` micro-branches excluded by design. The `return try { … }` carrier is intentionally left out (finalizer-threading in return position is risky and was not requested). Verified: 43 kotlin-visitor tests (5 new), full CFG unit+integration suites (676) green, CDG snapshot byte-identical, bench --check PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cfg): Java colon-form value switch — yield ends the arm, no fallthrough (#2211) Tri-review (adversarial + correctness lanes) found that a value-position colon-form switch expression — `int x = switch(k){ case 1: yield a(); case 2: yield b(); }` (valid Java 14+) — reused the statement `visitSwitch` fallthrough logic, wiring a spurious `fallthrough` edge between the yield-terminated colon groups. A switch EXPRESSION never falls through between arms; the false edge dropped an arm's control-dependence edge and added a false reaching-defs propagation edge (verified by a real-parser probe). Arrow-form value switches were already correct. Root cause: `visitYield` modeled `yield e;` as a block that CONTINUES to the next statement. Semantically `yield` produces the switch-expression's value and EXITS the switch. Fix: `visitYield` now terminates the arm, jumping to the enclosing switch's exit and threading any finalizer it crosses — exactly like a `break` out of the switch, but carrying the yielded value's facts. Adds `ControlFlowContext.resolveYield()` (nearest SWITCH frame, never an intervening loop). `yield` is Java-only here (C# `yield return` is iterator semantics, untouched). Tests: a colon-form value switch asserting NO `fallthrough` edge and that BOTH arms are control-dependent on the dispatch (specific controller→ dependent pairs), plus a `return switch(…)` inside `try/finally` asserting `finally-return` threading per arm. Verified: full CFG unit+integration suites green, CDG snapshot byte- identical, bench --check PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cfg): Dart value switch — guarded `_` is not a catch-all; guard is a dispatch test (#2211) Tri-review (adversarial + correctness lanes) found two issues in the Dart `visitSwitchExpr` value-position modeling: 1. Catch-all detection was `pattern.text === '_'`, ignoring guards. A guarded `_ when c => …` is NOT exhaustive (Dart throws at runtime if no arm + guard matches), so falsely treating it as a catch-all suppressed the conservative no-match edge — asserting an exhaustive switch that isn't. The sibling C# visitor already gated catch-all on `!guard`. 2. A `when` guard parses as a bare sibling between the pattern and the value (no wrapper node), so it fell into the arm-VALUE children and was harvested as an unconditional arm-value use instead of a conditional dispatch test. Fix: new `armParts()` splits a `switch_expression_case` at the `=>` token into pattern / guard(s) / value(s). The pattern AND guard are harvested conditionally onto the dispatch block (they evaluate before the body, only when earlier arms missed); the arm-value facts come from the post-`=>` children only; the catch-all is gated on an unguarded `_`. Removes the now- unused dart-harvest `switchExprArm{Value,Pattern}Facts` (the visitor harvests per-child via the existing `facts`/`factsConditional`). Tests: a guarded value switch asserting the no-match edge is present (3 switch-case successors from the dispatch) and EXIT stays reachable, plus a test that the guard's use is recorded on the dispatch block, not an arm. Verified: full CFG suites green, CDG snapshot byte-identical, bench --check PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cfg): Kotlin return try {…} models the value-position try (#2205, #2211) Tri-review (maintainability + testing lanes) caught a doc-vs-code mismatch: the visitor header documents a `return <branch>` carrier that includes `try`, but `visitReturn` only matched `when_expression`/`if_expression`, so `return try { … } catch { … }` fell through to the single inline-block path — its arms were not modeled. Fix: `visitReturn` now also matches `try_expression`, making the `return` carrier uniform with the binding / assignment / expression-body carriers (all route a value-position `try` through `isModelableValueBranch` → `visitTry`, threading the active finalizers per arm). No new machinery — just completes the carrier set the docstring already claimed. Tests: `return try {…} catch {…}` (throw + return edges, CDG-bearing, EXIT reachable) and `x = try {…} catch {…}` assignment-RHS (the assignment carrier's try path, previously untested). Verified: full CFG suites green, CDG snapshot byte-identical, bench --check PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(cfg): cover the no-match edge of non-exhaustive C#/PHP value switches (#2211) The testing lane noted the `hasCatchAll`/`hasDefault === false` branch — where a value-position `switch`/`match` with no catch-all/default arm adds a conservative no-match edge so EXIT stays reachable — was untested (every existing fixture used a `_`/`default` arm). Adds a C# `x switch { 1 => …, 2 => … }` and a PHP `match($x){ 1 => …, 2 => … }` (no default) test, each asserting the dispatch fans to (arms + 1) `switch-case` successors and `isExitReachableFromAllBlocks` holds. Verified: full CFG suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(cfg): clarify Kotlin value-position try gate covers the finally-only path (#2211) Tri-review (maintainability lane) noted the inline comment at the `try_expression` branch of `isModelableValueBranch` said only "a catch's value", but the gate fires on `catch_block || finally_block`. Reword to acknowledge that a value-position `try` with a `catch` OR a `finally` is a modelable branch. Comment-only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(cfg): document the deliberate C# declaratorInit duplication (#2211) Tri-review (maintainability lane) flagged the byte-identical `declaratorInit` helper in `csharp.ts` (visitor) and `csharp-harvest.ts` (harvester). The two are standalone classes with no shared base (repo convention) and the only module both import is the generic `utils/ast-helpers` (types only) — not a home for a C#-grammar-specific helper. Resolve the lowest-risk way: a cross-reference comment at each definition noting the deliberate duplication and the keep-in-sync requirement. No new shared module; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(cfg): unwrap the paren in Dart visitSwitch for dispatch consistency (#2211) Tri-review (maintainability lane) noted `visitSwitch` (statement form) used the raw parenthesized `condition` (dispatch text `switch (x)`) while the new `visitSwitchExpr` unwraps it (`switch x`). Probed the vendored tree-sitter-dart: the `switch_statement` condition IS a `parenthesized_expression`, so apply `unwrapParen` in `visitSwitch` too. The harvest walks into the paren either way, so the discriminant's def/use facts are unchanged — only the dispatch block's text string normalizes. Verified byte-identical (cdg-snapshot + bench --check unchanged; the Dart unit tests assert topology, not block text). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(cfg): Swift single-entry value switch stays inline (#2211) Tri-review (testing lane) noted the existing Swift "stays inline" test used a plain call (`let x = g()`), which never exercises the single-entry switch gate. Add a real one-entry value switch (`let x = switch v { default: g() }`) asserting it coalesces (no switch-case edge), pinning the `>= 2` switch_entry threshold in `isModelableValueBranch`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(cfg): cover the Kotlin expression-body try carrier (#2205, #2211) Tri-review (testing lane) noted the `fun f() = try { … } catch { … }` expression-body carrier (visitExprBody -> isModelableValueBranch accepting try_expression) existed but was untested. Add a regression asserting the expr-body try is modeled (throw + return edges, CDG-bearing, EXIT reachable). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(cfg): pin the value-branch carriers never throw on a truncated AST (R4) (#2211) Tri-review (adversarial lane) noted the new value-position branch carriers must return undefined / never throw on a malformed AST, or a single bad function would drop the whole file's CFG group (the R4 invariant). Add a per-language regression feeding a TRUNCATED value-branch carrier (an unterminated `var x = switch/match/if/when (…)`) through the existing `collectFunctions` + `buildFunctionCfg(...).not.toThrow()` graceful-undefined harness, for all six languages whose value-branch path is new (Java/C#/PHP/Dart/Swift/Kotlin). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
505 lines
24 KiB
TypeScript
505 lines
24 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { requireVendoredGrammar } from '../../../src/core/tree-sitter/vendored-grammars.js';
|
|
import { createKotlinCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/kotlin.js';
|
|
import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js';
|
|
import {
|
|
makeCfgHarness,
|
|
type CfgHarness,
|
|
block,
|
|
edgeKinds,
|
|
reaches,
|
|
reachable,
|
|
bindingIdx,
|
|
} from '../../helpers/cfg-harness.js';
|
|
import { isExitReachableFromAllBlocks } from '../../../src/core/ingestion/cfg/post-dominators.js';
|
|
import { computeControlDependence } from '../../../src/core/ingestion/cfg/control-dependence.js';
|
|
|
|
// The Kotlin CfgVisitor, one hazard per test (real-parser regression, NOT
|
|
// snapshot-pinning). Kotlin's grammar is VENDORED (not an npm package): the
|
|
// grammar loads from vendor/ via `requireVendoredGrammar('tree-sitter-kotlin')`,
|
|
// exactly like the Swift test loads the vendored tree-sitter-swift. 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 kotlinGrammar = requireVendoredGrammar('tree-sitter-kotlin') as Parameters<
|
|
typeof makeCfgHarness
|
|
>[0];
|
|
|
|
const kotlin: CfgHarness = makeCfgHarness(kotlinGrammar, createKotlinCfgVisitor(), 'fixture.kt');
|
|
|
|
const definesBinding = (cfg: FunctionCfg, idx: number): boolean =>
|
|
cfg.blocks.some((bl) => bl.statements?.some((s) => s.defs.includes(idx)));
|
|
const usesBinding = (cfg: FunctionCfg, idx: number): boolean =>
|
|
cfg.blocks.some((bl) => bl.statements?.some((s) => s.uses.includes(idx)));
|
|
|
|
describe('Kotlin CfgVisitor — structure', () => {
|
|
it('straight-line body: ENTRY → block → EXIT (seq)', () => {
|
|
const cfg = kotlin.cfgOf(`fun f() { 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 = kotlin.cfgOf(`fun f() {}`);
|
|
expect(cfg.blocks).toHaveLength(2);
|
|
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
|
});
|
|
|
|
it('expression-body function: ENTRY → block → EXIT', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(x: Int) = x + 1`);
|
|
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
|
expect(edgeKinds(cfg).has('return')).toBe(true);
|
|
// The parameter is bound and used.
|
|
expect(definesBinding(cfg, bindingIdx(cfg, 'x'))).toBe(true);
|
|
});
|
|
|
|
it('an unmodeled shape produces a graceful partial CFG (never throws)', () => {
|
|
// An abstract / interface method (no body) — buildFunctionCfg must return
|
|
// undefined rather than throw; a real function still builds.
|
|
const root = kotlin.parse(`interface P { fun f() }`);
|
|
const fns = kotlin.collectFunctions(root);
|
|
for (const fn of fns) {
|
|
expect(() => createKotlinCfgVisitor().buildFunctionCfg(fn, 'p.kt')).not.toThrow();
|
|
}
|
|
const cfg = kotlin.cfgOf(`fun g() { x() }`);
|
|
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
|
});
|
|
|
|
it('a truncated value-position when never throws out of the carrier path (R4) (#2211)', () => {
|
|
const root = kotlin.parse(`fun f(k: Int) { val x = when (k) { 0 ->`);
|
|
for (const fn of kotlin.collectFunctions(root)) {
|
|
expect(() => createKotlinCfgVisitor().buildFunctionCfg(fn, 'p.kt')).not.toThrow();
|
|
}
|
|
});
|
|
|
|
it('a class method is a CFG-bearing function', () => {
|
|
const cfg = kotlin.cfgOf(`class C { fun m(a: Int) { g(a) } }`);
|
|
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
|
expect(definesBinding(cfg, bindingIdx(cfg, 'a'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Kotlin CfgVisitor — branching (if/else)', () => {
|
|
it('if/else: cond-true to then, cond-false to else, both reach the join', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(x: Int) { 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 chain branches each condition', () => {
|
|
const cfg = kotlin.cfgOf(
|
|
`fun f(x: Int) { if (x == 1) { a() } else if (x == 2) { b() } else { c() }; d() }`,
|
|
);
|
|
const join = block(cfg, 'd()');
|
|
expect(reaches(cfg, block(cfg, 'a()'), join)).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'b()'), join)).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'c()'), join)).toBe(true);
|
|
});
|
|
|
|
it('if without braces (bare control_structure_body) still branches', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(x: Int) { if (x > 0) a() else b(); c() }`);
|
|
expect(edgeKinds(cfg).has('cond-true')).toBe(true);
|
|
expect(edgeKinds(cfg).has('cond-false')).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'a()'), block(cfg, 'c()'))).toBe(true);
|
|
});
|
|
|
|
it('an if as a binding VALUE is modeled as a branch with literal arms (#2205)', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(x: Int) { val y = if (x > 0) 1 else 2; use(y) }`);
|
|
// value-position `if` (with else) IS modeled now: both arms branch, y binds
|
|
// at the rejoin (previously this whole decl coalesced into one block).
|
|
expect(edgeKinds(cfg).has('cond-true')).toBe(true);
|
|
expect(edgeKinds(cfg).has('cond-false')).toBe(true);
|
|
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
|
expect(definesBinding(cfg, bindingIdx(cfg, 'y'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Kotlin CfgVisitor — when (no fallthrough)', () => {
|
|
it('when with subject: each arm dispatches and rejoins after (no fallthrough)', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(x: Int) {
|
|
when (x) {
|
|
1 -> one()
|
|
2 -> two()
|
|
else -> other()
|
|
}
|
|
after()
|
|
}`);
|
|
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
|
// arm 1 does NOT fall into arm 2 (Kotlin when has no fallthrough).
|
|
expect(reaches(cfg, block(cfg, 'one()'), block(cfg, 'two()'))).toBe(false);
|
|
// every arm reaches the post-when continuation.
|
|
expect(reaches(cfg, block(cfg, 'one()'), block(cfg, 'after()'))).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'two()'), block(cfg, 'after()'))).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'other()'), block(cfg, 'after()'))).toBe(true);
|
|
});
|
|
|
|
it('when WITHOUT subject (guard form) dispatches each condition', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(x: Int) {
|
|
when {
|
|
x > 0 -> pos()
|
|
else -> nonpos()
|
|
}
|
|
after()
|
|
}`);
|
|
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
|
expect(reachable(cfg, block(cfg, 'pos()'))).toBe(true);
|
|
expect(reachable(cfg, block(cfg, 'nonpos()'))).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'pos()'), block(cfg, 'after()'))).toBe(true);
|
|
});
|
|
|
|
it('a when with NO else lets the no-match path fall to the join', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(x: Int) { when (x) { 1 -> a() }; after() }`);
|
|
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
|
// the dispatch can reach after() without entering arm 1 (no-match path).
|
|
expect(reachable(cfg, block(cfg, 'after()'))).toBe(true);
|
|
});
|
|
|
|
it('all-empty arms WITH else still dispatch (no orphaned join, EXIT reverse-reachable)', () => {
|
|
// `when(k){0->{};else->{}}`: every arm body is empty and the `else` suppresses
|
|
// the no-match edge — the dispatch must still reach the join, else EXIT is not
|
|
// reverse-reachable and the whole function's CDG is dropped (was a real bug).
|
|
const cfg = kotlin.cfgOf(`fun f(k: Int) { when (k) { 0 -> {}; else -> {} }; after() }`);
|
|
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
|
expect(reaches(cfg, cfg.entryIndex, block(cfg, 'after()'))).toBe(true);
|
|
});
|
|
|
|
it('all-empty arms in value position (val x = when) stay EXIT reverse-reachable', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(k: Int) { val x = when (k) { 0 -> {}; else -> {} }; use(x) }`);
|
|
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Kotlin CfgVisitor — value-position branches (#2205)', () => {
|
|
it('val x = when (...) models the arms as control flow (CDG-bearing), binds x at the join', () => {
|
|
const cfg = kotlin.cfgOf(
|
|
`fun f(k: Int) { val x = when (k) { 0 -> a(); 1 -> b(); else -> c() }; use(x) }`,
|
|
);
|
|
// Arms are modeled (not collapsed into one straight-line block)…
|
|
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
|
expect(block(cfg, 'a()')).toBeGreaterThanOrEqual(0);
|
|
expect(block(cfg, 'c()')).toBeGreaterThanOrEqual(0);
|
|
// …each arm is control-dependent on the dispatch…
|
|
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
|
// …and x is defined at the rejoin and used downstream.
|
|
expect(definesBinding(cfg, bindingIdx(cfg, 'x'))).toBe(true);
|
|
expect(usesBinding(cfg, bindingIdx(cfg, 'x'))).toBe(true);
|
|
});
|
|
|
|
it('val r = if (c) ... else ... models both arms (cond-true/cond-false), binds r', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(c: Boolean) { val r = if (c) x() else y(); use(r) }`);
|
|
expect(edgeKinds(cfg).has('cond-true')).toBe(true);
|
|
expect(edgeKinds(cfg).has('cond-false')).toBe(true);
|
|
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
|
expect(definesBinding(cfg, bindingIdx(cfg, 'r'))).toBe(true);
|
|
});
|
|
|
|
it('return when (...) models the arms; each arm returns', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(k: Int): Int { return when (k) { 0 -> a(); else -> b() } }`);
|
|
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
|
expect(edgeKinds(cfg).has('return')).toBe(true);
|
|
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('expression-body fun f() = when (...) models the arms (CDG-bearing)', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(k: Int): Int = when (k) { 0 -> a(); 1 -> b(); else -> c() }`);
|
|
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
|
expect(edgeKinds(cfg).has('return')).toBe(true);
|
|
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('argument-position when stays inline — val x = f(when (...)) is NOT split', () => {
|
|
// The DIRECT value is the call `f(...)`, not the nested `when`, so it is a
|
|
// single straight-line block (no switch-case from a top-level dispatch).
|
|
const cfg = kotlin.cfgOf(`fun g(k: Int) { val x = f(when (k) { 0 -> 1; else -> 2 }) }`);
|
|
expect(edgeKinds(cfg).has('switch-case')).toBe(false);
|
|
});
|
|
|
|
it('single-arm when in value position stays inline (no real control dependence)', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(k: Int) { val x = when (k) { else -> a() }; use(x) }`);
|
|
expect(edgeKinds(cfg).has('switch-case')).toBe(false);
|
|
});
|
|
|
|
it('x = when (...) assignment RHS models the arms; binds the target (#2205)', () => {
|
|
const cfg = kotlin.cfgOf(
|
|
`fun f(k: Int) { var x = 0; x = when (k) { 0 -> a(); else -> b() }; use(x) }`,
|
|
);
|
|
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
|
expect(reaches(cfg, block(cfg, 'a()'), block(cfg, 'use(x)'))).toBe(true);
|
|
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
|
expect(definesBinding(cfg, bindingIdx(cfg, 'x'))).toBe(true);
|
|
expect(usesBinding(cfg, bindingIdx(cfg, 'x'))).toBe(true);
|
|
});
|
|
|
|
it('x = if (c) ... else ... assignment RHS models both arms (#2205)', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(c: Boolean) { var x = 0; x = if (c) a() else b(); use(x) }`);
|
|
expect(edgeKinds(cfg).has('cond-true')).toBe(true);
|
|
expect(edgeKinds(cfg).has('cond-false')).toBe(true);
|
|
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
|
expect(definesBinding(cfg, bindingIdx(cfg, 'x'))).toBe(true);
|
|
});
|
|
|
|
it('val x = try { ... } catch { ... } models the value-position try (#2205)', () => {
|
|
const cfg = kotlin.cfgOf(
|
|
`fun f() { val x = try { risky() } catch (e: Exception) { fallback() }; use(x) }`,
|
|
);
|
|
// the try/catch is modeled as control flow (a throw edge to the handler)…
|
|
expect(edgeKinds(cfg).has('throw')).toBe(true);
|
|
// …and is CDG-bearing, with x bound at the rejoin and used downstream.
|
|
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
|
expect(definesBinding(cfg, bindingIdx(cfg, 'x'))).toBe(true);
|
|
expect(usesBinding(cfg, bindingIdx(cfg, 'x'))).toBe(true);
|
|
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
|
});
|
|
|
|
it('x = try { ... } catch { ... } assignment RHS models the value-position try (#2205)', () => {
|
|
const cfg = kotlin.cfgOf(
|
|
`fun f() { var x = 0; x = try { risky() } catch (e: Exception) { fallback() }; use(x) }`,
|
|
);
|
|
expect(edgeKinds(cfg).has('throw')).toBe(true);
|
|
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
|
expect(definesBinding(cfg, bindingIdx(cfg, 'x'))).toBe(true);
|
|
expect(usesBinding(cfg, bindingIdx(cfg, 'x'))).toBe(true);
|
|
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
|
});
|
|
|
|
it('return try { ... } catch { ... } models the value-position try; each arm returns (#2205, #2211)', () => {
|
|
const cfg = kotlin.cfgOf(
|
|
`fun f(): Int { return try { risky() } catch (e: Exception) { fallback() } }`,
|
|
);
|
|
// the value-position try is modeled as control flow (throw edge to the handler)…
|
|
expect(edgeKinds(cfg).has('throw')).toBe(true);
|
|
expect(edgeKinds(cfg).has('return')).toBe(true);
|
|
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
|
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
|
});
|
|
|
|
it('fun f() = try { ... } catch { ... } expression body models the value-position try (#2205, #2211)', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(): Int = try { risky() } catch (e: Exception) { fallback() }`);
|
|
// visitExprBody routes the value-position try through control flow (throw edge),
|
|
// each arm yielding the function result (return), CDG-bearing.
|
|
expect(edgeKinds(cfg).has('throw')).toBe(true);
|
|
expect(edgeKinds(cfg).has('return')).toBe(true);
|
|
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
|
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
|
});
|
|
|
|
it('a compound `x += ...` / a plain call RHS stays inline (not a value-branch carrier)', () => {
|
|
const compound = kotlin.cfgOf(`fun f(k: Int) { var x = 0; x += k; use(x) }`);
|
|
expect(edgeKinds(compound).has('switch-case')).toBe(false);
|
|
const call = kotlin.cfgOf(`fun f(k: Int) { var x = 0; x = compute(k); use(x) }`);
|
|
expect(edgeKinds(call).has('switch-case')).toBe(false);
|
|
expect(edgeKinds(call).has('cond-true')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Kotlin CfgVisitor — loops', () => {
|
|
it('for-in: header + body + loop-back + exit; binds the loop var', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(xs: List<Int>) { for (x in xs) { step(x) }; done() }`);
|
|
const header = block(cfg, 'for (x in xs)');
|
|
const body = block(cfg, 'step(x)');
|
|
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);
|
|
expect(definesBinding(cfg, bindingIdx(cfg, 'x'))).toBe(true);
|
|
});
|
|
|
|
it('while: header tests first, body loops back', () => {
|
|
const cfg = kotlin.cfgOf(`fun f() { while (cond()) { step() }; done() }`);
|
|
const header = block(cfg, 'while (cond())');
|
|
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 = kotlin.cfgOf(`fun f() { do { step() } while (cond()); done() }`);
|
|
const body = block(cfg, 'step()');
|
|
const cond = block(cfg, 'while (cond())');
|
|
expect(reaches(cfg, cfg.entryIndex, body)).toBe(true); // body runs first
|
|
expect(reaches(cfg, body, cond)).toBe(true); // condition tests at the bottom
|
|
expect(cfg.edges).toContainEqual({ from: cond, to: body, kind: 'loop-back' });
|
|
expect(reaches(cfg, cond, block(cfg, 'done()'))).toBe(true);
|
|
});
|
|
|
|
it('while (true) {} keeps EXIT reverse-reachable AND emits CDG > 0', () => {
|
|
// The inner `if` is a real control point; assert through the production
|
|
// post-dom/CDG passes (matching go/python/ruby/rust/vue) — CDG is only
|
|
// computed when EXIT stays reverse-reachable, so a non-empty CDG proves the
|
|
// structural exit-escape edge keeps the function CDG-bearing.
|
|
const cfg = kotlin.cfgOf(`fun f(x: Boolean) { while (true) { if (x) { g() } } }`);
|
|
expect(edgeKinds(cfg).has('cond-false')).toBe(true);
|
|
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
|
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('do {} while (true) keeps EXIT reverse-reachable', () => {
|
|
const cfg = kotlin.cfgOf(`fun f() { do { work() } while (true) }`);
|
|
expect(edgeKinds(cfg).has('cond-false')).toBe(true);
|
|
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
|
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Kotlin CfgVisitor — try/catch/finally', () => {
|
|
it('try/catch/finally: a throw edge runs to the handler; finally completion edges', () => {
|
|
const cfg = kotlin.cfgOf(`fun f() {
|
|
try { risky() } catch (e: Exception) { handle(e) } finally { cleanup() }
|
|
after()
|
|
}`);
|
|
const kinds = edgeKinds(cfg);
|
|
expect(kinds.has('throw')).toBe(true);
|
|
const handler = block(cfg, 'handle(e)');
|
|
expect(reaches(cfg, block(cfg, 'risky()'), handler)).toBe(true);
|
|
// the finally runs on both normal and exception exit.
|
|
const fin = block(cfg, 'cleanup()');
|
|
expect(reaches(cfg, block(cfg, 'risky()'), fin)).toBe(true);
|
|
expect(reaches(cfg, handler, fin)).toBe(true);
|
|
// after() is still reachable (finally completion rejoins).
|
|
expect(reachable(cfg, block(cfg, 'after()'))).toBe(true);
|
|
// the catch binds the error `e`.
|
|
expect(definesBinding(cfg, bindingIdx(cfg, 'e'))).toBe(true);
|
|
});
|
|
|
|
it('a return inside a try threads through the finally (finally-return completion)', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(): Int {
|
|
try { return compute() } finally { cleanup() }
|
|
}`);
|
|
const kinds = edgeKinds(cfg);
|
|
expect(kinds.has('return')).toBe(true);
|
|
expect(kinds.has('finally-return')).toBe(true);
|
|
const fin = block(cfg, 'cleanup()');
|
|
expect(reaches(cfg, fin, cfg.exitIndex)).toBe(true);
|
|
});
|
|
|
|
it('a throw with NO enclosing try/catch routes to EXIT and ends its block', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(x: Boolean) { if (x) throw RuntimeException(); done() }`);
|
|
const thr = block(cfg, 'throw RuntimeException()');
|
|
expect(cfg.edges).toContainEqual({ from: thr, to: cfg.exitIndex, kind: 'throw' });
|
|
// throw terminates its block — control does not fall into done() from it.
|
|
expect(reaches(cfg, thr, block(cfg, 'done()'))).toBe(false);
|
|
expect(reachable(cfg, block(cfg, 'done()'))).toBe(true); // via the if false branch
|
|
});
|
|
|
|
it('empty try {} with catch + finally keeps the catch handler reachable (#2195)', () => {
|
|
// An empty try body still establishes a protected region — the catch (and
|
|
// its error binding) must not be orphaned/unreachable from ENTRY.
|
|
const cfg = kotlin.cfgOf(
|
|
`fun f() { try {} catch (e: Exception) { handle(e) } finally { cl() }; a() }`,
|
|
);
|
|
expect(reachable(cfg, block(cfg, 'handle(e)'))).toBe(true);
|
|
expect(reachable(cfg, block(cfg, 'cl()'))).toBe(true);
|
|
expect(reachable(cfg, block(cfg, 'a()'))).toBe(true);
|
|
expect(definesBinding(cfg, bindingIdx(cfg, 'e'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Kotlin CfgVisitor — labeled break/continue', () => {
|
|
it('labeled break@outer escapes BOTH loops and reaches done()', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(xs: List<Int>, ys: List<Int>) {
|
|
outer@ for (i in xs) {
|
|
for (j in ys) { break@outer }
|
|
}
|
|
done()
|
|
}`);
|
|
const brk = block(cfg, 'break@outer');
|
|
expect(edgeKinds(cfg).has('break')).toBe(true);
|
|
expect(reaches(cfg, brk, block(cfg, 'done()'))).toBe(true);
|
|
});
|
|
|
|
it('continue@loop targets the outer loop header', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(xs: List<Int>, ys: List<Int>) {
|
|
loop@ for (i in xs) {
|
|
for (j in ys) { continue@loop }
|
|
}
|
|
done()
|
|
}`);
|
|
expect(edgeKinds(cfg).has('continue')).toBe(true);
|
|
const outer = block(cfg, 'for (i in xs)');
|
|
const cont = block(cfg, 'continue@loop');
|
|
expect(reaches(cfg, cont, outer)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Kotlin CfgVisitor — lambdas (own CFG)', () => {
|
|
it('a lambda is collected as its own CFG; return@label routes to the lambda EXIT', () => {
|
|
const cfgs = kotlin.cfgsOf(`fun f(xs: List<Int>) {
|
|
xs.forEach { x ->
|
|
if (x < 0) return@forEach
|
|
use(x)
|
|
}
|
|
}`);
|
|
// f and the lambda are both CFG-bearing.
|
|
expect(cfgs.length).toBeGreaterThanOrEqual(2);
|
|
for (const cfg of cfgs) expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
|
// The lambda's CFG (the one containing return@forEach) keeps EXIT reachable.
|
|
const lambdaCfg = cfgs.find((c) => c.blocks.some((b) => b.text.includes('return@forEach')));
|
|
expect(lambdaCfg).toBeDefined();
|
|
const ret = lambdaCfg!.blocks.find((b) => b.text.includes('return@forEach'))!.index;
|
|
expect(reaches(lambdaCfg!, ret, lambdaCfg!.exitIndex)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Kotlin CfgVisitor — def/use harvest', () => {
|
|
it('val x = compute(); use(x) produces a def of x and a use in the consumer', () => {
|
|
const cfg = kotlin.cfgOf(`fun f() { val x = compute(); use(x) }`);
|
|
const x = bindingIdx(cfg, 'x');
|
|
expect(definesBinding(cfg, x)).toBe(true);
|
|
expect(usesBinding(cfg, x)).toBe(true);
|
|
});
|
|
|
|
it('destructuring `val (a, b) = p` defines both names', () => {
|
|
const cfg = kotlin.cfgOf(`fun f(p: Pair<Int, Int>) { val (a, b) = p; use(a); use(b) }`);
|
|
for (const name of ['a', 'b']) {
|
|
const idx = bindingIdx(cfg, name);
|
|
expect(definesBinding(cfg, idx)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('var reassignment defines the variable again', () => {
|
|
const cfg = kotlin.cfgOf(`fun f() { var y = 0; y = compute(); use(y) }`);
|
|
const y = bindingIdx(cfg, 'y');
|
|
expect(definesBinding(cfg, y)).toBe(true);
|
|
expect(usesBinding(cfg, y)).toBe(true);
|
|
});
|
|
|
|
it('compound assign `x += 1` defines AND uses x', () => {
|
|
const cfg = kotlin.cfgOf(`fun f() { var x = 0; x += step() }`);
|
|
const x = bindingIdx(cfg, 'x');
|
|
expect(definesBinding(cfg, x)).toBe(true);
|
|
expect(usesBinding(cfg, x)).toBe(true);
|
|
});
|
|
|
|
const defStmtCount = (cfg: FunctionCfg, idx: number): number =>
|
|
cfg.blocks.flatMap((bl) => bl.statements ?? []).filter((s) => s.defs.includes(idx)).length;
|
|
|
|
it('postfix `x++` defines AND uses the operand (#2195 P2)', () => {
|
|
const cfg = kotlin.cfgOf(`fun f() { var x = 0; x++ }`);
|
|
const x = bindingIdx(cfg, 'x');
|
|
// `var x = 0` defs x once; `x++` must def it AGAIN (the loop-counter
|
|
// reaching-def the bug dropped) — not record x as a use-only.
|
|
expect(defStmtCount(cfg, x)).toBe(2);
|
|
expect(usesBinding(cfg, x)).toBe(true);
|
|
});
|
|
|
|
it('prefix `--x` defines the operand too (#2195 P2)', () => {
|
|
const cfg = kotlin.cfgOf(`fun f() { var x = 0; --x }`);
|
|
const x = bindingIdx(cfg, 'x');
|
|
expect(defStmtCount(cfg, x)).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('Kotlin CfgVisitor — functionStartColumn', () => {
|
|
it('two same-line functions get distinct functionStartColumn', () => {
|
|
const cfgs = kotlin.cfgsOf(`fun a() { x() }; fun b() { y() }`);
|
|
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
|
|
});
|
|
});
|