@optave/codegraph 3.9.0 → 3.9.1

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 (104) hide show
  1. package/README.md +7 -6
  2. package/dist/ast-analysis/engine.d.ts.map +1 -1
  3. package/dist/ast-analysis/engine.js +78 -48
  4. package/dist/ast-analysis/engine.js.map +1 -1
  5. package/dist/ast-analysis/visitors/ast-store-visitor.d.ts.map +1 -1
  6. package/dist/ast-analysis/visitors/ast-store-visitor.js +15 -18
  7. package/dist/ast-analysis/visitors/ast-store-visitor.js.map +1 -1
  8. package/dist/db/connection.d.ts +1 -0
  9. package/dist/db/connection.d.ts.map +1 -1
  10. package/dist/db/connection.js +22 -4
  11. package/dist/db/connection.js.map +1 -1
  12. package/dist/db/repository/base.d.ts +35 -0
  13. package/dist/db/repository/base.d.ts.map +1 -1
  14. package/dist/db/repository/base.js +8 -0
  15. package/dist/db/repository/base.js.map +1 -1
  16. package/dist/db/repository/index.d.ts +1 -0
  17. package/dist/db/repository/index.d.ts.map +1 -1
  18. package/dist/db/repository/index.js.map +1 -1
  19. package/dist/db/repository/native-repository.d.ts +7 -1
  20. package/dist/db/repository/native-repository.d.ts.map +1 -1
  21. package/dist/db/repository/native-repository.js +46 -1
  22. package/dist/db/repository/native-repository.js.map +1 -1
  23. package/dist/domain/analysis/dependencies.d.ts +1 -28
  24. package/dist/domain/analysis/dependencies.d.ts.map +1 -1
  25. package/dist/domain/analysis/dependencies.js +12 -0
  26. package/dist/domain/analysis/dependencies.js.map +1 -1
  27. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  28. package/dist/domain/graph/builder/incremental.js +18 -0
  29. package/dist/domain/graph/builder/incremental.js.map +1 -1
  30. package/dist/domain/graph/builder/pipeline.d.ts.map +1 -1
  31. package/dist/domain/graph/builder/pipeline.js +293 -296
  32. package/dist/domain/graph/builder/pipeline.js.map +1 -1
  33. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  34. package/dist/domain/graph/builder/stages/build-edges.js +29 -2
  35. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  36. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  37. package/dist/domain/graph/builder/stages/resolve-imports.js +19 -23
  38. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  39. package/dist/domain/graph/watcher.d.ts.map +1 -1
  40. package/dist/domain/graph/watcher.js +99 -95
  41. package/dist/domain/graph/watcher.js.map +1 -1
  42. package/dist/domain/parser.d.ts.map +1 -1
  43. package/dist/domain/parser.js +2 -0
  44. package/dist/domain/parser.js.map +1 -1
  45. package/dist/extractors/go.js +53 -35
  46. package/dist/extractors/go.js.map +1 -1
  47. package/dist/extractors/javascript.js +66 -27
  48. package/dist/extractors/javascript.js.map +1 -1
  49. package/dist/features/complexity.d.ts.map +1 -1
  50. package/dist/features/complexity.js +78 -58
  51. package/dist/features/complexity.js.map +1 -1
  52. package/dist/features/dataflow.d.ts.map +1 -1
  53. package/dist/features/dataflow.js +109 -118
  54. package/dist/features/dataflow.js.map +1 -1
  55. package/dist/features/structure.d.ts.map +1 -1
  56. package/dist/features/structure.js +147 -97
  57. package/dist/features/structure.js.map +1 -1
  58. package/dist/graph/algorithms/louvain.d.ts.map +1 -1
  59. package/dist/graph/algorithms/louvain.js +4 -2
  60. package/dist/graph/algorithms/louvain.js.map +1 -1
  61. package/dist/graph/classifiers/roles.d.ts +2 -0
  62. package/dist/graph/classifiers/roles.d.ts.map +1 -1
  63. package/dist/graph/classifiers/roles.js +13 -5
  64. package/dist/graph/classifiers/roles.js.map +1 -1
  65. package/dist/presentation/communities.d.ts.map +1 -1
  66. package/dist/presentation/communities.js +38 -34
  67. package/dist/presentation/communities.js.map +1 -1
  68. package/dist/presentation/manifesto.d.ts.map +1 -1
  69. package/dist/presentation/manifesto.js +31 -33
  70. package/dist/presentation/manifesto.js.map +1 -1
  71. package/dist/presentation/queries-cli/inspect.d.ts.map +1 -1
  72. package/dist/presentation/queries-cli/inspect.js +47 -46
  73. package/dist/presentation/queries-cli/inspect.js.map +1 -1
  74. package/dist/shared/file-utils.d.ts.map +1 -1
  75. package/dist/shared/file-utils.js +94 -72
  76. package/dist/shared/file-utils.js.map +1 -1
  77. package/dist/types.d.ts +81 -1
  78. package/dist/types.d.ts.map +1 -1
  79. package/package.json +7 -7
  80. package/src/ast-analysis/engine.ts +99 -55
  81. package/src/ast-analysis/visitors/ast-store-visitor.ts +19 -21
  82. package/src/db/connection.ts +24 -5
  83. package/src/db/repository/base.ts +43 -0
  84. package/src/db/repository/index.ts +1 -0
  85. package/src/db/repository/native-repository.ts +67 -1
  86. package/src/domain/analysis/dependencies.ts +13 -0
  87. package/src/domain/graph/builder/incremental.ts +21 -0
  88. package/src/domain/graph/builder/pipeline.ts +392 -362
  89. package/src/domain/graph/builder/stages/build-edges.ts +30 -1
  90. package/src/domain/graph/builder/stages/resolve-imports.ts +20 -20
  91. package/src/domain/graph/watcher.ts +118 -98
  92. package/src/domain/parser.ts +2 -0
  93. package/src/extractors/go.ts +57 -32
  94. package/src/extractors/javascript.ts +67 -27
  95. package/src/features/complexity.ts +94 -58
  96. package/src/features/dataflow.ts +153 -132
  97. package/src/features/structure.ts +167 -95
  98. package/src/graph/algorithms/louvain.ts +5 -2
  99. package/src/graph/classifiers/roles.ts +14 -5
  100. package/src/presentation/communities.ts +44 -39
  101. package/src/presentation/manifesto.ts +35 -38
  102. package/src/presentation/queries-cli/inspect.ts +48 -46
  103. package/src/shared/file-utils.ts +116 -77
  104. package/src/types.ts +85 -0
@@ -119,6 +119,23 @@ function buildImportEdges(
119
119
  : 'imports';
120
120
  allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
121
121
 
122
+ // Type-only imports: create symbol-level edges so the target symbols
123
+ // get fan-in credit and aren't falsely classified as dead code.
124
+ if (imp.typeOnly && ctx.nodesByNameAndFile) {
125
+ for (const name of imp.names) {
126
+ const cleanName = name.replace(/^\*\s+as\s+/, '');
127
+ let targetFile = resolvedPath;
128
+ if (isBarrelFile(ctx, resolvedPath)) {
129
+ const actual = resolveBarrelExport(ctx, resolvedPath, cleanName);
130
+ if (actual) targetFile = actual;
131
+ }
132
+ const candidates = ctx.nodesByNameAndFile.get(`${cleanName}|${targetFile}`);
133
+ if (candidates && candidates.length > 0) {
134
+ allEdgeRows.push([fileNodeId, candidates[0]!.id, 'imports-type', 1.0, 0]);
135
+ }
136
+ }
137
+ }
138
+
122
139
  if (!imp.reexport && isBarrelFile(ctx, resolvedPath)) {
123
140
  buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeIdStmt, allEdgeRows);
124
141
  }
@@ -280,7 +297,18 @@ function buildImportEdgesNative(
280
297
  }
281
298
  }
282
299
 
283
- // 6. Call native
300
+ // 6. Build symbol node entries for type-only import resolution
301
+ const symbolNodes: Array<{ name: string; file: string; nodeId: number }> = [];
302
+ if (ctx.nodesByNameAndFile) {
303
+ for (const [key, nodes] of ctx.nodesByNameAndFile) {
304
+ if (nodes.length > 0) {
305
+ const [name, file] = key.split('|');
306
+ symbolNodes.push({ name: name!, file: file!, nodeId: nodes[0]!.id });
307
+ }
308
+ }
309
+ }
310
+
311
+ // 7. Call native
284
312
  const nativeEdges = native.buildImportEdges!(
285
313
  files,
286
314
  resolvedImports,
@@ -288,6 +316,7 @@ function buildImportEdgesNative(
288
316
  fileNodeIds,
289
317
  barrelFiles,
290
318
  rootDir,
319
+ symbolNodes,
291
320
  ) as NativeEdge[];
292
321
 
293
322
  for (const e of nativeEdges) {
@@ -180,6 +180,13 @@ export function isBarrelFile(ctx: PipelineContext, relPath: string): boolean {
180
180
  return reexports.length >= ownDefs;
181
181
  }
182
182
 
183
+ /** Check if a re-export source directly defines the symbol. */
184
+ function sourceDefinesSymbol(ctx: PipelineContext, source: string, symbolName: string): boolean {
185
+ const targetSymbols = ctx.fileSymbols.get(source);
186
+ if (!targetSymbols) return false;
187
+ return targetSymbols.definitions.some((d) => d.name === symbolName);
188
+ }
189
+
183
190
  export function resolveBarrelExport(
184
191
  ctx: PipelineContext,
185
192
  barrelPath: string,
@@ -188,31 +195,24 @@ export function resolveBarrelExport(
188
195
  ): string | null {
189
196
  if (visited.has(barrelPath)) return null;
190
197
  visited.add(barrelPath);
198
+
191
199
  const reexports = ctx.reexportMap.get(barrelPath) as ReexportEntry[] | undefined;
192
200
  if (!reexports) return null;
201
+
193
202
  for (const re of reexports) {
203
+ // Named re-export: only follow if the symbol is in the export list
194
204
  if (re.names.length > 0 && !re.wildcardReexport) {
195
- if (re.names.includes(symbolName)) {
196
- const targetSymbols = ctx.fileSymbols.get(re.source);
197
- if (targetSymbols) {
198
- const hasDef = targetSymbols.definitions.some((d) => d.name === symbolName);
199
- if (hasDef) return re.source;
200
- const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
201
- if (deeper) return deeper;
202
- }
203
- return re.source;
204
- }
205
- continue;
206
- }
207
- if (re.wildcardReexport || re.names.length === 0) {
208
- const targetSymbols = ctx.fileSymbols.get(re.source);
209
- if (targetSymbols) {
210
- const hasDef = targetSymbols.definitions.some((d) => d.name === symbolName);
211
- if (hasDef) return re.source;
212
- const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
213
- if (deeper) return deeper;
214
- }
205
+ if (!re.names.includes(symbolName)) continue;
206
+ if (sourceDefinesSymbol(ctx, re.source, symbolName)) return re.source;
207
+ const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
208
+ return deeper ?? re.source;
215
209
  }
210
+
211
+ // Wildcard or namespace re-export: check if target defines the symbol
212
+ if (sourceDefinesSymbol(ctx, re.source, symbolName)) return re.source;
213
+ const deeper = resolveBarrelExport(ctx, re.source, symbolName, visited);
214
+ if (deeper) return deeper;
216
215
  }
216
+
217
217
  return null;
218
218
  }
@@ -2,20 +2,16 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { closeDb, getNodeId as getNodeIdQuery, initSchema, openDb } from '../../db/index.js';
4
4
  import { debug, info } from '../../infrastructure/logger.js';
5
- import { EXTENSIONS, IGNORE_DIRS, normalizePath } from '../../shared/constants.js';
5
+ import { isSupportedFile, normalizePath, shouldIgnore } from '../../shared/constants.js';
6
6
  import { DbError } from '../../shared/errors.js';
7
7
  import { createParseTreeCache, getActiveEngine } from '../parser.js';
8
8
  import { type IncrementalStmts, rebuildFile } from './builder/incremental.js';
9
9
  import { appendChangeEvents, buildChangeEvent, diffSymbols } from './change-journal.js';
10
10
  import { appendJournalEntries } from './journal.js';
11
11
 
12
- function shouldIgnore(filePath: string): boolean {
12
+ function shouldIgnorePath(filePath: string): boolean {
13
13
  const parts = filePath.split(path.sep);
14
- return parts.some((p) => IGNORE_DIRS.has(p));
15
- }
16
-
17
- function isTrackedExt(filePath: string): boolean {
18
- return EXTENSIONS.has(path.extname(filePath));
14
+ return parts.some((p) => shouldIgnore(p));
19
15
  }
20
16
 
21
17
  /** Prepare all SQL statements needed by the watcher's incremental rebuild. */
@@ -141,24 +137,35 @@ function collectTrackedFiles(dir: string, result: string[]): void {
141
137
  let entries: fs.Dirent[];
142
138
  try {
143
139
  entries = fs.readdirSync(dir, { withFileTypes: true });
144
- } catch {
140
+ } catch (e: unknown) {
141
+ debug(`collectTrackedFiles: cannot read ${dir}: ${(e as Error).message}`);
145
142
  return;
146
143
  }
147
144
  for (const entry of entries) {
148
- if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
145
+ if (shouldIgnore(entry.name)) continue;
149
146
  const full = path.join(dir, entry.name);
150
147
  if (entry.isDirectory()) {
151
148
  collectTrackedFiles(full, result);
152
- } else if (EXTENSIONS.has(path.extname(entry.name))) {
149
+ } else if (isSupportedFile(entry.name)) {
153
150
  result.push(full);
154
151
  }
155
152
  }
156
153
  }
157
154
 
158
- export async function watchProject(
159
- rootDir: string,
160
- opts: { engine?: string; poll?: boolean; pollInterval?: number } = {},
161
- ): Promise<void> {
155
+ /** Shared watcher state passed between setup and watcher sub-functions. */
156
+ interface WatcherContext {
157
+ rootDir: string;
158
+ db: ReturnType<typeof openDb>;
159
+ stmts: IncrementalStmts;
160
+ engineOpts: import('../../types.js').EngineOpts;
161
+ cache: ReturnType<typeof createParseTreeCache>;
162
+ pending: Set<string>;
163
+ timer: ReturnType<typeof setTimeout> | null;
164
+ debounceMs: number;
165
+ }
166
+
167
+ /** Initialize DB, engine, cache, and statements for watch mode. */
168
+ function setupWatcher(rootDir: string, opts: { engine?: string }): WatcherContext {
162
169
  const dbPath = path.join(rootDir, '.codegraph', 'graph.db');
163
170
  if (!fs.existsSync(dbPath)) {
164
171
  throw new DbError('No graph.db found. Run `codegraph build` first.', { file: dbPath });
@@ -183,111 +190,124 @@ export async function watchProject(
183
190
 
184
191
  const stmts = prepareWatcherStatements(db);
185
192
 
186
- const pending = new Set<string>();
187
- let timer: ReturnType<typeof setTimeout> | null = null;
188
- const DEBOUNCE_MS = 300;
189
-
190
- const usePoll = opts.poll ?? process.platform === 'win32';
191
- const POLL_INTERVAL_MS = opts.pollInterval ?? 2000;
193
+ return {
194
+ rootDir,
195
+ db,
196
+ stmts,
197
+ engineOpts,
198
+ cache,
199
+ pending: new Set<string>(),
200
+ timer: null,
201
+ debounceMs: 300,
202
+ };
203
+ }
192
204
 
193
- info(`Watching ${rootDir} for changes${usePoll ? ' (polling mode)' : ''}...`);
194
- info('Press Ctrl+C to stop.');
205
+ /** Schedule debounced processing of pending files. */
206
+ function scheduleDebouncedProcess(ctx: WatcherContext): void {
207
+ if (ctx.timer) clearTimeout(ctx.timer);
208
+ ctx.timer = setTimeout(async () => {
209
+ const files = [...ctx.pending];
210
+ ctx.pending.clear();
211
+ await processPendingFiles(files, ctx.db, ctx.rootDir, ctx.stmts, ctx.engineOpts, ctx.cache);
212
+ }, ctx.debounceMs);
213
+ }
195
214
 
196
- let cleanup: () => void;
215
+ /** Start polling-based file watcher. Returns cleanup function. */
216
+ function startPollingWatcher(ctx: WatcherContext, pollIntervalMs: number): () => void {
217
+ const mtimeMap = new Map<string, number>();
218
+
219
+ const initial: string[] = [];
220
+ collectTrackedFiles(ctx.rootDir, initial);
221
+ for (const f of initial) {
222
+ try {
223
+ mtimeMap.set(f, fs.statSync(f).mtimeMs);
224
+ } catch {
225
+ /* deleted between collect and stat */
226
+ }
227
+ }
228
+ info(`Polling ${initial.length} tracked files every ${pollIntervalMs}ms`);
197
229
 
198
- if (usePoll) {
199
- // Polling mode: avoids native OS file watchers (NtNotifyChangeDirectoryFileEx)
200
- // which can crash ReFS drivers on Windows Dev Drives.
201
- const mtimeMap = new Map<string, number>();
230
+ const pollTimer = setInterval(() => {
231
+ const current: string[] = [];
232
+ collectTrackedFiles(ctx.rootDir, current);
233
+ const currentSet = new Set(current);
202
234
 
203
- // Seed initial mtimes
204
- const initial: string[] = [];
205
- collectTrackedFiles(rootDir, initial);
206
- for (const f of initial) {
235
+ for (const f of current) {
207
236
  try {
208
- mtimeMap.set(f, fs.statSync(f).mtimeMs);
237
+ const mtime = fs.statSync(f).mtimeMs;
238
+ const prev = mtimeMap.get(f);
239
+ if (prev === undefined || mtime !== prev) {
240
+ mtimeMap.set(f, mtime);
241
+ ctx.pending.add(f);
242
+ }
209
243
  } catch {
210
244
  /* deleted between collect and stat */
211
245
  }
212
246
  }
213
- info(`Polling ${initial.length} tracked files every ${POLL_INTERVAL_MS}ms`);
214
-
215
- const pollTimer = setInterval(() => {
216
- const current: string[] = [];
217
- collectTrackedFiles(rootDir, current);
218
- const currentSet = new Set(current);
219
-
220
- // Detect modified or new files
221
- for (const f of current) {
222
- try {
223
- const mtime = fs.statSync(f).mtimeMs;
224
- const prev = mtimeMap.get(f);
225
- if (prev === undefined || mtime !== prev) {
226
- mtimeMap.set(f, mtime);
227
- pending.add(f);
228
- }
229
- } catch {
230
- /* deleted between collect and stat */
231
- }
232
- }
233
247
 
234
- // Detect deleted files
235
- for (const f of mtimeMap.keys()) {
236
- if (!currentSet.has(f)) {
237
- mtimeMap.delete(f);
238
- pending.add(f);
239
- }
248
+ for (const f of mtimeMap.keys()) {
249
+ if (!currentSet.has(f)) {
250
+ mtimeMap.delete(f);
251
+ ctx.pending.add(f);
240
252
  }
253
+ }
241
254
 
242
- if (pending.size > 0) {
243
- if (timer) clearTimeout(timer);
244
- timer = setTimeout(async () => {
245
- const files = [...pending];
246
- pending.clear();
247
- await processPendingFiles(files, db, rootDir, stmts, engineOpts, cache);
248
- }, DEBOUNCE_MS);
249
- }
250
- }, POLL_INTERVAL_MS);
251
-
252
- cleanup = () => clearInterval(pollTimer);
253
- } else {
254
- // Native OS watcher — efficient but can trigger ReFS crashes on Windows Dev Drives.
255
- // Use --poll if you experience BSOD/HYPERVISOR_ERROR on ReFS volumes.
256
- const watcher = fs.watch(rootDir, { recursive: true }, (_eventType, filename) => {
257
- if (!filename) return;
258
- if (shouldIgnore(filename)) return;
259
- if (!isTrackedExt(filename)) return;
260
-
261
- const fullPath = path.join(rootDir, filename);
262
- pending.add(fullPath);
263
-
264
- if (timer) clearTimeout(timer);
265
- timer = setTimeout(async () => {
266
- const files = [...pending];
267
- pending.clear();
268
- await processPendingFiles(files, db, rootDir, stmts, engineOpts, cache);
269
- }, DEBOUNCE_MS);
270
- });
271
-
272
- cleanup = () => watcher.close();
273
- }
255
+ if (ctx.pending.size > 0) {
256
+ scheduleDebouncedProcess(ctx);
257
+ }
258
+ }, pollIntervalMs);
259
+
260
+ return () => clearInterval(pollTimer);
261
+ }
262
+
263
+ /** Start native OS file watcher. Returns cleanup function. */
264
+ function startNativeWatcher(ctx: WatcherContext): () => void {
265
+ const watcher = fs.watch(ctx.rootDir, { recursive: true }, (_eventType, filename) => {
266
+ if (!filename) return;
267
+ if (shouldIgnorePath(filename)) return;
268
+ if (!isSupportedFile(filename)) return;
274
269
 
275
- process.on('SIGINT', () => {
270
+ ctx.pending.add(path.join(ctx.rootDir, filename));
271
+ scheduleDebouncedProcess(ctx);
272
+ });
273
+
274
+ return () => watcher.close();
275
+ }
276
+
277
+ /** Register SIGINT handler to flush journal and clean up. */
278
+ function setupShutdownHandler(ctx: WatcherContext, cleanup: () => void): void {
279
+ process.once('SIGINT', () => {
276
280
  info('Stopping watcher...');
277
281
  cleanup();
278
- // Flush any pending file paths to journal before exit
279
- if (pending.size > 0) {
280
- const entries = [...pending].map((filePath) => ({
281
- file: normalizePath(path.relative(rootDir, filePath)),
282
+ if (ctx.pending.size > 0) {
283
+ const entries = [...ctx.pending].map((filePath) => ({
284
+ file: normalizePath(path.relative(ctx.rootDir, filePath)),
282
285
  }));
283
286
  try {
284
- appendJournalEntries(rootDir, entries);
287
+ appendJournalEntries(ctx.rootDir, entries);
285
288
  } catch (e: unknown) {
286
289
  debug(`Journal flush on exit failed (non-fatal): ${(e as Error).message}`);
287
290
  }
288
291
  }
289
- if (cache) cache.clear();
290
- closeDb(db);
292
+ if (ctx.cache) ctx.cache.clear();
293
+ closeDb(ctx.db);
291
294
  process.exit(0);
292
295
  });
293
296
  }
297
+
298
+ export async function watchProject(
299
+ rootDir: string,
300
+ opts: { engine?: string; poll?: boolean; pollInterval?: number } = {},
301
+ ): Promise<void> {
302
+ const ctx = setupWatcher(rootDir, opts);
303
+
304
+ const usePoll = opts.poll ?? process.platform === 'win32';
305
+ const pollIntervalMs = opts.pollInterval ?? 2000;
306
+
307
+ info(`Watching ${rootDir} for changes${usePoll ? ' (polling mode)' : ''}...`);
308
+ info('Press Ctrl+C to stop.');
309
+
310
+ const cleanup = usePoll ? startPollingWatcher(ctx, pollIntervalMs) : startNativeWatcher(ctx);
311
+
312
+ setupShutdownHandler(ctx, cleanup);
313
+ }
@@ -143,6 +143,8 @@ const COMMON_QUERY_PATTERNS: string[] = [
143
143
  '(call_expression function: (identifier) @callfn_name) @callfn_node',
144
144
  '(call_expression function: (member_expression) @callmem_fn) @callmem_node',
145
145
  '(call_expression function: (subscript_expression) @callsub_fn) @callsub_node',
146
+ '(new_expression constructor: (identifier) @newfn_name) @newfn_node',
147
+ '(new_expression constructor: (member_expression) @newmem_fn) @newmem_node',
146
148
  '(expression_statement (assignment_expression left: (member_expression) @assign_left right: (_) @assign_right)) @assign_node',
147
149
  ];
148
150
 
@@ -266,44 +266,69 @@ function handleTypedIdentifiers(
266
266
  }
267
267
 
268
268
  /** Infer type from a single RHS expression in a short var declaration. */
269
- function inferShortVarType(
269
+ /** x := Struct{...} — composite literal (confidence 1.0). */
270
+ function inferCompositeLiteral(
270
271
  varNode: TreeSitterNode,
271
272
  rhs: TreeSitterNode,
272
273
  typeMap: Map<string, { type: string; confidence: number }>,
273
- ): void {
274
- // x := Struct{...} composite literal (confidence 1.0)
275
- if (rhs.type === 'composite_literal') {
276
- const typeNode = rhs.childForFieldName('type');
277
- if (typeNode) {
278
- const typeName = extractGoTypeName(typeNode);
279
- if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 1.0);
280
- }
281
- }
282
- // x := &Struct{...} — address-of composite literal (confidence 1.0)
283
- if (rhs.type === 'unary_expression') {
284
- const operand = rhs.childForFieldName('operand');
285
- if (operand && operand.type === 'composite_literal') {
286
- const typeNode = operand.childForFieldName('type');
287
- if (typeNode) {
288
- const typeName = extractGoTypeName(typeNode);
289
- if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 1.0);
290
- }
291
- }
292
- }
293
- // x := NewFoo() or x := pkg.NewFoo() — factory function (confidence 0.7)
294
- if (rhs.type === 'call_expression') {
295
- const fn = rhs.childForFieldName('function');
296
- if (fn && fn.type === 'selector_expression') {
297
- const field = fn.childForFieldName('field');
298
- if (field?.text.startsWith('New')) {
299
- const typeName = field.text.slice(3);
300
- if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 0.7);
301
- }
302
- } else if (fn && fn.type === 'identifier' && fn.text.startsWith('New')) {
303
- const typeName = fn.text.slice(3);
274
+ ): boolean {
275
+ if (rhs.type !== 'composite_literal') return false;
276
+ const typeNode = rhs.childForFieldName('type');
277
+ if (!typeNode) return false;
278
+ const typeName = extractGoTypeName(typeNode);
279
+ if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 1.0);
280
+ return true;
281
+ }
282
+
283
+ /** x := &Struct{...} — address-of composite literal (confidence 1.0). */
284
+ function inferAddressOfComposite(
285
+ varNode: TreeSitterNode,
286
+ rhs: TreeSitterNode,
287
+ typeMap: Map<string, { type: string; confidence: number }>,
288
+ ): boolean {
289
+ if (rhs.type !== 'unary_expression') return false;
290
+ const operand = rhs.childForFieldName('operand');
291
+ if (!operand || operand.type !== 'composite_literal') return false;
292
+ const typeNode = operand.childForFieldName('type');
293
+ if (!typeNode) return false;
294
+ const typeName = extractGoTypeName(typeNode);
295
+ if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 1.0);
296
+ return true;
297
+ }
298
+
299
+ /** x := NewFoo() or x := pkg.NewFoo() — factory function (confidence 0.7). */
300
+ function inferFactoryCall(
301
+ varNode: TreeSitterNode,
302
+ rhs: TreeSitterNode,
303
+ typeMap: Map<string, { type: string; confidence: number }>,
304
+ ): boolean {
305
+ if (rhs.type !== 'call_expression') return false;
306
+ const fn = rhs.childForFieldName('function');
307
+ if (!fn) return false;
308
+
309
+ if (fn.type === 'selector_expression') {
310
+ const field = fn.childForFieldName('field');
311
+ if (field?.text.startsWith('New')) {
312
+ const typeName = field.text.slice(3);
304
313
  if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 0.7);
314
+ return true;
305
315
  }
316
+ } else if (fn.type === 'identifier' && fn.text.startsWith('New')) {
317
+ const typeName = fn.text.slice(3);
318
+ if (typeName) setTypeMapEntry(typeMap, varNode.text, typeName, 0.7);
319
+ return true;
306
320
  }
321
+ return false;
322
+ }
323
+
324
+ function inferShortVarType(
325
+ varNode: TreeSitterNode,
326
+ rhs: TreeSitterNode,
327
+ typeMap: Map<string, { type: string; confidence: number }>,
328
+ ): void {
329
+ if (inferCompositeLiteral(varNode, rhs, typeMap)) return;
330
+ if (inferAddressOfComposite(varNode, rhs, typeMap)) return;
331
+ inferFactoryCall(varNode, rhs, typeMap);
307
332
  }
308
333
 
309
334
  /** Handle short_var_declaration: x := Struct{}, x := &Struct{}, x := NewFoo(). */
@@ -202,6 +202,48 @@ function handleExportCapture(
202
202
  }
203
203
  }
204
204
 
205
+ function handleInterfaceCapture(
206
+ c: Record<string, TreeSitterNode>,
207
+ definitions: Definition[],
208
+ ): void {
209
+ const ifaceNode = c.iface_node!;
210
+ const ifaceName = c.iface_name!.text;
211
+ definitions.push({
212
+ name: ifaceName,
213
+ kind: 'interface',
214
+ line: ifaceNode.startPosition.row + 1,
215
+ endLine: nodeEndLine(ifaceNode),
216
+ });
217
+ const body =
218
+ ifaceNode.childForFieldName('body') ||
219
+ findChild(ifaceNode, 'interface_body') ||
220
+ findChild(ifaceNode, 'object_type');
221
+ if (body) extractInterfaceMethods(body, ifaceName, definitions);
222
+ }
223
+
224
+ function handleTypeCapture(c: Record<string, TreeSitterNode>, definitions: Definition[]): void {
225
+ const typeNode = c.type_node!;
226
+ definitions.push({
227
+ name: c.type_name!.text,
228
+ kind: 'type',
229
+ line: typeNode.startPosition.row + 1,
230
+ endLine: nodeEndLine(typeNode),
231
+ });
232
+ }
233
+
234
+ function handleImportCapture(c: Record<string, TreeSitterNode>, imports: Import[]): void {
235
+ const impNode = c.imp_node!;
236
+ const isTypeOnly = impNode.text.startsWith('import type');
237
+ const modPath = c.imp_source!.text.replace(/['"]/g, '');
238
+ const names = extractImportNames(impNode);
239
+ imports.push({
240
+ source: modPath,
241
+ names,
242
+ line: impNode.startPosition.row + 1,
243
+ typeOnly: isTypeOnly,
244
+ });
245
+ }
246
+
205
247
  /** Dispatch a single query match to the appropriate handler. */
206
248
  function dispatchQueryMatch(
207
249
  c: Record<string, TreeSitterNode>,
@@ -220,35 +262,11 @@ function dispatchQueryMatch(
220
262
  } else if (c.meth_node) {
221
263
  handleMethodCapture(c, definitions);
222
264
  } else if (c.iface_node) {
223
- const ifaceName = c.iface_name!.text;
224
- definitions.push({
225
- name: ifaceName,
226
- kind: 'interface',
227
- line: c.iface_node.startPosition.row + 1,
228
- endLine: nodeEndLine(c.iface_node),
229
- });
230
- const body =
231
- c.iface_node.childForFieldName('body') ||
232
- findChild(c.iface_node, 'interface_body') ||
233
- findChild(c.iface_node, 'object_type');
234
- if (body) extractInterfaceMethods(body, ifaceName, definitions);
265
+ handleInterfaceCapture(c, definitions);
235
266
  } else if (c.type_node) {
236
- definitions.push({
237
- name: c.type_name!.text,
238
- kind: 'type',
239
- line: c.type_node.startPosition.row + 1,
240
- endLine: nodeEndLine(c.type_node),
241
- });
267
+ handleTypeCapture(c, definitions);
242
268
  } else if (c.imp_node) {
243
- const isTypeOnly = c.imp_node.text.startsWith('import type');
244
- const modPath = c.imp_source!.text.replace(/['"]/g, '');
245
- const names = extractImportNames(c.imp_node);
246
- imports.push({
247
- source: modPath,
248
- names,
249
- line: c.imp_node.startPosition.row + 1,
250
- typeOnly: isTypeOnly,
251
- });
269
+ handleImportCapture(c, imports);
252
270
  } else if (c.exp_node) {
253
271
  handleExportCapture(c, exps, imports);
254
272
  } else if (c.callfn_node) {
@@ -264,6 +282,14 @@ function dispatchQueryMatch(
264
282
  } else if (c.callsub_node) {
265
283
  const callInfo = extractCallInfo(c.callsub_fn!, c.callsub_node);
266
284
  if (callInfo) calls.push(callInfo);
285
+ } else if (c.newfn_node) {
286
+ calls.push({
287
+ name: c.newfn_name!.text,
288
+ line: c.newfn_node.startPosition.row + 1,
289
+ });
290
+ } else if (c.newmem_node) {
291
+ const callInfo = extractCallInfo(c.newmem_fn!, c.newmem_node);
292
+ if (callInfo) calls.push(callInfo);
267
293
  } else if (c.assign_node) {
268
294
  handleCommonJSAssignment(c.assign_left!, c.assign_right!, c.assign_node, imports);
269
295
  }
@@ -502,6 +528,9 @@ function walkJavaScriptNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
502
528
  case 'call_expression':
503
529
  handleCallExpr(node, ctx);
504
530
  break;
531
+ case 'new_expression':
532
+ handleNewExpr(node, ctx);
533
+ break;
505
534
  case 'import_statement':
506
535
  handleImportStmt(node, ctx);
507
536
  break;
@@ -689,6 +718,17 @@ function handleCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
689
718
  }
690
719
  }
691
720
 
721
+ function handleNewExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
722
+ const ctor = node.childForFieldName('constructor') || node.child(1);
723
+ if (!ctor) return;
724
+ if (ctor.type === 'identifier') {
725
+ ctx.calls.push({ name: ctor.text, line: node.startPosition.row + 1 });
726
+ } else if (ctor.type === 'member_expression') {
727
+ const callInfo = extractCallInfo(ctor, node);
728
+ if (callInfo) ctx.calls.push(callInfo);
729
+ }
730
+ }
731
+
692
732
  /** Handle a dynamic import() call expression and add to imports if static. */
693
733
  function handleDynamicImportCall(node: TreeSitterNode, imports: Import[]): void {
694
734
  const args = node.childForFieldName('arguments') || findChild(node, 'arguments');