@optave/codegraph 3.1.0 → 3.1.2
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/README.md +5 -5
- package/grammars/tree-sitter-go.wasm +0 -0
- package/package.json +8 -9
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -0
- package/src/ast-analysis/rules/csharp.js +201 -0
- package/src/ast-analysis/rules/go.js +182 -0
- package/src/ast-analysis/rules/index.js +82 -0
- package/src/ast-analysis/rules/java.js +175 -0
- package/src/ast-analysis/rules/javascript.js +246 -0
- package/src/ast-analysis/rules/php.js +219 -0
- package/src/ast-analysis/rules/python.js +196 -0
- package/src/ast-analysis/rules/ruby.js +204 -0
- package/src/ast-analysis/rules/rust.js +173 -0
- package/src/ast-analysis/shared.js +223 -0
- package/src/ast-analysis/visitor-utils.js +176 -0
- package/src/ast-analysis/visitor.js +162 -0
- package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
- package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
- package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
- package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
- package/src/ast.js +26 -166
- package/src/audit.js +2 -88
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +82 -172
- package/src/builder.js +48 -184
- package/src/cfg.js +148 -1174
- package/src/check.js +1 -84
- package/src/cli.js +118 -197
- package/src/cochange.js +1 -39
- package/src/commands/audit.js +88 -0
- package/src/commands/batch.js +26 -0
- package/src/commands/branch-compare.js +97 -0
- package/src/commands/cfg.js +55 -0
- package/src/commands/check.js +82 -0
- package/src/commands/cochange.js +37 -0
- package/src/commands/communities.js +69 -0
- package/src/commands/complexity.js +77 -0
- package/src/commands/dataflow.js +110 -0
- package/src/commands/flow.js +70 -0
- package/src/commands/manifesto.js +77 -0
- package/src/commands/owners.js +52 -0
- package/src/commands/query.js +21 -0
- package/src/commands/sequence.js +33 -0
- package/src/commands/structure.js +64 -0
- package/src/commands/triage.js +49 -0
- package/src/communities.js +22 -96
- package/src/complexity.js +234 -1591
- package/src/cycles.js +1 -1
- package/src/dataflow.js +274 -1352
- package/src/db/connection.js +88 -0
- package/src/db/migrations.js +312 -0
- package/src/db/query-builder.js +280 -0
- package/src/db/repository/build-stmts.js +104 -0
- package/src/db/repository/cfg.js +83 -0
- package/src/db/repository/cochange.js +41 -0
- package/src/db/repository/complexity.js +15 -0
- package/src/db/repository/dataflow.js +12 -0
- package/src/db/repository/edges.js +259 -0
- package/src/db/repository/embeddings.js +40 -0
- package/src/db/repository/graph-read.js +39 -0
- package/src/db/repository/index.js +42 -0
- package/src/db/repository/nodes.js +236 -0
- package/src/db.js +58 -399
- package/src/embedder.js +158 -174
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +153 -222
- package/src/index.js +53 -16
- package/src/infrastructure/result-formatter.js +21 -0
- package/src/infrastructure/test-filter.js +7 -0
- package/src/kinds.js +50 -0
- package/src/manifesto.js +1 -82
- package/src/mcp.js +37 -20
- package/src/owners.js +127 -182
- package/src/queries-cli.js +866 -0
- package/src/queries.js +1271 -2416
- package/src/sequence.js +179 -223
- package/src/structure.js +211 -269
- package/src/triage.js +117 -212
- package/src/viewer.js +1 -1
- package/src/watcher.js +7 -4
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visitor: Compute cognitive/cyclomatic complexity, max nesting, and Halstead metrics.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the computeAllMetrics() DFS walk in complexity.js with a visitor that
|
|
5
|
+
* plugs into the unified walkWithVisitors framework. Operates per-function:
|
|
6
|
+
* resets accumulators on enterFunction, emits results on exitFunction.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
computeHalsteadDerived,
|
|
11
|
+
computeLOCMetrics,
|
|
12
|
+
computeMaintainabilityIndex,
|
|
13
|
+
} from '../metrics.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a complexity visitor for use with walkWithVisitors.
|
|
17
|
+
*
|
|
18
|
+
* When used in file-level mode (walking an entire file), this visitor collects
|
|
19
|
+
* per-function metrics using enterFunction/exitFunction hooks. When used in
|
|
20
|
+
* function-level mode (walking a single function node), it collects one result.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} cRules - COMPLEXITY_RULES for the language
|
|
23
|
+
* @param {object} [hRules] - HALSTEAD_RULES for the language (null if unavailable)
|
|
24
|
+
* @param {object} [options]
|
|
25
|
+
* @param {boolean} [options.fileLevelWalk=false] - true when walking an entire file
|
|
26
|
+
* @returns {Visitor}
|
|
27
|
+
*/
|
|
28
|
+
export function createComplexityVisitor(cRules, hRules, options = {}) {
|
|
29
|
+
const { fileLevelWalk = false, langId = null } = options;
|
|
30
|
+
|
|
31
|
+
// Per-function accumulators
|
|
32
|
+
let cognitive = 0;
|
|
33
|
+
let cyclomatic = 1;
|
|
34
|
+
let maxNesting = 0;
|
|
35
|
+
let operators = hRules ? new Map() : null;
|
|
36
|
+
let operands = hRules ? new Map() : null;
|
|
37
|
+
let halsteadSkipDepth = 0;
|
|
38
|
+
|
|
39
|
+
// In file-level mode, we only count when inside a function
|
|
40
|
+
let activeFuncNode = null;
|
|
41
|
+
let activeFuncName = null;
|
|
42
|
+
// Nesting depth relative to the active function (for nested functions)
|
|
43
|
+
let funcDepth = 0;
|
|
44
|
+
|
|
45
|
+
// Collected results (one per function)
|
|
46
|
+
const results = [];
|
|
47
|
+
|
|
48
|
+
function reset() {
|
|
49
|
+
cognitive = 0;
|
|
50
|
+
cyclomatic = 1;
|
|
51
|
+
maxNesting = 0;
|
|
52
|
+
operators = hRules ? new Map() : null;
|
|
53
|
+
operands = hRules ? new Map() : null;
|
|
54
|
+
halsteadSkipDepth = 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function collectResult(funcNode) {
|
|
58
|
+
const halstead =
|
|
59
|
+
hRules && operators && operands ? computeHalsteadDerived(operators, operands) : null;
|
|
60
|
+
const loc = computeLOCMetrics(funcNode, langId);
|
|
61
|
+
const volume = halstead ? halstead.volume : 0;
|
|
62
|
+
const commentRatio = loc.loc > 0 ? loc.commentLines / loc.loc : 0;
|
|
63
|
+
const mi = computeMaintainabilityIndex(volume, cyclomatic, loc.sloc, commentRatio);
|
|
64
|
+
|
|
65
|
+
return { cognitive, cyclomatic, maxNesting, halstead, loc, mi };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
name: 'complexity',
|
|
70
|
+
functionNodeTypes: cRules.functionNodes,
|
|
71
|
+
|
|
72
|
+
enterFunction(funcNode, funcName, _context) {
|
|
73
|
+
if (fileLevelWalk) {
|
|
74
|
+
if (!activeFuncNode) {
|
|
75
|
+
// Top-level function: start fresh
|
|
76
|
+
reset();
|
|
77
|
+
activeFuncNode = funcNode;
|
|
78
|
+
activeFuncName = funcName;
|
|
79
|
+
funcDepth = 0;
|
|
80
|
+
} else {
|
|
81
|
+
// Nested function: increase nesting for complexity
|
|
82
|
+
funcDepth++;
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
// Function-level mode: track nested functions for correct nesting depth
|
|
86
|
+
funcDepth++;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
exitFunction(funcNode, _funcName, _context) {
|
|
91
|
+
if (fileLevelWalk) {
|
|
92
|
+
if (funcNode === activeFuncNode) {
|
|
93
|
+
// Leaving the top-level function: emit result
|
|
94
|
+
results.push({
|
|
95
|
+
funcNode,
|
|
96
|
+
funcName: activeFuncName,
|
|
97
|
+
metrics: collectResult(funcNode),
|
|
98
|
+
});
|
|
99
|
+
activeFuncNode = null;
|
|
100
|
+
activeFuncName = null;
|
|
101
|
+
} else {
|
|
102
|
+
funcDepth--;
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
funcDepth--;
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
enterNode(node, context) {
|
|
110
|
+
// In file-level mode, skip nodes outside any function
|
|
111
|
+
if (fileLevelWalk && !activeFuncNode) return;
|
|
112
|
+
|
|
113
|
+
const type = node.type;
|
|
114
|
+
const nestingLevel = fileLevelWalk ? context.nestingLevel + funcDepth : context.nestingLevel;
|
|
115
|
+
|
|
116
|
+
// ── Halstead classification ──
|
|
117
|
+
if (hRules) {
|
|
118
|
+
if (hRules.skipTypes.has(type)) halsteadSkipDepth++;
|
|
119
|
+
if (halsteadSkipDepth === 0) {
|
|
120
|
+
if (hRules.compoundOperators.has(type)) {
|
|
121
|
+
operators.set(type, (operators.get(type) || 0) + 1);
|
|
122
|
+
}
|
|
123
|
+
if (node.childCount === 0) {
|
|
124
|
+
if (hRules.operatorLeafTypes.has(type)) {
|
|
125
|
+
operators.set(type, (operators.get(type) || 0) + 1);
|
|
126
|
+
} else if (hRules.operandLeafTypes.has(type)) {
|
|
127
|
+
const text = node.text;
|
|
128
|
+
operands.set(text, (operands.get(text) || 0) + 1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Complexity: track nesting depth ──
|
|
135
|
+
if (nestingLevel > maxNesting) maxNesting = nestingLevel;
|
|
136
|
+
|
|
137
|
+
// Handle logical operators in binary expressions
|
|
138
|
+
if (type === cRules.logicalNodeType) {
|
|
139
|
+
const op = node.child(1)?.type;
|
|
140
|
+
if (op && cRules.logicalOperators.has(op)) {
|
|
141
|
+
cyclomatic++;
|
|
142
|
+
const parent = node.parent;
|
|
143
|
+
let sameSequence = false;
|
|
144
|
+
if (parent && parent.type === cRules.logicalNodeType) {
|
|
145
|
+
const parentOp = parent.child(1)?.type;
|
|
146
|
+
if (parentOp === op) sameSequence = true;
|
|
147
|
+
}
|
|
148
|
+
if (!sameSequence) cognitive++;
|
|
149
|
+
// Don't skip children — walker handles recursion
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Handle optional chaining (cyclomatic only)
|
|
154
|
+
if (type === cRules.optionalChainType) {
|
|
155
|
+
cyclomatic++;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handle branch/control flow nodes (skip keyword leaf tokens)
|
|
159
|
+
if (cRules.branchNodes.has(type) && node.childCount > 0) {
|
|
160
|
+
// Pattern A: else clause wraps if (JS/C#/Rust)
|
|
161
|
+
if (cRules.elseNodeType && type === cRules.elseNodeType) {
|
|
162
|
+
const firstChild = node.namedChild(0);
|
|
163
|
+
if (firstChild && firstChild.type === cRules.ifNodeType) {
|
|
164
|
+
// else-if: the if_statement child handles its own increment
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
cognitive++;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Pattern B: explicit elif node (Python/Ruby/PHP)
|
|
172
|
+
if (cRules.elifNodeType && type === cRules.elifNodeType) {
|
|
173
|
+
cognitive++;
|
|
174
|
+
cyclomatic++;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Detect else-if via Pattern A or C
|
|
179
|
+
let isElseIf = false;
|
|
180
|
+
if (type === cRules.ifNodeType) {
|
|
181
|
+
if (cRules.elseViaAlternative) {
|
|
182
|
+
isElseIf =
|
|
183
|
+
node.parent?.type === cRules.ifNodeType &&
|
|
184
|
+
node.parent.childForFieldName('alternative')?.id === node.id;
|
|
185
|
+
} else if (cRules.elseNodeType) {
|
|
186
|
+
isElseIf = node.parent?.type === cRules.elseNodeType;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (isElseIf) {
|
|
191
|
+
cognitive++;
|
|
192
|
+
cyclomatic++;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Regular branch node
|
|
197
|
+
cognitive += 1 + nestingLevel;
|
|
198
|
+
cyclomatic++;
|
|
199
|
+
|
|
200
|
+
if (cRules.switchLikeNodes?.has(type)) {
|
|
201
|
+
cyclomatic--;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Nesting nodes are handled by the walker's nestingNodeTypes option
|
|
205
|
+
// But we still need them to count in complexity — they already do above
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Pattern C plain else: block that is the alternative of an if_statement (Go/Java)
|
|
209
|
+
if (
|
|
210
|
+
cRules.elseViaAlternative &&
|
|
211
|
+
type !== cRules.ifNodeType &&
|
|
212
|
+
node.parent?.type === cRules.ifNodeType &&
|
|
213
|
+
node.parent.childForFieldName('alternative')?.id === node.id
|
|
214
|
+
) {
|
|
215
|
+
cognitive++;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Handle case nodes (cyclomatic only, skip keyword leaves)
|
|
219
|
+
if (cRules.caseNodes.has(type) && node.childCount > 0) {
|
|
220
|
+
cyclomatic++;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Handle nested function definitions (increase nesting)
|
|
224
|
+
// In file-level mode funcDepth handles this; in function-level mode the
|
|
225
|
+
// nestingNodeTypes option should include function nodes
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
exitNode(node) {
|
|
229
|
+
// Decrement skip depth when leaving a skip-type subtree
|
|
230
|
+
if (hRules?.skipTypes.has(node.type)) {
|
|
231
|
+
halsteadSkipDepth--;
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
finish() {
|
|
236
|
+
if (fileLevelWalk) {
|
|
237
|
+
return results;
|
|
238
|
+
}
|
|
239
|
+
// Function-level mode: return single result (no funcNode reference needed)
|
|
240
|
+
return collectResult({ text: '' });
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visitor: Extract dataflow information (define-use chains, arg flows, mutations).
|
|
3
|
+
*
|
|
4
|
+
* Replaces the standalone extractDataflow() visit logic in dataflow.js with a
|
|
5
|
+
* visitor that plugs into the unified walkWithVisitors framework.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: The original dataflow walk uses `node.namedChildren` while the visitor
|
|
8
|
+
* framework uses `node.child(i)` (all children). This visitor handles both
|
|
9
|
+
* named and unnamed children correctly since the classification logic only
|
|
10
|
+
* cares about specific node types/fields, not about traversal order.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
collectIdentifiers,
|
|
15
|
+
extractParamNames,
|
|
16
|
+
extractParams,
|
|
17
|
+
functionName,
|
|
18
|
+
isIdent,
|
|
19
|
+
memberReceiver,
|
|
20
|
+
resolveCalleeName,
|
|
21
|
+
truncate,
|
|
22
|
+
} from '../visitor-utils.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a dataflow visitor for use with walkWithVisitors.
|
|
26
|
+
*
|
|
27
|
+
* @param {object} rules - DATAFLOW_RULES for the language
|
|
28
|
+
* @returns {Visitor}
|
|
29
|
+
*/
|
|
30
|
+
export function createDataflowVisitor(rules) {
|
|
31
|
+
const isCallNode = rules.callNodes ? (t) => rules.callNodes.has(t) : (t) => t === rules.callNode;
|
|
32
|
+
|
|
33
|
+
const parameters = [];
|
|
34
|
+
const returns = [];
|
|
35
|
+
const assignments = [];
|
|
36
|
+
const argFlows = [];
|
|
37
|
+
const mutations = [];
|
|
38
|
+
|
|
39
|
+
const scopeStack = [];
|
|
40
|
+
|
|
41
|
+
function currentScope() {
|
|
42
|
+
return scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function findBinding(name) {
|
|
46
|
+
for (let i = scopeStack.length - 1; i >= 0; i--) {
|
|
47
|
+
const scope = scopeStack[i];
|
|
48
|
+
if (scope.params.has(name))
|
|
49
|
+
return { type: 'param', index: scope.params.get(name), funcName: scope.funcName };
|
|
50
|
+
if (scope.locals.has(name))
|
|
51
|
+
return { type: 'local', source: scope.locals.get(name), funcName: scope.funcName };
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function bindingConfidence(binding) {
|
|
57
|
+
if (!binding) return 0.5;
|
|
58
|
+
if (binding.type === 'param') return 1.0;
|
|
59
|
+
if (binding.type === 'local') {
|
|
60
|
+
if (binding.source?.type === 'call_return') return 0.9;
|
|
61
|
+
if (binding.source?.type === 'destructured') return 0.8;
|
|
62
|
+
return 0.9;
|
|
63
|
+
}
|
|
64
|
+
return 0.5;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function unwrapAwait(node) {
|
|
68
|
+
if (rules.awaitNode && node.type === rules.awaitNode) {
|
|
69
|
+
return node.namedChildren[0] || node;
|
|
70
|
+
}
|
|
71
|
+
return node;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isCall(node) {
|
|
75
|
+
return node && isCallNode(node.type);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function handleVarDeclarator(node) {
|
|
79
|
+
let nameNode = node.childForFieldName(rules.varNameField);
|
|
80
|
+
let valueNode = rules.varValueField ? node.childForFieldName(rules.varValueField) : null;
|
|
81
|
+
|
|
82
|
+
if (!valueNode && rules.equalsClauseType) {
|
|
83
|
+
for (const child of node.namedChildren) {
|
|
84
|
+
if (child.type === rules.equalsClauseType) {
|
|
85
|
+
valueNode = child.childForFieldName('value') || child.namedChildren[0];
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!valueNode) {
|
|
92
|
+
for (const child of node.namedChildren) {
|
|
93
|
+
if (child !== nameNode && isCall(unwrapAwait(child))) {
|
|
94
|
+
valueNode = child;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (rules.expressionListType) {
|
|
101
|
+
if (nameNode?.type === rules.expressionListType) nameNode = nameNode.namedChildren[0];
|
|
102
|
+
if (valueNode?.type === rules.expressionListType) valueNode = valueNode.namedChildren[0];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const scope = currentScope();
|
|
106
|
+
if (!nameNode || !valueNode || !scope) return;
|
|
107
|
+
|
|
108
|
+
const unwrapped = unwrapAwait(valueNode);
|
|
109
|
+
const callExpr = isCall(unwrapped) ? unwrapped : null;
|
|
110
|
+
|
|
111
|
+
if (callExpr) {
|
|
112
|
+
const callee = resolveCalleeName(callExpr, rules);
|
|
113
|
+
if (callee && scope.funcName) {
|
|
114
|
+
if (
|
|
115
|
+
(rules.objectDestructType && nameNode.type === rules.objectDestructType) ||
|
|
116
|
+
(rules.arrayDestructType && nameNode.type === rules.arrayDestructType)
|
|
117
|
+
) {
|
|
118
|
+
const names = extractParamNames(nameNode, rules);
|
|
119
|
+
for (const n of names) {
|
|
120
|
+
assignments.push({
|
|
121
|
+
varName: n,
|
|
122
|
+
callerFunc: scope.funcName,
|
|
123
|
+
sourceCallName: callee,
|
|
124
|
+
expression: truncate(node.text),
|
|
125
|
+
line: node.startPosition.row + 1,
|
|
126
|
+
});
|
|
127
|
+
scope.locals.set(n, { type: 'destructured', callee });
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
const varName =
|
|
131
|
+
nameNode.type === 'identifier' || nameNode.type === rules.paramIdentifier
|
|
132
|
+
? nameNode.text
|
|
133
|
+
: nameNode.text;
|
|
134
|
+
assignments.push({
|
|
135
|
+
varName,
|
|
136
|
+
callerFunc: scope.funcName,
|
|
137
|
+
sourceCallName: callee,
|
|
138
|
+
expression: truncate(node.text),
|
|
139
|
+
line: node.startPosition.row + 1,
|
|
140
|
+
});
|
|
141
|
+
scope.locals.set(varName, { type: 'call_return', callee });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function handleAssignment(node) {
|
|
148
|
+
const left = node.childForFieldName(rules.assignLeftField);
|
|
149
|
+
const right = node.childForFieldName(rules.assignRightField);
|
|
150
|
+
const scope = currentScope();
|
|
151
|
+
if (!scope?.funcName) return;
|
|
152
|
+
|
|
153
|
+
if (left && rules.memberNode && left.type === rules.memberNode) {
|
|
154
|
+
const receiver = memberReceiver(left, rules);
|
|
155
|
+
if (receiver) {
|
|
156
|
+
const binding = findBinding(receiver);
|
|
157
|
+
if (binding) {
|
|
158
|
+
mutations.push({
|
|
159
|
+
funcName: scope.funcName,
|
|
160
|
+
receiverName: receiver,
|
|
161
|
+
binding,
|
|
162
|
+
mutatingExpr: truncate(node.text),
|
|
163
|
+
line: node.startPosition.row + 1,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (left && isIdent(left.type, rules) && right) {
|
|
170
|
+
const unwrapped = unwrapAwait(right);
|
|
171
|
+
const callExpr = isCall(unwrapped) ? unwrapped : null;
|
|
172
|
+
if (callExpr) {
|
|
173
|
+
const callee = resolveCalleeName(callExpr, rules);
|
|
174
|
+
if (callee) {
|
|
175
|
+
assignments.push({
|
|
176
|
+
varName: left.text,
|
|
177
|
+
callerFunc: scope.funcName,
|
|
178
|
+
sourceCallName: callee,
|
|
179
|
+
expression: truncate(node.text),
|
|
180
|
+
line: node.startPosition.row + 1,
|
|
181
|
+
});
|
|
182
|
+
scope.locals.set(left.text, { type: 'call_return', callee });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function handleCallExpr(node) {
|
|
189
|
+
const callee = resolveCalleeName(node, rules);
|
|
190
|
+
const argsNode = node.childForFieldName(rules.callArgsField);
|
|
191
|
+
const scope = currentScope();
|
|
192
|
+
if (!callee || !argsNode || !scope?.funcName) return;
|
|
193
|
+
|
|
194
|
+
let argIndex = 0;
|
|
195
|
+
for (let arg of argsNode.namedChildren) {
|
|
196
|
+
if (rules.argumentWrapperType && arg.type === rules.argumentWrapperType) {
|
|
197
|
+
arg = arg.namedChildren[0] || arg;
|
|
198
|
+
}
|
|
199
|
+
const unwrapped =
|
|
200
|
+
rules.spreadType && arg.type === rules.spreadType ? arg.namedChildren[0] || arg : arg;
|
|
201
|
+
if (!unwrapped) {
|
|
202
|
+
argIndex++;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const argName = isIdent(unwrapped.type, rules) ? unwrapped.text : null;
|
|
207
|
+
const argMember =
|
|
208
|
+
rules.memberNode && unwrapped.type === rules.memberNode
|
|
209
|
+
? memberReceiver(unwrapped, rules)
|
|
210
|
+
: null;
|
|
211
|
+
const trackedName = argName || argMember;
|
|
212
|
+
|
|
213
|
+
if (trackedName) {
|
|
214
|
+
const binding = findBinding(trackedName);
|
|
215
|
+
if (binding) {
|
|
216
|
+
argFlows.push({
|
|
217
|
+
callerFunc: scope.funcName,
|
|
218
|
+
calleeName: callee,
|
|
219
|
+
argIndex,
|
|
220
|
+
argName: trackedName,
|
|
221
|
+
binding,
|
|
222
|
+
confidence: bindingConfidence(binding),
|
|
223
|
+
expression: truncate(arg.text),
|
|
224
|
+
line: node.startPosition.row + 1,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
argIndex++;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function handleExprStmtMutation(node) {
|
|
233
|
+
if (rules.mutatingMethods.size === 0) return;
|
|
234
|
+
const expr = node.namedChildren[0];
|
|
235
|
+
if (!expr || !isCall(expr)) return;
|
|
236
|
+
|
|
237
|
+
let methodName = null;
|
|
238
|
+
let receiver = null;
|
|
239
|
+
|
|
240
|
+
const fn = expr.childForFieldName(rules.callFunctionField);
|
|
241
|
+
if (fn && fn.type === rules.memberNode) {
|
|
242
|
+
const prop = fn.childForFieldName(rules.memberPropertyField);
|
|
243
|
+
methodName = prop ? prop.text : null;
|
|
244
|
+
receiver = memberReceiver(fn, rules);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!receiver && rules.callObjectField) {
|
|
248
|
+
const obj = expr.childForFieldName(rules.callObjectField);
|
|
249
|
+
const name = expr.childForFieldName(rules.callFunctionField);
|
|
250
|
+
if (obj && name) {
|
|
251
|
+
methodName = name.text;
|
|
252
|
+
receiver = isIdent(obj.type, rules) ? obj.text : null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!methodName || !rules.mutatingMethods.has(methodName)) return;
|
|
257
|
+
|
|
258
|
+
const scope = currentScope();
|
|
259
|
+
if (!receiver || !scope?.funcName) return;
|
|
260
|
+
|
|
261
|
+
const binding = findBinding(receiver);
|
|
262
|
+
if (binding) {
|
|
263
|
+
mutations.push({
|
|
264
|
+
funcName: scope.funcName,
|
|
265
|
+
receiverName: receiver,
|
|
266
|
+
binding,
|
|
267
|
+
mutatingExpr: truncate(expr.text),
|
|
268
|
+
line: node.startPosition.row + 1,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
name: 'dataflow',
|
|
275
|
+
functionNodeTypes: rules.functionNodes,
|
|
276
|
+
|
|
277
|
+
enterFunction(funcNode, _funcName, _context) {
|
|
278
|
+
const name = functionName(funcNode, rules);
|
|
279
|
+
const paramsNode = funcNode.childForFieldName(rules.paramListField);
|
|
280
|
+
const paramList = extractParams(paramsNode, rules);
|
|
281
|
+
const paramMap = new Map();
|
|
282
|
+
for (const p of paramList) {
|
|
283
|
+
paramMap.set(p.name, p.index);
|
|
284
|
+
if (name) {
|
|
285
|
+
parameters.push({
|
|
286
|
+
funcName: name,
|
|
287
|
+
paramName: p.name,
|
|
288
|
+
paramIndex: p.index,
|
|
289
|
+
line: (paramsNode?.startPosition?.row ?? funcNode.startPosition.row) + 1,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
scopeStack.push({ funcName: name, funcNode, params: paramMap, locals: new Map() });
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
exitFunction(_funcNode, _funcName, _context) {
|
|
297
|
+
scopeStack.pop();
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
enterNode(node, _context) {
|
|
301
|
+
const t = node.type;
|
|
302
|
+
|
|
303
|
+
// Skip function nodes — handled by enterFunction/exitFunction
|
|
304
|
+
if (rules.functionNodes.has(t)) return;
|
|
305
|
+
|
|
306
|
+
// Return statements (skip keyword tokens inside return statements, e.g. Ruby's
|
|
307
|
+
// `return` node nests a `return` keyword child with the same type string)
|
|
308
|
+
if (rules.returnNode && t === rules.returnNode) {
|
|
309
|
+
if (node.parent?.type === rules.returnNode) return; // keyword token, not statement
|
|
310
|
+
|
|
311
|
+
const scope = currentScope();
|
|
312
|
+
if (scope?.funcName) {
|
|
313
|
+
const expr = node.namedChildren[0];
|
|
314
|
+
const referencedNames = [];
|
|
315
|
+
if (expr) collectIdentifiers(expr, referencedNames, rules);
|
|
316
|
+
returns.push({
|
|
317
|
+
funcName: scope.funcName,
|
|
318
|
+
expression: truncate(expr ? expr.text : ''),
|
|
319
|
+
referencedNames,
|
|
320
|
+
line: node.startPosition.row + 1,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Variable declarations
|
|
327
|
+
if (rules.varDeclaratorNode && t === rules.varDeclaratorNode) {
|
|
328
|
+
handleVarDeclarator(node);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (rules.varDeclaratorNodes?.has(t)) {
|
|
332
|
+
handleVarDeclarator(node);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Call expressions
|
|
337
|
+
if (isCallNode(t)) {
|
|
338
|
+
handleCallExpr(node);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Assignment expressions
|
|
343
|
+
if (rules.assignmentNode && t === rules.assignmentNode) {
|
|
344
|
+
handleAssignment(node);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Mutation detection via expression_statement
|
|
349
|
+
if (rules.expressionStmtNode && t === rules.expressionStmtNode) {
|
|
350
|
+
handleExprStmtMutation(node);
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
finish() {
|
|
355
|
+
return { parameters, returns, assignments, argFlows, mutations };
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
}
|