@optave/codegraph 3.1.1 → 3.1.3

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.
Files changed (72) hide show
  1. package/README.md +6 -6
  2. package/package.json +7 -7
  3. package/src/ast-analysis/engine.js +365 -0
  4. package/src/ast-analysis/metrics.js +118 -0
  5. package/src/ast-analysis/visitor-utils.js +176 -0
  6. package/src/ast-analysis/visitor.js +162 -0
  7. package/src/ast-analysis/visitors/ast-store-visitor.js +150 -0
  8. package/src/ast-analysis/visitors/cfg-visitor.js +792 -0
  9. package/src/ast-analysis/visitors/complexity-visitor.js +243 -0
  10. package/src/ast-analysis/visitors/dataflow-visitor.js +358 -0
  11. package/src/ast.js +13 -140
  12. package/src/audit.js +2 -87
  13. package/src/batch.js +0 -25
  14. package/src/boundaries.js +1 -1
  15. package/src/branch-compare.js +1 -96
  16. package/src/builder.js +60 -178
  17. package/src/cfg.js +89 -883
  18. package/src/check.js +1 -84
  19. package/src/cli.js +31 -22
  20. package/src/cochange.js +1 -39
  21. package/src/commands/audit.js +88 -0
  22. package/src/commands/batch.js +26 -0
  23. package/src/commands/branch-compare.js +97 -0
  24. package/src/commands/cfg.js +55 -0
  25. package/src/commands/check.js +82 -0
  26. package/src/commands/cochange.js +37 -0
  27. package/src/commands/communities.js +69 -0
  28. package/src/commands/complexity.js +77 -0
  29. package/src/commands/dataflow.js +110 -0
  30. package/src/commands/flow.js +70 -0
  31. package/src/commands/manifesto.js +77 -0
  32. package/src/commands/owners.js +52 -0
  33. package/src/commands/query.js +21 -0
  34. package/src/commands/sequence.js +33 -0
  35. package/src/commands/structure.js +64 -0
  36. package/src/commands/triage.js +49 -0
  37. package/src/communities.js +12 -83
  38. package/src/complexity.js +43 -357
  39. package/src/cycles.js +1 -1
  40. package/src/dataflow.js +12 -665
  41. package/src/db/repository/build-stmts.js +104 -0
  42. package/src/db/repository/cached-stmt.js +19 -0
  43. package/src/db/repository/cfg.js +72 -0
  44. package/src/db/repository/cochange.js +54 -0
  45. package/src/db/repository/complexity.js +20 -0
  46. package/src/db/repository/dataflow.js +17 -0
  47. package/src/db/repository/edges.js +281 -0
  48. package/src/db/repository/embeddings.js +51 -0
  49. package/src/db/repository/graph-read.js +59 -0
  50. package/src/db/repository/index.js +43 -0
  51. package/src/db/repository/nodes.js +247 -0
  52. package/src/db.js +40 -1
  53. package/src/embedder.js +14 -34
  54. package/src/export.js +1 -1
  55. package/src/extractors/javascript.js +130 -5
  56. package/src/flow.js +2 -70
  57. package/src/index.js +30 -20
  58. package/src/{result-formatter.js → infrastructure/result-formatter.js} +1 -1
  59. package/src/kinds.js +1 -0
  60. package/src/manifesto.js +0 -76
  61. package/src/native.js +31 -9
  62. package/src/owners.js +1 -56
  63. package/src/parser.js +53 -2
  64. package/src/queries-cli.js +1 -1
  65. package/src/queries.js +79 -280
  66. package/src/sequence.js +5 -44
  67. package/src/structure.js +16 -75
  68. package/src/triage.js +1 -54
  69. package/src/viewer.js +1 -1
  70. package/src/watcher.js +7 -4
  71. package/src/db/repository.js +0 -134
  72. /package/src/{test-filter.js → infrastructure/test-filter.js} +0 -0
@@ -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
+ }