@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,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared AST helper functions used by multiple visitors (dataflow, etc.).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from dataflow.js to be reusable across the visitor framework.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Truncate a string to a maximum length.
|
|
9
|
+
*/
|
|
10
|
+
export function truncate(str, max = 120) {
|
|
11
|
+
if (!str) return '';
|
|
12
|
+
return str.length > max ? `${str.slice(0, max)}…` : str;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the name of a function node from the AST using rules.
|
|
17
|
+
*/
|
|
18
|
+
export function functionName(fnNode, rules) {
|
|
19
|
+
if (!fnNode) return null;
|
|
20
|
+
const nameNode = fnNode.childForFieldName(rules.nameField);
|
|
21
|
+
if (nameNode) return nameNode.text;
|
|
22
|
+
|
|
23
|
+
const parent = fnNode.parent;
|
|
24
|
+
if (parent) {
|
|
25
|
+
if (rules.varAssignedFnParent && parent.type === rules.varAssignedFnParent) {
|
|
26
|
+
const n = parent.childForFieldName('name');
|
|
27
|
+
return n ? n.text : null;
|
|
28
|
+
}
|
|
29
|
+
if (rules.pairFnParent && parent.type === rules.pairFnParent) {
|
|
30
|
+
const keyNode = parent.childForFieldName('key');
|
|
31
|
+
return keyNode ? keyNode.text : null;
|
|
32
|
+
}
|
|
33
|
+
if (rules.assignmentFnParent && parent.type === rules.assignmentFnParent) {
|
|
34
|
+
const left = parent.childForFieldName(rules.assignLeftField);
|
|
35
|
+
return left ? left.text : null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract parameter names and indices from a formal_parameters node.
|
|
43
|
+
*/
|
|
44
|
+
export function extractParams(paramsNode, rules) {
|
|
45
|
+
if (!paramsNode) return [];
|
|
46
|
+
const result = [];
|
|
47
|
+
let index = 0;
|
|
48
|
+
for (const child of paramsNode.namedChildren) {
|
|
49
|
+
const names = extractParamNames(child, rules);
|
|
50
|
+
for (const name of names) {
|
|
51
|
+
result.push({ name, index });
|
|
52
|
+
}
|
|
53
|
+
index++;
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract parameter names from a single parameter node.
|
|
60
|
+
*/
|
|
61
|
+
export function extractParamNames(node, rules) {
|
|
62
|
+
if (!node) return [];
|
|
63
|
+
const t = node.type;
|
|
64
|
+
|
|
65
|
+
if (rules.extractParamName) {
|
|
66
|
+
const result = rules.extractParamName(node);
|
|
67
|
+
if (result) return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (t === rules.paramIdentifier) return [node.text];
|
|
71
|
+
|
|
72
|
+
if (rules.paramWrapperTypes.has(t)) {
|
|
73
|
+
const pattern = node.childForFieldName('pattern') || node.childForFieldName('name');
|
|
74
|
+
return pattern ? extractParamNames(pattern, rules) : [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (rules.defaultParamType && t === rules.defaultParamType) {
|
|
78
|
+
const left = node.childForFieldName('left') || node.childForFieldName('name');
|
|
79
|
+
return left ? extractParamNames(left, rules) : [];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (rules.restParamType && t === rules.restParamType) {
|
|
83
|
+
const nameNode = node.childForFieldName('name');
|
|
84
|
+
if (nameNode) return [nameNode.text];
|
|
85
|
+
for (const child of node.namedChildren) {
|
|
86
|
+
if (child.type === rules.paramIdentifier) return [child.text];
|
|
87
|
+
}
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (rules.objectDestructType && t === rules.objectDestructType) {
|
|
92
|
+
const names = [];
|
|
93
|
+
for (const child of node.namedChildren) {
|
|
94
|
+
if (rules.shorthandPropPattern && child.type === rules.shorthandPropPattern) {
|
|
95
|
+
names.push(child.text);
|
|
96
|
+
} else if (rules.pairPatternType && child.type === rules.pairPatternType) {
|
|
97
|
+
const value = child.childForFieldName('value');
|
|
98
|
+
if (value) names.push(...extractParamNames(value, rules));
|
|
99
|
+
} else if (rules.restParamType && child.type === rules.restParamType) {
|
|
100
|
+
names.push(...extractParamNames(child, rules));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return names;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (rules.arrayDestructType && t === rules.arrayDestructType) {
|
|
107
|
+
const names = [];
|
|
108
|
+
for (const child of node.namedChildren) {
|
|
109
|
+
names.push(...extractParamNames(child, rules));
|
|
110
|
+
}
|
|
111
|
+
return names;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if a node type is identifier-like for this language.
|
|
119
|
+
*/
|
|
120
|
+
export function isIdent(nodeType, rules) {
|
|
121
|
+
if (nodeType === 'identifier' || nodeType === rules.paramIdentifier) return true;
|
|
122
|
+
return rules.extraIdentifierTypes ? rules.extraIdentifierTypes.has(nodeType) : false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve the name a call expression is calling using rules.
|
|
127
|
+
*/
|
|
128
|
+
export function resolveCalleeName(callNode, rules) {
|
|
129
|
+
const fn = callNode.childForFieldName(rules.callFunctionField);
|
|
130
|
+
if (!fn) {
|
|
131
|
+
const nameNode = callNode.childForFieldName('name') || callNode.childForFieldName('method');
|
|
132
|
+
return nameNode ? nameNode.text : null;
|
|
133
|
+
}
|
|
134
|
+
if (isIdent(fn.type, rules)) return fn.text;
|
|
135
|
+
if (fn.type === rules.memberNode) {
|
|
136
|
+
const prop = fn.childForFieldName(rules.memberPropertyField);
|
|
137
|
+
return prop ? prop.text : null;
|
|
138
|
+
}
|
|
139
|
+
if (rules.optionalChainNode && fn.type === rules.optionalChainNode) {
|
|
140
|
+
const target = fn.namedChildren[0];
|
|
141
|
+
if (!target) return null;
|
|
142
|
+
if (target.type === rules.memberNode) {
|
|
143
|
+
const prop = target.childForFieldName(rules.memberPropertyField);
|
|
144
|
+
return prop ? prop.text : null;
|
|
145
|
+
}
|
|
146
|
+
if (target.type === 'identifier') return target.text;
|
|
147
|
+
const prop = fn.childForFieldName(rules.memberPropertyField);
|
|
148
|
+
return prop ? prop.text : null;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get the receiver (object) of a member expression using rules.
|
|
155
|
+
*/
|
|
156
|
+
export function memberReceiver(memberExpr, rules) {
|
|
157
|
+
const obj = memberExpr.childForFieldName(rules.memberObjectField);
|
|
158
|
+
if (!obj) return null;
|
|
159
|
+
if (isIdent(obj.type, rules)) return obj.text;
|
|
160
|
+
if (obj.type === rules.memberNode) return memberReceiver(obj, rules);
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Collect all identifier names referenced within a node.
|
|
166
|
+
*/
|
|
167
|
+
export function collectIdentifiers(node, out, rules) {
|
|
168
|
+
if (!node) return;
|
|
169
|
+
if (isIdent(node.type, rules)) {
|
|
170
|
+
out.push(node.text);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
for (const child of node.namedChildren) {
|
|
174
|
+
collectIdentifiers(child, out, rules);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared DFS walker with pluggable visitors for AST analysis.
|
|
3
|
+
*
|
|
4
|
+
* Provides a single tree traversal that multiple analysis visitors can hook into,
|
|
5
|
+
* avoiding redundant walks over the same AST. Two hook styles:
|
|
6
|
+
*
|
|
7
|
+
* - Node-level: enterNode / exitNode (called for every node)
|
|
8
|
+
* - Function-level: enterFunction / exitFunction (called at function boundaries)
|
|
9
|
+
*
|
|
10
|
+
* The walker maintains shared context (nestingLevel, scopeStack, currentFunction)
|
|
11
|
+
* so individual visitors don't need to track traversal state themselves.
|
|
12
|
+
*
|
|
13
|
+
* @typedef {object} VisitorContext
|
|
14
|
+
* @property {number} nestingLevel - Current nesting depth (for complexity)
|
|
15
|
+
* @property {object} currentFunction - Enclosing function node (or null)
|
|
16
|
+
* @property {string} langId - Language ID
|
|
17
|
+
* @property {Array} scopeStack - Function scope stack [{funcName, funcNode, params, locals}]
|
|
18
|
+
*
|
|
19
|
+
* @typedef {object} Visitor
|
|
20
|
+
* @property {string} name
|
|
21
|
+
* @property {function} [init](langId, rules) - Called once before the walk
|
|
22
|
+
* @property {function} [enterNode](node, context) - Called entering each node; return { skipChildren: true } to skip this visitor's hooks for descendants
|
|
23
|
+
* @property {function} [exitNode](node, context) - Called leaving each node
|
|
24
|
+
* @property {function} [enterFunction](funcNode, funcName, context) - Called entering a function
|
|
25
|
+
* @property {function} [exitFunction](funcNode, funcName, context) - Called leaving a function
|
|
26
|
+
* @property {function} [finish]() - Called after the walk; return collected data
|
|
27
|
+
* @property {Set} [functionNodeTypes] - Extra function node types this visitor cares about
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Walk an AST root with multiple visitors in a single DFS pass.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} rootNode - tree-sitter root node to walk
|
|
34
|
+
* @param {Visitor[]} visitors - array of visitor objects
|
|
35
|
+
* @param {string} langId - language identifier
|
|
36
|
+
* @param {object} [options]
|
|
37
|
+
* @param {Set} [options.functionNodeTypes] - set of node types that are function boundaries
|
|
38
|
+
* @param {Set} [options.nestingNodeTypes] - set of node types that increase nesting depth
|
|
39
|
+
* @param {function} [options.getFunctionName] - (funcNode) => string|null
|
|
40
|
+
* @returns {object} Map of visitor.name → finish() result
|
|
41
|
+
*/
|
|
42
|
+
export function walkWithVisitors(rootNode, visitors, langId, options = {}) {
|
|
43
|
+
const {
|
|
44
|
+
functionNodeTypes = new Set(),
|
|
45
|
+
nestingNodeTypes = new Set(),
|
|
46
|
+
getFunctionName = () => null,
|
|
47
|
+
} = options;
|
|
48
|
+
|
|
49
|
+
// Merge all visitors' functionNodeTypes into the master set
|
|
50
|
+
const allFuncTypes = new Set(functionNodeTypes);
|
|
51
|
+
for (const v of visitors) {
|
|
52
|
+
if (v.functionNodeTypes) {
|
|
53
|
+
for (const t of v.functionNodeTypes) allFuncTypes.add(t);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Initialize visitors
|
|
58
|
+
for (const v of visitors) {
|
|
59
|
+
if (v.init) v.init(langId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Shared context object (mutated during walk)
|
|
63
|
+
const scopeStack = [];
|
|
64
|
+
const context = {
|
|
65
|
+
nestingLevel: 0,
|
|
66
|
+
currentFunction: null,
|
|
67
|
+
langId,
|
|
68
|
+
scopeStack,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Track which visitors have requested skipChildren at each depth
|
|
72
|
+
// Key: visitor index, Value: depth at which skip was requested
|
|
73
|
+
const skipDepths = new Map();
|
|
74
|
+
|
|
75
|
+
function walk(node, depth) {
|
|
76
|
+
if (!node) return;
|
|
77
|
+
|
|
78
|
+
const type = node.type;
|
|
79
|
+
const isFunction = allFuncTypes.has(type);
|
|
80
|
+
let funcName = null;
|
|
81
|
+
|
|
82
|
+
// Function boundary: enter
|
|
83
|
+
if (isFunction) {
|
|
84
|
+
funcName = getFunctionName(node);
|
|
85
|
+
context.currentFunction = node;
|
|
86
|
+
scopeStack.push({ funcName, funcNode: node, params: new Map(), locals: new Map() });
|
|
87
|
+
for (let i = 0; i < visitors.length; i++) {
|
|
88
|
+
const v = visitors[i];
|
|
89
|
+
if (v.enterFunction && !isSkipped(i, depth)) {
|
|
90
|
+
v.enterFunction(node, funcName, context);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// enterNode hooks
|
|
96
|
+
for (let i = 0; i < visitors.length; i++) {
|
|
97
|
+
const v = visitors[i];
|
|
98
|
+
if (v.enterNode && !isSkipped(i, depth)) {
|
|
99
|
+
const result = v.enterNode(node, context);
|
|
100
|
+
if (result?.skipChildren) {
|
|
101
|
+
skipDepths.set(i, depth);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Nesting tracking
|
|
107
|
+
const addsNesting = nestingNodeTypes.has(type);
|
|
108
|
+
if (addsNesting) context.nestingLevel++;
|
|
109
|
+
|
|
110
|
+
// Recurse children using node.child(i) (all children, not just named)
|
|
111
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
112
|
+
walk(node.child(i), depth + 1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Undo nesting
|
|
116
|
+
if (addsNesting) context.nestingLevel--;
|
|
117
|
+
|
|
118
|
+
// exitNode hooks
|
|
119
|
+
for (let i = 0; i < visitors.length; i++) {
|
|
120
|
+
const v = visitors[i];
|
|
121
|
+
if (v.exitNode && !isSkipped(i, depth)) {
|
|
122
|
+
v.exitNode(node, context);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Clear skip for any visitor that started skipping at this depth
|
|
127
|
+
for (let i = 0; i < visitors.length; i++) {
|
|
128
|
+
if (skipDepths.get(i) === depth) {
|
|
129
|
+
skipDepths.delete(i);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Function boundary: exit
|
|
134
|
+
if (isFunction) {
|
|
135
|
+
for (let i = 0; i < visitors.length; i++) {
|
|
136
|
+
const v = visitors[i];
|
|
137
|
+
if (v.exitFunction && !isSkipped(i, depth)) {
|
|
138
|
+
v.exitFunction(node, funcName, context);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
scopeStack.pop();
|
|
142
|
+
context.currentFunction =
|
|
143
|
+
scopeStack.length > 0 ? scopeStack[scopeStack.length - 1].funcNode : null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isSkipped(visitorIndex, currentDepth) {
|
|
148
|
+
const skipAt = skipDepths.get(visitorIndex);
|
|
149
|
+
// Skipped if skip was requested at a shallower (or equal) depth
|
|
150
|
+
// We skip descendants, not the node itself, so skip when currentDepth > skipAt
|
|
151
|
+
return skipAt !== undefined && currentDepth > skipAt;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
walk(rootNode, 0);
|
|
155
|
+
|
|
156
|
+
// Collect results
|
|
157
|
+
const results = {};
|
|
158
|
+
for (const v of visitors) {
|
|
159
|
+
results[v.name] = v.finish ? v.finish() : undefined;
|
|
160
|
+
}
|
|
161
|
+
return results;
|
|
162
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visitor: Extract new/throw/await/string/regex AST nodes during a shared walk.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the standalone walkAst() DFS in ast.js with a visitor that plugs
|
|
5
|
+
* into the unified walkWithVisitors framework.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Max length for the `text` column. */
|
|
9
|
+
const TEXT_MAX = 200;
|
|
10
|
+
|
|
11
|
+
function truncate(s, max = TEXT_MAX) {
|
|
12
|
+
if (!s) return null;
|
|
13
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function extractNewName(node) {
|
|
17
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
18
|
+
const child = node.child(i);
|
|
19
|
+
if (child.type === 'identifier') return child.text;
|
|
20
|
+
if (child.type === 'member_expression') return child.text;
|
|
21
|
+
}
|
|
22
|
+
return node.text?.split('(')[0]?.replace('new ', '').trim() || '?';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function extractExpressionText(node) {
|
|
26
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
27
|
+
const child = node.child(i);
|
|
28
|
+
if (child.type !== 'throw' && child.type !== 'await') {
|
|
29
|
+
return truncate(child.text);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return truncate(node.text);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function extractName(kind, node) {
|
|
36
|
+
if (kind === 'throw') {
|
|
37
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
38
|
+
const child = node.child(i);
|
|
39
|
+
if (child.type === 'new_expression') return extractNewName(child);
|
|
40
|
+
if (child.type === 'call_expression') {
|
|
41
|
+
const fn = child.childForFieldName('function');
|
|
42
|
+
return fn ? fn.text : child.text?.split('(')[0] || '?';
|
|
43
|
+
}
|
|
44
|
+
if (child.type === 'identifier') return child.text;
|
|
45
|
+
}
|
|
46
|
+
return truncate(node.text);
|
|
47
|
+
}
|
|
48
|
+
if (kind === 'await') {
|
|
49
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
50
|
+
const child = node.child(i);
|
|
51
|
+
if (child.type === 'call_expression') {
|
|
52
|
+
const fn = child.childForFieldName('function');
|
|
53
|
+
return fn ? fn.text : child.text?.split('(')[0] || '?';
|
|
54
|
+
}
|
|
55
|
+
if (child.type === 'identifier' || child.type === 'member_expression') {
|
|
56
|
+
return child.text;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return truncate(node.text);
|
|
60
|
+
}
|
|
61
|
+
return truncate(node.text);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create an AST-store visitor for use with walkWithVisitors.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} astTypeMap - node type → kind mapping (e.g. JS_TS_AST_TYPES)
|
|
68
|
+
* @param {object[]} defs - symbol definitions for parent lookup
|
|
69
|
+
* @param {string} relPath - relative file path
|
|
70
|
+
* @param {Map} nodeIdMap - def key → node ID mapping
|
|
71
|
+
* @returns {Visitor}
|
|
72
|
+
*/
|
|
73
|
+
export function createAstStoreVisitor(astTypeMap, defs, relPath, nodeIdMap) {
|
|
74
|
+
const rows = [];
|
|
75
|
+
// Track which nodes we've already matched to avoid duplicates in recursive walk
|
|
76
|
+
const matched = new Set();
|
|
77
|
+
|
|
78
|
+
function findParentDef(line) {
|
|
79
|
+
let best = null;
|
|
80
|
+
for (const def of defs) {
|
|
81
|
+
if (def.line <= line && (def.endLine == null || def.endLine >= line)) {
|
|
82
|
+
if (!best || def.endLine - def.line < best.endLine - best.line) {
|
|
83
|
+
best = def;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return best;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolveParentNodeId(line) {
|
|
91
|
+
const parentDef = findParentDef(line);
|
|
92
|
+
if (!parentDef) return null;
|
|
93
|
+
return nodeIdMap.get(`${parentDef.name}|${parentDef.kind}|${parentDef.line}`) || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
name: 'ast-store',
|
|
98
|
+
|
|
99
|
+
enterNode(node, _context) {
|
|
100
|
+
if (matched.has(node.id)) return;
|
|
101
|
+
|
|
102
|
+
const kind = astTypeMap[node.type];
|
|
103
|
+
if (!kind) return;
|
|
104
|
+
|
|
105
|
+
const line = node.startPosition.row + 1;
|
|
106
|
+
let name;
|
|
107
|
+
let text = null;
|
|
108
|
+
|
|
109
|
+
if (kind === 'new') {
|
|
110
|
+
name = extractNewName(node);
|
|
111
|
+
text = truncate(node.text);
|
|
112
|
+
} else if (kind === 'throw') {
|
|
113
|
+
name = extractName('throw', node);
|
|
114
|
+
text = extractExpressionText(node);
|
|
115
|
+
} else if (kind === 'await') {
|
|
116
|
+
name = extractName('await', node);
|
|
117
|
+
text = extractExpressionText(node);
|
|
118
|
+
} else if (kind === 'string') {
|
|
119
|
+
const content = node.text?.replace(/^['"`]|['"`]$/g, '') || '';
|
|
120
|
+
if (content.length < 2) return; // skip trivial strings, walker still descends
|
|
121
|
+
name = truncate(content, 100);
|
|
122
|
+
text = truncate(node.text);
|
|
123
|
+
} else if (kind === 'regex') {
|
|
124
|
+
name = node.text || '?';
|
|
125
|
+
text = truncate(node.text);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
rows.push({
|
|
129
|
+
file: relPath,
|
|
130
|
+
line,
|
|
131
|
+
kind,
|
|
132
|
+
name,
|
|
133
|
+
text,
|
|
134
|
+
receiver: null,
|
|
135
|
+
parentNodeId: resolveParentNodeId(line),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
matched.add(node.id);
|
|
139
|
+
|
|
140
|
+
// Don't recurse into children for new/throw/await (same as original walkAst)
|
|
141
|
+
if (kind !== 'string' && kind !== 'regex') {
|
|
142
|
+
return { skipChildren: true };
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
finish() {
|
|
147
|
+
return rows;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|