@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
|
@@ -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
|
+
}
|