gitnexus 1.6.8-rc.37 → 1.6.8-rc.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/core/ingestion/cfg/control-flow-context.d.ts +9 -0
  2. package/dist/core/ingestion/cfg/control-flow-context.js +11 -0
  3. package/dist/core/ingestion/cfg/visitors/csharp-harvest.d.ts +15 -1
  4. package/dist/core/ingestion/cfg/visitors/csharp-harvest.js +29 -1
  5. package/dist/core/ingestion/cfg/visitors/csharp.d.ts +6 -0
  6. package/dist/core/ingestion/cfg/visitors/csharp.js +161 -1
  7. package/dist/core/ingestion/cfg/visitors/dart-harvest.d.ts +8 -0
  8. package/dist/core/ingestion/cfg/visitors/dart-harvest.js +26 -0
  9. package/dist/core/ingestion/cfg/visitors/dart.d.ts +7 -4
  10. package/dist/core/ingestion/cfg/visitors/dart.js +148 -1
  11. package/dist/core/ingestion/cfg/visitors/java-harvest.d.ts +8 -0
  12. package/dist/core/ingestion/cfg/visitors/java-harvest.js +19 -0
  13. package/dist/core/ingestion/cfg/visitors/java.d.ts +6 -5
  14. package/dist/core/ingestion/cfg/visitors/java.js +106 -10
  15. package/dist/core/ingestion/cfg/visitors/kotlin-harvest.d.ts +9 -0
  16. package/dist/core/ingestion/cfg/visitors/kotlin-harvest.js +20 -0
  17. package/dist/core/ingestion/cfg/visitors/kotlin.d.ts +8 -6
  18. package/dist/core/ingestion/cfg/visitors/kotlin.js +58 -9
  19. package/dist/core/ingestion/cfg/visitors/php-harvest.d.ts +8 -0
  20. package/dist/core/ingestion/cfg/visitors/php-harvest.js +20 -0
  21. package/dist/core/ingestion/cfg/visitors/php.d.ts +8 -6
  22. package/dist/core/ingestion/cfg/visitors/php.js +110 -1
  23. package/dist/core/ingestion/cfg/visitors/swift-harvest.d.ts +8 -0
  24. package/dist/core/ingestion/cfg/visitors/swift-harvest.js +18 -0
  25. package/dist/core/ingestion/cfg/visitors/swift.d.ts +6 -0
  26. package/dist/core/ingestion/cfg/visitors/swift.js +66 -0
  27. package/package.json +1 -1
@@ -62,6 +62,15 @@ export declare class ControlFlowContext {
62
62
  resolveBreak(label?: string): JumpResolution | undefined;
63
63
  /** Resolve a `continue`: like {@link resolveBreak} but only loop frames match. */
64
64
  resolveContinue(label?: string): JumpResolution | undefined;
65
+ /**
66
+ * Resolve a Java `yield e` (switch-EXPRESSION arm exit): the nearest enclosing
67
+ * SWITCH frame's exit, threading the finalizers stacked above it. Unlike a
68
+ * `break`, a `yield` ALWAYS targets the switch — never an intervening loop — so
69
+ * it cannot match a loop frame (a `yield` inside a loop inside a switch arm
70
+ * still exits the whole switch). Returns `undefined` when there is no enclosing
71
+ * switch (malformed input); the caller falls back to its conservative routing.
72
+ */
73
+ resolveYield(): JumpResolution | undefined;
65
74
  /** Every active finalizer, innermost first — what a `return` must cross. */
66
75
  finalizersForReturn(): readonly FinalizerFrame[];
67
76
  /**
@@ -39,6 +39,17 @@ export class ControlFlowContext {
39
39
  resolveContinue(label) {
40
40
  return this.resolve((f) => f.kind === 'loop' && (label === undefined || f.labels.includes(label)), (f) => f.continueTo);
41
41
  }
42
+ /**
43
+ * Resolve a Java `yield e` (switch-EXPRESSION arm exit): the nearest enclosing
44
+ * SWITCH frame's exit, threading the finalizers stacked above it. Unlike a
45
+ * `break`, a `yield` ALWAYS targets the switch — never an intervening loop — so
46
+ * it cannot match a loop frame (a `yield` inside a loop inside a switch arm
47
+ * still exits the whole switch). Returns `undefined` when there is no enclosing
48
+ * switch (malformed input); the caller falls back to its conservative routing.
49
+ */
50
+ resolveYield() {
51
+ return this.resolve((f) => f.kind === 'switch');
52
+ }
42
53
  /** Every active finalizer, innermost first — what a `return` must cross. */
43
54
  finalizersForReturn() {
44
55
  const fins = [];
@@ -61,6 +61,14 @@ export declare class CsharpHarvester extends ScopeTreeHarvester {
61
61
  facts(node: SyntaxNode): StatementFacts;
62
62
  /** Facts for an expression whose WHOLE evaluation is conditional (case tests). */
63
63
  factsConditional(node: SyntaxNode): StatementFacts;
64
+ /**
65
+ * Def-ONLY facts for a value-position binding carrier (`var x = k switch {…}`,
66
+ * #2207): just the declared name(s)' def, attached to the continuation block the
67
+ * switch arms rejoin. The discriminant + arm-value USES are already harvested
68
+ * onto the branch's own blocks ({@link facts} on each arm), so this must NOT
69
+ * re-walk the initializer — only each `variable_declarator`'s name is a def here.
70
+ */
71
+ bindingDefFacts(stmt: SyntaxNode): StatementFacts | undefined;
64
72
  /** Facts for a `foreach (decl in right)` head: decl binds, right is used. */
65
73
  forEachHeadFacts(stmt: SyntaxNode): StatementFacts;
66
74
  /** ENTRY-block facts for the function's parameters (defs only). */
@@ -101,7 +109,13 @@ export declare class CsharpHarvester extends ScopeTreeHarvester {
101
109
  * identifier; `skipFinalRead` suppresses it when that access is the callee.
102
110
  */
103
111
  private walkChain;
104
- /** The initializer value of a `variable_declarator` — the named child after `name`. */
112
+ /**
113
+ * The initializer value of a `variable_declarator` — the named child after
114
+ * `name`. NOTE: deliberately duplicated in `csharp.ts` (the visitor is a
115
+ * standalone class with no shared base — repo convention). The two copies must
116
+ * stay in sync; there is no C#-specific shared module to host it, and the only
117
+ * module both files share is the generic `utils/ast-helpers` (types only).
118
+ */
105
119
  private declaratorInit;
106
120
  /** Whether a unary expression is `++`/`--` (the only writing unary ops). */
107
121
  private isIncDec;
@@ -170,6 +170,28 @@ export class CsharpHarvester extends ScopeTreeHarvester {
170
170
  this.conditional(() => this.walkValue(node, acc));
171
171
  return acc.finish();
172
172
  }
173
+ /**
174
+ * Def-ONLY facts for a value-position binding carrier (`var x = k switch {…}`,
175
+ * #2207): just the declared name(s)' def, attached to the continuation block the
176
+ * switch arms rejoin. The discriminant + arm-value USES are already harvested
177
+ * onto the branch's own blocks ({@link facts} on each arm), so this must NOT
178
+ * re-walk the initializer — only each `variable_declarator`'s name is a def here.
179
+ */
180
+ bindingDefFacts(stmt) {
181
+ const acc = new FactAccumulator(stmt.startPosition.row + 1);
182
+ const decl = stmt.namedChildren.find((c) => c.type === 'variable_declaration');
183
+ if (decl) {
184
+ for (let i = 0; i < decl.namedChildCount; i++) {
185
+ const d = decl.namedChild(i);
186
+ if (d?.type !== 'variable_declarator')
187
+ continue;
188
+ const name = d.childForFieldName('name');
189
+ if (name)
190
+ this.def(name, acc);
191
+ }
192
+ }
193
+ return acc.defCount() ? acc.finish() : undefined;
194
+ }
173
195
  /** Facts for a `foreach (decl in right)` head: decl binds, right is used. */
174
196
  forEachHeadFacts(stmt) {
175
197
  const acc = new FactAccumulator(stmt.startPosition.row + 1);
@@ -517,7 +539,13 @@ export class CsharpHarvester extends ScopeTreeHarvester {
517
539
  : undefined;
518
540
  return { path, rootIdx };
519
541
  }
520
- /** The initializer value of a `variable_declarator` — the named child after `name`. */
542
+ /**
543
+ * The initializer value of a `variable_declarator` — the named child after
544
+ * `name`. NOTE: deliberately duplicated in `csharp.ts` (the visitor is a
545
+ * standalone class with no shared base — repo convention). The two copies must
546
+ * stay in sync; there is no C#-specific shared module to host it, and the only
547
+ * module both files share is the generic `utils/ast-helpers` (types only).
548
+ */
521
549
  declaratorInit(declarator) {
522
550
  const name = declarator.childForFieldName('name');
523
551
  for (let i = 0; i < declarator.namedChildCount; i++) {
@@ -66,6 +66,12 @@
66
66
  * unresolved label.
67
67
  * - Async/await suspension points are modeled as straight-line (the awaited
68
68
  * continuation is not a separate flow), consistent with the TS visitor.
69
+ * - A value-position `switch_expression` (`k switch {…}`) with ≥2 arms IS modeled
70
+ * as a `switch-case` dispatch in three carriers (#2207): a single-declarator
71
+ * `var x = k switch {…}` (arms rejoin at a binding continuation), `return k
72
+ * switch {…}`, and an `=> k switch {…}` expression body (each arm returns).
73
+ * A value switch in any OTHER position — an assignment RHS (`x = k switch …`),
74
+ * a call argument, or a multi-declarator decl — stays INLINE (one block).
69
75
  * - Def/use harvest scope: see `csharp-harvest.ts` — member/element writes are
70
76
  * not scalar defs; nested-function bodies are opaque in both directions.
71
77
  *
@@ -92,7 +92,7 @@ class CsharpCfgWalk {
92
92
  dangling = [...scope.exits];
93
93
  break; // the rest of the sequence is consumed by the dispose scope
94
94
  }
95
- if (CONTROL_FLOW_TYPES.has(stmt.type)) {
95
+ if (this.breaksBlock(stmt)) {
96
96
  openSimple = undefined; // close any open straight-line block
97
97
  const res = this.visitStmt(stmt);
98
98
  if (res === null)
@@ -123,9 +123,29 @@ class CsharpCfgWalk {
123
123
  return { entry, exits: dangling };
124
124
  });
125
125
  }
126
+ /**
127
+ * Whether a statement breaks the current straight-line block. Adds the
128
+ * value-position switch carrier to the base {@link CONTROL_FLOW_TYPES} set: a
129
+ * `local_declaration_statement` whose single initializer is a modelable
130
+ * `switch_expression` (`var x = k switch {…}`, #2207) breaks so `visitStmt`
131
+ * models the arms as control flow instead of collapsing the decl to one block.
132
+ */
133
+ breaksBlock(stmt) {
134
+ if (this.isValueSwitchDecl(stmt))
135
+ return true;
136
+ return CONTROL_FLOW_TYPES.has(stmt.type);
137
+ }
126
138
  /** Dispatch one statement to its handler. Non-null except for empty blocks. */
127
139
  visitStmt(stmt) {
128
140
  switch (stmt.type) {
141
+ case 'local_declaration_statement': {
142
+ // `var x = k switch { … }` (#2207): the initializer is a value-position
143
+ // branch — model it as control flow and bind the result on the rejoin.
144
+ const branch = this.declValueSwitch(stmt);
145
+ if (branch)
146
+ return this.visitBindBranch(stmt, branch);
147
+ return this.visitSimple(stmt);
148
+ }
129
149
  case 'if_statement':
130
150
  return this.visitIf(stmt);
131
151
  case 'while_statement':
@@ -169,6 +189,18 @@ class CsharpCfgWalk {
169
189
  return { entry: idx, exits: [idx] };
170
190
  }
171
191
  visitReturn(stmt) {
192
+ // `return k switch { … };` (#2207): the returned value is a value-position
193
+ // branch — model it as control flow, with each arm returning (its value IS
194
+ // the function result), threading every active finalizer per arm.
195
+ const branch = stmt.namedChildren.find((c) => c.type !== 'comment');
196
+ if (branch && this.isModelableValueBranch(branch)) {
197
+ const res = this.visitBranchExpr(branch);
198
+ const finalizers = this.cfc.finalizersForReturn();
199
+ for (const ex of res.exits) {
200
+ wireJumpThroughFinalizers(this.builder, ex, finalizers, this.builder.exitIndex, 'return');
201
+ }
202
+ return { entry: res.entry, exits: [] };
203
+ }
172
204
  const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text, 'normal', this.harvest.facts(stmt));
173
205
  // A return crosses EVERY active finally (try/using/lock) before EXIT.
174
206
  wireJumpThroughFinalizers(this.builder, idx, this.cfc.finalizersForReturn(), this.builder.exitIndex, 'return');
@@ -446,6 +478,126 @@ class CsharpCfgWalk {
446
478
  isStatementLike(node) {
447
479
  return node.type.endsWith('_statement') || node.type === 'block';
448
480
  }
481
+ // ── value-position switch expression (#2207) ────────────────────────────────
482
+ /**
483
+ * The `switch_expression` initializer of a single-declarator
484
+ * `local_declaration_statement` (`var x = k switch {…}`) when it is a modelable
485
+ * value branch, else undefined. A `using` decl and a multi-declarator decl are
486
+ * excluded (the `using` dispose path / multi-declarator stay inline).
487
+ */
488
+ declValueSwitch(stmt) {
489
+ if (stmt.type !== 'local_declaration_statement')
490
+ return undefined;
491
+ if (this.isUsingLocalDecl(stmt))
492
+ return undefined;
493
+ const decl = stmt.namedChildren.find((c) => c.type === 'variable_declaration');
494
+ if (!decl)
495
+ return undefined;
496
+ const declarators = decl.namedChildren.filter((c) => c.type === 'variable_declarator');
497
+ if (declarators.length !== 1)
498
+ return undefined;
499
+ const init = this.declaratorInit(declarators[0]);
500
+ return init && this.isModelableValueBranch(init) ? init : undefined;
501
+ }
502
+ isValueSwitchDecl(stmt) {
503
+ return this.declValueSwitch(stmt) !== undefined;
504
+ }
505
+ /**
506
+ * The initializer of a `variable_declarator` — its named child after `name`.
507
+ * NOTE: deliberately duplicated in `csharp-harvest.ts` (the harvester is a
508
+ * standalone class with no shared base — repo convention). The two copies must
509
+ * stay in sync; there is no C#-specific shared module to host it, and the only
510
+ * module both files share is the generic `utils/ast-helpers` (types only).
511
+ */
512
+ declaratorInit(declarator) {
513
+ const name = declarator.childForFieldName('name');
514
+ for (let i = 0; i < declarator.namedChildCount; i++) {
515
+ const c = declarator.namedChild(i);
516
+ if (c && c.id !== name?.id)
517
+ return c;
518
+ }
519
+ return undefined;
520
+ }
521
+ /**
522
+ * Whether `node` is a value-position branch worth modeling as control flow
523
+ * (#2207): a `switch_expression` (`k switch {…}`) with ≥2 arms — a real
524
+ * dispatch. C# value-position `if` does not exist (the ternary `?:` is excluded,
525
+ * like elvis in Kotlin).
526
+ */
527
+ isModelableValueBranch(node) {
528
+ if (node.type !== 'switch_expression')
529
+ return false;
530
+ return node.namedChildren.filter((c) => c.type === 'switch_expression_arm').length >= 2;
531
+ }
532
+ /**
533
+ * Model a value-position `switch_expression` (`k switch { p => v, … }`) as a CFG
534
+ * dispatch: a discriminant block, each arm's value expression a block reached by
535
+ * a `switch-case` edge, all arms rejoining at a single exit. The arm patterns /
536
+ * `when` guards are harvested as conditional uses on the dispatch (a later arm
537
+ * test runs only when earlier arms didn't match), mirroring {@link visitSwitch}.
538
+ */
539
+ visitSwitchExpr(node) {
540
+ const arms = node.namedChildren.filter((c) => c.type === 'switch_expression_arm');
541
+ const discriminant = node.namedChildren.find((c) => c.type !== 'switch_expression_arm') ?? node;
542
+ const dispatch = this.builder.newBlock(startLineOf(node), endLineOf(discriminant), discriminant.text, 'normal', this.harvest.facts(discriminant));
543
+ const switchExit = this.builder.newBlock(endLineOf(node), endLineOf(node), '');
544
+ let hasCatchAll = false;
545
+ for (const arm of arms) {
546
+ const pattern = arm.namedChild(0);
547
+ const guard = arm.namedChildren.find((c) => c.type === 'when_clause');
548
+ if (pattern)
549
+ this.builder.attachFacts(dispatch, this.harvest.factsConditional(pattern));
550
+ if (guard) {
551
+ const inner = guard.namedChild(0);
552
+ if (inner)
553
+ this.builder.attachFacts(dispatch, this.harvest.factsConditional(inner));
554
+ }
555
+ // An unguarded `_`/`var` arm matches everything — the exhaustive default.
556
+ if (!guard && pattern && (pattern.type === 'discard' || pattern.type === 'var_pattern')) {
557
+ hasCatchAll = true;
558
+ }
559
+ const value = this.armValue(arm);
560
+ const armBlock = this.builder.newBlock(startLineOf(value ?? arm), endLineOf(value ?? arm), (value ?? arm).text, 'normal', value ? this.harvest.facts(value) : undefined);
561
+ this.builder.edge(dispatch, armBlock, 'switch-case');
562
+ this.builder.edge(armBlock, switchExit, 'seq');
563
+ }
564
+ // A non-exhaustive switch throws at runtime; conservatively keep EXIT directly
565
+ // reachable from the dispatch when no catch-all arm covers the no-match path.
566
+ if (!hasCatchAll)
567
+ this.builder.edge(dispatch, switchExit, 'switch-case');
568
+ return { entry: dispatch, exits: [switchExit] };
569
+ }
570
+ /** The value expression of a `switch_expression_arm` (the child after `=>`). */
571
+ armValue(arm) {
572
+ // pattern [when_clause] => value — the value is the LAST named child.
573
+ return arm.namedChild(arm.namedChildCount - 1) ?? undefined;
574
+ }
575
+ /** Model a value-position branch as control flow (only `switch_expression`). */
576
+ visitBranchExpr(node) {
577
+ return this.visitSwitchExpr(node);
578
+ }
579
+ /**
580
+ * An expression-bodied member's value (`=> k switch {…}`, #2207): if it is a
581
+ * modelable value branch, model its arms as control flow (each arm returns the
582
+ * function result); otherwise return null so the caller falls back to a single
583
+ * inline block.
584
+ */
585
+ tryVisitValueBranchBody(expr) {
586
+ return this.isModelableValueBranch(expr) ? this.visitBranchExpr(expr) : null;
587
+ }
588
+ /**
589
+ * `var x = k switch { … }` (#2207): visit the switch as control flow, then
590
+ * rejoin its arms at a facts-only continuation carrying ONLY the bound name's
591
+ * def (the discriminant + arm-value uses are already on the switch's blocks).
592
+ * The arms are now control-dependent on the dispatch, and `x` is defined at the
593
+ * join — mirrors the Java / Kotlin / Rust value-position binding.
594
+ */
595
+ visitBindBranch(stmt, branch) {
596
+ const res = this.visitBranchExpr(branch);
597
+ const cont = this.builder.newBlock(startLineOf(stmt), startLineOf(stmt), '', 'normal', this.harvest.bindingDefFacts(stmt));
598
+ this.builder.connect(res.exits, cont, 'seq');
599
+ return { entry: res.entry, exits: [cont] };
600
+ }
449
601
  visitTry(stmt) {
450
602
  const bodyNode = stmt.childForFieldName('body');
451
603
  const catchClauses = [];
@@ -673,6 +825,14 @@ function buildFunctionCfg(fnNode, filePath) {
673
825
  // Expression-bodied member / single-expression lambda: one block whose
674
826
  // value is returned. For an arrow clause the value is its inner expression.
675
827
  const expr = body.type === 'arrow_expression_clause' ? (body.namedChild(0) ?? body) : body;
828
+ // `=> k switch { … }` (#2207): model the arms as control flow, each arm
829
+ // returning the function result, instead of one inline block.
830
+ const branchRes = new CsharpCfgWalk(builder, harvest).tryVisitValueBranchBody(expr);
831
+ if (branchRes) {
832
+ builder.edge(builder.entryIndex, branchRes.entry, 'seq');
833
+ builder.connect(branchRes.exits, builder.exitIndex, 'return');
834
+ return builder.finish(harvest.bindingTable());
835
+ }
676
836
  const blk = builder.newBlock(startLineOf(expr), endLineOf(expr), expr.text, 'normal', harvest.facts(expr));
677
837
  builder.edge(builder.entryIndex, blk, 'seq');
678
838
  builder.edge(blk, builder.exitIndex, 'return');
@@ -131,6 +131,14 @@ export declare class DartHarvester {
131
131
  facts(node: SyntaxNode): StatementFacts;
132
132
  /** Facts for an expression whose WHOLE evaluation is conditional (case tests). */
133
133
  factsConditional(node: SyntaxNode): StatementFacts;
134
+ /**
135
+ * Def-ONLY facts for a value-position binding carrier (`var x = switch (…) {…}`,
136
+ * #2207): just the declared name(s)' def, attached to the continuation block the
137
+ * switch arms rejoin. The subject + arm-value USES are already harvested onto
138
+ * the branch's own blocks, so this must NOT re-walk the value — only each
139
+ * `initialized_variable_definition`'s `name` (and trailing binders) is a def.
140
+ */
141
+ bindingDefFacts(stmt: SyntaxNode): StatementFacts | undefined;
134
142
  /**
135
143
  * Facts for a `for` head. For-in: the loop var name is a def, the collection a
136
144
  * use. C-style: the init/condition/update sub-expressions are walked for
@@ -186,6 +186,32 @@ export class DartHarvester {
186
186
  this.conditional(() => this.walkValue(node, acc));
187
187
  return acc.finish();
188
188
  }
189
+ /**
190
+ * Def-ONLY facts for a value-position binding carrier (`var x = switch (…) {…}`,
191
+ * #2207): just the declared name(s)' def, attached to the continuation block the
192
+ * switch arms rejoin. The subject + arm-value USES are already harvested onto
193
+ * the branch's own blocks, so this must NOT re-walk the value — only each
194
+ * `initialized_variable_definition`'s `name` (and trailing binders) is a def.
195
+ */
196
+ bindingDefFacts(stmt) {
197
+ const acc = new FactAccumulator(stmt.startPosition.row + 1);
198
+ for (const def of stmt.namedChildren) {
199
+ if (def.type !== 'initialized_variable_definition')
200
+ continue;
201
+ const name = def.childForFieldName('name');
202
+ if (name)
203
+ this.def(name, acc);
204
+ for (let i = 0; i < def.namedChildCount; i++) {
205
+ const c = def.namedChild(i);
206
+ if (c?.type !== 'initialized_identifier')
207
+ continue;
208
+ const id = c.namedChildren.find((g) => g.type === 'identifier');
209
+ if (id)
210
+ this.def(id, acc);
211
+ }
212
+ }
213
+ return acc.defCount() ? acc.finish() : undefined;
214
+ }
189
215
  /**
190
216
  * Facts for a `for` head. For-in: the loop var name is a def, the collection a
191
217
  * use. C-style: the init/condition/update sub-expressions are walked for
@@ -88,10 +88,13 @@
88
88
  * - a closure (`function_expression`) is collected as its OWN function by
89
89
  * `isFunction`, so its body gets a standalone CFG; in the ENCLOSING function it
90
90
  * is an opaque straight-line value (its body is not followed inline).
91
- * - `switch_expression` / `if`-as-expression / `?:` / `??` / `?.` used as a VALUE
92
- * are left INLINE inside their owning statement's block their conditional
93
- * sub-evaluation is a HARVEST may-def concern (see dart-harvest.ts), not a CFG
94
- * split (consistent with the TS `&&`/`??` treatment).
91
+ * - a value-position `switch_expression` (Dart 3) with ≥2 arms IS modeled as a
92
+ * `switch-case` dispatch in two carriers (#2207): a single-binding `var x =
93
+ * switch (v) {…}` (arms rejoin at a binding continuation) and `return switch
94
+ * (v) {…}` (each arm returns). A `switch_expression` in any OTHER position — a
95
+ * call argument, a multi-binding decl — stays INLINE (its conditional arm
96
+ * sub-evaluation is a HARVEST may-def concern, see dart-harvest.ts). `?:` /
97
+ * `??` / `?.` micro-branches are excluded by design (like the TS treatment).
95
98
  *
96
99
  * Known limitations:
97
100
  * - block-scope shadowing in the harvest is flattened to one function table (see
@@ -115,6 +115,12 @@ class DartCfgWalk {
115
115
  return true; // a stray label sibling — queue it
116
116
  if (isThrowStatement(stmt) || isRethrowStatement(stmt))
117
117
  return true;
118
+ // `var x = switch (v) { … }` (#2207): a value-position switch breaks so
119
+ // `visitStmt` models the arms as control flow instead of coalescing.
120
+ if (stmt.type === 'local_variable_declaration') {
121
+ const v = this.directValue(stmt);
122
+ return v !== undefined && this.isModelableValueBranch(v);
123
+ }
118
124
  return CONTROL_FLOW_TYPES.has(stmt.type);
119
125
  }
120
126
  /**
@@ -141,6 +147,14 @@ class DartCfgWalk {
141
147
  if (isRethrowStatement(stmt))
142
148
  return this.visitRethrow(stmt);
143
149
  switch (stmt.type) {
150
+ case 'local_variable_declaration': {
151
+ // `var x = switch (v) { … }` (#2207): the value is a value-position
152
+ // branch — model it as control flow and bind the result on the rejoin.
153
+ const value = this.directValue(stmt);
154
+ if (value && this.isModelableValueBranch(value))
155
+ return this.visitBindBranch(stmt, value);
156
+ return this.visitSimple(stmt);
157
+ }
144
158
  case 'if_statement':
145
159
  return this.visitIf(stmt);
146
160
  case 'for_statement':
@@ -180,6 +194,18 @@ class DartCfgWalk {
180
194
  // ── jumps (return / throw / rethrow / break / continue / assert) ──────────
181
195
  /** `return [expr];` — threads through every active finalizer before EXIT. */
182
196
  visitReturn(stmt) {
197
+ // `return switch (v) { … };` (#2207): the returned value is a value-position
198
+ // branch — model it as control flow, with each arm returning (its value IS
199
+ // the function result), threading every active finalizer per arm.
200
+ const branch = stmt.namedChildren.find((c) => !isComment(c));
201
+ if (branch && this.isModelableValueBranch(branch)) {
202
+ const res = this.visitBranchExpr(branch);
203
+ const finalizers = this.cfc.finalizersForReturn();
204
+ for (const ex of res.exits) {
205
+ wireJumpThroughFinalizers(this.builder, ex, finalizers, this.builder.exitIndex, 'return');
206
+ }
207
+ return { entry: res.entry, exits: [] };
208
+ }
183
209
  const idx = this.builder.newBlock(startLineOf(stmt), endLineOf(stmt), stmt.text, 'normal', this.harvest.facts(stmt));
184
210
  wireJumpThroughFinalizers(this.builder, idx, this.cfc.finalizersForReturn(), this.builder.exitIndex, 'return');
185
211
  return { entry: idx, exits: [] };
@@ -366,7 +392,12 @@ class DartCfgWalk {
366
392
  */
367
393
  visitSwitch(stmt) {
368
394
  const labels = this.takeLabels();
369
- const value = stmt.childForFieldName('condition');
395
+ // The `condition` field is a `parenthesized_expression` (verified) — unwrap it
396
+ // so the dispatch text/discriminant matches the value-position `visitSwitchExpr`
397
+ // form (`switch x`, not `switch (x)`). The harvest walks into the paren either
398
+ // way, so the def/use facts are unchanged — only the block text normalizes.
399
+ const condRaw = stmt.childForFieldName('condition');
400
+ const value = condRaw ? this.unwrapParen(condRaw) : undefined;
370
401
  const dispatch = this.builder.newBlock(startLineOf(stmt), value ? endLineOf(value) : startLineOf(stmt), value ? `switch ${value.text}` : 'switch', 'normal', value ? this.harvest.facts(value) : undefined);
371
402
  const switchExit = this.builder.newBlock(endLineOf(stmt), endLineOf(stmt), '');
372
403
  const block = stmt.childForFieldName('body');
@@ -469,6 +500,122 @@ class DartCfgWalk {
469
500
  const id = cont.namedChildren.find((ch) => ch.type === 'identifier');
470
501
  return id?.text || undefined;
471
502
  }
503
+ // ── value-position switch expression (#2207) ────────────────────────────────
504
+ /**
505
+ * The direct value of a `local_variable_declaration` with a SINGLE
506
+ * `initialized_variable_definition` (`var x = <value>`): its `value` field.
507
+ * Returns undefined for a multi-binding decl (`var a = …, b = …`) — modeling
508
+ * those arm-by-arm is out of scope, so they coalesce inline.
509
+ */
510
+ directValue(stmt) {
511
+ const defs = stmt.namedChildren.filter((c) => c.type === 'initialized_variable_definition');
512
+ if (defs.length !== 1)
513
+ return undefined;
514
+ return defs[0].childForFieldName('value') ?? undefined;
515
+ }
516
+ /**
517
+ * Whether `node` is a value-position branch worth modeling as control flow
518
+ * (#2207): a `switch_expression` (Dart 3) with ≥2 arms — a real dispatch. Dart's
519
+ * value-position `if` does not exist; the ternary `?:` is excluded by design.
520
+ */
521
+ isModelableValueBranch(node) {
522
+ if (node.type !== 'switch_expression')
523
+ return false;
524
+ return node.namedChildren.filter((c) => c.type === 'switch_expression_case').length >= 2;
525
+ }
526
+ /** Model a value-position branch as control flow (only `switch_expression`). */
527
+ visitBranchExpr(node) {
528
+ return this.visitSwitchExpr(node);
529
+ }
530
+ /**
531
+ * Model a value-position `switch (v) { p [when g] => e, _ => e }` (Dart 3) as a
532
+ * CFG dispatch: a discriminant block, each arm's value a block reached by a
533
+ * `switch-case` edge, all arms rejoining at one exit (no fallthrough). The arm
534
+ * PATTERN and any `when` GUARD are harvested as conditional uses on the dispatch
535
+ * (they evaluate before the body, only when earlier arms missed); a Dart call
536
+ * value parses as `identifier` + `selector` (multiple children), so the arm-value
537
+ * facts come from each post-`=>` child. Only an UNGUARDED `_` arm is the
538
+ * exhaustive catch-all — a guarded `_ when …` is NOT (the no-match path still
539
+ * needs the conservative edge), mirroring the C# `visitSwitchExpr`.
540
+ */
541
+ visitSwitchExpr(node) {
542
+ const condRaw = node.childForFieldName('condition');
543
+ const cond = condRaw ? this.unwrapParen(condRaw) : node;
544
+ const dispatch = this.builder.newBlock(startLineOf(node), endLineOf(cond), `switch ${cond.text}`, 'normal', this.harvest.facts(cond));
545
+ const switchExit = this.builder.newBlock(endLineOf(node), endLineOf(node), '');
546
+ const arms = node.namedChildren.filter((c) => c.type === 'switch_expression_case');
547
+ let hasCatchAll = false;
548
+ for (const arm of arms) {
549
+ const { pattern, guards, values } = this.armParts(arm);
550
+ // The pattern + `when` guard are conditional dispatch tests, NOT arm-value
551
+ // uses — harvest them onto the dispatch (mirrors casePatterns for switch_statement).
552
+ if (pattern)
553
+ this.builder.attachFacts(dispatch, this.harvest.factsConditional(pattern));
554
+ for (const g of guards)
555
+ this.builder.attachFacts(dispatch, this.harvest.factsConditional(g));
556
+ if (pattern && pattern.text === '_' && guards.length === 0)
557
+ hasCatchAll = true;
558
+ const first = values[0] ?? arm;
559
+ const last = values[values.length - 1] ?? arm;
560
+ const armBlock = this.builder.newBlock(startLineOf(first), endLineOf(last), values.map((c) => c.text).join('') || arm.text, 'normal', undefined);
561
+ for (const v of values)
562
+ this.builder.attachFacts(armBlock, this.harvest.facts(v));
563
+ this.builder.edge(dispatch, armBlock, 'switch-case');
564
+ this.builder.edge(armBlock, switchExit, 'seq');
565
+ }
566
+ // A non-exhaustive Dart switch expression throws at runtime; conservatively
567
+ // keep EXIT reachable via a no-match edge when no `_` catch-all arm exists.
568
+ if (!hasCatchAll)
569
+ this.builder.edge(dispatch, switchExit, 'switch-case');
570
+ return { entry: dispatch, exits: [switchExit] };
571
+ }
572
+ /**
573
+ * Split a `switch_expression_case` at the `=>` token: the PATTERN (first named
574
+ * child before `=>`), any `when` GUARD (named children between the pattern and
575
+ * `=>` — tree-sitter-dart parses the guard as a bare sibling, not a wrapper),
576
+ * and the VALUE expression (named children after `=>` — a Dart call is split
577
+ * across `identifier` + `selector`, hence an array).
578
+ */
579
+ armParts(arm) {
580
+ const before = [];
581
+ const values = [];
582
+ let seenArrow = false;
583
+ for (let i = 0; i < arm.childCount; i++) {
584
+ const c = arm.child(i);
585
+ if (!c)
586
+ continue;
587
+ if (!c.isNamed) {
588
+ if (c.text === '=>')
589
+ seenArrow = true;
590
+ continue;
591
+ }
592
+ if (isComment(c))
593
+ continue;
594
+ (seenArrow ? values : before).push(c);
595
+ }
596
+ return { pattern: before[0], guards: before.slice(1), values };
597
+ }
598
+ /** Strip a `parenthesized_expression` wrapper (a switch/if condition). */
599
+ unwrapParen(node) {
600
+ if (node.type === 'parenthesized_expression') {
601
+ const inner = node.namedChildren.find((c) => !isComment(c));
602
+ if (inner)
603
+ return inner;
604
+ }
605
+ return node;
606
+ }
607
+ /**
608
+ * `var x = switch (v) { … }` (#2207): visit the switch as control flow, then
609
+ * rejoin its arms at a facts-only continuation carrying ONLY the declared name's
610
+ * def (the subject + arm-value uses are already on the switch's blocks). The
611
+ * arms are now control-dependent on the dispatch — mirrors Java / Kotlin / Rust.
612
+ */
613
+ visitBindBranch(stmt, branch) {
614
+ const res = this.visitBranchExpr(branch);
615
+ const cont = this.builder.newBlock(startLineOf(stmt), startLineOf(stmt), '', 'normal', this.harvest.bindingDefFacts(stmt));
616
+ this.builder.connect(res.exits, cont, 'seq');
617
+ return { entry: res.entry, exits: [cont] };
618
+ }
472
619
  // ── try / on / catch / finally ─────────────────────────────────────────────
473
620
  /**
474
621
  * `try body:block (on TYPE? catch_clause? block)* finally_clause?`. The handler
@@ -58,6 +58,14 @@ export declare class JavaHarvester extends ScopeTreeHarvester {
58
58
  facts(node: SyntaxNode): StatementFacts;
59
59
  /** Facts for an expression whose WHOLE evaluation is conditional (case tests). */
60
60
  factsConditional(node: SyntaxNode): StatementFacts;
61
+ /**
62
+ * Def-ONLY facts for a value-position binding carrier (`var x = switch (…) {…}`,
63
+ * #2207): just the declared name(s)' def, attached to the continuation block the
64
+ * switch arms rejoin. The switch subject + arm-value USES are already harvested
65
+ * onto the branch's own blocks ({@link facts} on each arm), so this must NOT
66
+ * re-walk the value — only each `variable_declarator`'s `name` is a def here.
67
+ */
68
+ bindingDefFacts(stmt: SyntaxNode): StatementFacts | undefined;
61
69
  /** Facts for a `for (T name : value)` head: name binds, value is used. */
62
70
  forEachHeadFacts(stmt: SyntaxNode): StatementFacts;
63
71
  /** Facts for the resource-close finalizer: each resource is USED on close. */
@@ -147,6 +147,25 @@ export class JavaHarvester extends ScopeTreeHarvester {
147
147
  this.conditional(() => this.walkValue(node, acc));
148
148
  return acc.finish();
149
149
  }
150
+ /**
151
+ * Def-ONLY facts for a value-position binding carrier (`var x = switch (…) {…}`,
152
+ * #2207): just the declared name(s)' def, attached to the continuation block the
153
+ * switch arms rejoin. The switch subject + arm-value USES are already harvested
154
+ * onto the branch's own blocks ({@link facts} on each arm), so this must NOT
155
+ * re-walk the value — only each `variable_declarator`'s `name` is a def here.
156
+ */
157
+ bindingDefFacts(stmt) {
158
+ const acc = new FactAccumulator(stmt.startPosition.row + 1);
159
+ for (let i = 0; i < stmt.namedChildCount; i++) {
160
+ const d = stmt.namedChild(i);
161
+ if (d?.type !== 'variable_declarator')
162
+ continue;
163
+ const name = d.childForFieldName('name');
164
+ if (name)
165
+ this.def(name, acc);
166
+ }
167
+ return acc.defCount() ? acc.finish() : undefined;
168
+ }
150
169
  /** Facts for a `for (T name : value)` head: name binds, value is used. */
151
170
  forEachHeadFacts(stmt) {
152
171
  const acc = new FactAccumulator(stmt.startPosition.row + 1);
@@ -74,11 +74,12 @@
74
74
  * TS `visitTry` over-approximation.
75
75
  *
76
76
  * Known limitations:
77
- * - `switch` as an EXPRESSION value (`int r = switch (x) { … };`) is left INLINE
78
- * inside its owning statement's block its arms are not modeled as separate
79
- * CFG blocks (the value flows to the assignment). Only a `switch` used as a
80
- * STATEMENT (a direct statement child) is modeled as a dispatch construct.
81
- * This mirrors the C# `switch_expression`-in-return handlingdocumented gap.
77
+ * - A value-position `switch` with ≥2 arms is modeled as control flow in the two
78
+ * highest-value carriers (#2207): a single-declarator `var x = switch (…) {…}`
79
+ * (arms rejoin at a binding continuation) and `return switch (…) {…}` (each arm
80
+ * returns). A value-position `switch` in any OTHER position an assignment RHS
81
+ * (`x = switch …`), a call argument, or a multi-declarator declis still left
82
+ * INLINE inside its owning block (the value flows to one coalesced block).
82
83
  * - `yield` (in a switch expression) continues to the next statement (it yields
83
84
  * one value to the enclosing switch and the arm ends); the switch-expression
84
85
  * state machine is not modeled, consistent with the inline-value-switch gap.