@optave/codegraph 2.6.0 → 3.0.0

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.
@@ -22,12 +22,14 @@ export function extractPythonSymbols(tree, _filePath) {
22
22
  const parentClass = findPythonParentClass(node);
23
23
  const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text;
24
24
  const kind = parentClass ? 'method' : 'function';
25
+ const fnChildren = extractPythonParameters(node);
25
26
  definitions.push({
26
27
  name: fullName,
27
28
  kind,
28
29
  line: node.startPosition.row + 1,
29
30
  endLine: nodeEndLine(node),
30
31
  decorators,
32
+ children: fnChildren.length > 0 ? fnChildren : undefined,
31
33
  });
32
34
  }
33
35
  break;
@@ -36,11 +38,13 @@ export function extractPythonSymbols(tree, _filePath) {
36
38
  case 'class_definition': {
37
39
  const nameNode = node.childForFieldName('name');
38
40
  if (nameNode) {
41
+ const clsChildren = extractPythonClassProperties(node);
39
42
  definitions.push({
40
43
  name: nameNode.text,
41
44
  kind: 'class',
42
45
  line: node.startPosition.row + 1,
43
46
  endLine: nodeEndLine(node),
47
+ children: clsChildren.length > 0 ? clsChildren : undefined,
44
48
  });
45
49
  const superclasses =
46
50
  node.childForFieldName('superclasses') || findChild(node, 'argument_list');
@@ -108,6 +112,24 @@ export function extractPythonSymbols(tree, _filePath) {
108
112
  break;
109
113
  }
110
114
 
115
+ case 'expression_statement': {
116
+ // Module-level UPPER_CASE assignments → constants
117
+ if (node.parent && node.parent.type === 'module') {
118
+ const assignment = findChild(node, 'assignment');
119
+ if (assignment) {
120
+ const left = assignment.childForFieldName('left');
121
+ if (left && left.type === 'identifier' && /^[A-Z_][A-Z0-9_]*$/.test(left.text)) {
122
+ definitions.push({
123
+ name: left.text,
124
+ kind: 'constant',
125
+ line: node.startPosition.row + 1,
126
+ });
127
+ }
128
+ }
129
+ }
130
+ break;
131
+ }
132
+
111
133
  case 'import_from_statement': {
112
134
  let source = '';
113
135
  const names = [];
@@ -133,6 +155,118 @@ export function extractPythonSymbols(tree, _filePath) {
133
155
  for (let i = 0; i < node.childCount; i++) walkPythonNode(node.child(i));
134
156
  }
135
157
 
158
+ function extractPythonParameters(fnNode) {
159
+ const params = [];
160
+ const paramsNode = fnNode.childForFieldName('parameters') || findChild(fnNode, 'parameters');
161
+ if (!paramsNode) return params;
162
+ for (let i = 0; i < paramsNode.childCount; i++) {
163
+ const child = paramsNode.child(i);
164
+ if (!child) continue;
165
+ const t = child.type;
166
+ if (t === 'identifier') {
167
+ params.push({ name: child.text, kind: 'parameter', line: child.startPosition.row + 1 });
168
+ } else if (
169
+ t === 'typed_parameter' ||
170
+ t === 'default_parameter' ||
171
+ t === 'typed_default_parameter'
172
+ ) {
173
+ const nameNode = child.childForFieldName('name') || child.child(0);
174
+ if (nameNode && nameNode.type === 'identifier') {
175
+ params.push({
176
+ name: nameNode.text,
177
+ kind: 'parameter',
178
+ line: child.startPosition.row + 1,
179
+ });
180
+ }
181
+ } else if (t === 'list_splat_pattern' || t === 'dictionary_splat_pattern') {
182
+ // *args, **kwargs
183
+ for (let j = 0; j < child.childCount; j++) {
184
+ const inner = child.child(j);
185
+ if (inner && inner.type === 'identifier') {
186
+ params.push({ name: inner.text, kind: 'parameter', line: child.startPosition.row + 1 });
187
+ break;
188
+ }
189
+ }
190
+ }
191
+ }
192
+ return params;
193
+ }
194
+
195
+ function extractPythonClassProperties(classNode) {
196
+ const props = [];
197
+ const seen = new Set();
198
+ const body = classNode.childForFieldName('body') || findChild(classNode, 'block');
199
+ if (!body) return props;
200
+
201
+ for (let i = 0; i < body.childCount; i++) {
202
+ const child = body.child(i);
203
+ if (!child) continue;
204
+
205
+ // Direct class attribute assignments: x = 5
206
+ if (child.type === 'expression_statement') {
207
+ const assignment = findChild(child, 'assignment');
208
+ if (assignment) {
209
+ const left = assignment.childForFieldName('left');
210
+ if (left && left.type === 'identifier' && !seen.has(left.text)) {
211
+ seen.add(left.text);
212
+ props.push({ name: left.text, kind: 'property', line: child.startPosition.row + 1 });
213
+ }
214
+ }
215
+ }
216
+
217
+ // __init__ method: self.x = ... assignments
218
+ if (child.type === 'function_definition') {
219
+ const fnName = child.childForFieldName('name');
220
+ if (fnName && fnName.text === '__init__') {
221
+ const initBody = child.childForFieldName('body') || findChild(child, 'block');
222
+ if (initBody) {
223
+ walkInitBody(initBody, seen, props);
224
+ }
225
+ }
226
+ }
227
+
228
+ // decorated __init__
229
+ if (child.type === 'decorated_definition') {
230
+ for (let j = 0; j < child.childCount; j++) {
231
+ const inner = child.child(j);
232
+ if (inner && inner.type === 'function_definition') {
233
+ const fnName = inner.childForFieldName('name');
234
+ if (fnName && fnName.text === '__init__') {
235
+ const initBody = inner.childForFieldName('body') || findChild(inner, 'block');
236
+ if (initBody) {
237
+ walkInitBody(initBody, seen, props);
238
+ }
239
+ }
240
+ }
241
+ }
242
+ }
243
+ }
244
+ return props;
245
+ }
246
+
247
+ function walkInitBody(bodyNode, seen, props) {
248
+ for (let i = 0; i < bodyNode.childCount; i++) {
249
+ const stmt = bodyNode.child(i);
250
+ if (!stmt || stmt.type !== 'expression_statement') continue;
251
+ const assignment = findChild(stmt, 'assignment');
252
+ if (!assignment) continue;
253
+ const left = assignment.childForFieldName('left');
254
+ if (!left || left.type !== 'attribute') continue;
255
+ const obj = left.childForFieldName('object');
256
+ const attr = left.childForFieldName('attribute');
257
+ if (
258
+ obj &&
259
+ obj.text === 'self' &&
260
+ attr &&
261
+ attr.type === 'identifier' &&
262
+ !seen.has(attr.text)
263
+ ) {
264
+ seen.add(attr.text);
265
+ props.push({ name: attr.text, kind: 'property', line: stmt.startPosition.row + 1 });
266
+ }
267
+ }
268
+ }
269
+
136
270
  function findPythonParentClass(node) {
137
271
  let current = node.parent;
138
272
  while (current) {
@@ -31,11 +31,13 @@ export function extractRubySymbols(tree, _filePath) {
31
31
  case 'class': {
32
32
  const nameNode = node.childForFieldName('name');
33
33
  if (nameNode) {
34
+ const classChildren = extractRubyClassChildren(node);
34
35
  definitions.push({
35
36
  name: nameNode.text,
36
37
  kind: 'class',
37
38
  line: node.startPosition.row + 1,
38
39
  endLine: nodeEndLine(node),
40
+ children: classChildren.length > 0 ? classChildren : undefined,
39
41
  });
40
42
  const superclass = node.childForFieldName('superclass');
41
43
  if (superclass) {
@@ -73,11 +75,13 @@ export function extractRubySymbols(tree, _filePath) {
73
75
  case 'module': {
74
76
  const nameNode = node.childForFieldName('name');
75
77
  if (nameNode) {
78
+ const moduleChildren = extractRubyBodyConstants(node);
76
79
  definitions.push({
77
80
  name: nameNode.text,
78
81
  kind: 'module',
79
82
  line: node.startPosition.row + 1,
80
83
  endLine: nodeEndLine(node),
84
+ children: moduleChildren.length > 0 ? moduleChildren : undefined,
81
85
  });
82
86
  }
83
87
  break;
@@ -88,11 +92,13 @@ export function extractRubySymbols(tree, _filePath) {
88
92
  if (nameNode) {
89
93
  const parentClass = findRubyParentClass(node);
90
94
  const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text;
95
+ const params = extractRubyParameters(node);
91
96
  definitions.push({
92
97
  name: fullName,
93
98
  kind: 'method',
94
99
  line: node.startPosition.row + 1,
95
100
  endLine: nodeEndLine(node),
101
+ children: params.length > 0 ? params : undefined,
96
102
  });
97
103
  }
98
104
  break;
@@ -103,16 +109,34 @@ export function extractRubySymbols(tree, _filePath) {
103
109
  if (nameNode) {
104
110
  const parentClass = findRubyParentClass(node);
105
111
  const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text;
112
+ const params = extractRubyParameters(node);
106
113
  definitions.push({
107
114
  name: fullName,
108
115
  kind: 'function',
109
116
  line: node.startPosition.row + 1,
110
117
  endLine: nodeEndLine(node),
118
+ children: params.length > 0 ? params : undefined,
111
119
  });
112
120
  }
113
121
  break;
114
122
  }
115
123
 
124
+ case 'assignment': {
125
+ // Top-level constant assignments (parent is program)
126
+ if (node.parent && node.parent.type === 'program') {
127
+ const left = node.childForFieldName('left');
128
+ if (left && left.type === 'constant') {
129
+ definitions.push({
130
+ name: left.text,
131
+ kind: 'constant',
132
+ line: node.startPosition.row + 1,
133
+ endLine: nodeEndLine(node),
134
+ });
135
+ }
136
+ }
137
+ break;
138
+ }
139
+
116
140
  case 'call': {
117
141
  const methodNode = node.childForFieldName('method');
118
142
  if (methodNode) {
@@ -186,3 +210,68 @@ export function extractRubySymbols(tree, _filePath) {
186
210
  walkRubyNode(tree.rootNode);
187
211
  return { definitions, calls, imports, classes, exports };
188
212
  }
213
+
214
+ // ── Child extraction helpers ────────────────────────────────────────────────
215
+
216
+ const RUBY_PARAM_TYPES = new Set([
217
+ 'identifier',
218
+ 'optional_parameter',
219
+ 'splat_parameter',
220
+ 'hash_splat_parameter',
221
+ 'block_parameter',
222
+ 'keyword_parameter',
223
+ ]);
224
+
225
+ function extractRubyParameters(methodNode) {
226
+ const params = [];
227
+ const paramList =
228
+ methodNode.childForFieldName('parameters') || findChild(methodNode, 'method_parameters');
229
+ if (!paramList) return params;
230
+ for (let i = 0; i < paramList.childCount; i++) {
231
+ const param = paramList.child(i);
232
+ if (!param || !RUBY_PARAM_TYPES.has(param.type)) continue;
233
+ let name;
234
+ if (param.type === 'identifier') {
235
+ name = param.text;
236
+ } else {
237
+ // Compound parameter types have an identifier child for the name
238
+ const id = findChild(param, 'identifier');
239
+ name = id ? id.text : param.text;
240
+ }
241
+ params.push({ name, kind: 'parameter', line: param.startPosition.row + 1 });
242
+ }
243
+ return params;
244
+ }
245
+
246
+ function extractRubyBodyConstants(containerNode) {
247
+ const children = [];
248
+ const body = containerNode.childForFieldName('body') || findChild(containerNode, 'body');
249
+ if (!body) return children;
250
+ for (let i = 0; i < body.childCount; i++) {
251
+ const child = body.child(i);
252
+ if (!child || child.type !== 'assignment') continue;
253
+ const left = child.childForFieldName('left');
254
+ if (left && left.type === 'constant') {
255
+ children.push({ name: left.text, kind: 'constant', line: child.startPosition.row + 1 });
256
+ }
257
+ }
258
+ return children;
259
+ }
260
+
261
+ function extractRubyClassChildren(classNode) {
262
+ const children = [];
263
+ const body = classNode.childForFieldName('body') || findChild(classNode, 'body');
264
+ if (!body) return children;
265
+ for (let i = 0; i < body.childCount; i++) {
266
+ const child = body.child(i);
267
+ if (!child || child.type !== 'assignment') continue;
268
+ const left = child.childForFieldName('left');
269
+ if (!left) continue;
270
+ if (left.type === 'instance_variable') {
271
+ children.push({ name: left.text, kind: 'property', line: child.startPosition.row + 1 });
272
+ } else if (left.type === 'constant') {
273
+ children.push({ name: left.text, kind: 'constant', line: child.startPosition.row + 1 });
274
+ }
275
+ }
276
+ return children;
277
+ }
@@ -1,4 +1,4 @@
1
- import { nodeEndLine } from './helpers.js';
1
+ import { findChild, nodeEndLine } from './helpers.js';
2
2
 
3
3
  /**
4
4
  * Extract symbols from Rust files.
@@ -30,11 +30,13 @@ export function extractRustSymbols(tree, _filePath) {
30
30
  const implType = findCurrentImpl(node);
31
31
  const fullName = implType ? `${implType}.${nameNode.text}` : nameNode.text;
32
32
  const kind = implType ? 'method' : 'function';
33
+ const params = extractRustParameters(node.childForFieldName('parameters'));
33
34
  definitions.push({
34
35
  name: fullName,
35
36
  kind,
36
37
  line: node.startPosition.row + 1,
37
38
  endLine: nodeEndLine(node),
39
+ children: params.length > 0 ? params : undefined,
38
40
  });
39
41
  }
40
42
  break;
@@ -43,11 +45,13 @@ export function extractRustSymbols(tree, _filePath) {
43
45
  case 'struct_item': {
44
46
  const nameNode = node.childForFieldName('name');
45
47
  if (nameNode) {
48
+ const fields = extractStructFields(node);
46
49
  definitions.push({
47
50
  name: nameNode.text,
48
51
  kind: 'struct',
49
52
  line: node.startPosition.row + 1,
50
53
  endLine: nodeEndLine(node),
54
+ children: fields.length > 0 ? fields : undefined,
51
55
  });
52
56
  }
53
57
  break;
@@ -56,11 +60,26 @@ export function extractRustSymbols(tree, _filePath) {
56
60
  case 'enum_item': {
57
61
  const nameNode = node.childForFieldName('name');
58
62
  if (nameNode) {
63
+ const variants = extractEnumVariants(node);
59
64
  definitions.push({
60
65
  name: nameNode.text,
61
66
  kind: 'enum',
62
67
  line: node.startPosition.row + 1,
63
68
  endLine: nodeEndLine(node),
69
+ children: variants.length > 0 ? variants : undefined,
70
+ });
71
+ }
72
+ break;
73
+ }
74
+
75
+ case 'const_item': {
76
+ const nameNode = node.childForFieldName('name');
77
+ if (nameNode) {
78
+ definitions.push({
79
+ name: nameNode.text,
80
+ kind: 'constant',
81
+ line: node.startPosition.row + 1,
82
+ endLine: nodeEndLine(node),
64
83
  });
65
84
  }
66
85
  break;
@@ -170,6 +189,57 @@ export function extractRustSymbols(tree, _filePath) {
170
189
  return { definitions, calls, imports, classes, exports };
171
190
  }
172
191
 
192
+ // ── Child extraction helpers ────────────────────────────────────────────────
193
+
194
+ function extractRustParameters(paramListNode) {
195
+ const params = [];
196
+ if (!paramListNode) return params;
197
+ for (let i = 0; i < paramListNode.childCount; i++) {
198
+ const param = paramListNode.child(i);
199
+ if (!param) continue;
200
+ if (param.type === 'self_parameter') {
201
+ params.push({ name: 'self', kind: 'parameter', line: param.startPosition.row + 1 });
202
+ } else if (param.type === 'parameter') {
203
+ const pattern = param.childForFieldName('pattern');
204
+ if (pattern) {
205
+ params.push({ name: pattern.text, kind: 'parameter', line: param.startPosition.row + 1 });
206
+ }
207
+ }
208
+ }
209
+ return params;
210
+ }
211
+
212
+ function extractStructFields(structNode) {
213
+ const fields = [];
214
+ const fieldList =
215
+ structNode.childForFieldName('body') || findChild(structNode, 'field_declaration_list');
216
+ if (!fieldList) return fields;
217
+ for (let i = 0; i < fieldList.childCount; i++) {
218
+ const field = fieldList.child(i);
219
+ if (!field || field.type !== 'field_declaration') continue;
220
+ const nameNode = field.childForFieldName('name');
221
+ if (nameNode) {
222
+ fields.push({ name: nameNode.text, kind: 'property', line: field.startPosition.row + 1 });
223
+ }
224
+ }
225
+ return fields;
226
+ }
227
+
228
+ function extractEnumVariants(enumNode) {
229
+ const variants = [];
230
+ const body = enumNode.childForFieldName('body') || findChild(enumNode, 'enum_variant_list');
231
+ if (!body) return variants;
232
+ for (let i = 0; i < body.childCount; i++) {
233
+ const variant = body.child(i);
234
+ if (!variant || variant.type !== 'enum_variant') continue;
235
+ const nameNode = variant.childForFieldName('name');
236
+ if (nameNode) {
237
+ variants.push({ name: nameNode.text, kind: 'constant', line: variant.startPosition.row + 1 });
238
+ }
239
+ }
240
+ return variants;
241
+ }
242
+
173
243
  function extractRustUsePath(node) {
174
244
  if (!node) return [];
175
245
 
package/src/index.js CHANGED
@@ -5,16 +5,35 @@
5
5
  * import { buildGraph, queryNameData, findCycles, exportDOT } from 'codegraph';
6
6
  */
7
7
 
8
+ // AST node queries
9
+ export { AST_NODE_KINDS, astQuery, astQueryData } from './ast.js';
8
10
  // Audit (composite report)
9
11
  export { audit, auditData } from './audit.js';
10
12
  // Batch querying
11
- export { BATCH_COMMANDS, batch, batchData } from './batch.js';
13
+ export {
14
+ BATCH_COMMANDS,
15
+ batch,
16
+ batchData,
17
+ batchQuery,
18
+ multiBatchData,
19
+ splitTargets,
20
+ } from './batch.js';
12
21
  // Architecture boundary rules
13
22
  export { evaluateBoundaries, PRESETS, validateBoundaryConfig } from './boundaries.js';
14
23
  // Branch comparison
15
24
  export { branchCompareData, branchCompareMermaid } from './branch-compare.js';
16
25
  // Graph building
17
26
  export { buildGraph, collectFiles, loadPathAliases, resolveImportPath } from './builder.js';
27
+ // Control flow graph (intraprocedural)
28
+ export {
29
+ buildCFGData,
30
+ buildFunctionCFG,
31
+ CFG_RULES,
32
+ cfg,
33
+ cfgData,
34
+ cfgToDOT,
35
+ cfgToMermaid,
36
+ } from './cfg.js';
18
37
  // Check (CI validation predicates)
19
38
  export { check, checkData } from './check.js';
20
39
  // Co-change analysis
@@ -37,6 +56,7 @@ export {
37
56
  computeHalsteadMetrics,
38
57
  computeLOCMetrics,
39
58
  computeMaintainabilityIndex,
59
+ findFunctionNode,
40
60
  HALSTEAD_RULES,
41
61
  iterComplexity,
42
62
  } from './complexity.js';
@@ -46,6 +66,15 @@ export { loadConfig } from './config.js';
46
66
  export { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
47
67
  // Circular dependency detection
48
68
  export { findCycles, formatCycles } from './cycles.js';
69
+ // Dataflow analysis
70
+ export {
71
+ buildDataflowEdges,
72
+ dataflow,
73
+ dataflowData,
74
+ dataflowImpactData,
75
+ dataflowPathData,
76
+ extractDataflow,
77
+ } from './dataflow.js';
49
78
  // Database utilities
50
79
  export {
51
80
  findDbPath,
@@ -71,8 +100,15 @@ export {
71
100
  search,
72
101
  searchData,
73
102
  } from './embedder.js';
74
- // Export (DOT/Mermaid/JSON)
75
- export { exportDOT, exportJSON, exportMermaid } from './export.js';
103
+ // Export (DOT/Mermaid/JSON/GraphML/GraphSON/Neo4j CSV)
104
+ export {
105
+ exportDOT,
106
+ exportGraphML,
107
+ exportGraphSON,
108
+ exportJSON,
109
+ exportMermaid,
110
+ exportNeo4jCSV,
111
+ } from './export.js';
76
112
  // Execution flow tracing
77
113
  export { entryPointType, flowData, listEntryPointsData } from './flow.js';
78
114
  // Logger
@@ -91,13 +127,21 @@ export { getActiveEngine, parseFileAuto, parseFilesAuto } from './parser.js';
91
127
  // Query functions (data-returning)
92
128
  export {
93
129
  ALL_SYMBOL_KINDS,
130
+ CORE_EDGE_KINDS,
131
+ CORE_SYMBOL_KINDS,
132
+ childrenData,
94
133
  contextData,
95
134
  diffImpactData,
96
135
  diffImpactMermaid,
136
+ EVERY_EDGE_KIND,
137
+ EVERY_SYMBOL_KIND,
138
+ EXTENDED_SYMBOL_KINDS,
97
139
  explainData,
140
+ exportsData,
98
141
  FALSE_POSITIVE_CALLER_THRESHOLD,
99
142
  FALSE_POSITIVE_NAMES,
100
143
  fileDepsData,
144
+ fileExports,
101
145
  fnDepsData,
102
146
  fnImpactData,
103
147
  impactAnalysisData,
@@ -106,9 +150,11 @@ export {
106
150
  iterWhere,
107
151
  kindIcon,
108
152
  moduleMapData,
153
+ normalizeSymbol,
109
154
  pathData,
110
155
  queryNameData,
111
156
  rolesData,
157
+ STRUCTURAL_EDGE_KINDS,
112
158
  statsData,
113
159
  VALID_ROLES,
114
160
  whereData,
@@ -147,5 +193,7 @@ export {
147
193
  } from './structure.js';
148
194
  // Triage — composite risk audit
149
195
  export { triage, triageData } from './triage.js';
196
+ // Interactive HTML viewer
197
+ export { generatePlotHTML, loadPlotConfig } from './viewer.js';
150
198
  // Watch mode
151
199
  export { watchProject } from './watcher.js';