@optave/codegraph 3.9.3 → 3.9.4

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 (40) hide show
  1. package/README.md +8 -8
  2. package/dist/ast-analysis/visitor.d.ts.map +1 -1
  3. package/dist/ast-analysis/visitor.js +14 -0
  4. package/dist/ast-analysis/visitor.js.map +1 -1
  5. package/dist/domain/graph/builder/context.d.ts +15 -0
  6. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  7. package/dist/domain/graph/builder/context.js +7 -0
  8. package/dist/domain/graph/builder/context.js.map +1 -1
  9. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  10. package/dist/domain/graph/builder/pipeline.js +92 -48
  11. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  12. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  13. package/dist/domain/graph/builder/stages/build-edges.js +67 -6
  14. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  15. package/dist/domain/graph/builder/stages/build-structure.js +2 -2
  16. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  17. package/dist/domain/graph/builder/stages/detect-changes.js +51 -10
  18. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  19. package/dist/domain/graph/builder/stages/finalize.d.ts.map +1 -1
  20. package/dist/domain/graph/builder/stages/finalize.js +10 -4
  21. package/dist/domain/graph/builder/stages/finalize.js.map +1 -1
  22. package/dist/domain/graph/builder/stages/run-analyses.d.ts.map +1 -1
  23. package/dist/domain/graph/builder/stages/run-analyses.js +5 -20
  24. package/dist/domain/graph/builder/stages/run-analyses.js.map +1 -1
  25. package/dist/extractors/javascript.js +120 -0
  26. package/dist/extractors/javascript.js.map +1 -1
  27. package/dist/features/structure.d.ts.map +1 -1
  28. package/dist/features/structure.js +14 -1
  29. package/dist/features/structure.js.map +1 -1
  30. package/package.json +7 -7
  31. package/src/ast-analysis/visitor.ts +15 -0
  32. package/src/domain/graph/builder/context.ts +17 -0
  33. package/src/domain/graph/builder/pipeline.ts +93 -46
  34. package/src/domain/graph/builder/stages/build-edges.ts +80 -6
  35. package/src/domain/graph/builder/stages/build-structure.ts +2 -2
  36. package/src/domain/graph/builder/stages/detect-changes.ts +61 -12
  37. package/src/domain/graph/builder/stages/finalize.ts +11 -4
  38. package/src/domain/graph/builder/stages/run-analyses.ts +5 -26
  39. package/src/extractors/javascript.ts +142 -0
  40. package/src/features/structure.ts +17 -1
@@ -274,14 +274,17 @@ function dispatchQueryMatch(
274
274
  name: c.callfn_name!.text,
275
275
  line: c.callfn_node.startPosition.row + 1,
276
276
  });
277
+ calls.push(...extractCallbackReferenceCalls(c.callfn_node));
277
278
  } else if (c.callmem_node) {
278
279
  const callInfo = extractCallInfo(c.callmem_fn!, c.callmem_node);
279
280
  if (callInfo) calls.push(callInfo);
280
281
  const cbDef = extractCallbackDefinition(c.callmem_node, c.callmem_fn);
281
282
  if (cbDef) definitions.push(cbDef);
283
+ calls.push(...extractCallbackReferenceCalls(c.callmem_node));
282
284
  } else if (c.callsub_node) {
283
285
  const callInfo = extractCallInfo(c.callsub_fn!, c.callsub_node);
284
286
  if (callInfo) calls.push(callInfo);
287
+ calls.push(...extractCallbackReferenceCalls(c.callsub_node));
285
288
  } else if (c.newfn_node) {
286
289
  calls.push({
287
290
  name: c.newfn_name!.text,
@@ -321,6 +324,9 @@ function extractSymbolsQuery(tree: TreeSitterTree, query: TreeSitterQuery): Extr
321
324
  // Extract typeMap from type annotations and new expressions
322
325
  extractTypeMapWalk(tree.rootNode, typeMap);
323
326
 
327
+ // Extract definitions from destructured bindings (query patterns don't match object_pattern)
328
+ extractDestructuredBindingsWalk(tree.rootNode, definitions);
329
+
324
330
  return { definitions, calls, imports, classes, exports: exps, typeMap };
325
331
  }
326
332
 
@@ -334,6 +340,20 @@ const FUNCTION_SCOPE_TYPES = new Set([
334
340
  'generator_function',
335
341
  ]);
336
342
 
343
+ /**
344
+ * Return true when `node` has an ancestor whose type is in FUNCTION_SCOPE_TYPES.
345
+ * Used by the walk path to skip declarations inside function bodies, matching
346
+ * the query path's top-down FUNCTION_SCOPE_TYPES filter.
347
+ */
348
+ function hasFunctionScopeAncestor(node: TreeSitterNode): boolean {
349
+ let p: TreeSitterNode | null = node.parent ?? null;
350
+ while (p) {
351
+ if (FUNCTION_SCOPE_TYPES.has(p.type)) return true;
352
+ p = p.parent ?? null;
353
+ }
354
+ return false;
355
+ }
356
+
337
357
  /**
338
358
  * Recursively walk the AST to extract `const x = <literal>` as constants.
339
359
  * Skips nodes inside function scopes so only file-level / block-level constants
@@ -363,6 +383,48 @@ function extractConstantsWalk(node: TreeSitterNode, definitions: Definition[]):
363
383
  }
364
384
  }
365
385
 
386
+ /**
387
+ * Walk the AST to find destructured const bindings (query patterns don't match object_pattern).
388
+ * e.g. `const { handleToken, checkPermissions } = initAuth(config)`
389
+ */
390
+ function extractDestructuredBindingsWalk(node: TreeSitterNode, definitions: Definition[]): void {
391
+ for (let i = 0; i < node.childCount; i++) {
392
+ const child = node.child(i);
393
+ if (!child) continue;
394
+ if (FUNCTION_SCOPE_TYPES.has(child.type)) continue;
395
+
396
+ let declNode = child;
397
+ if (child.type === 'export_statement') {
398
+ const inner = child.childForFieldName('declaration');
399
+ if (inner) declNode = inner;
400
+ }
401
+
402
+ const t = declNode.type;
403
+ if (
404
+ (t === 'lexical_declaration' || t === 'variable_declaration') &&
405
+ declNode.text.startsWith('const ')
406
+ ) {
407
+ for (let j = 0; j < declNode.childCount; j++) {
408
+ const declarator = declNode.child(j);
409
+ if (!declarator || declarator.type !== 'variable_declarator') continue;
410
+ const nameN = declarator.childForFieldName('name');
411
+ if (nameN && nameN.type === 'object_pattern') {
412
+ extractDestructuredBindings(
413
+ nameN,
414
+ declNode.startPosition.row + 1,
415
+ nodeEndLine(declNode),
416
+ definitions,
417
+ );
418
+ }
419
+ }
420
+ }
421
+
422
+ if (child.type !== 'export_statement') {
423
+ extractDestructuredBindingsWalk(child, definitions);
424
+ }
425
+ }
426
+ }
427
+
366
428
  /** Extract constant definitions from a `const` declaration node. */
367
429
  function extractConstDeclarators(declNode: TreeSitterNode, definitions: Definition[]): void {
368
430
  const t = declNode.type;
@@ -637,6 +699,39 @@ function handleTypeAliasDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
637
699
  }
638
700
  }
639
701
 
702
+ /**
703
+ * Extract definitions from destructured object bindings.
704
+ * `const { handleToken, checkPermissions } = initAuth(...)` creates definitions
705
+ * for handleToken and checkPermissions so they can be resolved as call targets.
706
+ */
707
+ function extractDestructuredBindings(
708
+ pattern: TreeSitterNode,
709
+ line: number,
710
+ endLine: number,
711
+ definitions: Definition[],
712
+ ): void {
713
+ for (let i = 0; i < pattern.childCount; i++) {
714
+ const child = pattern.child(i);
715
+ if (!child) continue;
716
+ if (
717
+ child.type === 'shorthand_property_identifier_pattern' ||
718
+ child.type === 'shorthand_property_identifier'
719
+ ) {
720
+ // { handleToken } — shorthand binding
721
+ definitions.push({ name: child.text, kind: 'function', line, endLine });
722
+ } else if (child.type === 'pair_pattern' || child.type === 'pair') {
723
+ // { original: renamed } — renamed binding, use the local alias
724
+ const value = child.childForFieldName('value');
725
+ if (
726
+ value &&
727
+ (value.type === 'identifier' || value.type === 'shorthand_property_identifier_pattern')
728
+ ) {
729
+ definitions.push({ name: value.text, kind: 'function', line, endLine });
730
+ }
731
+ }
732
+ }
733
+ }
734
+
640
735
  function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
641
736
  const isConst = node.text.startsWith('const ');
642
737
  for (let i = 0; i < node.childCount; i++) {
@@ -667,6 +762,20 @@ function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
667
762
  line: node.startPosition.row + 1,
668
763
  endLine: nodeEndLine(node),
669
764
  });
765
+ } else if (isConst && nameN.type === 'object_pattern' && !hasFunctionScopeAncestor(node)) {
766
+ // Destructured bindings: const { handleToken, checkPermissions } = initAuth(...)
767
+ // Each destructured property becomes a function definition so it can be
768
+ // resolved when passed as a callback (e.g. router.use(handleToken)).
769
+ // Restricted to const to avoid creating spurious definitions for
770
+ // transient let/var destructuring (e.g. let { userId } = parseRequest(req)).
771
+ // Scope guard mirrors extractDestructuredBindingsWalk (query path) and
772
+ // handle_var_decl (Rust path) — skips bindings inside function bodies.
773
+ extractDestructuredBindings(
774
+ nameN,
775
+ node.startPosition.row + 1,
776
+ nodeEndLine(node),
777
+ ctx.definitions,
778
+ );
670
779
  }
671
780
  }
672
781
  }
@@ -715,6 +824,7 @@ function handleCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
715
824
  const cbDef = extractCallbackDefinition(node, fn);
716
825
  if (cbDef) ctx.definitions.push(cbDef);
717
826
  }
827
+ ctx.calls.push(...extractCallbackReferenceCalls(node));
718
828
  }
719
829
  }
720
830
 
@@ -1167,6 +1277,38 @@ function extractSubscriptCallInfo(fn: TreeSitterNode, callNode: TreeSitterNode):
1167
1277
  return null;
1168
1278
  }
1169
1279
 
1280
+ /**
1281
+ * Extract Call entries for named function references passed as arguments.
1282
+ * e.g. `router.use(handleToken, checkAuth)` yields calls to handleToken and checkAuth.
1283
+ * `app.use(auth.validate)` yields a call to validate with receiver auth.
1284
+ * Skips literals, objects, arrays, anonymous functions, and call expressions (already handled).
1285
+ */
1286
+ function extractCallbackReferenceCalls(callNode: TreeSitterNode): Call[] {
1287
+ const args = callNode.childForFieldName('arguments') || findChild(callNode, 'arguments');
1288
+ if (!args) return [];
1289
+
1290
+ const result: Call[] = [];
1291
+ const callLine = callNode.startPosition.row + 1;
1292
+
1293
+ for (let i = 0; i < args.childCount; i++) {
1294
+ const child = args.child(i);
1295
+ if (!child) continue;
1296
+
1297
+ if (child.type === 'identifier') {
1298
+ result.push({ name: child.text, line: callLine, dynamic: true });
1299
+ } else if (child.type === 'member_expression') {
1300
+ const prop = child.childForFieldName('property');
1301
+ const obj = child.childForFieldName('object');
1302
+ if (prop) {
1303
+ const receiver = extractReceiverName(obj);
1304
+ result.push({ name: prop.text, line: callLine, dynamic: true, receiver });
1305
+ }
1306
+ }
1307
+ }
1308
+
1309
+ return result;
1310
+ }
1311
+
1170
1312
  function findAnonymousCallback(argsNode: TreeSitterNode): TreeSitterNode | null {
1171
1313
  for (let i = 0; i < argsNode.childCount; i++) {
1172
1314
  const child = argsNode.child(i);
@@ -166,6 +166,22 @@ function computeFileMetrics(
166
166
  fanOutMap: Map<string, number>,
167
167
  ): void {
168
168
  db.transaction(() => {
169
+ // Batch-load import counts per file (distinct imported files,
170
+ // matching the fast-path semantics in updateChangedFileMetrics).
171
+ // Runs inside the transaction for parity with the Rust path.
172
+ const importCountMap = new Map<string, number>();
173
+ for (const row of db
174
+ .prepare(
175
+ `SELECT n1.file AS src, COUNT(DISTINCT n2.file) AS cnt FROM edges e
176
+ JOIN nodes n1 ON e.source_id = n1.id
177
+ JOIN nodes n2 ON e.target_id = n2.id
178
+ WHERE e.kind = 'imports'
179
+ GROUP BY n1.file`,
180
+ )
181
+ .all() as { src: string; cnt: number }[]) {
182
+ importCountMap.set(row.src, row.cnt);
183
+ }
184
+
169
185
  for (const [relPath, symbols] of fileSymbols) {
170
186
  const fileRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
171
187
  if (!fileRow) continue;
@@ -180,7 +196,7 @@ function computeFileMetrics(
180
196
  symbolCount++;
181
197
  }
182
198
  }
183
- const importCount = symbols.imports.length;
199
+ const importCount = importCountMap.get(relPath) || 0;
184
200
  const exportCount = symbols.exports.length;
185
201
  const fanIn = fanInMap.get(relPath) || 0;
186
202
  const fanOut = fanOutMap.get(relPath) || 0;