@optave/codegraph 3.1.5 → 3.2.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.
Files changed (91) hide show
  1. package/README.md +3 -2
  2. package/package.json +7 -7
  3. package/src/ast-analysis/engine.js +252 -258
  4. package/src/ast-analysis/shared.js +0 -12
  5. package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
  6. package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
  7. package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
  8. package/src/cli/commands/ast.js +2 -1
  9. package/src/cli/commands/audit.js +2 -1
  10. package/src/cli/commands/batch.js +2 -1
  11. package/src/cli/commands/brief.js +12 -0
  12. package/src/cli/commands/cfg.js +2 -1
  13. package/src/cli/commands/check.js +20 -23
  14. package/src/cli/commands/children.js +6 -1
  15. package/src/cli/commands/complexity.js +2 -1
  16. package/src/cli/commands/context.js +6 -1
  17. package/src/cli/commands/dataflow.js +2 -1
  18. package/src/cli/commands/deps.js +8 -3
  19. package/src/cli/commands/flow.js +2 -1
  20. package/src/cli/commands/fn-impact.js +6 -1
  21. package/src/cli/commands/owners.js +4 -2
  22. package/src/cli/commands/query.js +6 -1
  23. package/src/cli/commands/roles.js +2 -1
  24. package/src/cli/commands/search.js +8 -2
  25. package/src/cli/commands/sequence.js +2 -1
  26. package/src/cli/commands/triage.js +38 -27
  27. package/src/db/connection.js +18 -12
  28. package/src/db/migrations.js +41 -64
  29. package/src/db/query-builder.js +60 -4
  30. package/src/db/repository/in-memory-repository.js +27 -16
  31. package/src/db/repository/nodes.js +8 -10
  32. package/src/domain/analysis/brief.js +155 -0
  33. package/src/domain/analysis/context.js +174 -190
  34. package/src/domain/analysis/dependencies.js +200 -146
  35. package/src/domain/analysis/exports.js +3 -2
  36. package/src/domain/analysis/impact.js +267 -152
  37. package/src/domain/analysis/module-map.js +247 -221
  38. package/src/domain/analysis/roles.js +8 -5
  39. package/src/domain/analysis/symbol-lookup.js +7 -5
  40. package/src/domain/graph/builder/helpers.js +1 -1
  41. package/src/domain/graph/builder/incremental.js +116 -90
  42. package/src/domain/graph/builder/pipeline.js +106 -80
  43. package/src/domain/graph/builder/stages/build-edges.js +318 -239
  44. package/src/domain/graph/builder/stages/detect-changes.js +198 -177
  45. package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
  46. package/src/domain/graph/watcher.js +2 -2
  47. package/src/domain/parser.js +20 -11
  48. package/src/domain/queries.js +1 -0
  49. package/src/domain/search/search/filters.js +9 -5
  50. package/src/domain/search/search/keyword.js +12 -5
  51. package/src/domain/search/search/prepare.js +13 -5
  52. package/src/extractors/csharp.js +224 -207
  53. package/src/extractors/go.js +176 -172
  54. package/src/extractors/hcl.js +94 -78
  55. package/src/extractors/java.js +213 -207
  56. package/src/extractors/javascript.js +274 -304
  57. package/src/extractors/php.js +234 -221
  58. package/src/extractors/python.js +252 -250
  59. package/src/extractors/ruby.js +192 -185
  60. package/src/extractors/rust.js +182 -167
  61. package/src/features/ast.js +5 -3
  62. package/src/features/audit.js +4 -2
  63. package/src/features/boundaries.js +98 -83
  64. package/src/features/cfg.js +134 -143
  65. package/src/features/communities.js +68 -53
  66. package/src/features/complexity.js +143 -132
  67. package/src/features/dataflow.js +146 -149
  68. package/src/features/export.js +3 -3
  69. package/src/features/graph-enrichment.js +2 -2
  70. package/src/features/manifesto.js +9 -6
  71. package/src/features/owners.js +4 -3
  72. package/src/features/sequence.js +152 -141
  73. package/src/features/shared/find-nodes.js +31 -0
  74. package/src/features/structure.js +130 -99
  75. package/src/features/triage.js +83 -68
  76. package/src/graph/classifiers/risk.js +3 -2
  77. package/src/graph/classifiers/roles.js +6 -3
  78. package/src/index.js +1 -0
  79. package/src/mcp/server.js +65 -56
  80. package/src/mcp/tool-registry.js +13 -0
  81. package/src/mcp/tools/brief.js +8 -0
  82. package/src/mcp/tools/index.js +2 -0
  83. package/src/presentation/brief.js +51 -0
  84. package/src/presentation/queries-cli/exports.js +21 -14
  85. package/src/presentation/queries-cli/impact.js +55 -39
  86. package/src/presentation/queries-cli/inspect.js +184 -189
  87. package/src/presentation/queries-cli/overview.js +57 -58
  88. package/src/presentation/queries-cli/path.js +36 -29
  89. package/src/presentation/table.js +0 -8
  90. package/src/shared/generators.js +7 -3
  91. package/src/shared/kinds.js +1 -1
@@ -76,249 +76,262 @@ function extractPhpEnumCases(enumNode) {
76
76
  * Extract symbols from PHP files.
77
77
  */
78
78
  export function extractPHPSymbols(tree, _filePath) {
79
- const definitions = [];
80
- const calls = [];
81
- const imports = [];
82
- const classes = [];
83
- const exports = [];
79
+ const ctx = {
80
+ definitions: [],
81
+ calls: [],
82
+ imports: [],
83
+ classes: [],
84
+ exports: [],
85
+ };
84
86
 
85
- function findPHPParentClass(node) {
86
- let current = node.parent;
87
- while (current) {
88
- if (
89
- current.type === 'class_declaration' ||
90
- current.type === 'trait_declaration' ||
91
- current.type === 'enum_declaration'
92
- ) {
93
- const nameNode = current.childForFieldName('name');
94
- return nameNode ? nameNode.text : null;
95
- }
96
- current = current.parent;
97
- }
98
- return null;
87
+ walkPhpNode(tree.rootNode, ctx);
88
+ return ctx;
89
+ }
90
+
91
+ function walkPhpNode(node, ctx) {
92
+ switch (node.type) {
93
+ case 'function_definition':
94
+ handlePhpFuncDef(node, ctx);
95
+ break;
96
+ case 'class_declaration':
97
+ handlePhpClassDecl(node, ctx);
98
+ break;
99
+ case 'interface_declaration':
100
+ handlePhpInterfaceDecl(node, ctx);
101
+ break;
102
+ case 'trait_declaration':
103
+ handlePhpTraitDecl(node, ctx);
104
+ break;
105
+ case 'enum_declaration':
106
+ handlePhpEnumDecl(node, ctx);
107
+ break;
108
+ case 'method_declaration':
109
+ handlePhpMethodDecl(node, ctx);
110
+ break;
111
+ case 'namespace_use_declaration':
112
+ handlePhpNamespaceUse(node, ctx);
113
+ break;
114
+ case 'function_call_expression':
115
+ handlePhpFuncCall(node, ctx);
116
+ break;
117
+ case 'member_call_expression':
118
+ handlePhpMemberCall(node, ctx);
119
+ break;
120
+ case 'scoped_call_expression':
121
+ handlePhpScopedCall(node, ctx);
122
+ break;
123
+ case 'object_creation_expression':
124
+ handlePhpObjectCreation(node, ctx);
125
+ break;
99
126
  }
100
127
 
101
- function walkPhpNode(node) {
102
- switch (node.type) {
103
- case 'function_definition': {
104
- const nameNode = node.childForFieldName('name');
105
- if (nameNode) {
106
- const params = extractPhpParameters(node);
107
- definitions.push({
108
- name: nameNode.text,
109
- kind: 'function',
110
- line: node.startPosition.row + 1,
111
- endLine: nodeEndLine(node),
112
- children: params.length > 0 ? params : undefined,
113
- });
114
- }
115
- break;
116
- }
128
+ for (let i = 0; i < node.childCount; i++) walkPhpNode(node.child(i), ctx);
129
+ }
117
130
 
118
- case 'class_declaration': {
119
- const nameNode = node.childForFieldName('name');
120
- if (nameNode) {
121
- const classChildren = extractPhpClassChildren(node);
122
- definitions.push({
123
- name: nameNode.text,
124
- kind: 'class',
125
- line: node.startPosition.row + 1,
126
- endLine: nodeEndLine(node),
127
- children: classChildren.length > 0 ? classChildren : undefined,
128
- });
131
+ // ── Walk-path per-node-type handlers ────────────────────────────────────────
129
132
 
130
- // Check base clause (extends)
131
- const baseClause =
132
- node.childForFieldName('base_clause') || findChild(node, 'base_clause');
133
- if (baseClause) {
134
- for (let i = 0; i < baseClause.childCount; i++) {
135
- const child = baseClause.child(i);
136
- if (child && (child.type === 'name' || child.type === 'qualified_name')) {
137
- classes.push({
138
- name: nameNode.text,
139
- extends: child.text,
140
- line: node.startPosition.row + 1,
141
- });
142
- break;
143
- }
144
- }
145
- }
133
+ function handlePhpFuncDef(node, ctx) {
134
+ const nameNode = node.childForFieldName('name');
135
+ if (!nameNode) return;
136
+ const params = extractPhpParameters(node);
137
+ ctx.definitions.push({
138
+ name: nameNode.text,
139
+ kind: 'function',
140
+ line: node.startPosition.row + 1,
141
+ endLine: nodeEndLine(node),
142
+ children: params.length > 0 ? params : undefined,
143
+ });
144
+ }
146
145
 
147
- // Check class interface clause (implements)
148
- const interfaceClause = findChild(node, 'class_interface_clause');
149
- if (interfaceClause) {
150
- for (let i = 0; i < interfaceClause.childCount; i++) {
151
- const child = interfaceClause.child(i);
152
- if (child && (child.type === 'name' || child.type === 'qualified_name')) {
153
- classes.push({
154
- name: nameNode.text,
155
- implements: child.text,
156
- line: node.startPosition.row + 1,
157
- });
158
- }
159
- }
160
- }
161
- }
146
+ function handlePhpClassDecl(node, ctx) {
147
+ const nameNode = node.childForFieldName('name');
148
+ if (!nameNode) return;
149
+ const classChildren = extractPhpClassChildren(node);
150
+ ctx.definitions.push({
151
+ name: nameNode.text,
152
+ kind: 'class',
153
+ line: node.startPosition.row + 1,
154
+ endLine: nodeEndLine(node),
155
+ children: classChildren.length > 0 ? classChildren : undefined,
156
+ });
157
+ const baseClause = node.childForFieldName('base_clause') || findChild(node, 'base_clause');
158
+ if (baseClause) {
159
+ for (let i = 0; i < baseClause.childCount; i++) {
160
+ const child = baseClause.child(i);
161
+ if (child && (child.type === 'name' || child.type === 'qualified_name')) {
162
+ ctx.classes.push({
163
+ name: nameNode.text,
164
+ extends: child.text,
165
+ line: node.startPosition.row + 1,
166
+ });
162
167
  break;
163
168
  }
164
-
165
- case 'interface_declaration': {
166
- const nameNode = node.childForFieldName('name');
167
- if (nameNode) {
168
- definitions.push({
169
- name: nameNode.text,
170
- kind: 'interface',
171
- line: node.startPosition.row + 1,
172
- endLine: nodeEndLine(node),
173
- });
174
- const body = node.childForFieldName('body');
175
- if (body) {
176
- for (let i = 0; i < body.childCount; i++) {
177
- const child = body.child(i);
178
- if (child && child.type === 'method_declaration') {
179
- const methName = child.childForFieldName('name');
180
- if (methName) {
181
- definitions.push({
182
- name: `${nameNode.text}.${methName.text}`,
183
- kind: 'method',
184
- line: child.startPosition.row + 1,
185
- endLine: child.endPosition.row + 1,
186
- });
187
- }
188
- }
189
- }
190
- }
191
- }
192
- break;
169
+ }
170
+ }
171
+ const interfaceClause = findChild(node, 'class_interface_clause');
172
+ if (interfaceClause) {
173
+ for (let i = 0; i < interfaceClause.childCount; i++) {
174
+ const child = interfaceClause.child(i);
175
+ if (child && (child.type === 'name' || child.type === 'qualified_name')) {
176
+ ctx.classes.push({
177
+ name: nameNode.text,
178
+ implements: child.text,
179
+ line: node.startPosition.row + 1,
180
+ });
193
181
  }
182
+ }
183
+ }
184
+ }
194
185
 
195
- case 'trait_declaration': {
196
- const nameNode = node.childForFieldName('name');
197
- if (nameNode) {
198
- definitions.push({
199
- name: nameNode.text,
200
- kind: 'trait',
201
- line: node.startPosition.row + 1,
202
- endLine: nodeEndLine(node),
186
+ function handlePhpInterfaceDecl(node, ctx) {
187
+ const nameNode = node.childForFieldName('name');
188
+ if (!nameNode) return;
189
+ ctx.definitions.push({
190
+ name: nameNode.text,
191
+ kind: 'interface',
192
+ line: node.startPosition.row + 1,
193
+ endLine: nodeEndLine(node),
194
+ });
195
+ const body = node.childForFieldName('body');
196
+ if (body) {
197
+ for (let i = 0; i < body.childCount; i++) {
198
+ const child = body.child(i);
199
+ if (child && child.type === 'method_declaration') {
200
+ const methName = child.childForFieldName('name');
201
+ if (methName) {
202
+ ctx.definitions.push({
203
+ name: `${nameNode.text}.${methName.text}`,
204
+ kind: 'method',
205
+ line: child.startPosition.row + 1,
206
+ endLine: child.endPosition.row + 1,
203
207
  });
204
208
  }
205
- break;
206
209
  }
210
+ }
211
+ }
212
+ }
207
213
 
208
- case 'enum_declaration': {
209
- const nameNode = node.childForFieldName('name');
210
- if (nameNode) {
211
- const enumChildren = extractPhpEnumCases(node);
212
- definitions.push({
213
- name: nameNode.text,
214
- kind: 'enum',
215
- line: node.startPosition.row + 1,
216
- endLine: nodeEndLine(node),
217
- children: enumChildren.length > 0 ? enumChildren : undefined,
218
- });
219
- }
220
- break;
221
- }
214
+ function handlePhpTraitDecl(node, ctx) {
215
+ const nameNode = node.childForFieldName('name');
216
+ if (!nameNode) return;
217
+ ctx.definitions.push({
218
+ name: nameNode.text,
219
+ kind: 'trait',
220
+ line: node.startPosition.row + 1,
221
+ endLine: nodeEndLine(node),
222
+ });
223
+ }
222
224
 
223
- case 'method_declaration': {
224
- const nameNode = node.childForFieldName('name');
225
- if (nameNode) {
226
- const parentClass = findPHPParentClass(node);
227
- const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text;
228
- const params = extractPhpParameters(node);
229
- definitions.push({
230
- name: fullName,
231
- kind: 'method',
232
- line: node.startPosition.row + 1,
233
- endLine: nodeEndLine(node),
234
- children: params.length > 0 ? params : undefined,
235
- visibility: extractModifierVisibility(node),
236
- });
237
- }
238
- break;
239
- }
225
+ function handlePhpEnumDecl(node, ctx) {
226
+ const nameNode = node.childForFieldName('name');
227
+ if (!nameNode) return;
228
+ const enumChildren = extractPhpEnumCases(node);
229
+ ctx.definitions.push({
230
+ name: nameNode.text,
231
+ kind: 'enum',
232
+ line: node.startPosition.row + 1,
233
+ endLine: nodeEndLine(node),
234
+ children: enumChildren.length > 0 ? enumChildren : undefined,
235
+ });
236
+ }
240
237
 
241
- case 'namespace_use_declaration': {
242
- // use App\Models\User;
243
- for (let i = 0; i < node.childCount; i++) {
244
- const child = node.child(i);
245
- if (child && child.type === 'namespace_use_clause') {
246
- const nameNode = findChild(child, 'qualified_name') || findChild(child, 'name');
247
- if (nameNode) {
248
- const fullPath = nameNode.text;
249
- const lastName = fullPath.split('\\').pop();
250
- const alias = child.childForFieldName('alias');
251
- imports.push({
252
- source: fullPath,
253
- names: [alias ? alias.text : lastName],
254
- line: node.startPosition.row + 1,
255
- phpUse: true,
256
- });
257
- }
258
- }
259
- // Single use clause without wrapper
260
- if (child && (child.type === 'qualified_name' || child.type === 'name')) {
261
- const fullPath = child.text;
262
- const lastName = fullPath.split('\\').pop();
263
- imports.push({
264
- source: fullPath,
265
- names: [lastName],
266
- line: node.startPosition.row + 1,
267
- phpUse: true,
268
- });
269
- }
270
- }
271
- break;
272
- }
238
+ function handlePhpMethodDecl(node, ctx) {
239
+ // Skip interface methods already emitted by handlePhpInterfaceDecl
240
+ if (node.parent?.parent?.type === 'interface_declaration') return;
241
+ const nameNode = node.childForFieldName('name');
242
+ if (!nameNode) return;
243
+ const parentClass = findPHPParentClass(node);
244
+ const fullName = parentClass ? `${parentClass}.${nameNode.text}` : nameNode.text;
245
+ const params = extractPhpParameters(node);
246
+ ctx.definitions.push({
247
+ name: fullName,
248
+ kind: 'method',
249
+ line: node.startPosition.row + 1,
250
+ endLine: nodeEndLine(node),
251
+ children: params.length > 0 ? params : undefined,
252
+ visibility: extractModifierVisibility(node),
253
+ });
254
+ }
273
255
 
274
- case 'function_call_expression': {
275
- const fn = node.childForFieldName('function') || node.child(0);
276
- if (fn) {
277
- if (fn.type === 'name' || fn.type === 'identifier') {
278
- calls.push({ name: fn.text, line: node.startPosition.row + 1 });
279
- } else if (fn.type === 'qualified_name') {
280
- const parts = fn.text.split('\\');
281
- calls.push({ name: parts[parts.length - 1], line: node.startPosition.row + 1 });
282
- }
283
- }
284
- break;
256
+ function handlePhpNamespaceUse(node, ctx) {
257
+ for (let i = 0; i < node.childCount; i++) {
258
+ const child = node.child(i);
259
+ if (child && child.type === 'namespace_use_clause') {
260
+ const nameNode = findChild(child, 'qualified_name') || findChild(child, 'name');
261
+ if (nameNode) {
262
+ const fullPath = nameNode.text;
263
+ const lastName = fullPath.split('\\').pop();
264
+ const alias = child.childForFieldName('alias');
265
+ ctx.imports.push({
266
+ source: fullPath,
267
+ names: [alias ? alias.text : lastName],
268
+ line: node.startPosition.row + 1,
269
+ phpUse: true,
270
+ });
285
271
  }
272
+ }
273
+ if (child && (child.type === 'qualified_name' || child.type === 'name')) {
274
+ const fullPath = child.text;
275
+ const lastName = fullPath.split('\\').pop();
276
+ ctx.imports.push({
277
+ source: fullPath,
278
+ names: [lastName],
279
+ line: node.startPosition.row + 1,
280
+ phpUse: true,
281
+ });
282
+ }
283
+ }
284
+ }
286
285
 
287
- case 'member_call_expression': {
288
- const name = node.childForFieldName('name');
289
- if (name) {
290
- const obj = node.childForFieldName('object');
291
- const call = { name: name.text, line: node.startPosition.row + 1 };
292
- if (obj) call.receiver = obj.text;
293
- calls.push(call);
294
- }
295
- break;
296
- }
286
+ function handlePhpFuncCall(node, ctx) {
287
+ const fn = node.childForFieldName('function') || node.child(0);
288
+ if (!fn) return;
289
+ if (fn.type === 'name' || fn.type === 'identifier') {
290
+ ctx.calls.push({ name: fn.text, line: node.startPosition.row + 1 });
291
+ } else if (fn.type === 'qualified_name') {
292
+ const parts = fn.text.split('\\');
293
+ ctx.calls.push({ name: parts[parts.length - 1], line: node.startPosition.row + 1 });
294
+ }
295
+ }
297
296
 
298
- case 'scoped_call_expression': {
299
- const name = node.childForFieldName('name');
300
- if (name) {
301
- const scope = node.childForFieldName('scope');
302
- const call = { name: name.text, line: node.startPosition.row + 1 };
303
- if (scope) call.receiver = scope.text;
304
- calls.push(call);
305
- }
306
- break;
307
- }
297
+ function handlePhpMemberCall(node, ctx) {
298
+ const name = node.childForFieldName('name');
299
+ if (!name) return;
300
+ const obj = node.childForFieldName('object');
301
+ const call = { name: name.text, line: node.startPosition.row + 1 };
302
+ if (obj) call.receiver = obj.text;
303
+ ctx.calls.push(call);
304
+ }
308
305
 
309
- case 'object_creation_expression': {
310
- const classNode = node.child(1); // skip 'new' keyword
311
- if (classNode && (classNode.type === 'name' || classNode.type === 'qualified_name')) {
312
- const parts = classNode.text.split('\\');
313
- calls.push({ name: parts[parts.length - 1], line: node.startPosition.row + 1 });
314
- }
315
- break;
316
- }
317
- }
306
+ function handlePhpScopedCall(node, ctx) {
307
+ const name = node.childForFieldName('name');
308
+ if (!name) return;
309
+ const scope = node.childForFieldName('scope');
310
+ const call = { name: name.text, line: node.startPosition.row + 1 };
311
+ if (scope) call.receiver = scope.text;
312
+ ctx.calls.push(call);
313
+ }
318
314
 
319
- for (let i = 0; i < node.childCount; i++) walkPhpNode(node.child(i));
315
+ function handlePhpObjectCreation(node, ctx) {
316
+ const classNode = node.child(1);
317
+ if (classNode && (classNode.type === 'name' || classNode.type === 'qualified_name')) {
318
+ const parts = classNode.text.split('\\');
319
+ ctx.calls.push({ name: parts[parts.length - 1], line: node.startPosition.row + 1 });
320
320
  }
321
+ }
321
322
 
322
- walkPhpNode(tree.rootNode);
323
- return { definitions, calls, imports, classes, exports };
323
+ function findPHPParentClass(node) {
324
+ let current = node.parent;
325
+ while (current) {
326
+ if (
327
+ current.type === 'class_declaration' ||
328
+ current.type === 'trait_declaration' ||
329
+ current.type === 'enum_declaration'
330
+ ) {
331
+ const nameNode = current.childForFieldName('name');
332
+ return nameNode ? nameNode.text : null;
333
+ }
334
+ current = current.parent;
335
+ }
336
+ return null;
324
337
  }