@optave/codegraph 3.1.1 → 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/package.json +7 -7
- package/src/ast-analysis/engine.js +365 -0
- package/src/ast-analysis/metrics.js +118 -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 +13 -140
- package/src/audit.js +2 -87
- package/src/batch.js +0 -25
- package/src/boundaries.js +1 -1
- package/src/branch-compare.js +1 -96
- package/src/builder.js +48 -179
- package/src/cfg.js +89 -883
- package/src/check.js +1 -84
- package/src/cli.js +20 -19
- 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 +12 -83
- package/src/complexity.js +42 -356
- package/src/cycles.js +1 -1
- package/src/dataflow.js +12 -665
- 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 +40 -1
- package/src/embedder.js +14 -34
- package/src/export.js +1 -1
- package/src/extractors/javascript.js +130 -5
- package/src/flow.js +2 -70
- package/src/index.js +23 -19
- package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
- package/src/kinds.js +1 -0
- package/src/manifesto.js +0 -76
- package/src/owners.js +1 -56
- package/src/queries-cli.js +1 -1
- package/src/queries.js +79 -280
- package/src/sequence.js +5 -44
- package/src/structure.js +16 -75
- package/src/triage.js +1 -54
- package/src/viewer.js +1 -1
- package/src/watcher.js +7 -4
- package/src/db/repository.js +0 -134
- /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
package/src/dataflow.js
CHANGED
|
@@ -17,13 +17,13 @@ import {
|
|
|
17
17
|
buildExtensionSet,
|
|
18
18
|
buildExtToLangMap,
|
|
19
19
|
} from './ast-analysis/shared.js';
|
|
20
|
-
import {
|
|
20
|
+
import { walkWithVisitors } from './ast-analysis/visitor.js';
|
|
21
|
+
import { createDataflowVisitor } from './ast-analysis/visitors/dataflow-visitor.js';
|
|
22
|
+
import { hasDataflowTable, openReadonlyOrFail } from './db.js';
|
|
23
|
+
import { isTestFile } from './infrastructure/test-filter.js';
|
|
21
24
|
import { info } from './logger.js';
|
|
22
25
|
import { paginateResult } from './paginate.js';
|
|
23
|
-
|
|
24
26
|
import { ALL_SYMBOL_KINDS, normalizeSymbol } from './queries.js';
|
|
25
|
-
import { outputResult } from './result-formatter.js';
|
|
26
|
-
import { isTestFile } from './test-filter.js';
|
|
27
27
|
|
|
28
28
|
// Re-export for backward compatibility
|
|
29
29
|
export { DATAFLOW_RULES };
|
|
@@ -31,172 +31,13 @@ export { _makeDataflowRules as makeDataflowRules };
|
|
|
31
31
|
|
|
32
32
|
export const DATAFLOW_EXTENSIONS = buildExtensionSet(DATAFLOW_RULES);
|
|
33
33
|
|
|
34
|
-
// ── AST helpers
|
|
35
|
-
|
|
36
|
-
function truncate(str, max = 120) {
|
|
37
|
-
if (!str) return '';
|
|
38
|
-
return str.length > max ? `${str.slice(0, max)}…` : str;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Get the name of a function node from the AST using rules.
|
|
43
|
-
*/
|
|
44
|
-
function functionName(fnNode, rules) {
|
|
45
|
-
if (!fnNode) return null;
|
|
46
|
-
// Try the standard name field first (works for most languages)
|
|
47
|
-
const nameNode = fnNode.childForFieldName(rules.nameField);
|
|
48
|
-
if (nameNode) return nameNode.text;
|
|
49
|
-
|
|
50
|
-
// JS-specific: arrow_function/function_expression assigned to variable, pair, or assignment
|
|
51
|
-
const parent = fnNode.parent;
|
|
52
|
-
if (parent) {
|
|
53
|
-
if (rules.varAssignedFnParent && parent.type === rules.varAssignedFnParent) {
|
|
54
|
-
const n = parent.childForFieldName('name');
|
|
55
|
-
return n ? n.text : null;
|
|
56
|
-
}
|
|
57
|
-
if (rules.pairFnParent && parent.type === rules.pairFnParent) {
|
|
58
|
-
const keyNode = parent.childForFieldName('key');
|
|
59
|
-
return keyNode ? keyNode.text : null;
|
|
60
|
-
}
|
|
61
|
-
if (rules.assignmentFnParent && parent.type === rules.assignmentFnParent) {
|
|
62
|
-
const left = parent.childForFieldName(rules.assignLeftField);
|
|
63
|
-
return left ? left.text : null;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Extract parameter names and indices from a formal_parameters node.
|
|
71
|
-
*/
|
|
72
|
-
function extractParams(paramsNode, rules) {
|
|
73
|
-
if (!paramsNode) return [];
|
|
74
|
-
const result = [];
|
|
75
|
-
let index = 0;
|
|
76
|
-
for (const child of paramsNode.namedChildren) {
|
|
77
|
-
const names = extractParamNames(child, rules);
|
|
78
|
-
for (const name of names) {
|
|
79
|
-
result.push({ name, index });
|
|
80
|
-
}
|
|
81
|
-
index++;
|
|
82
|
-
}
|
|
83
|
-
return result;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function extractParamNames(node, rules) {
|
|
87
|
-
if (!node) return [];
|
|
88
|
-
const t = node.type;
|
|
89
|
-
|
|
90
|
-
// Language-specific override (Go, Rust, Java, C#, PHP, Ruby)
|
|
91
|
-
if (rules.extractParamName) {
|
|
92
|
-
const result = rules.extractParamName(node);
|
|
93
|
-
if (result) return result;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Leaf identifier
|
|
97
|
-
if (t === rules.paramIdentifier) return [node.text];
|
|
98
|
-
|
|
99
|
-
// Wrapper types (TS required_parameter, Python typed_parameter, etc.)
|
|
100
|
-
if (rules.paramWrapperTypes.has(t)) {
|
|
101
|
-
const pattern = node.childForFieldName('pattern') || node.childForFieldName('name');
|
|
102
|
-
return pattern ? extractParamNames(pattern, rules) : [];
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Default parameter (assignment_pattern / default_parameter)
|
|
106
|
-
if (rules.defaultParamType && t === rules.defaultParamType) {
|
|
107
|
-
const left = node.childForFieldName('left') || node.childForFieldName('name');
|
|
108
|
-
return left ? extractParamNames(left, rules) : [];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Rest / splat parameter
|
|
112
|
-
if (rules.restParamType && t === rules.restParamType) {
|
|
113
|
-
// Try name field first, then fall back to scanning children
|
|
114
|
-
const nameNode = node.childForFieldName('name');
|
|
115
|
-
if (nameNode) return [nameNode.text];
|
|
116
|
-
for (const child of node.namedChildren) {
|
|
117
|
-
if (child.type === rules.paramIdentifier) return [child.text];
|
|
118
|
-
}
|
|
119
|
-
return [];
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Object destructuring (JS only)
|
|
123
|
-
if (rules.objectDestructType && t === rules.objectDestructType) {
|
|
124
|
-
const names = [];
|
|
125
|
-
for (const child of node.namedChildren) {
|
|
126
|
-
if (rules.shorthandPropPattern && child.type === rules.shorthandPropPattern) {
|
|
127
|
-
names.push(child.text);
|
|
128
|
-
} else if (rules.pairPatternType && child.type === rules.pairPatternType) {
|
|
129
|
-
const value = child.childForFieldName('value');
|
|
130
|
-
if (value) names.push(...extractParamNames(value, rules));
|
|
131
|
-
} else if (rules.restParamType && child.type === rules.restParamType) {
|
|
132
|
-
names.push(...extractParamNames(child, rules));
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return names;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Array destructuring (JS only)
|
|
139
|
-
if (rules.arrayDestructType && t === rules.arrayDestructType) {
|
|
140
|
-
const names = [];
|
|
141
|
-
for (const child of node.namedChildren) {
|
|
142
|
-
names.push(...extractParamNames(child, rules));
|
|
143
|
-
}
|
|
144
|
-
return names;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return [];
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Check if a node type is identifier-like for this language. */
|
|
151
|
-
function isIdent(nodeType, rules) {
|
|
152
|
-
if (nodeType === 'identifier' || nodeType === rules.paramIdentifier) return true;
|
|
153
|
-
return rules.extraIdentifierTypes ? rules.extraIdentifierTypes.has(nodeType) : false;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Resolve the name a call expression is calling using rules.
|
|
158
|
-
*/
|
|
159
|
-
function resolveCalleeName(callNode, rules) {
|
|
160
|
-
const fn = callNode.childForFieldName(rules.callFunctionField);
|
|
161
|
-
if (!fn) {
|
|
162
|
-
// Some languages (Java method_invocation, Ruby call) use 'name' field directly
|
|
163
|
-
const nameNode = callNode.childForFieldName('name') || callNode.childForFieldName('method');
|
|
164
|
-
return nameNode ? nameNode.text : null;
|
|
165
|
-
}
|
|
166
|
-
if (isIdent(fn.type, rules)) return fn.text;
|
|
167
|
-
if (fn.type === rules.memberNode) {
|
|
168
|
-
const prop = fn.childForFieldName(rules.memberPropertyField);
|
|
169
|
-
return prop ? prop.text : null;
|
|
170
|
-
}
|
|
171
|
-
if (rules.optionalChainNode && fn.type === rules.optionalChainNode) {
|
|
172
|
-
const target = fn.namedChildren[0];
|
|
173
|
-
if (!target) return null;
|
|
174
|
-
if (target.type === rules.memberNode) {
|
|
175
|
-
const prop = target.childForFieldName(rules.memberPropertyField);
|
|
176
|
-
return prop ? prop.text : null;
|
|
177
|
-
}
|
|
178
|
-
if (target.type === 'identifier') return target.text;
|
|
179
|
-
const prop = fn.childForFieldName(rules.memberPropertyField);
|
|
180
|
-
return prop ? prop.text : null;
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Get the receiver (object) of a member expression using rules.
|
|
187
|
-
*/
|
|
188
|
-
function memberReceiver(memberExpr, rules) {
|
|
189
|
-
const obj = memberExpr.childForFieldName(rules.memberObjectField);
|
|
190
|
-
if (!obj) return null;
|
|
191
|
-
if (isIdent(obj.type, rules)) return obj.text;
|
|
192
|
-
if (obj.type === rules.memberNode) return memberReceiver(obj, rules);
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
34
|
+
// ── AST helpers (now in ast-analysis/visitor-utils.js, kept as re-exports) ──
|
|
195
35
|
|
|
196
36
|
// ── extractDataflow ──────────────────────────────────────────────────────────
|
|
197
37
|
|
|
198
38
|
/**
|
|
199
39
|
* Extract dataflow information from a parsed AST.
|
|
40
|
+
* Delegates to the dataflow visitor via the unified walker.
|
|
200
41
|
*
|
|
201
42
|
* @param {object} tree - tree-sitter parse tree
|
|
202
43
|
* @param {string} filePath - relative file path
|
|
@@ -208,385 +49,13 @@ export function extractDataflow(tree, _filePath, _definitions, langId = 'javascr
|
|
|
208
49
|
const rules = DATAFLOW_RULES.get(langId);
|
|
209
50
|
if (!rules) return { parameters: [], returns: [], assignments: [], argFlows: [], mutations: [] };
|
|
210
51
|
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const argFlows = [];
|
|
217
|
-
const mutations = [];
|
|
218
|
-
|
|
219
|
-
const scopeStack = [];
|
|
220
|
-
|
|
221
|
-
function currentScope() {
|
|
222
|
-
return scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : null;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function findBinding(name) {
|
|
226
|
-
for (let i = scopeStack.length - 1; i >= 0; i--) {
|
|
227
|
-
const scope = scopeStack[i];
|
|
228
|
-
if (scope.params.has(name))
|
|
229
|
-
return { type: 'param', index: scope.params.get(name), funcName: scope.funcName };
|
|
230
|
-
if (scope.locals.has(name))
|
|
231
|
-
return { type: 'local', source: scope.locals.get(name), funcName: scope.funcName };
|
|
232
|
-
}
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function enterScope(fnNode) {
|
|
237
|
-
const name = functionName(fnNode, rules);
|
|
238
|
-
const paramsNode = fnNode.childForFieldName(rules.paramListField);
|
|
239
|
-
const paramList = extractParams(paramsNode, rules);
|
|
240
|
-
const paramMap = new Map();
|
|
241
|
-
for (const p of paramList) {
|
|
242
|
-
paramMap.set(p.name, p.index);
|
|
243
|
-
if (name) {
|
|
244
|
-
parameters.push({
|
|
245
|
-
funcName: name,
|
|
246
|
-
paramName: p.name,
|
|
247
|
-
paramIndex: p.index,
|
|
248
|
-
line: (paramsNode?.startPosition?.row ?? fnNode.startPosition.row) + 1,
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
scopeStack.push({ funcName: name, funcNode: fnNode, params: paramMap, locals: new Map() });
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function exitScope() {
|
|
256
|
-
scopeStack.pop();
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function bindingConfidence(binding) {
|
|
260
|
-
if (!binding) return 0.5;
|
|
261
|
-
if (binding.type === 'param') return 1.0;
|
|
262
|
-
if (binding.type === 'local') {
|
|
263
|
-
if (binding.source?.type === 'call_return') return 0.9;
|
|
264
|
-
if (binding.source?.type === 'destructured') return 0.8;
|
|
265
|
-
return 0.9;
|
|
266
|
-
}
|
|
267
|
-
return 0.5;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/** Unwrap await if present, returning the inner expression. */
|
|
271
|
-
function unwrapAwait(node) {
|
|
272
|
-
if (rules.awaitNode && node.type === rules.awaitNode) {
|
|
273
|
-
return node.namedChildren[0] || node;
|
|
274
|
-
}
|
|
275
|
-
return node;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/** Check if a node is a call expression (single or multi-type). */
|
|
279
|
-
function isCall(node) {
|
|
280
|
-
return node && isCallNode(node.type);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/** Handle a variable declarator / short_var_declaration node. */
|
|
284
|
-
function handleVarDeclarator(node) {
|
|
285
|
-
let nameNode = node.childForFieldName(rules.varNameField);
|
|
286
|
-
let valueNode = rules.varValueField ? node.childForFieldName(rules.varValueField) : null;
|
|
287
|
-
|
|
288
|
-
// C#: initializer is inside equals_value_clause child
|
|
289
|
-
if (!valueNode && rules.equalsClauseType) {
|
|
290
|
-
for (const child of node.namedChildren) {
|
|
291
|
-
if (child.type === rules.equalsClauseType) {
|
|
292
|
-
valueNode = child.childForFieldName('value') || child.namedChildren[0];
|
|
293
|
-
break;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Fallback: initializer is a direct unnamed child (C# variable_declarator)
|
|
299
|
-
if (!valueNode) {
|
|
300
|
-
for (const child of node.namedChildren) {
|
|
301
|
-
if (child !== nameNode && isCall(unwrapAwait(child))) {
|
|
302
|
-
valueNode = child;
|
|
303
|
-
break;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Go: expression_list wraps LHS/RHS — unwrap to first named child
|
|
309
|
-
if (rules.expressionListType) {
|
|
310
|
-
if (nameNode?.type === rules.expressionListType) nameNode = nameNode.namedChildren[0];
|
|
311
|
-
if (valueNode?.type === rules.expressionListType) valueNode = valueNode.namedChildren[0];
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const scope = currentScope();
|
|
315
|
-
if (!nameNode || !valueNode || !scope) return;
|
|
316
|
-
|
|
317
|
-
const unwrapped = unwrapAwait(valueNode);
|
|
318
|
-
const callExpr = isCall(unwrapped) ? unwrapped : null;
|
|
319
|
-
|
|
320
|
-
if (callExpr) {
|
|
321
|
-
const callee = resolveCalleeName(callExpr, rules);
|
|
322
|
-
if (callee && scope.funcName) {
|
|
323
|
-
// Destructuring: const { a, b } = foo()
|
|
324
|
-
if (
|
|
325
|
-
(rules.objectDestructType && nameNode.type === rules.objectDestructType) ||
|
|
326
|
-
(rules.arrayDestructType && nameNode.type === rules.arrayDestructType)
|
|
327
|
-
) {
|
|
328
|
-
const names = extractParamNames(nameNode, rules);
|
|
329
|
-
for (const n of names) {
|
|
330
|
-
assignments.push({
|
|
331
|
-
varName: n,
|
|
332
|
-
callerFunc: scope.funcName,
|
|
333
|
-
sourceCallName: callee,
|
|
334
|
-
expression: truncate(node.text),
|
|
335
|
-
line: node.startPosition.row + 1,
|
|
336
|
-
});
|
|
337
|
-
scope.locals.set(n, { type: 'destructured', callee });
|
|
338
|
-
}
|
|
339
|
-
} else {
|
|
340
|
-
const varName =
|
|
341
|
-
nameNode.type === 'identifier' || nameNode.type === rules.paramIdentifier
|
|
342
|
-
? nameNode.text
|
|
343
|
-
: nameNode.text;
|
|
344
|
-
assignments.push({
|
|
345
|
-
varName,
|
|
346
|
-
callerFunc: scope.funcName,
|
|
347
|
-
sourceCallName: callee,
|
|
348
|
-
expression: truncate(node.text),
|
|
349
|
-
line: node.startPosition.row + 1,
|
|
350
|
-
});
|
|
351
|
-
scope.locals.set(varName, { type: 'call_return', callee });
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/** Handle assignment expressions (mutation detection + call captures). */
|
|
358
|
-
function handleAssignment(node) {
|
|
359
|
-
const left = node.childForFieldName(rules.assignLeftField);
|
|
360
|
-
const right = node.childForFieldName(rules.assignRightField);
|
|
361
|
-
const scope = currentScope();
|
|
362
|
-
if (!scope?.funcName) return;
|
|
363
|
-
|
|
364
|
-
// Mutation: obj.prop = value
|
|
365
|
-
if (left && rules.memberNode && left.type === rules.memberNode) {
|
|
366
|
-
const receiver = memberReceiver(left, rules);
|
|
367
|
-
if (receiver) {
|
|
368
|
-
const binding = findBinding(receiver);
|
|
369
|
-
if (binding) {
|
|
370
|
-
mutations.push({
|
|
371
|
-
funcName: scope.funcName,
|
|
372
|
-
receiverName: receiver,
|
|
373
|
-
binding,
|
|
374
|
-
mutatingExpr: truncate(node.text),
|
|
375
|
-
line: node.startPosition.row + 1,
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Non-declaration assignment: x = foo()
|
|
382
|
-
if (left && isIdent(left.type, rules) && right) {
|
|
383
|
-
const unwrapped = unwrapAwait(right);
|
|
384
|
-
const callExpr = isCall(unwrapped) ? unwrapped : null;
|
|
385
|
-
if (callExpr) {
|
|
386
|
-
const callee = resolveCalleeName(callExpr, rules);
|
|
387
|
-
if (callee) {
|
|
388
|
-
assignments.push({
|
|
389
|
-
varName: left.text,
|
|
390
|
-
callerFunc: scope.funcName,
|
|
391
|
-
sourceCallName: callee,
|
|
392
|
-
expression: truncate(node.text),
|
|
393
|
-
line: node.startPosition.row + 1,
|
|
394
|
-
});
|
|
395
|
-
scope.locals.set(left.text, { type: 'call_return', callee });
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/** Handle call expressions: track argument flows. */
|
|
402
|
-
function handleCallExpr(node) {
|
|
403
|
-
const callee = resolveCalleeName(node, rules);
|
|
404
|
-
const argsNode = node.childForFieldName(rules.callArgsField);
|
|
405
|
-
const scope = currentScope();
|
|
406
|
-
if (!callee || !argsNode || !scope?.funcName) return;
|
|
407
|
-
|
|
408
|
-
let argIndex = 0;
|
|
409
|
-
for (let arg of argsNode.namedChildren) {
|
|
410
|
-
// PHP/Java: unwrap argument wrapper
|
|
411
|
-
if (rules.argumentWrapperType && arg.type === rules.argumentWrapperType) {
|
|
412
|
-
arg = arg.namedChildren[0] || arg;
|
|
413
|
-
}
|
|
414
|
-
const unwrapped =
|
|
415
|
-
rules.spreadType && arg.type === rules.spreadType ? arg.namedChildren[0] || arg : arg;
|
|
416
|
-
if (!unwrapped) {
|
|
417
|
-
argIndex++;
|
|
418
|
-
continue;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const argName = isIdent(unwrapped.type, rules) ? unwrapped.text : null;
|
|
422
|
-
const argMember =
|
|
423
|
-
rules.memberNode && unwrapped.type === rules.memberNode
|
|
424
|
-
? memberReceiver(unwrapped, rules)
|
|
425
|
-
: null;
|
|
426
|
-
const trackedName = argName || argMember;
|
|
427
|
-
|
|
428
|
-
if (trackedName) {
|
|
429
|
-
const binding = findBinding(trackedName);
|
|
430
|
-
if (binding) {
|
|
431
|
-
argFlows.push({
|
|
432
|
-
callerFunc: scope.funcName,
|
|
433
|
-
calleeName: callee,
|
|
434
|
-
argIndex,
|
|
435
|
-
argName: trackedName,
|
|
436
|
-
binding,
|
|
437
|
-
confidence: bindingConfidence(binding),
|
|
438
|
-
expression: truncate(arg.text),
|
|
439
|
-
line: node.startPosition.row + 1,
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
argIndex++;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/** Detect mutating method calls in expression statements. */
|
|
448
|
-
function handleExprStmtMutation(node) {
|
|
449
|
-
if (rules.mutatingMethods.size === 0) return;
|
|
450
|
-
const expr = node.namedChildren[0];
|
|
451
|
-
if (!expr || !isCall(expr)) return;
|
|
452
|
-
|
|
453
|
-
let methodName = null;
|
|
454
|
-
let receiver = null;
|
|
455
|
-
|
|
456
|
-
// Standard pattern: call(fn: member(obj, prop))
|
|
457
|
-
const fn = expr.childForFieldName(rules.callFunctionField);
|
|
458
|
-
if (fn && fn.type === rules.memberNode) {
|
|
459
|
-
const prop = fn.childForFieldName(rules.memberPropertyField);
|
|
460
|
-
methodName = prop ? prop.text : null;
|
|
461
|
-
receiver = memberReceiver(fn, rules);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Java/combined pattern: call node itself has object + name fields
|
|
465
|
-
if (!receiver && rules.callObjectField) {
|
|
466
|
-
const obj = expr.childForFieldName(rules.callObjectField);
|
|
467
|
-
const name = expr.childForFieldName(rules.callFunctionField);
|
|
468
|
-
if (obj && name) {
|
|
469
|
-
methodName = name.text;
|
|
470
|
-
receiver = isIdent(obj.type, rules) ? obj.text : null;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
if (!methodName || !rules.mutatingMethods.has(methodName)) return;
|
|
475
|
-
|
|
476
|
-
const scope = currentScope();
|
|
477
|
-
if (!receiver || !scope?.funcName) return;
|
|
478
|
-
|
|
479
|
-
const binding = findBinding(receiver);
|
|
480
|
-
if (binding) {
|
|
481
|
-
mutations.push({
|
|
482
|
-
funcName: scope.funcName,
|
|
483
|
-
receiverName: receiver,
|
|
484
|
-
binding,
|
|
485
|
-
mutatingExpr: truncate(expr.text),
|
|
486
|
-
line: node.startPosition.row + 1,
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Recursive AST walk
|
|
492
|
-
function visit(node) {
|
|
493
|
-
if (!node) return;
|
|
494
|
-
const t = node.type;
|
|
495
|
-
|
|
496
|
-
// Enter function scopes
|
|
497
|
-
if (rules.functionNodes.has(t)) {
|
|
498
|
-
enterScope(node);
|
|
499
|
-
for (const child of node.namedChildren) {
|
|
500
|
-
visit(child);
|
|
501
|
-
}
|
|
502
|
-
exitScope();
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// Return statements
|
|
507
|
-
if (rules.returnNode && t === rules.returnNode) {
|
|
508
|
-
const scope = currentScope();
|
|
509
|
-
if (scope?.funcName) {
|
|
510
|
-
const expr = node.namedChildren[0];
|
|
511
|
-
const referencedNames = [];
|
|
512
|
-
if (expr) collectIdentifiers(expr, referencedNames, rules);
|
|
513
|
-
returns.push({
|
|
514
|
-
funcName: scope.funcName,
|
|
515
|
-
expression: truncate(expr ? expr.text : ''),
|
|
516
|
-
referencedNames,
|
|
517
|
-
line: node.startPosition.row + 1,
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
for (const child of node.namedChildren) {
|
|
521
|
-
visit(child);
|
|
522
|
-
}
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Variable declarations
|
|
527
|
-
if (rules.varDeclaratorNode && t === rules.varDeclaratorNode) {
|
|
528
|
-
handleVarDeclarator(node);
|
|
529
|
-
for (const child of node.namedChildren) {
|
|
530
|
-
visit(child);
|
|
531
|
-
}
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
if (rules.varDeclaratorNodes?.has(t)) {
|
|
535
|
-
handleVarDeclarator(node);
|
|
536
|
-
for (const child of node.namedChildren) {
|
|
537
|
-
visit(child);
|
|
538
|
-
}
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Call expressions
|
|
543
|
-
if (isCallNode(t)) {
|
|
544
|
-
handleCallExpr(node);
|
|
545
|
-
for (const child of node.namedChildren) {
|
|
546
|
-
visit(child);
|
|
547
|
-
}
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Assignment expressions
|
|
552
|
-
if (rules.assignmentNode && t === rules.assignmentNode) {
|
|
553
|
-
handleAssignment(node);
|
|
554
|
-
for (const child of node.namedChildren) {
|
|
555
|
-
visit(child);
|
|
556
|
-
}
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Mutation detection via expression_statement
|
|
561
|
-
if (rules.expressionStmtNode && t === rules.expressionStmtNode) {
|
|
562
|
-
handleExprStmtMutation(node);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Default: visit all children
|
|
566
|
-
for (const child of node.namedChildren) {
|
|
567
|
-
visit(child);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
visit(tree.rootNode);
|
|
572
|
-
|
|
573
|
-
return { parameters, returns, assignments, argFlows, mutations };
|
|
574
|
-
}
|
|
52
|
+
const visitor = createDataflowVisitor(rules);
|
|
53
|
+
const results = walkWithVisitors(tree.rootNode, [visitor], langId, {
|
|
54
|
+
functionNodeTypes: rules.functionNodes,
|
|
55
|
+
getFunctionName: () => null, // dataflow visitor handles its own name extraction
|
|
56
|
+
});
|
|
575
57
|
|
|
576
|
-
|
|
577
|
-
* Collect all identifier names referenced within a node.
|
|
578
|
-
* Uses isIdent() to support language-specific identifier node types
|
|
579
|
-
* (e.g. PHP's `variable_name`).
|
|
580
|
-
*/
|
|
581
|
-
function collectIdentifiers(node, out, rules) {
|
|
582
|
-
if (!node) return;
|
|
583
|
-
if (isIdent(node.type, rules)) {
|
|
584
|
-
out.push(node.text);
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
for (const child of node.namedChildren) {
|
|
588
|
-
collectIdentifiers(child, out, rules);
|
|
589
|
-
}
|
|
58
|
+
return results.dataflow;
|
|
590
59
|
}
|
|
591
60
|
|
|
592
61
|
// ── buildDataflowEdges ──────────────────────────────────────────────────────
|
|
@@ -792,18 +261,6 @@ function findNodes(db, name, opts = {}) {
|
|
|
792
261
|
return opts.noTests ? rows.filter((n) => !isTestFile(n.file)) : rows;
|
|
793
262
|
}
|
|
794
263
|
|
|
795
|
-
/**
|
|
796
|
-
* Check if the dataflow table exists and has data.
|
|
797
|
-
*/
|
|
798
|
-
function hasDataflowTable(db) {
|
|
799
|
-
try {
|
|
800
|
-
const row = db.prepare('SELECT COUNT(*) as c FROM dataflow').get();
|
|
801
|
-
return row.c > 0;
|
|
802
|
-
} catch {
|
|
803
|
-
return false;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
264
|
/**
|
|
808
265
|
* Return all dataflow edges for a symbol.
|
|
809
266
|
*
|
|
@@ -1147,113 +604,3 @@ export function dataflowImpactData(name, customDbPath, opts = {}) {
|
|
|
1147
604
|
db.close();
|
|
1148
605
|
}
|
|
1149
606
|
}
|
|
1150
|
-
|
|
1151
|
-
// ── Display formatters ──────────────────────────────────────────────────────
|
|
1152
|
-
|
|
1153
|
-
/**
|
|
1154
|
-
* CLI display for dataflow command.
|
|
1155
|
-
*/
|
|
1156
|
-
export function dataflow(name, customDbPath, opts = {}) {
|
|
1157
|
-
if (opts.impact) {
|
|
1158
|
-
return dataflowImpact(name, customDbPath, opts);
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
const data = dataflowData(name, customDbPath, opts);
|
|
1162
|
-
|
|
1163
|
-
if (outputResult(data, 'results', opts)) return;
|
|
1164
|
-
|
|
1165
|
-
if (data.warning) {
|
|
1166
|
-
console.log(`⚠ ${data.warning}`);
|
|
1167
|
-
return;
|
|
1168
|
-
}
|
|
1169
|
-
if (data.results.length === 0) {
|
|
1170
|
-
console.log(`No symbols matching "${name}".`);
|
|
1171
|
-
return;
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
for (const r of data.results) {
|
|
1175
|
-
console.log(`\n${r.kind} ${r.name} (${r.file}:${r.line})`);
|
|
1176
|
-
console.log('─'.repeat(60));
|
|
1177
|
-
|
|
1178
|
-
if (r.flowsTo.length > 0) {
|
|
1179
|
-
console.log('\n Data flows TO:');
|
|
1180
|
-
for (const f of r.flowsTo) {
|
|
1181
|
-
const conf = f.confidence < 1.0 ? ` [${(f.confidence * 100).toFixed(0)}%]` : '';
|
|
1182
|
-
console.log(` → ${f.target} (${f.file}:${f.line}) arg[${f.paramIndex}]${conf}`);
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
if (r.flowsFrom.length > 0) {
|
|
1187
|
-
console.log('\n Data flows FROM:');
|
|
1188
|
-
for (const f of r.flowsFrom) {
|
|
1189
|
-
const conf = f.confidence < 1.0 ? ` [${(f.confidence * 100).toFixed(0)}%]` : '';
|
|
1190
|
-
console.log(` ← ${f.source} (${f.file}:${f.line}) arg[${f.paramIndex}]${conf}`);
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
if (r.returns.length > 0) {
|
|
1195
|
-
console.log('\n Return value consumed by:');
|
|
1196
|
-
for (const c of r.returns) {
|
|
1197
|
-
console.log(` → ${c.consumer} (${c.file}:${c.line}) ${c.expression}`);
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
if (r.returnedBy.length > 0) {
|
|
1202
|
-
console.log('\n Uses return value of:');
|
|
1203
|
-
for (const p of r.returnedBy) {
|
|
1204
|
-
console.log(` ← ${p.producer} (${p.file}:${p.line}) ${p.expression}`);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
if (r.mutates.length > 0) {
|
|
1209
|
-
console.log('\n Mutates:');
|
|
1210
|
-
for (const m of r.mutates) {
|
|
1211
|
-
console.log(` ✎ ${m.expression} (line ${m.line})`);
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
if (r.mutatedBy.length > 0) {
|
|
1216
|
-
console.log('\n Mutated by:');
|
|
1217
|
-
for (const m of r.mutatedBy) {
|
|
1218
|
-
console.log(` ✎ ${m.source} — ${m.expression} (line ${m.line})`);
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
/**
|
|
1225
|
-
* CLI display for dataflow --impact.
|
|
1226
|
-
*/
|
|
1227
|
-
function dataflowImpact(name, customDbPath, opts = {}) {
|
|
1228
|
-
const data = dataflowImpactData(name, customDbPath, {
|
|
1229
|
-
noTests: opts.noTests,
|
|
1230
|
-
depth: opts.depth ? Number(opts.depth) : 5,
|
|
1231
|
-
file: opts.file,
|
|
1232
|
-
kind: opts.kind,
|
|
1233
|
-
limit: opts.limit,
|
|
1234
|
-
offset: opts.offset,
|
|
1235
|
-
});
|
|
1236
|
-
|
|
1237
|
-
if (outputResult(data, 'results', opts)) return;
|
|
1238
|
-
|
|
1239
|
-
if (data.warning) {
|
|
1240
|
-
console.log(`⚠ ${data.warning}`);
|
|
1241
|
-
return;
|
|
1242
|
-
}
|
|
1243
|
-
if (data.results.length === 0) {
|
|
1244
|
-
console.log(`No symbols matching "${name}".`);
|
|
1245
|
-
return;
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
for (const r of data.results) {
|
|
1249
|
-
console.log(
|
|
1250
|
-
`\n${r.kind} ${r.name} (${r.file}:${r.line}) — ${r.totalAffected} data-dependent consumer${r.totalAffected !== 1 ? 's' : ''}`,
|
|
1251
|
-
);
|
|
1252
|
-
for (const [level, items] of Object.entries(r.levels)) {
|
|
1253
|
-
console.log(` Level ${level}:`);
|
|
1254
|
-
for (const item of items) {
|
|
1255
|
-
console.log(` ${item.name} (${item.file}:${item.line})`);
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
}
|