mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
Merge branch 'main' into perf/2201-ssa-sparse-reaching-defs
This commit is contained in:
commit
703f0b6682
19 changed files with 1370 additions and 68 deletions
|
|
@ -126,6 +126,18 @@ export class ControlFlowContext {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a Java `yield e` (switch-EXPRESSION arm exit): the nearest enclosing
|
||||
* SWITCH frame's exit, threading the finalizers stacked above it. Unlike a
|
||||
* `break`, a `yield` ALWAYS targets the switch — never an intervening loop — so
|
||||
* it cannot match a loop frame (a `yield` inside a loop inside a switch arm
|
||||
* still exits the whole switch). Returns `undefined` when there is no enclosing
|
||||
* switch (malformed input); the caller falls back to its conservative routing.
|
||||
*/
|
||||
resolveYield(): JumpResolution | undefined {
|
||||
return this.resolve((f) => f.kind === 'switch');
|
||||
}
|
||||
|
||||
/** Every active finalizer, innermost first — what a `return` must cross. */
|
||||
finalizersForReturn(): readonly FinalizerFrame[] {
|
||||
const fins: FinalizerFrame[] = [];
|
||||
|
|
|
|||
|
|
@ -220,6 +220,27 @@ export class CsharpHarvester extends ScopeTreeHarvester {
|
|||
return acc.finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Def-ONLY facts for a value-position binding carrier (`var x = k switch {…}`,
|
||||
* #2207): just the declared name(s)' def, attached to the continuation block the
|
||||
* switch arms rejoin. The discriminant + arm-value USES are already harvested
|
||||
* onto the branch's own blocks ({@link facts} on each arm), so this must NOT
|
||||
* re-walk the initializer — only each `variable_declarator`'s name is a def here.
|
||||
*/
|
||||
bindingDefFacts(stmt: SyntaxNode): StatementFacts | undefined {
|
||||
const acc = new FactAccumulator(stmt.startPosition.row + 1);
|
||||
const decl = stmt.namedChildren.find((c) => c.type === 'variable_declaration');
|
||||
if (decl) {
|
||||
for (let i = 0; i < decl.namedChildCount; i++) {
|
||||
const d = decl.namedChild(i);
|
||||
if (d?.type !== 'variable_declarator') continue;
|
||||
const name = d.childForFieldName('name');
|
||||
if (name) this.def(name, acc);
|
||||
}
|
||||
}
|
||||
return acc.defCount() ? acc.finish() : undefined;
|
||||
}
|
||||
|
||||
/** Facts for a `foreach (decl in right)` head: decl binds, right is used. */
|
||||
forEachHeadFacts(stmt: SyntaxNode): StatementFacts {
|
||||
const acc = new FactAccumulator(stmt.startPosition.row + 1);
|
||||
|
|
@ -548,7 +569,13 @@ export class CsharpHarvester extends ScopeTreeHarvester {
|
|||
return { path, rootIdx };
|
||||
}
|
||||
|
||||
/** The initializer value of a `variable_declarator` — the named child after `name`. */
|
||||
/**
|
||||
* The initializer value of a `variable_declarator` — the named child after
|
||||
* `name`. NOTE: deliberately duplicated in `csharp.ts` (the visitor is a
|
||||
* standalone class with no shared base — repo convention). The two copies must
|
||||
* stay in sync; there is no C#-specific shared module to host it, and the only
|
||||
* module both files share is the generic `utils/ast-helpers` (types only).
|
||||
*/
|
||||
private declaratorInit(declarator: SyntaxNode): SyntaxNode | undefined {
|
||||
const name = declarator.childForFieldName('name');
|
||||
for (let i = 0; i < declarator.namedChildCount; i++) {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,12 @@
|
|||
* unresolved label.
|
||||
* - Async/await suspension points are modeled as straight-line (the awaited
|
||||
* continuation is not a separate flow), consistent with the TS visitor.
|
||||
* - A value-position `switch_expression` (`k switch {…}`) with ≥2 arms IS modeled
|
||||
* as a `switch-case` dispatch in three carriers (#2207): a single-declarator
|
||||
* `var x = k switch {…}` (arms rejoin at a binding continuation), `return k
|
||||
* switch {…}`, and an `=> k switch {…}` expression body (each arm returns).
|
||||
* A value switch in any OTHER position — an assignment RHS (`x = k switch …`),
|
||||
* a call argument, or a multi-declarator decl — stays INLINE (one block).
|
||||
* - Def/use harvest scope: see `csharp-harvest.ts` — member/element writes are
|
||||
* not scalar defs; nested-function bodies are opaque in both directions.
|
||||
*
|
||||
|
|
@ -186,7 +192,7 @@ class CsharpCfgWalk {
|
|||
dangling = [...scope.exits];
|
||||
break; // the rest of the sequence is consumed by the dispose scope
|
||||
}
|
||||
if (CONTROL_FLOW_TYPES.has(stmt.type)) {
|
||||
if (this.breaksBlock(stmt)) {
|
||||
openSimple = undefined; // close any open straight-line block
|
||||
const res = this.visitStmt(stmt);
|
||||
if (res === null) continue; // transparent (empty nested block)
|
||||
|
|
@ -222,9 +228,28 @@ class CsharpCfgWalk {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a statement breaks the current straight-line block. Adds the
|
||||
* value-position switch carrier to the base {@link CONTROL_FLOW_TYPES} set: a
|
||||
* `local_declaration_statement` whose single initializer is a modelable
|
||||
* `switch_expression` (`var x = k switch {…}`, #2207) breaks so `visitStmt`
|
||||
* models the arms as control flow instead of collapsing the decl to one block.
|
||||
*/
|
||||
private breaksBlock(stmt: SyntaxNode): boolean {
|
||||
if (this.isValueSwitchDecl(stmt)) return true;
|
||||
return CONTROL_FLOW_TYPES.has(stmt.type);
|
||||
}
|
||||
|
||||
/** Dispatch one statement to its handler. Non-null except for empty blocks. */
|
||||
visitStmt(stmt: SyntaxNode): SeqResult {
|
||||
switch (stmt.type) {
|
||||
case 'local_declaration_statement': {
|
||||
// `var x = k switch { … }` (#2207): the initializer is a value-position
|
||||
// branch — model it as control flow and bind the result on the rejoin.
|
||||
const branch = this.declValueSwitch(stmt);
|
||||
if (branch) return this.visitBindBranch(stmt, branch);
|
||||
return this.visitSimple(stmt);
|
||||
}
|
||||
case 'if_statement':
|
||||
return this.visitIf(stmt);
|
||||
case 'while_statement':
|
||||
|
|
@ -276,6 +301,18 @@ class CsharpCfgWalk {
|
|||
}
|
||||
|
||||
private visitReturn(stmt: SyntaxNode): TraversalResult {
|
||||
// `return k switch { … };` (#2207): the returned value is a value-position
|
||||
// branch — model it as control flow, with each arm returning (its value IS
|
||||
// the function result), threading every active finalizer per arm.
|
||||
const branch = stmt.namedChildren.find((c) => c.type !== 'comment');
|
||||
if (branch && this.isModelableValueBranch(branch)) {
|
||||
const res = this.visitBranchExpr(branch);
|
||||
const finalizers = this.cfc.finalizersForReturn();
|
||||
for (const ex of res.exits) {
|
||||
wireJumpThroughFinalizers(this.builder, ex, finalizers, this.builder.exitIndex, 'return');
|
||||
}
|
||||
return { entry: res.entry, exits: [] };
|
||||
}
|
||||
const idx = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(stmt),
|
||||
|
|
@ -656,6 +693,147 @@ class CsharpCfgWalk {
|
|||
return node.type.endsWith('_statement') || node.type === 'block';
|
||||
}
|
||||
|
||||
// ── value-position switch expression (#2207) ────────────────────────────────
|
||||
|
||||
/**
|
||||
* The `switch_expression` initializer of a single-declarator
|
||||
* `local_declaration_statement` (`var x = k switch {…}`) when it is a modelable
|
||||
* value branch, else undefined. A `using` decl and a multi-declarator decl are
|
||||
* excluded (the `using` dispose path / multi-declarator stay inline).
|
||||
*/
|
||||
private declValueSwitch(stmt: SyntaxNode): SyntaxNode | undefined {
|
||||
if (stmt.type !== 'local_declaration_statement') return undefined;
|
||||
if (this.isUsingLocalDecl(stmt)) return undefined;
|
||||
const decl = stmt.namedChildren.find((c) => c.type === 'variable_declaration');
|
||||
if (!decl) return undefined;
|
||||
const declarators = decl.namedChildren.filter((c) => c.type === 'variable_declarator');
|
||||
if (declarators.length !== 1) return undefined;
|
||||
const init = this.declaratorInit(declarators[0]);
|
||||
return init && this.isModelableValueBranch(init) ? init : undefined;
|
||||
}
|
||||
|
||||
private isValueSwitchDecl(stmt: SyntaxNode): boolean {
|
||||
return this.declValueSwitch(stmt) !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The initializer of a `variable_declarator` — its named child after `name`.
|
||||
* NOTE: deliberately duplicated in `csharp-harvest.ts` (the harvester is a
|
||||
* standalone class with no shared base — repo convention). The two copies must
|
||||
* stay in sync; there is no C#-specific shared module to host it, and the only
|
||||
* module both files share is the generic `utils/ast-helpers` (types only).
|
||||
*/
|
||||
private declaratorInit(declarator: SyntaxNode): SyntaxNode | undefined {
|
||||
const name = declarator.childForFieldName('name');
|
||||
for (let i = 0; i < declarator.namedChildCount; i++) {
|
||||
const c = declarator.namedChild(i);
|
||||
if (c && c.id !== name?.id) return c;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `node` is a value-position branch worth modeling as control flow
|
||||
* (#2207): a `switch_expression` (`k switch {…}`) with ≥2 arms — a real
|
||||
* dispatch. C# value-position `if` does not exist (the ternary `?:` is excluded,
|
||||
* like elvis in Kotlin).
|
||||
*/
|
||||
private isModelableValueBranch(node: SyntaxNode): boolean {
|
||||
if (node.type !== 'switch_expression') return false;
|
||||
return node.namedChildren.filter((c) => c.type === 'switch_expression_arm').length >= 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model a value-position `switch_expression` (`k switch { p => v, … }`) as a CFG
|
||||
* dispatch: a discriminant block, each arm's value expression a block reached by
|
||||
* a `switch-case` edge, all arms rejoining at a single exit. The arm patterns /
|
||||
* `when` guards are harvested as conditional uses on the dispatch (a later arm
|
||||
* test runs only when earlier arms didn't match), mirroring {@link visitSwitch}.
|
||||
*/
|
||||
private visitSwitchExpr(node: SyntaxNode): TraversalResult {
|
||||
const arms = node.namedChildren.filter((c) => c.type === 'switch_expression_arm');
|
||||
const discriminant = node.namedChildren.find((c) => c.type !== 'switch_expression_arm') ?? node;
|
||||
const dispatch = this.builder.newBlock(
|
||||
startLineOf(node),
|
||||
endLineOf(discriminant),
|
||||
discriminant.text,
|
||||
'normal',
|
||||
this.harvest.facts(discriminant),
|
||||
);
|
||||
const switchExit = this.builder.newBlock(endLineOf(node), endLineOf(node), '');
|
||||
|
||||
let hasCatchAll = false;
|
||||
for (const arm of arms) {
|
||||
const pattern = arm.namedChild(0);
|
||||
const guard = arm.namedChildren.find((c) => c.type === 'when_clause');
|
||||
if (pattern) this.builder.attachFacts(dispatch, this.harvest.factsConditional(pattern));
|
||||
if (guard) {
|
||||
const inner = guard.namedChild(0);
|
||||
if (inner) this.builder.attachFacts(dispatch, this.harvest.factsConditional(inner));
|
||||
}
|
||||
// An unguarded `_`/`var` arm matches everything — the exhaustive default.
|
||||
if (!guard && pattern && (pattern.type === 'discard' || pattern.type === 'var_pattern')) {
|
||||
hasCatchAll = true;
|
||||
}
|
||||
const value = this.armValue(arm);
|
||||
const armBlock = this.builder.newBlock(
|
||||
startLineOf(value ?? arm),
|
||||
endLineOf(value ?? arm),
|
||||
(value ?? arm).text,
|
||||
'normal',
|
||||
value ? this.harvest.facts(value) : undefined,
|
||||
);
|
||||
this.builder.edge(dispatch, armBlock, 'switch-case');
|
||||
this.builder.edge(armBlock, switchExit, 'seq');
|
||||
}
|
||||
// A non-exhaustive switch throws at runtime; conservatively keep EXIT directly
|
||||
// reachable from the dispatch when no catch-all arm covers the no-match path.
|
||||
if (!hasCatchAll) this.builder.edge(dispatch, switchExit, 'switch-case');
|
||||
|
||||
return { entry: dispatch, exits: [switchExit] };
|
||||
}
|
||||
|
||||
/** The value expression of a `switch_expression_arm` (the child after `=>`). */
|
||||
private armValue(arm: SyntaxNode): SyntaxNode | undefined {
|
||||
// pattern [when_clause] => value — the value is the LAST named child.
|
||||
return arm.namedChild(arm.namedChildCount - 1) ?? undefined;
|
||||
}
|
||||
|
||||
/** Model a value-position branch as control flow (only `switch_expression`). */
|
||||
private visitBranchExpr(node: SyntaxNode): TraversalResult {
|
||||
return this.visitSwitchExpr(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* An expression-bodied member's value (`=> k switch {…}`, #2207): if it is a
|
||||
* modelable value branch, model its arms as control flow (each arm returns the
|
||||
* function result); otherwise return null so the caller falls back to a single
|
||||
* inline block.
|
||||
*/
|
||||
tryVisitValueBranchBody(expr: SyntaxNode): TraversalResult | null {
|
||||
return this.isModelableValueBranch(expr) ? this.visitBranchExpr(expr) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* `var x = k switch { … }` (#2207): visit the switch as control flow, then
|
||||
* rejoin its arms at a facts-only continuation carrying ONLY the bound name's
|
||||
* def (the discriminant + arm-value uses are already on the switch's blocks).
|
||||
* The arms are now control-dependent on the dispatch, and `x` is defined at the
|
||||
* join — mirrors the Java / Kotlin / Rust value-position binding.
|
||||
*/
|
||||
private visitBindBranch(stmt: SyntaxNode, branch: SyntaxNode): TraversalResult {
|
||||
const res = this.visitBranchExpr(branch);
|
||||
const cont = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
startLineOf(stmt),
|
||||
'',
|
||||
'normal',
|
||||
this.harvest.bindingDefFacts(stmt),
|
||||
);
|
||||
this.builder.connect(res.exits, cont, 'seq');
|
||||
return { entry: res.entry, exits: [cont] };
|
||||
}
|
||||
|
||||
private visitTry(stmt: SyntaxNode): SeqResult {
|
||||
const bodyNode = stmt.childForFieldName('body');
|
||||
const catchClauses: SyntaxNode[] = [];
|
||||
|
|
@ -924,6 +1102,14 @@ function buildFunctionCfg(fnNode: SyntaxNode, filePath: string): FunctionCfg | u
|
|||
// Expression-bodied member / single-expression lambda: one block whose
|
||||
// value is returned. For an arrow clause the value is its inner expression.
|
||||
const expr = body.type === 'arrow_expression_clause' ? (body.namedChild(0) ?? body) : body;
|
||||
// `=> k switch { … }` (#2207): model the arms as control flow, each arm
|
||||
// returning the function result, instead of one inline block.
|
||||
const branchRes = new CsharpCfgWalk(builder, harvest).tryVisitValueBranchBody(expr);
|
||||
if (branchRes) {
|
||||
builder.edge(builder.entryIndex, branchRes.entry, 'seq');
|
||||
builder.connect(branchRes.exits, builder.exitIndex, 'return');
|
||||
return builder.finish(harvest.bindingTable());
|
||||
}
|
||||
const blk = builder.newBlock(
|
||||
startLineOf(expr),
|
||||
endLineOf(expr),
|
||||
|
|
|
|||
|
|
@ -280,6 +280,29 @@ export class DartHarvester {
|
|||
return acc.finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Def-ONLY facts for a value-position binding carrier (`var x = switch (…) {…}`,
|
||||
* #2207): just the declared name(s)' def, attached to the continuation block the
|
||||
* switch arms rejoin. The subject + arm-value USES are already harvested onto
|
||||
* the branch's own blocks, so this must NOT re-walk the value — only each
|
||||
* `initialized_variable_definition`'s `name` (and trailing binders) is a def.
|
||||
*/
|
||||
bindingDefFacts(stmt: SyntaxNode): StatementFacts | undefined {
|
||||
const acc = new FactAccumulator(stmt.startPosition.row + 1);
|
||||
for (const def of stmt.namedChildren) {
|
||||
if (def.type !== 'initialized_variable_definition') continue;
|
||||
const name = def.childForFieldName('name');
|
||||
if (name) this.def(name, acc);
|
||||
for (let i = 0; i < def.namedChildCount; i++) {
|
||||
const c = def.namedChild(i);
|
||||
if (c?.type !== 'initialized_identifier') continue;
|
||||
const id = c.namedChildren.find((g) => g.type === 'identifier');
|
||||
if (id) this.def(id, acc);
|
||||
}
|
||||
}
|
||||
return acc.defCount() ? acc.finish() : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Facts for a `for` head. For-in: the loop var name is a def, the collection a
|
||||
* use. C-style: the init/condition/update sub-expressions are walked for
|
||||
|
|
|
|||
|
|
@ -88,10 +88,13 @@
|
|||
* - a closure (`function_expression`) is collected as its OWN function by
|
||||
* `isFunction`, so its body gets a standalone CFG; in the ENCLOSING function it
|
||||
* is an opaque straight-line value (its body is not followed inline).
|
||||
* - `switch_expression` / `if`-as-expression / `?:` / `??` / `?.` used as a VALUE
|
||||
* are left INLINE inside their owning statement's block — their conditional
|
||||
* sub-evaluation is a HARVEST may-def concern (see dart-harvest.ts), not a CFG
|
||||
* split (consistent with the TS `&&`/`??` treatment).
|
||||
* - a value-position `switch_expression` (Dart 3) with ≥2 arms IS modeled as a
|
||||
* `switch-case` dispatch in two carriers (#2207): a single-binding `var x =
|
||||
* switch (v) {…}` (arms rejoin at a binding continuation) and `return switch
|
||||
* (v) {…}` (each arm returns). A `switch_expression` in any OTHER position — a
|
||||
* call argument, a multi-binding decl — stays INLINE (its conditional arm
|
||||
* sub-evaluation is a HARVEST may-def concern, see dart-harvest.ts). `?:` /
|
||||
* `??` / `?.` micro-branches are excluded by design (like the TS treatment).
|
||||
*
|
||||
* Known limitations:
|
||||
* - block-scope shadowing in the harvest is flattened to one function table (see
|
||||
|
|
@ -249,6 +252,12 @@ class DartCfgWalk {
|
|||
private isControlFlow(stmt: SyntaxNode): boolean {
|
||||
if (this.isLabelError(stmt)) return true; // a stray label sibling — queue it
|
||||
if (isThrowStatement(stmt) || isRethrowStatement(stmt)) return true;
|
||||
// `var x = switch (v) { … }` (#2207): a value-position switch breaks so
|
||||
// `visitStmt` models the arms as control flow instead of coalescing.
|
||||
if (stmt.type === 'local_variable_declaration') {
|
||||
const v = this.directValue(stmt);
|
||||
return v !== undefined && this.isModelableValueBranch(v);
|
||||
}
|
||||
return CONTROL_FLOW_TYPES.has(stmt.type);
|
||||
}
|
||||
|
||||
|
|
@ -273,6 +282,13 @@ class DartCfgWalk {
|
|||
if (isThrowStatement(stmt)) return this.visitThrow(stmt);
|
||||
if (isRethrowStatement(stmt)) return this.visitRethrow(stmt);
|
||||
switch (stmt.type) {
|
||||
case 'local_variable_declaration': {
|
||||
// `var x = switch (v) { … }` (#2207): the value is a value-position
|
||||
// branch — model it as control flow and bind the result on the rejoin.
|
||||
const value = this.directValue(stmt);
|
||||
if (value && this.isModelableValueBranch(value)) return this.visitBindBranch(stmt, value);
|
||||
return this.visitSimple(stmt);
|
||||
}
|
||||
case 'if_statement':
|
||||
return this.visitIf(stmt);
|
||||
case 'for_statement':
|
||||
|
|
@ -322,6 +338,18 @@ class DartCfgWalk {
|
|||
|
||||
/** `return [expr];` — threads through every active finalizer before EXIT. */
|
||||
private visitReturn(stmt: SyntaxNode): TraversalResult {
|
||||
// `return switch (v) { … };` (#2207): the returned value is a value-position
|
||||
// branch — model it as control flow, with each arm returning (its value IS
|
||||
// the function result), threading every active finalizer per arm.
|
||||
const branch = stmt.namedChildren.find((c) => !isComment(c));
|
||||
if (branch && this.isModelableValueBranch(branch)) {
|
||||
const res = this.visitBranchExpr(branch);
|
||||
const finalizers = this.cfc.finalizersForReturn();
|
||||
for (const ex of res.exits) {
|
||||
wireJumpThroughFinalizers(this.builder, ex, finalizers, this.builder.exitIndex, 'return');
|
||||
}
|
||||
return { entry: res.entry, exits: [] };
|
||||
}
|
||||
const idx = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(stmt),
|
||||
|
|
@ -579,7 +607,12 @@ class DartCfgWalk {
|
|||
*/
|
||||
private visitSwitch(stmt: SyntaxNode): TraversalResult {
|
||||
const labels = this.takeLabels();
|
||||
const value = stmt.childForFieldName('condition');
|
||||
// The `condition` field is a `parenthesized_expression` (verified) — unwrap it
|
||||
// so the dispatch text/discriminant matches the value-position `visitSwitchExpr`
|
||||
// form (`switch x`, not `switch (x)`). The harvest walks into the paren either
|
||||
// way, so the def/use facts are unchanged — only the block text normalizes.
|
||||
const condRaw = stmt.childForFieldName('condition');
|
||||
const value = condRaw ? this.unwrapParen(condRaw) : undefined;
|
||||
const dispatch = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
value ? endLineOf(value) : startLineOf(stmt),
|
||||
|
|
@ -705,6 +738,143 @@ class DartCfgWalk {
|
|||
return id?.text || undefined;
|
||||
}
|
||||
|
||||
// ── value-position switch expression (#2207) ────────────────────────────────
|
||||
|
||||
/**
|
||||
* The direct value of a `local_variable_declaration` with a SINGLE
|
||||
* `initialized_variable_definition` (`var x = <value>`): its `value` field.
|
||||
* Returns undefined for a multi-binding decl (`var a = …, b = …`) — modeling
|
||||
* those arm-by-arm is out of scope, so they coalesce inline.
|
||||
*/
|
||||
private directValue(stmt: SyntaxNode): SyntaxNode | undefined {
|
||||
const defs = stmt.namedChildren.filter((c) => c.type === 'initialized_variable_definition');
|
||||
if (defs.length !== 1) return undefined;
|
||||
return defs[0].childForFieldName('value') ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `node` is a value-position branch worth modeling as control flow
|
||||
* (#2207): a `switch_expression` (Dart 3) with ≥2 arms — a real dispatch. Dart's
|
||||
* value-position `if` does not exist; the ternary `?:` is excluded by design.
|
||||
*/
|
||||
private isModelableValueBranch(node: SyntaxNode): boolean {
|
||||
if (node.type !== 'switch_expression') return false;
|
||||
return node.namedChildren.filter((c) => c.type === 'switch_expression_case').length >= 2;
|
||||
}
|
||||
|
||||
/** Model a value-position branch as control flow (only `switch_expression`). */
|
||||
private visitBranchExpr(node: SyntaxNode): TraversalResult {
|
||||
return this.visitSwitchExpr(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Model a value-position `switch (v) { p [when g] => e, _ => e }` (Dart 3) as a
|
||||
* CFG dispatch: a discriminant block, each arm's value a block reached by a
|
||||
* `switch-case` edge, all arms rejoining at one exit (no fallthrough). The arm
|
||||
* PATTERN and any `when` GUARD are harvested as conditional uses on the dispatch
|
||||
* (they evaluate before the body, only when earlier arms missed); a Dart call
|
||||
* value parses as `identifier` + `selector` (multiple children), so the arm-value
|
||||
* facts come from each post-`=>` child. Only an UNGUARDED `_` arm is the
|
||||
* exhaustive catch-all — a guarded `_ when …` is NOT (the no-match path still
|
||||
* needs the conservative edge), mirroring the C# `visitSwitchExpr`.
|
||||
*/
|
||||
private visitSwitchExpr(node: SyntaxNode): TraversalResult {
|
||||
const condRaw = node.childForFieldName('condition');
|
||||
const cond = condRaw ? this.unwrapParen(condRaw) : node;
|
||||
const dispatch = this.builder.newBlock(
|
||||
startLineOf(node),
|
||||
endLineOf(cond),
|
||||
`switch ${cond.text}`,
|
||||
'normal',
|
||||
this.harvest.facts(cond),
|
||||
);
|
||||
const switchExit = this.builder.newBlock(endLineOf(node), endLineOf(node), '');
|
||||
|
||||
const arms = node.namedChildren.filter((c) => c.type === 'switch_expression_case');
|
||||
let hasCatchAll = false;
|
||||
for (const arm of arms) {
|
||||
const { pattern, guards, values } = this.armParts(arm);
|
||||
// The pattern + `when` guard are conditional dispatch tests, NOT arm-value
|
||||
// uses — harvest them onto the dispatch (mirrors casePatterns for switch_statement).
|
||||
if (pattern) this.builder.attachFacts(dispatch, this.harvest.factsConditional(pattern));
|
||||
for (const g of guards) this.builder.attachFacts(dispatch, this.harvest.factsConditional(g));
|
||||
if (pattern && pattern.text === '_' && guards.length === 0) hasCatchAll = true;
|
||||
const first = values[0] ?? arm;
|
||||
const last = values[values.length - 1] ?? arm;
|
||||
const armBlock = this.builder.newBlock(
|
||||
startLineOf(first),
|
||||
endLineOf(last),
|
||||
values.map((c) => c.text).join('') || arm.text,
|
||||
'normal',
|
||||
undefined,
|
||||
);
|
||||
for (const v of values) this.builder.attachFacts(armBlock, this.harvest.facts(v));
|
||||
this.builder.edge(dispatch, armBlock, 'switch-case');
|
||||
this.builder.edge(armBlock, switchExit, 'seq');
|
||||
}
|
||||
// A non-exhaustive Dart switch expression throws at runtime; conservatively
|
||||
// keep EXIT reachable via a no-match edge when no `_` catch-all arm exists.
|
||||
if (!hasCatchAll) this.builder.edge(dispatch, switchExit, 'switch-case');
|
||||
|
||||
return { entry: dispatch, exits: [switchExit] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a `switch_expression_case` at the `=>` token: the PATTERN (first named
|
||||
* child before `=>`), any `when` GUARD (named children between the pattern and
|
||||
* `=>` — tree-sitter-dart parses the guard as a bare sibling, not a wrapper),
|
||||
* and the VALUE expression (named children after `=>` — a Dart call is split
|
||||
* across `identifier` + `selector`, hence an array).
|
||||
*/
|
||||
private armParts(arm: SyntaxNode): {
|
||||
pattern: SyntaxNode | undefined;
|
||||
guards: SyntaxNode[];
|
||||
values: SyntaxNode[];
|
||||
} {
|
||||
const before: SyntaxNode[] = [];
|
||||
const values: SyntaxNode[] = [];
|
||||
let seenArrow = false;
|
||||
for (let i = 0; i < arm.childCount; i++) {
|
||||
const c = arm.child(i);
|
||||
if (!c) continue;
|
||||
if (!c.isNamed) {
|
||||
if (c.text === '=>') seenArrow = true;
|
||||
continue;
|
||||
}
|
||||
if (isComment(c)) continue;
|
||||
(seenArrow ? values : before).push(c);
|
||||
}
|
||||
return { pattern: before[0], guards: before.slice(1), values };
|
||||
}
|
||||
|
||||
/** Strip a `parenthesized_expression` wrapper (a switch/if condition). */
|
||||
private unwrapParen(node: SyntaxNode): SyntaxNode {
|
||||
if (node.type === 'parenthesized_expression') {
|
||||
const inner = node.namedChildren.find((c) => !isComment(c));
|
||||
if (inner) return inner;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* `var x = switch (v) { … }` (#2207): visit the switch as control flow, then
|
||||
* rejoin its arms at a facts-only continuation carrying ONLY the declared name's
|
||||
* def (the subject + arm-value uses are already on the switch's blocks). The
|
||||
* arms are now control-dependent on the dispatch — mirrors Java / Kotlin / Rust.
|
||||
*/
|
||||
private visitBindBranch(stmt: SyntaxNode, branch: SyntaxNode): TraversalResult {
|
||||
const res = this.visitBranchExpr(branch);
|
||||
const cont = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
startLineOf(stmt),
|
||||
'',
|
||||
'normal',
|
||||
this.harvest.bindingDefFacts(stmt),
|
||||
);
|
||||
this.builder.connect(res.exits, cont, 'seq');
|
||||
return { entry: res.entry, exits: [cont] };
|
||||
}
|
||||
|
||||
// ── try / on / catch / finally ─────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -196,6 +196,24 @@ export class JavaHarvester extends ScopeTreeHarvester {
|
|||
return acc.finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Def-ONLY facts for a value-position binding carrier (`var x = switch (…) {…}`,
|
||||
* #2207): just the declared name(s)' def, attached to the continuation block the
|
||||
* switch arms rejoin. The switch subject + arm-value USES are already harvested
|
||||
* onto the branch's own blocks ({@link facts} on each arm), so this must NOT
|
||||
* re-walk the value — only each `variable_declarator`'s `name` is a def here.
|
||||
*/
|
||||
bindingDefFacts(stmt: SyntaxNode): StatementFacts | undefined {
|
||||
const acc = new FactAccumulator(stmt.startPosition.row + 1);
|
||||
for (let i = 0; i < stmt.namedChildCount; i++) {
|
||||
const d = stmt.namedChild(i);
|
||||
if (d?.type !== 'variable_declarator') continue;
|
||||
const name = d.childForFieldName('name');
|
||||
if (name) this.def(name, acc);
|
||||
}
|
||||
return acc.defCount() ? acc.finish() : undefined;
|
||||
}
|
||||
|
||||
/** 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);
|
||||
|
|
|
|||
|
|
@ -74,11 +74,12 @@
|
|||
* 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.
|
||||
* - A value-position `switch` with ≥2 arms is modeled as control flow in the two
|
||||
* highest-value carriers (#2207): a single-declarator `var x = switch (…) {…}`
|
||||
* (arms rejoin at a binding continuation) and `return switch (…) {…}` (each arm
|
||||
* returns). A value-position `switch` in any OTHER position — an assignment RHS
|
||||
* (`x = switch …`), a call argument, or a multi-declarator decl — is still left
|
||||
* INLINE inside its owning block (the value flows to one coalesced block).
|
||||
* - `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.
|
||||
|
|
@ -197,12 +198,7 @@ class JavaCfgWalk {
|
|||
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) {
|
||||
if (this.breaksBlock(stmt)) {
|
||||
openSimple = undefined; // close any open straight-line block
|
||||
const res = this.visitStmt(stmt);
|
||||
if (res === null) continue; // transparent (empty nested block)
|
||||
|
|
@ -238,9 +234,35 @@ class JavaCfgWalk {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a statement breaks the current straight-line block. A
|
||||
* `switch_expression` breaks only when it is a STATEMENT switch (a value-
|
||||
* position switch used directly inside a `block` coalesces). A
|
||||
* `local_variable_declaration` whose value is a modelable value-position switch
|
||||
* (`var x = switch (…) {…}`, #2207) also breaks — `visitStmt` then models the
|
||||
* arms as control flow instead of collapsing the decl to one inline block.
|
||||
*/
|
||||
private breaksBlock(stmt: SyntaxNode): boolean {
|
||||
if (stmt.type === 'local_variable_declaration') {
|
||||
const v = this.directValue(stmt);
|
||||
return v !== undefined && this.isModelableValueBranch(v);
|
||||
}
|
||||
if (!CONTROL_FLOW_TYPES.has(stmt.type)) return false;
|
||||
if (stmt.type === 'switch_expression') return this.isStatementSwitch(stmt);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Dispatch one statement to its handler. Non-null except for empty blocks. */
|
||||
visitStmt(stmt: SyntaxNode): SeqResult {
|
||||
switch (stmt.type) {
|
||||
case 'local_variable_declaration': {
|
||||
// `var x = switch (k) { … }` (#2207): the value is a value-position
|
||||
// branch — model it as control flow and bind the result on the rejoin,
|
||||
// instead of collapsing the whole decl to one block.
|
||||
const value = this.directValue(stmt);
|
||||
if (value && this.isModelableValueBranch(value)) return this.visitBindBranch(stmt, value);
|
||||
return this.visitSimple(stmt);
|
||||
}
|
||||
case 'if_statement':
|
||||
return this.visitIf(stmt);
|
||||
case 'while_statement':
|
||||
|
|
@ -289,6 +311,18 @@ class JavaCfgWalk {
|
|||
}
|
||||
|
||||
private visitReturn(stmt: SyntaxNode): TraversalResult {
|
||||
// `return switch (k) { … };` (#2207): the returned value is a value-position
|
||||
// branch — model it as control flow, with each arm returning (its value IS
|
||||
// the function result), threading every active finalizer per arm.
|
||||
const branch = stmt.namedChildren.find((c) => !isComment(c));
|
||||
if (branch && this.isModelableValueBranch(branch)) {
|
||||
const res = this.visitBranchExpr(branch);
|
||||
const finalizers = this.cfc.finalizersForReturn();
|
||||
for (const ex of res.exits) {
|
||||
wireJumpThroughFinalizers(this.builder, ex, finalizers, this.builder.exitIndex, 'return');
|
||||
}
|
||||
return { entry: res.entry, exits: [] };
|
||||
}
|
||||
const idx = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(stmt),
|
||||
|
|
@ -321,10 +355,13 @@ class JavaCfgWalk {
|
|||
}
|
||||
|
||||
/**
|
||||
* `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.
|
||||
* `yield e;` (switch-expression arm value) — produces the switch-expression's
|
||||
* value and EXITS the enclosing switch (it does NOT fall through to the next
|
||||
* colon group). Modeled as a terminator that jumps to the switch exit, threading
|
||||
* any finalizer it crosses — exactly like a `break` out of the switch but
|
||||
* carrying the yielded value's def/use facts. (Reusing the statement `visitSwitch`
|
||||
* for a value-position colon switch would otherwise wire a spurious `fallthrough`
|
||||
* edge between yield-terminated arms — #2211 review.)
|
||||
*/
|
||||
private visitYield(stmt: SyntaxNode): TraversalResult {
|
||||
const idx = this.builder.newBlock(
|
||||
|
|
@ -334,7 +371,13 @@ class JavaCfgWalk {
|
|||
'normal',
|
||||
this.harvest.facts(stmt),
|
||||
);
|
||||
return { entry: idx, exits: [idx] };
|
||||
const res = this.cfc.resolveYield();
|
||||
const { target, finalizers } = res ?? {
|
||||
target: this.builder.exitIndex,
|
||||
finalizers: this.cfc.finalizersForReturn(),
|
||||
};
|
||||
wireJumpThroughFinalizers(this.builder, idx, finalizers, target, 'break');
|
||||
return { entry: idx, exits: [] };
|
||||
}
|
||||
|
||||
private visitBreak(stmt: SyntaxNode): TraversalResult {
|
||||
|
|
@ -717,6 +760,67 @@ class JavaCfgWalk {
|
|||
return label.namedChildren.filter((c) => !isComment(c)).length === 0;
|
||||
}
|
||||
|
||||
// ── value-position branches (#2207) ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* The direct value of a `local_variable_declaration` with a SINGLE declarator:
|
||||
* its `variable_declarator`'s `value` field (`var x = <value>`). Returns
|
||||
* undefined for a multi-declarator decl (`int a = …, b = …;`) — modeling those
|
||||
* arm-by-arm is out of scope, so they coalesce inline. The DIRECT value only:
|
||||
* `var x = f(switch …)` yields the call, not the nested switch, so an
|
||||
* argument-position switch stays inline.
|
||||
*/
|
||||
private directValue(stmt: SyntaxNode): SyntaxNode | undefined {
|
||||
const declarators = stmt.namedChildren.filter((c) => c.type === 'variable_declarator');
|
||||
if (declarators.length !== 1) return undefined;
|
||||
return declarators[0].childForFieldName('value') ?? undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `node` is a value-position branch worth modeling as control flow
|
||||
* (#2207): a `switch_expression` with ≥2 case groups (a real dispatch). Java has
|
||||
* no value-position `if` (the ternary `?:` is deliberately excluded, like elvis
|
||||
* in Kotlin), so `switch` is the only carrier.
|
||||
*/
|
||||
private isModelableValueBranch(node: SyntaxNode): boolean {
|
||||
if (node.type !== 'switch_expression') return false;
|
||||
const body = node.childForFieldName('body');
|
||||
if (!body) return false;
|
||||
const groups = body.namedChildren.filter(
|
||||
(c) => c.type === 'switch_block_statement_group' || c.type === 'switch_rule',
|
||||
);
|
||||
return groups.length >= 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model a value-position `switch` as control flow regardless of position —
|
||||
* {@link visitSeq}'s `isStatementSwitch` gate keeps value-position switches
|
||||
* inline, so call {@link visitSwitch} directly here.
|
||||
*/
|
||||
private visitBranchExpr(node: SyntaxNode): TraversalResult {
|
||||
return this.visitSwitch(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* `var x = switch (k) { … }` (#2207): visit the switch as control flow, then
|
||||
* rejoin its arms at a facts-only continuation carrying ONLY the bound name's
|
||||
* def (the subject + arm-value uses are already harvested onto the switch's
|
||||
* blocks). The arms are now control-dependent on the dispatch, and `x` is
|
||||
* defined at the join — mirrors the Kotlin / Rust value-position binding.
|
||||
*/
|
||||
private visitBindBranch(stmt: SyntaxNode, branch: SyntaxNode): TraversalResult {
|
||||
const res = this.visitBranchExpr(branch);
|
||||
const cont = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
startLineOf(stmt),
|
||||
'',
|
||||
'normal',
|
||||
this.harvest.bindingDefFacts(stmt),
|
||||
);
|
||||
this.builder.connect(res.exits, cont, 'seq');
|
||||
return { entry: res.entry, exits: [cont] };
|
||||
}
|
||||
|
||||
/**
|
||||
* try / catch / finally / try-with-resources. The `resources` of a
|
||||
* try-with-resources auto-close on BOTH normal and exception exit — exactly
|
||||
|
|
|
|||
|
|
@ -292,6 +292,25 @@ export class KotlinHarvester {
|
|||
return acc.defCount() ? acc.finish() : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Def-ONLY facts for a value-position assignment carrier (`x = when (k) {…}`,
|
||||
* #2205): just the LHS target, attached to the continuation block the branch
|
||||
* arms rejoin. The branch subject + arm-value USES are already harvested onto
|
||||
* the branch's own blocks, so this must NOT re-walk the RHS — only a plain `=`
|
||||
* to a simple-identifier lvalue defines (a member / index target is not a
|
||||
* scalar def; a compound `+=` is not a value-branch carrier).
|
||||
*/
|
||||
assignmentDefFacts(node: SyntaxNode): StatementFacts | undefined {
|
||||
if (this.assignmentOperator(node) !== '=') return undefined;
|
||||
const acc = new FactAccumulator(node.startPosition.row + 1);
|
||||
const lvalue = node.namedChildren.find((c) => c.type === 'directly_assignable_expression');
|
||||
if (lvalue) {
|
||||
const lv = this.unwrapAssignable(lvalue);
|
||||
if (lv.type === 'simple_identifier') this.def(lv, acc);
|
||||
}
|
||||
return acc.defCount() ? acc.finish() : undefined;
|
||||
}
|
||||
|
||||
/** ENTRY-block facts for the parameters (defs only). */
|
||||
paramFacts(): StatementFacts | undefined {
|
||||
const acc = new FactAccumulator(this.fnNode.startPosition.row + 1);
|
||||
|
|
|
|||
|
|
@ -80,12 +80,14 @@
|
|||
* Java/C#/TS over-approximation.
|
||||
*
|
||||
* Kotlin-specific modeling decisions (documented approximations):
|
||||
* - `if` / `when` / `try` used as an EXPRESSION VALUE (assigned, returned inline,
|
||||
* passed as an argument) is left INLINE inside its owning statement's block —
|
||||
* its arms are not modeled as separate CFG blocks (the value flows to the
|
||||
* consumer). Only a STATEMENT-position construct (a direct `statements` child)
|
||||
* becomes a dispatch/branch construct. This mirrors the Java inline-value-switch
|
||||
* gap — documented, not faked.
|
||||
* - a value-position `if` (with `else`) / `when` (≥2 arms) / `try` IS modeled as
|
||||
* control flow (#2205) in four carriers: a `val/var x = <branch>` binding, an
|
||||
* `x = <branch>` assignment, a `return <branch>`, and a `fun f() = <branch>`
|
||||
* expression body — its arms become separate CFG blocks that rejoin at a
|
||||
* binding/return continuation. A branch in any OTHER value position — nested in
|
||||
* a call argument (`f(when …)`), a deeper subexpression — is left INLINE (the
|
||||
* value flows to the consumer in one block). The ternary-like `?:` (elvis) and
|
||||
* `?.` micro-branches are excluded by design.
|
||||
* - a `lambda_literal` / nested `anonymous_function` / nested
|
||||
* `function_declaration` is collected as its OWN function by `isFunction`, so
|
||||
* its body gets a standalone CFG; in the ENCLOSING function it is an opaque
|
||||
|
|
@ -246,9 +248,10 @@ class KotlinCfgWalk {
|
|||
* Whether a statement breaks the current straight-line block. `if` / `when` /
|
||||
* `try` are EXPRESSIONS in Kotlin — they break a block when used as a STATEMENT
|
||||
* (a direct child of a `statements` list), OR when they are the value of a
|
||||
* `val/var x = <branch>` binding (#2205) — `visitStmt`'s `property_declaration`
|
||||
* case then models the arms as control flow. Other value positions (an
|
||||
* assignment RHS, a call argument) still coalesce — a remaining gap.
|
||||
* `val/var x = <branch>` binding or an `x = <branch>` assignment (#2205) —
|
||||
* `visitStmt`'s `property_declaration` / `assignment` case then models the arms
|
||||
* as control flow. A call argument value position still coalesces (a remaining
|
||||
* gap — the branch is nested in a call, harder to bind).
|
||||
*/
|
||||
private isControlFlow(stmt: SyntaxNode): boolean {
|
||||
if (stmt.type === 'label') return true; // queue label, emit no block
|
||||
|
|
@ -256,6 +259,7 @@ class KotlinCfgWalk {
|
|||
const v = this.directValue(stmt);
|
||||
return v !== undefined && this.isModelableValueBranch(v);
|
||||
}
|
||||
if (stmt.type === 'assignment') return this.assignmentBranch(stmt) !== undefined;
|
||||
if (!CONTROL_FLOW_TYPES.has(stmt.type)) return false;
|
||||
if (this.isExpressionConstruct(stmt.type)) return this.isStatementPosition(stmt);
|
||||
return true;
|
||||
|
|
@ -309,6 +313,13 @@ class KotlinCfgWalk {
|
|||
if (value && this.isModelableValueBranch(value)) return this.visitBindBranch(stmt, value);
|
||||
return this.visitSimple(stmt);
|
||||
}
|
||||
case 'assignment': {
|
||||
// `x = when (k) { … }` / `x = if (c) a else b` / `x = try { … }` (#2205):
|
||||
// model the RHS branch as control flow and bind the target on the rejoin.
|
||||
const branch = this.assignmentBranch(stmt);
|
||||
if (branch) return this.visitBindAssign(stmt, branch);
|
||||
return this.visitSimple(stmt);
|
||||
}
|
||||
default:
|
||||
return this.visitSimple(stmt);
|
||||
}
|
||||
|
|
@ -345,11 +356,13 @@ class KotlinCfgWalk {
|
|||
|
||||
/** `return [expr]` / `return@label` — threads through every active finalizer. */
|
||||
private visitReturn(stmt: SyntaxNode): TraversalResult {
|
||||
// `return when (k) { … }` / `return if (c) a else b` (#2205): the returned
|
||||
// value is a value-position branch — model it as control flow, with each arm
|
||||
// returning (its value IS the function result), threading finalizers per arm.
|
||||
// `return when (k) { … }` / `return if (c) a else b` / `return try { … }`
|
||||
// (#2205): the returned value is a value-position branch — model it as control
|
||||
// flow, with each arm returning (its value IS the function result), threading
|
||||
// finalizers per arm.
|
||||
const branch = stmt.namedChildren.find(
|
||||
(c) => c.type === 'when_expression' || c.type === 'if_expression',
|
||||
(c) =>
|
||||
c.type === 'when_expression' || c.type === 'if_expression' || c.type === 'try_expression',
|
||||
);
|
||||
if (branch && this.isModelableValueBranch(branch)) {
|
||||
const res = this.visitBranchExpr(branch);
|
||||
|
|
@ -470,16 +483,25 @@ class KotlinCfgWalk {
|
|||
return node.namedChildren.filter((c) => c.type === 'when_entry').length >= 2;
|
||||
}
|
||||
if (node.type === 'if_expression') return this.elseNodeOf(node) !== undefined;
|
||||
// `val x = try { … } catch { … }` / `try { … } finally { … }` (#2205): a
|
||||
// value-position `try` with a `catch` OR a `finally` is a real branch — its
|
||||
// value is the body's value, a catch's value, or the body's value threaded
|
||||
// through a finalizer — so model it as control flow.
|
||||
if (node.type === 'try_expression') {
|
||||
return node.namedChildren.some((c) => c.type === 'catch_block' || c.type === 'finally_block');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model a value-position `when`/`if` as control flow regardless of its
|
||||
* Model a value-position `when`/`if`/`try` as control flow regardless of its
|
||||
* statement/value position — {@link visitStmt}'s `isStatementPosition` gate keeps
|
||||
* value-position branches inline, so call the branch handlers directly here.
|
||||
*/
|
||||
private visitBranchExpr(node: SyntaxNode): TraversalResult {
|
||||
return node.type === 'when_expression' ? this.visitWhen(node) : this.visitIf(node);
|
||||
if (node.type === 'when_expression') return this.visitWhen(node);
|
||||
if (node.type === 'try_expression') return this.visitTry(node) ?? this.visitSimple(node);
|
||||
return this.visitIf(node);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -502,6 +524,40 @@ class KotlinCfgWalk {
|
|||
return { entry: res.entry, exits: [cont] };
|
||||
}
|
||||
|
||||
/**
|
||||
* The value-position branch on a plain `=` assignment RHS (`x = when (k) {…}` /
|
||||
* `x = if (c) a else b` / `x = try {…}`, #2205), or undefined. Only a plain `=`
|
||||
* (not a compound `+=`) with a modelable-branch RHS qualifies.
|
||||
*/
|
||||
private assignmentBranch(stmt: SyntaxNode): SyntaxNode | undefined {
|
||||
if (stmt.type !== 'assignment') return undefined;
|
||||
const eq = stmt.children.find((c) => !c.isNamed && c.text === '=');
|
||||
if (!eq) return undefined; // compound assignment (`+=` etc.) is not a carrier
|
||||
const rhs = stmt.namedChildren.find(
|
||||
(c) => c.type !== 'directly_assignable_expression' && !isComment(c),
|
||||
);
|
||||
return rhs && this.isModelableValueBranch(rhs) ? rhs : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* `x = <branch>` (#2205): visit the RHS branch as control flow, then rejoin its
|
||||
* arms at a facts-only continuation carrying ONLY the LHS target def (the branch
|
||||
* subject + arm-value uses are already on the branch's blocks). The arms are now
|
||||
* control-dependent on the branch — mirrors the Ruby value-branch assignment.
|
||||
*/
|
||||
private visitBindAssign(stmt: SyntaxNode, branch: SyntaxNode): TraversalResult {
|
||||
const res = this.visitBranchExpr(branch);
|
||||
const cont = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
startLineOf(stmt),
|
||||
'',
|
||||
'normal',
|
||||
this.harvest.assignmentDefFacts(stmt),
|
||||
);
|
||||
this.builder.connect(res.exits, cont, 'seq');
|
||||
return { entry: res.entry, exits: [cont] };
|
||||
}
|
||||
|
||||
/**
|
||||
* A `fun f() = EXPR` expression body (#2205). A value-position branch is modeled
|
||||
* as control flow (each arm yields the returned function result); any other
|
||||
|
|
|
|||
|
|
@ -303,6 +303,24 @@ export class PhpHarvester {
|
|||
return acc.finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Def-ONLY facts for a value-position assignment carrier (`$x = match($v) {…}`,
|
||||
* #2207): just the LHS target(s), attached to the continuation block the match
|
||||
* arms rejoin. The match condition + arm-value USES are already harvested onto
|
||||
* the branch's own blocks (visitMatch), so this must NOT re-walk the RHS. A
|
||||
* member/subscript target (`$this->x = match …`) has no scalar def → undefined.
|
||||
*/
|
||||
assignmentDefFacts(assignExpr: SyntaxNode): StatementFacts | undefined {
|
||||
const acc = new FactAccumulator(assignExpr.startPosition.row + 1);
|
||||
const left = assignExpr.childForFieldName('left');
|
||||
if (left) {
|
||||
const lv = this.unwrapParen(left);
|
||||
if (lv.type === 'variable_name') this.def(lv, acc);
|
||||
else if (lv.type === 'list_literal') for (const v of this.listTargets(lv)) this.def(v, acc);
|
||||
}
|
||||
return acc.defCount() ? acc.finish() : undefined;
|
||||
}
|
||||
|
||||
/** Facts for a `foreach ($it as [$k =>] $v)` head: targets bind, iterable used. */
|
||||
foreachHeadFacts(stmt: SyntaxNode): StatementFacts {
|
||||
const acc = new FactAccumulator(stmt.startPosition.row + 1);
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@
|
|||
* - loops (for / foreach / while / do-while) → `cond-true` / `loop-back` /
|
||||
* `cond-false`
|
||||
* - switch → `switch-case` / `fallthrough` (a `case` with no `break`/`return`
|
||||
* falls through to the next case); `match` is left INLINE as a value
|
||||
* (no fallthrough — see the limitations).
|
||||
* falls through to the next case); a value-position `match` with ≥2 arms also
|
||||
* dispatches as `switch-case` (no fallthrough), see the limitations.
|
||||
* - try/catch → `throw` (every protected-region block → the handler); a
|
||||
* `finally` runs on normal AND exception exit, so a `return`/`break`/`continue`
|
||||
* crossing it gets a `finally-*` completion edge.
|
||||
|
|
@ -68,10 +68,12 @@
|
|||
* region edges to the handler (an exception may fire mid-block).
|
||||
*
|
||||
* Known limitations:
|
||||
* - `match` is a value-position EXPRESSION (`$r = match($x) { … }`), kept INLINE
|
||||
* inside its owning statement's block — its arms are not modeled as separate
|
||||
* CFG blocks (the value flows to the assignment). Documented gap, mirroring the
|
||||
* Java inline-value-switch handling.
|
||||
* - A value-position `match($x) { … }` with ≥2 arms IS modeled as a `switch-case`
|
||||
* dispatch in two carriers (#2207): an `$x = match(…) {…}` assignment (arms
|
||||
* rejoin at a binding continuation) and `return match(…) {…}` (each arm
|
||||
* returns). A `match` in any OTHER position — a call argument, a nested
|
||||
* subexpression — stays INLINE inside its owning block. The ternary `?:` is
|
||||
* excluded by design (a micro-branch, like elvis in Kotlin).
|
||||
* - context-manager-style suppression and PHP's exception-from-mid-call outside
|
||||
* any `try` are not modeled (no edge), matching the other visitors.
|
||||
* - `goto` / named labels are modeled as straight-line blocks (the label is a
|
||||
|
|
@ -192,7 +194,11 @@ class PhpCfgWalk {
|
|||
for (const stmt of stmts) {
|
||||
// An `expression_statement` wrapping a bare `throw_expression` is a
|
||||
// terminator (PHP has no `throw_statement` node), so it breaks the block.
|
||||
const breaks = CONTROL_FLOW_TYPES.has(stmt.type) || this.isThrowStatement(stmt);
|
||||
// An `$x = match($v) {…}` value-position assignment breaks too (#2207).
|
||||
const breaks =
|
||||
CONTROL_FLOW_TYPES.has(stmt.type) ||
|
||||
this.isThrowStatement(stmt) ||
|
||||
this.isValueBranchAssignment(stmt);
|
||||
if (breaks) {
|
||||
openSimple = undefined; // close any open straight-line block
|
||||
const res = this.visitStmt(stmt);
|
||||
|
|
@ -232,6 +238,10 @@ class PhpCfgWalk {
|
|||
/** Dispatch one statement to its handler. Non-null except for empty blocks. */
|
||||
visitStmt(stmt: SyntaxNode): SeqResult {
|
||||
if (this.isThrowStatement(stmt)) return this.visitThrow(stmt);
|
||||
// `$x = match($v) { … };` (#2207): model the match arms as control flow and
|
||||
// bind the assignment target on the rejoin.
|
||||
const assign = this.assignmentBranch(stmt);
|
||||
if (assign) return this.visitBindAssign(stmt, assign.expr, assign.match);
|
||||
switch (stmt.type) {
|
||||
case 'if_statement':
|
||||
return this.visitIf(stmt);
|
||||
|
|
@ -285,6 +295,18 @@ class PhpCfgWalk {
|
|||
}
|
||||
|
||||
private visitReturn(stmt: SyntaxNode): TraversalResult {
|
||||
// `return match($v) { … };` (#2207): the returned value is a value-position
|
||||
// branch — model it as control flow, with each arm returning (its value IS
|
||||
// the function result), threading every active finally per arm.
|
||||
const branch = stmt.namedChildren.find((c) => !isComment(c));
|
||||
if (branch && this.isModelableValueBranch(branch)) {
|
||||
const res = this.visitBranchExpr(branch);
|
||||
const finalizers = this.cfc.finalizersForReturn();
|
||||
for (const ex of res.exits) {
|
||||
wireJumpThroughFinalizers(this.builder, ex, finalizers, this.builder.exitIndex, 'return');
|
||||
}
|
||||
return { entry: res.entry, exits: [] };
|
||||
}
|
||||
const idx = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(stmt),
|
||||
|
|
@ -670,6 +692,126 @@ class PhpCfgWalk {
|
|||
return group.childForFieldName('value') ?? undefined;
|
||||
}
|
||||
|
||||
// ── value-position match expression (#2207) ─────────────────────────────────
|
||||
|
||||
/**
|
||||
* The `{expr, match}` of an `$x = match($v) {…}` value-position assignment
|
||||
* carrier, or undefined. `expr` is the `assignment_expression` (for the target
|
||||
* def); `match` is the modelable `match_expression` RHS. Only a plain `=`
|
||||
* assignment qualifies (an augmented `??=` etc. is not a value-branch bind).
|
||||
*/
|
||||
private assignmentBranch(stmt: SyntaxNode): { expr: SyntaxNode; match: SyntaxNode } | undefined {
|
||||
if (stmt.type !== 'expression_statement') return undefined;
|
||||
const expr = stmt.namedChildren.find((c) => !isComment(c));
|
||||
if (!expr || expr.type !== 'assignment_expression') return undefined;
|
||||
const right = expr.childForFieldName('right');
|
||||
return right && this.isModelableValueBranch(right) ? { expr, match: right } : undefined;
|
||||
}
|
||||
|
||||
/** Whether a statement is an `$x = match(…) {…}` value-branch assignment. */
|
||||
private isValueBranchAssignment(stmt: SyntaxNode): boolean {
|
||||
return this.assignmentBranch(stmt) !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `node` is a value-position branch worth modeling as control flow
|
||||
* (#2207): a `match_expression` with ≥2 arms — a real dispatch. PHP `match` is
|
||||
* the only value-position branch (there is no `if`-expression); the ternary
|
||||
* `?:` is deliberately excluded, like elvis in Kotlin.
|
||||
*/
|
||||
private isModelableValueBranch(node: SyntaxNode): boolean {
|
||||
if (node.type !== 'match_expression') return false;
|
||||
const block = node.childForFieldName('body');
|
||||
if (!block) return false;
|
||||
return (
|
||||
block.namedChildren.filter(
|
||||
(c) => c.type === 'match_conditional_expression' || c.type === 'match_default_expression',
|
||||
).length >= 2
|
||||
);
|
||||
}
|
||||
|
||||
/** Model a value-position branch as control flow (only `match_expression`). */
|
||||
private visitBranchExpr(node: SyntaxNode): TraversalResult {
|
||||
return this.visitMatch(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Model a value-position `match($v) { c => v, default => v }` as a CFG dispatch:
|
||||
* a discriminant block, each arm's value expression a block reached by a
|
||||
* `switch-case` edge, all arms rejoining at one exit (no fallthrough — `match`
|
||||
* never falls through). The arm condition lists are harvested as conditional
|
||||
* uses on the dispatch (a later arm test runs only when earlier arms missed).
|
||||
*/
|
||||
private visitMatch(node: SyntaxNode): TraversalResult {
|
||||
const condRaw = node.childForFieldName('condition');
|
||||
const cond = condRaw ? this.unwrapParen(condRaw) : node;
|
||||
const dispatch = this.builder.newBlock(
|
||||
startLineOf(node),
|
||||
endLineOf(cond),
|
||||
cond.text,
|
||||
'normal',
|
||||
this.harvest.facts(cond),
|
||||
);
|
||||
const matchExit = this.builder.newBlock(endLineOf(node), endLineOf(node), '');
|
||||
|
||||
const block = node.childForFieldName('body');
|
||||
const arms = block
|
||||
? block.namedChildren.filter(
|
||||
(c) => c.type === 'match_conditional_expression' || c.type === 'match_default_expression',
|
||||
)
|
||||
: [];
|
||||
let hasDefault = false;
|
||||
for (const arm of arms) {
|
||||
const condList = arm.namedChildren.find((c) => c.type === 'match_condition_list');
|
||||
if (condList) this.builder.attachFacts(dispatch, this.harvest.factsConditional(condList));
|
||||
if (arm.type === 'match_default_expression') hasDefault = true;
|
||||
const value = this.matchArmValue(arm);
|
||||
const armBlock = this.builder.newBlock(
|
||||
startLineOf(value ?? arm),
|
||||
endLineOf(value ?? arm),
|
||||
(value ?? arm).text,
|
||||
'normal',
|
||||
value ? this.harvest.facts(value) : undefined,
|
||||
);
|
||||
this.builder.edge(dispatch, armBlock, 'switch-case');
|
||||
this.builder.edge(armBlock, matchExit, 'seq');
|
||||
}
|
||||
// `match` with no `default` throws `\UnhandledMatchError` on no match; keep
|
||||
// EXIT reachable via a conservative no-match edge when no default arm exists.
|
||||
if (!hasDefault) this.builder.edge(dispatch, matchExit, 'switch-case');
|
||||
|
||||
return { entry: dispatch, exits: [matchExit] };
|
||||
}
|
||||
|
||||
/** The value (result) expression of a match arm — its LAST named child. */
|
||||
private matchArmValue(arm: SyntaxNode): SyntaxNode | undefined {
|
||||
const named = arm.namedChildren.filter((c) => !isComment(c));
|
||||
return named[named.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* `$x = match($v) { … }` (#2207): visit the match as control flow, then rejoin
|
||||
* its arms at a facts-only continuation carrying ONLY the LHS target def (the
|
||||
* condition + arm-value uses are already on the match's blocks). The arms are
|
||||
* now control-dependent on the dispatch — mirrors the Ruby value-branch assign.
|
||||
*/
|
||||
private visitBindAssign(
|
||||
stmt: SyntaxNode,
|
||||
assignExpr: SyntaxNode,
|
||||
branch: SyntaxNode,
|
||||
): TraversalResult {
|
||||
const res = this.visitBranchExpr(branch);
|
||||
const cont = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
startLineOf(stmt),
|
||||
'',
|
||||
'normal',
|
||||
this.harvest.assignmentDefFacts(assignExpr),
|
||||
);
|
||||
this.builder.connect(res.exits, cont, 'seq');
|
||||
return { entry: res.entry, exits: [cont] };
|
||||
}
|
||||
|
||||
/**
|
||||
* try / catch / finally. A `finally` runs on BOTH normal and exception exit —
|
||||
* a `return`/`break`/`continue` crossing it threads through it (`finally-*`
|
||||
|
|
|
|||
|
|
@ -279,6 +279,24 @@ export class SwiftHarvester {
|
|||
return acc.finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Def-ONLY facts for a value-position binding carrier (`let x = if … / switch …`,
|
||||
* #2207): just the declared name pattern's leaves, attached to the continuation
|
||||
* block the branch arms rejoin. The condition + arm-value USES are already
|
||||
* harvested onto the branch's own blocks (visitIf / visitSwitch), so this must
|
||||
* NOT re-walk the value — only the `name`-field pattern leaves are defs here.
|
||||
*/
|
||||
bindingDefFacts(stmt: SyntaxNode): StatementFacts | undefined {
|
||||
const acc = new FactAccumulator(stmt.startPosition.row + 1);
|
||||
for (let i = 0; i < stmt.childCount; i++) {
|
||||
if (stmt.fieldNameForChild(i) === 'name') {
|
||||
const pat = stmt.child(i);
|
||||
if (pat) this.defPattern(pat, acc);
|
||||
}
|
||||
}
|
||||
return acc.defCount() ? acc.finish() : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* MAY-def facts for a `switch_pattern`'s value bindings (`case let n` /
|
||||
* `case .some(let v)`). The binding only takes effect when the case matches,
|
||||
|
|
|
|||
|
|
@ -88,6 +88,12 @@
|
|||
* trailing closure, which is unwrapped to model scope-exit flow.
|
||||
*
|
||||
* Known limitations:
|
||||
* - a value-position `if`/`switch` (Swift 5.9) IS modeled as control flow in two
|
||||
* carriers (#2207): a `let x = if … else … / switch … {…}` binding (arms rejoin
|
||||
* at a binding continuation) and `return if … / switch …` (each arm returns).
|
||||
* tree-sitter-swift reuses `if_statement` / `switch_statement` for the value
|
||||
* form. A value branch in any OTHER position (an argument, an interpolation)
|
||||
* stays inline; the ternary `?:` / `??` are excluded by design.
|
||||
* - computed properties (`var y: Int { get { … } set { … } }`) have their bodies
|
||||
* inside `computed_getter` / `computed_setter` rather than a function node; v1
|
||||
* does NOT build a CFG for them (documented gap, not faked).
|
||||
|
|
@ -232,6 +238,12 @@ class SwiftCfgWalk {
|
|||
private isControlFlow(stmt: SyntaxNode): boolean {
|
||||
if (stmt.type === 'statement_label') return true; // queue label, emit no block
|
||||
if (this.isDeferCall(stmt)) return true;
|
||||
// `let x = if … / switch …` (Swift 5.9, #2207): a value-position branch breaks
|
||||
// so `visitStmt` models the arms as control flow instead of coalescing.
|
||||
if (stmt.type === 'property_declaration') {
|
||||
const v = this.directValue(stmt);
|
||||
return v !== undefined && this.isModelableValueBranch(v);
|
||||
}
|
||||
return CONTROL_FLOW_TYPES.has(stmt.type);
|
||||
}
|
||||
|
||||
|
|
@ -245,6 +257,13 @@ class SwiftCfgWalk {
|
|||
}
|
||||
if (this.isDeferCall(stmt)) return this.visitDefer(stmt);
|
||||
switch (stmt.type) {
|
||||
case 'property_declaration': {
|
||||
// `let x = if … / switch …` (Swift 5.9, #2207): the value is a value-
|
||||
// position branch — model it as control flow and bind on the rejoin.
|
||||
const value = this.directValue(stmt);
|
||||
if (value && this.isModelableValueBranch(value)) return this.visitBindBranch(stmt, value);
|
||||
return this.visitSimple(stmt);
|
||||
}
|
||||
case 'if_statement':
|
||||
return this.visitIf(stmt);
|
||||
case 'guard_statement':
|
||||
|
|
@ -308,6 +327,20 @@ class SwiftCfgWalk {
|
|||
|
||||
/** `return [expr]` — threads through every active `defer` (LIFO) before EXIT. */
|
||||
private visitReturn(stmt: SyntaxNode): TraversalResult {
|
||||
// `return if … / switch …` (Swift 5.9, #2207): the returned value is a value-
|
||||
// position branch — model it as control flow, with each arm returning (its
|
||||
// value IS the function result), threading every active finalizer per arm.
|
||||
const branch = stmt.namedChildren.find(
|
||||
(c) => c.type === 'if_statement' || c.type === 'switch_statement',
|
||||
);
|
||||
if (branch && this.isModelableValueBranch(branch)) {
|
||||
const res = this.visitBranchExpr(branch);
|
||||
const finalizers = this.cfc.finalizersForReturn();
|
||||
for (const ex of res.exits) {
|
||||
wireJumpThroughFinalizers(this.builder, ex, finalizers, this.builder.exitIndex, 'return');
|
||||
}
|
||||
return { entry: res.entry, exits: [] };
|
||||
}
|
||||
const idx = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
endLineOf(stmt),
|
||||
|
|
@ -384,6 +417,58 @@ class SwiftCfgWalk {
|
|||
return labels;
|
||||
}
|
||||
|
||||
// ── value-position branches (#2207) ─────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* The value-position branch of a `property_declaration` (`let x = if … / switch
|
||||
* …`, Swift 5.9): the direct `if_statement` / `switch_statement` child (the value
|
||||
* after `=`), or undefined. tree-sitter-swift reuses the statement nodes for the
|
||||
* value form — there is no separate `if_expression` / `switch_expression`.
|
||||
*/
|
||||
private directValue(stmt: SyntaxNode): SyntaxNode | undefined {
|
||||
return stmt.namedChildren.find(
|
||||
(c) => c.type === 'if_statement' || c.type === 'switch_statement',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `node` is a value-position branch worth modeling as control flow
|
||||
* (#2207): an `if` with an `else` (a value-position `if` always has one), or a
|
||||
* `switch` with ≥2 entries — a real dispatch. The ternary `?:` and `??` are
|
||||
* excluded by design (micro-branches, like the Kotlin elvis).
|
||||
*/
|
||||
private isModelableValueBranch(node: SyntaxNode): boolean {
|
||||
if (node.type === 'if_statement') return this.elseNodeOf(node) !== undefined;
|
||||
if (node.type === 'switch_statement') {
|
||||
return node.namedChildren.filter((c) => c.type === 'switch_entry').length >= 2;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Model a value-position `if`/`switch` as control flow, bypassing position. */
|
||||
private visitBranchExpr(node: SyntaxNode): TraversalResult {
|
||||
return node.type === 'switch_statement' ? this.visitSwitch(node) : this.visitIf(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* `let x = if … / switch …` (#2207): visit the branch as control flow, then
|
||||
* rejoin its arms at a facts-only continuation carrying ONLY the bound name's
|
||||
* def (the condition + arm-value uses are already on the branch's blocks). The
|
||||
* arms are now control-dependent on the branch — mirrors Kotlin / Rust.
|
||||
*/
|
||||
private visitBindBranch(stmt: SyntaxNode, branch: SyntaxNode): TraversalResult {
|
||||
const res = this.visitBranchExpr(branch);
|
||||
const cont = this.builder.newBlock(
|
||||
startLineOf(stmt),
|
||||
startLineOf(stmt),
|
||||
'',
|
||||
'normal',
|
||||
this.harvest.bindingDefFacts(stmt),
|
||||
);
|
||||
this.builder.connect(res.exits, cont, 'seq');
|
||||
return { entry: res.entry, exits: [cont] };
|
||||
}
|
||||
|
||||
// ── branches ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -196,14 +196,67 @@ describe('C# CfgVisitor — switch', () => {
|
|||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
});
|
||||
|
||||
it('switch_expression arms each dispatch as a guarded branch (switch-case)', () => {
|
||||
it('return switch_expression: each arm dispatches and returns the result (#2207)', () => {
|
||||
const cfg = cs.cfgOf(
|
||||
`class C { int M(int x) { return x switch { 1 => a(), 2 => b(), _ => c() }; } }`,
|
||||
);
|
||||
// The switch-expression lives inside the return block — it does not break a
|
||||
// basic block, but the function still has a well-formed single-exit CFG.
|
||||
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
expect(edgeKinds(cfg).has('return')).toBe(true);
|
||||
// every arm reaches EXIT (its value IS the returned result).
|
||||
expect(reaches(cfg, block(cfg, 'a()'), cfg.exitIndex)).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'c()'), cfg.exitIndex)).toBe(true);
|
||||
// a() does NOT fall into b() (arms never fall through).
|
||||
expect(reaches(cfg, block(cfg, 'a()'), block(cfg, 'b()'))).toBe(false);
|
||||
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it('value-position switch declaration is modeled, def bound at the join (#2207)', () => {
|
||||
const cfg = cs.cfgOf(
|
||||
`class C { int M(int x) { var y = x switch { 1 => a(), _ => b() }; use(y); return 0; } }`,
|
||||
);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
// each arm rejoins and reaches the downstream use of the bound result.
|
||||
expect(reaches(cfg, block(cfg, 'a()'), block(cfg, 'use(y);'))).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'b()'), block(cfg, 'use(y);'))).toBe(true);
|
||||
const y = bindingIdx(cfg, 'y');
|
||||
expect(hasDef(cfg, y)).toBe(true);
|
||||
expect(hasUse(cfg, y)).toBe(true);
|
||||
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it('expression-bodied member `=> k switch {…}` models the arms (#2207)', () => {
|
||||
const cfg = cs.cfgOf(`class C { int G(int x) => x switch { 1 => a(), _ => b() }; }`);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
expect(edgeKinds(cfg).has('return')).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'a()'), cfg.exitIndex)).toBe(true);
|
||||
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it('assignment-RHS / single-arm value switch stays inline (documented gap)', () => {
|
||||
const assign = cs.cfgOf(
|
||||
`class C { int M(int x) { int y = 0; y = x switch { 1 => 1, _ => 2 }; return y; } }`,
|
||||
);
|
||||
expect(edgeKinds(assign).has('switch-case')).toBe(false);
|
||||
expect(reaches(assign, assign.entryIndex, assign.exitIndex)).toBe(true);
|
||||
|
||||
const oneArm = cs.cfgOf(`class C { int M(int x) { var y = x switch { _ => 0 }; return y; } }`);
|
||||
expect(edgeKinds(oneArm).has('switch-case')).toBe(false);
|
||||
});
|
||||
|
||||
it('non-exhaustive switch expression (no `_` arm) keeps a no-match edge (EXIT reachable) (#2211)', () => {
|
||||
const cfg = cs.cfgOf(
|
||||
`class C { int M(int x) { var y = x switch { 1 => a(), 2 => b() }; return y; } }`,
|
||||
);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
// 2 arms + the conservative no-match path = 3 switch-case successors from the dispatch.
|
||||
const dispatchIdx = block(cfg, 'x');
|
||||
expect(cfg.edges.filter((e) => e.from === dispatchIdx && e.kind === 'switch-case').length).toBe(
|
||||
3,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -414,6 +467,18 @@ describe('C# CfgVisitor — does not throw on exotic shapes', () => {
|
|||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('a truncated value-position switch never throws out of the carrier path (R4) (#2211)', () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
try {
|
||||
const root = cs.parse(`class C { int M(int x) { var y = x switch { 1 => a(`);
|
||||
for (const fn of cs.collectFunctions(root)) {
|
||||
expect(() => createCsharpCfgVisitor().buildFunctionCfg(fn, 'f.cs')).not.toThrow();
|
||||
}
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// U6 — call-site `sites[]` taint substrate. INERT BY DESIGN: no C# taint model
|
||||
|
|
|
|||
|
|
@ -71,6 +71,13 @@ describe('Dart CfgVisitor — structure', () => {
|
|||
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
});
|
||||
|
||||
it('a truncated value-position switch never throws out of the carrier path (R4) (#2211)', () => {
|
||||
const root = dart.parse(`int f(int v){ var x = switch (v) { 1 => a(`);
|
||||
for (const fn of dart.collectFunctions(root)) {
|
||||
expect(() => createDartCfgVisitor().buildFunctionCfg(fn, 'f.dart')).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('a class method is a CFG-bearing function and binds its params', () => {
|
||||
const cfg = dart.cfgOf(`class C { void m(int a) { g(a); } }`);
|
||||
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
|
|
@ -258,30 +265,89 @@ describe('Dart CfgVisitor — switch', () => {
|
|||
expect(cfg.edges.some((e) => e.from === tainted && e.to === sink)).toBe(false);
|
||||
});
|
||||
|
||||
it('a switch EXPRESSION used as a value stays inline (no branch edges)', () => {
|
||||
it('value-position switch declaration is modeled as a dispatch, def bound at the join (#2207)', () => {
|
||||
const cfg = dart.cfgOf(`void f(int x) {
|
||||
var y = switch (x) { 1 => one(), 2 => two(), _ => other() };
|
||||
var y = switch (x) { 1 => one(x), 2 => two(), _ => other() };
|
||||
use(y);
|
||||
}`);
|
||||
// The value-position switch expression coalesces; no switch-case edges.
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(false);
|
||||
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
expect(definesBinding(cfg, bindingIdx(cfg, 'y'))).toBe(true);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
// each arm rejoins and reaches the downstream use of the bound result.
|
||||
expect(reaches(cfg, block(cfg, 'one(x)'), block(cfg, 'use(y);'))).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'other()'), block(cfg, 'use(y);'))).toBe(true);
|
||||
const y = bindingIdx(cfg, 'y');
|
||||
expect(definesBinding(cfg, y)).toBe(true);
|
||||
expect(usesBinding(cfg, y)).toBe(true);
|
||||
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it('a switch-EXPRESSION arm write is a may-def, not a hard kill of the prior def (#2206)', () => {
|
||||
it('return switch (…) models each arm as returning the result (#2207)', () => {
|
||||
const cfg = dart.cfgOf(`int f(int x) {
|
||||
return switch (x) { 1 => a(x), 2 => b(), _ => c() };
|
||||
}`);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
expect(edgeKinds(cfg).has('return')).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'a(x)'), cfg.exitIndex)).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'c()'), cfg.exitIndex)).toBe(true);
|
||||
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it('a multi-binding decl with a switch-EXPRESSION value stays inline', () => {
|
||||
const cfg = dart.cfgOf(`void f(int x) {
|
||||
var y = switch (x) { _ => 0 }, z = 2;
|
||||
use(y + z);
|
||||
}`);
|
||||
// Modeling a multi-binding decl arm-by-arm is out of scope — it coalesces.
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(false);
|
||||
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
});
|
||||
|
||||
it('an INLINE switch-EXPRESSION arm write is a may-def, not a hard kill (#2206)', () => {
|
||||
// An argument-position switch expression is NOT a modeled value-branch carrier
|
||||
// (#2207 models only declaration / return), so it coalesces — and the harvest
|
||||
// must still treat each arm write as a MAY-def (only one arm runs).
|
||||
const cfg = dart.cfgOf(`void f(int x) {
|
||||
int z = 0;
|
||||
var y = switch (x) { 1 => z = 10, _ => z = 20 };
|
||||
use(z);
|
||||
use(switch (x) { 1 => z = 10, _ => z = 20 });
|
||||
sink(z);
|
||||
}`);
|
||||
const z = bindingIdx(cfg, 'z');
|
||||
// only one arm runs, so the arm writes (z=10 / z=20) are MAY-defs — they must
|
||||
// not unconditionally KILL the prior `int z = 0`.
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(false);
|
||||
expect(cfg.blocks.some((bl) => bl.statements?.some((s) => (s.mayDefs ?? []).includes(z)))).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('value switch without an unguarded `_` keeps the no-match edge (EXIT stays reachable) (#2211)', () => {
|
||||
// A guarded `_ when …` is NOT an exhaustive catch-all — the conservative
|
||||
// no-match path must remain (Dart throws at runtime if no arm + guard matches).
|
||||
const cfg = dart.cfgOf(`int f(int v) {
|
||||
return switch (v) { int n when n > 0 => a(n), _ when v < 0 => b() };
|
||||
}`);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
// the dispatch must reach the join WITHOUT going through an arm (the no-match edge).
|
||||
const dispatchIdx = block(cfg, 'switch v');
|
||||
const dispatchSucc = cfg.edges.filter(
|
||||
(e) => e.from === dispatchIdx && e.kind === 'switch-case',
|
||||
);
|
||||
// dispatch fans to 2 arms + the no-match join = 3 switch-case successors.
|
||||
expect(dispatchSucc.length).toBe(3);
|
||||
});
|
||||
|
||||
it('a value-switch `when` guard is a conditional dispatch use, not an arm-value use (#2211)', () => {
|
||||
const cfg = dart.cfgOf(`int f(int v) {
|
||||
var x = switch (v) { int n when guardOk(v) => a(n), _ => b() };
|
||||
use(x);
|
||||
}`);
|
||||
const vIdx = bindingIdx(cfg, 'v');
|
||||
// `v` (used by the guard `guardOk(v)`) is recorded as a use on the dispatch
|
||||
// block (text `switch v`), not buried in an arm-value block.
|
||||
const dispatch = cfg.blocks.find((b) => b.text === 'switch v')!;
|
||||
expect(dispatch.statements?.some((s) => s.uses.includes(vIdx))).toBe(true);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dart CfgVisitor — try/on/catch/finally', () => {
|
||||
|
|
|
|||
|
|
@ -294,13 +294,58 @@ describe('Java CfgVisitor — switch', () => {
|
|||
expect(hasUse(cfg, x)).toBe(true);
|
||||
});
|
||||
|
||||
it('switch EXPRESSION value with yield stays inline; method has a single-exit CFG', () => {
|
||||
it('value-position switch declaration is modeled as a dispatch, def bound at the join (#2207)', () => {
|
||||
const cfg = java.cfgOf(`class C { int m(int x) {
|
||||
int r = switch (x) { case 1 -> 10; default -> { yield 20; } };
|
||||
return r;
|
||||
use(r);
|
||||
} }`);
|
||||
// The arms are now real CFG blocks reached by switch-case dispatch edges.
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
// Each arm rejoins and reaches the use of the bound result.
|
||||
expect(reaches(cfg, block(cfg, '10'), block(cfg, 'use(r);'))).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'yield 20;'), block(cfg, 'use(r);'))).toBe(true);
|
||||
// `r` is defined (at the continuation) and used downstream — the chain is live.
|
||||
const r = bindingIdx(cfg, 'r');
|
||||
expect(hasDef(cfg, r)).toBe(true);
|
||||
expect(hasUse(cfg, r)).toBe(true);
|
||||
// Modeling the arms yields control dependence (the whole point of #2207).
|
||||
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
});
|
||||
|
||||
it('return switch (…) {…} models each arm as returning the function result (#2207)', () => {
|
||||
const cfg = java.cfgOf(`class C { int m(int x) {
|
||||
return switch (x) { case 1 -> a(); case 2 -> b(); default -> c(); };
|
||||
} }`);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
expect(edgeKinds(cfg).has('return')).toBe(true);
|
||||
// every arm reaches EXIT (its value IS the returned result).
|
||||
expect(reaches(cfg, block(cfg, 'a()'), cfg.exitIndex)).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'c()'), cfg.exitIndex)).toBe(true);
|
||||
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it('value-position switch with ONE group stays inline (no real control dependence)', () => {
|
||||
const cfg = java.cfgOf(`class C { int m(int x) {
|
||||
int r = switch (x) { default -> 0; };
|
||||
use(r);
|
||||
} }`);
|
||||
// A single-arm switch carries no branch — it coalesces into the declaration block.
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(false);
|
||||
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
});
|
||||
|
||||
it('assignment-RHS value switch stays inline (documented remaining gap)', () => {
|
||||
const cfg = java.cfgOf(`class C { int m(int x) {
|
||||
int r = 0;
|
||||
r = switch (x) { case 1 -> 10; default -> 20; };
|
||||
use(r);
|
||||
} }`);
|
||||
// Only declaration / return carriers are modeled; an assignment RHS coalesces.
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(false);
|
||||
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
});
|
||||
|
||||
it('statement switch with a yield arm builds a dispatch with a yield block', () => {
|
||||
|
|
@ -313,6 +358,48 @@ describe('Java CfgVisitor — switch', () => {
|
|||
// statement-position switch breaks a block → switch-case dispatch edges.
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
});
|
||||
|
||||
it('colon-form value switch: yield ends the arm, NO fallthrough; every arm is CDG-dependent (#2211)', () => {
|
||||
const cfg = java.cfgOf(`class C { int m(int k) {
|
||||
int x = switch (k) { case 1: yield one(); case 2: yield two(); default: yield zero(); };
|
||||
use(x);
|
||||
} }`);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
// a `yield` exits the switch — it does NOT fall through to the next colon group.
|
||||
expect(edgeKinds(cfg).has('fallthrough')).toBe(false);
|
||||
expect(reaches(cfg, block(cfg, 'one()'), block(cfg, 'two()'))).toBe(false);
|
||||
// every arm rejoins and reaches the downstream use of the bound result.
|
||||
expect(reaches(cfg, block(cfg, 'one()'), block(cfg, 'use(x);'))).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'two()'), block(cfg, 'use(x);'))).toBe(true);
|
||||
// each arm is control-dependent on the dispatch — pin the SPECIFIC pairs.
|
||||
const dispatch = block(cfg, 'k');
|
||||
const cdg = computeControlDependence(cfg);
|
||||
expect(
|
||||
cdg.edges.some(
|
||||
(e) => e.controllerBlock === dispatch && e.dependentBlock === block(cfg, 'one()'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
cdg.edges.some(
|
||||
(e) => e.controllerBlock === dispatch && e.dependentBlock === block(cfg, 'two()'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it('return switch (…) inside try/finally threads the finalizer per arm (#2211)', () => {
|
||||
const cfg = java.cfgOf(`class C { int m(int k) {
|
||||
try {
|
||||
return switch (k) { case 1 -> a(); default -> b(); };
|
||||
} finally { cleanup(); }
|
||||
} }`);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
// each arm's return threads the finally before EXIT.
|
||||
expect(edgeKinds(cfg).has('finally-return')).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'a()'), block(cfg, 'cleanup();'))).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'b()'), block(cfg, 'cleanup();'))).toBe(true);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Java CfgVisitor — try / catch / finally / try-with-resources', () => {
|
||||
|
|
@ -494,6 +581,18 @@ describe('Java CfgVisitor — does not throw on exotic shapes', () => {
|
|||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('a truncated value-position switch never throws out of the carrier path (R4) (#2211)', () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
try {
|
||||
const root = java.parse(`class C { int m(int k){ int x = switch (k) { case 1 -> a(`);
|
||||
for (const fn of java.collectFunctions(root)) {
|
||||
expect(() => createJavaCfgVisitor().buildFunctionCfg(fn, 'f.java')).not.toThrow();
|
||||
}
|
||||
} finally {
|
||||
warn.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// U6 — call-site `sites[]` taint substrate. INERT BY DESIGN: no Java taint model
|
||||
|
|
|
|||
|
|
@ -67,6 +67,13 @@ describe('Kotlin CfgVisitor — structure', () => {
|
|||
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);
|
||||
|
|
@ -217,6 +224,78 @@ describe('Kotlin CfgVisitor — value-position branches (#2205)', () => {
|
|||
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', () => {
|
||||
|
|
|
|||
|
|
@ -193,15 +193,51 @@ describe('PHP CfgVisitor — switch / match', () => {
|
|||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it('match is a value expression (no fallthrough), kept inline — value flows to the assign', () => {
|
||||
const cfg = php.cfgOf(wrap(`$r = match ($x) { 1, 2 => "low", default => "high" }; return $r;`));
|
||||
// match arms are NOT separate dispatch blocks (documented inline-value gap).
|
||||
it('value-position match assignment dispatches; target bound at the join (#2207)', () => {
|
||||
const cfg = php.cfgOf(
|
||||
wrap(`$r = match ($x) { 1, 2 => low($x), default => high() }; use_it($r);`),
|
||||
);
|
||||
// arms dispatch as switch-case, never fall through.
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
expect(edgeKinds(cfg).has('fallthrough')).toBe(false);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(false);
|
||||
// The match value and the return both reach EXIT.
|
||||
expect(reaches(cfg, block(cfg, 'match ($x)'), cfg.exitIndex)).toBe(true);
|
||||
// each arm rejoins and reaches the downstream use of the bound result.
|
||||
expect(reaches(cfg, block(cfg, 'low($x)'), block(cfg, 'use_it($r)'))).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'high()'), block(cfg, 'use_it($r)'))).toBe(true);
|
||||
const r = bindingIdx(cfg, '$r');
|
||||
expect(hasDef(cfg, r)).toBe(true);
|
||||
expect(hasUse(cfg, r)).toBe(true);
|
||||
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it('return match (…) models each arm as returning the result (#2207)', () => {
|
||||
const cfg = php.cfgOf(wrap(`return match ($x) { 1 => a($x), 2 => b(), default => c() };`));
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
expect(edgeKinds(cfg).has('return')).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'a($x)'), cfg.exitIndex)).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'c()'), cfg.exitIndex)).toBe(true);
|
||||
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it('single-arm match / ternary stays inline (no real control dependence)', () => {
|
||||
const oneArm = php.cfgOf(wrap(`$r = match ($x) { default => 0 }; return $r;`));
|
||||
expect(edgeKinds(oneArm).has('switch-case')).toBe(false);
|
||||
const ternary = php.cfgOf(wrap(`$r = $x > 0 ? a() : b(); return $r;`));
|
||||
expect(edgeKinds(ternary).has('switch-case')).toBe(false);
|
||||
expect(isExitReachableFromAllBlocks(ternary)).toBe(true);
|
||||
});
|
||||
|
||||
it('match without `default` keeps a no-match (UnhandledMatchError) edge; EXIT reachable (#2211)', () => {
|
||||
const cfg = php.cfgOf(wrap(`$r = match ($x) { 1 => a($x), 2 => b() }; use_it($r);`));
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
// 2 arms + the conservative no-match path = 3 switch-case successors from the dispatch.
|
||||
const dispatchIdx = block(cfg, '$x');
|
||||
expect(cfg.edges.filter((e) => e.from === dispatchIdx && e.kind === 'switch-case').length).toBe(
|
||||
3,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PHP CfgVisitor — try / catch / finally', () => {
|
||||
|
|
@ -384,4 +420,11 @@ describe('PHP CfgVisitor — robustness', () => {
|
|||
expect(reachable(cfg, block(cfg, 'done()'))).toBe(true);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it('a truncated value-position match never throws out of the carrier path (R4) (#2211)', () => {
|
||||
const root = php.parse(`<?php function f($x){ $r = match ($x) { 1 => a(`);
|
||||
for (const fn of php.collectFunctions(root)) {
|
||||
expect(() => createPhpCfgVisitor().buildFunctionCfg(fn, 'x.php')).not.toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { requireVendoredGrammar } from '../../../src/core/tree-sitter/vendored-grammars.js';
|
||||
import { createSwiftCfgVisitor } from '../../../src/core/ingestion/cfg/visitors/swift.js';
|
||||
import type { FunctionCfg } from '../../../src/core/ingestion/cfg/types.js';
|
||||
import {
|
||||
makeCfgHarness,
|
||||
type CfgHarness,
|
||||
|
|
@ -54,6 +55,13 @@ describe('Swift CfgVisitor — structure', () => {
|
|||
expect(reaches(cfg, cfg.entryIndex, cfg.exitIndex)).toBe(true);
|
||||
});
|
||||
|
||||
it('a truncated value-position if never throws out of the carrier path (R4) (#2211)', () => {
|
||||
const root = swift.parse(`func f(v: Int) { let x = if v > 0 {`);
|
||||
for (const fn of swift.collectFunctions(root)) {
|
||||
expect(() => createSwiftCfgVisitor().buildFunctionCfg(fn, 'f.swift')).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('init and deinit are CFG-bearing functions', () => {
|
||||
const cfgs = swift.cfgsOf(`class C { init(x: Int) { self.x = x } ; deinit { cleanup() } }`);
|
||||
expect(cfgs).toHaveLength(2);
|
||||
|
|
@ -229,6 +237,70 @@ describe('Swift CfgVisitor — switch (no implicit fallthrough)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Swift CfgVisitor — value-position if/switch (Swift 5.9, #2207)', () => {
|
||||
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)));
|
||||
|
||||
it('`let x = if … else …` is modeled as a branch; def bound at the join', () => {
|
||||
const cfg = swift.cfgOf(`func f(v: Int) {
|
||||
let x = if v > 0 { hi() } else { lo() }
|
||||
use(x)
|
||||
}`);
|
||||
expect(edgeKinds(cfg).has('cond-true')).toBe(true);
|
||||
expect(edgeKinds(cfg).has('cond-false')).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'hi()'), block(cfg, 'use(x)'))).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'lo()'), block(cfg, 'use(x)'))).toBe(true);
|
||||
const x = bindingIdx(cfg, 'x');
|
||||
expect(hasDef(cfg, x)).toBe(true);
|
||||
expect(hasUse(cfg, x)).toBe(true);
|
||||
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it('`let y = switch v { … }` is modeled as a dispatch', () => {
|
||||
const cfg = swift.cfgOf(`func f(v: Int) {
|
||||
let y = switch v { case 1: one() ; default: other() }
|
||||
use(y)
|
||||
}`);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'one()'), block(cfg, 'use(y)'))).toBe(true);
|
||||
const y = bindingIdx(cfg, 'y');
|
||||
expect(hasDef(cfg, y)).toBe(true);
|
||||
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it('`return if … else …` models each arm as returning the result', () => {
|
||||
const cfg = swift.cfgOf(`func f(v: Int) -> Int {
|
||||
return if v > 0 { a() } else { b() }
|
||||
}`);
|
||||
expect(edgeKinds(cfg).has('cond-true')).toBe(true);
|
||||
expect(edgeKinds(cfg).has('return')).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'a()'), cfg.exitIndex)).toBe(true);
|
||||
expect(reaches(cfg, block(cfg, 'b()'), cfg.exitIndex)).toBe(true);
|
||||
expect(computeControlDependence(cfg).edges.length).toBeGreaterThan(0);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it('an else-less `if` value / plain binding stays inline (no real control dependence)', () => {
|
||||
// `let x = g()` is a plain binding — no branch.
|
||||
const cfg = swift.cfgOf(`func f(v: Int) { let x = g()\n use(x) }`);
|
||||
expect(edgeKinds(cfg).has('cond-true')).toBe(false);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(false);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
|
||||
it('a single-entry value switch stays inline (below the >= 2 modeling threshold) (#2211)', () => {
|
||||
// `isModelableValueBranch` requires >= 2 `switch_entry`; a one-entry value
|
||||
// switch carries no real control dependence, so the decl coalesces inline.
|
||||
const cfg = swift.cfgOf(`func f(v: Int) { let x = switch v { default: g() }\n use(x) }`);
|
||||
expect(edgeKinds(cfg).has('switch-case')).toBe(false);
|
||||
expect(isExitReachableFromAllBlocks(cfg)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Swift CfgVisitor — do/catch (error handling)', () => {
|
||||
it('do/catch: a throw edge runs from each protected block to the handler', () => {
|
||||
const cfg = swift.cfgOf(`func f() {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue