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.
- package/dist/core/ingestion/cfg/control-flow-context.d.ts +9 -0
- package/dist/core/ingestion/cfg/control-flow-context.js +11 -0
- package/dist/core/ingestion/cfg/visitors/csharp-harvest.d.ts +15 -1
- package/dist/core/ingestion/cfg/visitors/csharp-harvest.js +29 -1
- package/dist/core/ingestion/cfg/visitors/csharp.d.ts +6 -0
- package/dist/core/ingestion/cfg/visitors/csharp.js +161 -1
- package/dist/core/ingestion/cfg/visitors/dart-harvest.d.ts +8 -0
- package/dist/core/ingestion/cfg/visitors/dart-harvest.js +26 -0
- package/dist/core/ingestion/cfg/visitors/dart.d.ts +7 -4
- package/dist/core/ingestion/cfg/visitors/dart.js +148 -1
- package/dist/core/ingestion/cfg/visitors/java-harvest.d.ts +8 -0
- package/dist/core/ingestion/cfg/visitors/java-harvest.js +19 -0
- package/dist/core/ingestion/cfg/visitors/java.d.ts +6 -5
- package/dist/core/ingestion/cfg/visitors/java.js +106 -10
- package/dist/core/ingestion/cfg/visitors/kotlin-harvest.d.ts +9 -0
- package/dist/core/ingestion/cfg/visitors/kotlin-harvest.js +20 -0
- package/dist/core/ingestion/cfg/visitors/kotlin.d.ts +8 -6
- package/dist/core/ingestion/cfg/visitors/kotlin.js +58 -9
- package/dist/core/ingestion/cfg/visitors/php-harvest.d.ts +8 -0
- package/dist/core/ingestion/cfg/visitors/php-harvest.js +20 -0
- package/dist/core/ingestion/cfg/visitors/php.d.ts +8 -6
- package/dist/core/ingestion/cfg/visitors/php.js +110 -1
- package/dist/core/ingestion/cfg/visitors/swift-harvest.d.ts +8 -0
- package/dist/core/ingestion/cfg/visitors/swift-harvest.js +18 -0
- package/dist/core/ingestion/cfg/visitors/swift.d.ts +6 -0
- package/dist/core/ingestion/cfg/visitors/swift.js +66 -0
- 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
|
-
/**
|
|
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
|
-
/**
|
|
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 (
|
|
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`
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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
|
-
|
|
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`
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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 decl — is 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.
|