@optave/codegraph 3.0.3 → 3.1.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.
package/src/parser.js CHANGED
@@ -183,77 +183,55 @@ function resolveEngine(opts = {}) {
183
183
  }
184
184
 
185
185
  /**
186
- * Normalize native engine output to match the camelCase convention
187
- * used by the WASM extractors.
186
+ * Patch native engine output in-place for the few remaining semantic transforms.
187
+ * With #[napi(js_name)] on Rust types, most fields already arrive as camelCase.
188
+ * This only handles:
189
+ * - _lineCount compat for builder.js
190
+ * - Backward compat for older native binaries missing js_name annotations
191
+ * - dataflow argFlows/mutations bindingType → binding wrapper
188
192
  */
189
- function normalizeNativeSymbols(result) {
190
- return {
191
- _lineCount: result.lineCount ?? result.line_count ?? null,
192
- definitions: (result.definitions || []).map((d) => ({
193
- name: d.name,
194
- kind: d.kind,
195
- line: d.line,
196
- endLine: d.endLine ?? d.end_line ?? null,
197
- decorators: d.decorators,
198
- complexity: d.complexity
199
- ? {
200
- cognitive: d.complexity.cognitive,
201
- cyclomatic: d.complexity.cyclomatic,
202
- maxNesting: d.complexity.maxNesting,
203
- halstead: d.complexity.halstead ?? null,
204
- loc: d.complexity.loc ?? null,
205
- maintainabilityIndex: d.complexity.maintainabilityIndex ?? null,
206
- }
207
- : null,
208
- children: d.children?.length
209
- ? d.children.map((c) => ({
210
- name: c.name,
211
- kind: c.kind,
212
- line: c.line,
213
- endLine: c.endLine ?? c.end_line ?? null,
214
- }))
215
- : undefined,
216
- })),
217
- calls: (result.calls || []).map((c) => ({
218
- name: c.name,
219
- line: c.line,
220
- dynamic: c.dynamic,
221
- receiver: c.receiver,
222
- })),
223
- imports: (result.imports || []).map((i) => ({
224
- source: i.source,
225
- names: i.names || [],
226
- line: i.line,
227
- typeOnly: i.typeOnly ?? i.type_only,
228
- reexport: i.reexport,
229
- wildcardReexport: i.wildcardReexport ?? i.wildcard_reexport,
230
- pythonImport: i.pythonImport ?? i.python_import,
231
- goImport: i.goImport ?? i.go_import,
232
- rustUse: i.rustUse ?? i.rust_use,
233
- javaImport: i.javaImport ?? i.java_import,
234
- csharpUsing: i.csharpUsing ?? i.csharp_using,
235
- rubyRequire: i.rubyRequire ?? i.ruby_require,
236
- phpUse: i.phpUse ?? i.php_use,
237
- })),
238
- classes: (result.classes || []).map((c) => ({
239
- name: c.name,
240
- extends: c.extends,
241
- implements: c.implements,
242
- line: c.line,
243
- })),
244
- exports: (result.exports || []).map((e) => ({
245
- name: e.name,
246
- kind: e.kind,
247
- line: e.line,
248
- })),
249
- astNodes: (result.astNodes ?? result.ast_nodes ?? []).map((n) => ({
250
- kind: n.kind,
251
- name: n.name,
252
- line: n.line,
253
- text: n.text ?? null,
254
- receiver: n.receiver ?? null,
255
- })),
256
- };
193
+ function patchNativeResult(r) {
194
+ // lineCount: napi(js_name) emits "lineCount"; older binaries may emit "line_count"
195
+ r.lineCount = r.lineCount ?? r.line_count ?? null;
196
+ r._lineCount = r.lineCount;
197
+
198
+ // Backward compat for older binaries missing js_name annotations
199
+ if (r.definitions) {
200
+ for (const d of r.definitions) {
201
+ if (d.endLine === undefined && d.end_line !== undefined) {
202
+ d.endLine = d.end_line;
203
+ }
204
+ }
205
+ }
206
+ if (r.imports) {
207
+ for (const i of r.imports) {
208
+ if (i.typeOnly === undefined) i.typeOnly = i.type_only;
209
+ if (i.wildcardReexport === undefined) i.wildcardReexport = i.wildcard_reexport;
210
+ if (i.pythonImport === undefined) i.pythonImport = i.python_import;
211
+ if (i.goImport === undefined) i.goImport = i.go_import;
212
+ if (i.rustUse === undefined) i.rustUse = i.rust_use;
213
+ if (i.javaImport === undefined) i.javaImport = i.java_import;
214
+ if (i.csharpUsing === undefined) i.csharpUsing = i.csharp_using;
215
+ if (i.rubyRequire === undefined) i.rubyRequire = i.ruby_require;
216
+ if (i.phpUse === undefined) i.phpUse = i.php_use;
217
+ }
218
+ }
219
+
220
+ // dataflow: wrap bindingType into binding object for argFlows and mutations
221
+ if (r.dataflow) {
222
+ if (r.dataflow.argFlows) {
223
+ for (const f of r.dataflow.argFlows) {
224
+ f.binding = f.bindingType ? { type: f.bindingType } : null;
225
+ }
226
+ }
227
+ if (r.dataflow.mutations) {
228
+ for (const m of r.dataflow.mutations) {
229
+ m.binding = m.bindingType ? { type: m.bindingType } : null;
230
+ }
231
+ }
232
+ }
233
+
234
+ return r;
257
235
  }
258
236
 
259
237
  /**
@@ -384,8 +362,8 @@ export async function parseFileAuto(filePath, source, opts = {}) {
384
362
  const { native } = resolveEngine(opts);
385
363
 
386
364
  if (native) {
387
- const result = native.parseFile(filePath, source);
388
- return result ? normalizeNativeSymbols(result) : null;
365
+ const result = native.parseFile(filePath, source, !!opts.dataflow, opts.ast !== false);
366
+ return result ? patchNativeResult(result) : null;
389
367
  }
390
368
 
391
369
  // WASM path
@@ -407,11 +385,16 @@ export async function parseFilesAuto(filePaths, rootDir, opts = {}) {
407
385
  const result = new Map();
408
386
 
409
387
  if (native) {
410
- const nativeResults = native.parseFiles(filePaths, rootDir);
388
+ const nativeResults = native.parseFiles(
389
+ filePaths,
390
+ rootDir,
391
+ !!opts.dataflow,
392
+ opts.ast !== false,
393
+ );
411
394
  for (const r of nativeResults) {
412
395
  if (!r) continue;
413
396
  const relPath = path.relative(rootDir, r.file).split(path.sep).join('/');
414
- result.set(relPath, normalizeNativeSymbols(r));
397
+ result.set(relPath, patchNativeResult(r));
415
398
  }
416
399
  return result;
417
400
  }
@@ -476,7 +459,7 @@ export function createParseTreeCache() {
476
459
  export async function parseFileIncremental(cache, filePath, source, opts = {}) {
477
460
  if (cache) {
478
461
  const result = cache.parseFile(filePath, source);
479
- return result ? normalizeNativeSymbols(result) : null;
462
+ return result ? patchNativeResult(result) : null;
480
463
  }
481
464
  return parseFileAuto(filePath, source, opts);
482
465
  }
package/src/queries.js CHANGED
@@ -163,7 +163,7 @@ function resolveMethodViaHierarchy(db, methodName) {
163
163
  * Find nodes matching a name query, ranked by relevance.
164
164
  * Scoring: exact=100, prefix=60, word-boundary=40, substring=10, plus fan-in tiebreaker.
165
165
  */
166
- function findMatchingNodes(db, name, opts = {}) {
166
+ export function findMatchingNodes(db, name, opts = {}) {
167
167
  const kinds = opts.kind ? [opts.kind] : FUNCTION_KINDS;
168
168
  const placeholders = kinds.map(() => '?').join(', ');
169
169
  const params = [`%${name}%`, ...kinds];
@@ -3134,31 +3134,49 @@ export function roles(customDbPath, opts = {}) {
3134
3134
 
3135
3135
  // ─── exportsData ─────────────────────────────────────────────────────
3136
3136
 
3137
- function exportsFileImpl(db, target, noTests, getFileLines) {
3137
+ function exportsFileImpl(db, target, noTests, getFileLines, unused) {
3138
3138
  const fileNodes = db
3139
3139
  .prepare(`SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'`)
3140
3140
  .all(`%${target}%`);
3141
3141
  if (fileNodes.length === 0) return [];
3142
3142
 
3143
+ // Detect whether exported column exists
3144
+ let hasExportedCol = false;
3145
+ try {
3146
+ db.prepare('SELECT exported FROM nodes LIMIT 0').raw();
3147
+ hasExportedCol = true;
3148
+ } catch {
3149
+ /* old DB without exported column */
3150
+ }
3151
+
3143
3152
  return fileNodes.map((fn) => {
3144
3153
  const symbols = db
3145
3154
  .prepare(`SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line`)
3146
3155
  .all(fn.file);
3147
3156
 
3148
- // IDs of symbols that have incoming calls from other files (exported)
3149
- const exportedIds = new Set(
3150
- db
3157
+ let exported;
3158
+ if (hasExportedCol) {
3159
+ // Use the exported column populated during build
3160
+ exported = db
3151
3161
  .prepare(
3152
- `SELECT DISTINCT e.target_id FROM edges e
3153
- JOIN nodes caller ON e.source_id = caller.id
3154
- JOIN nodes target ON e.target_id = target.id
3155
- WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
3162
+ "SELECT * FROM nodes WHERE file = ? AND kind != 'file' AND exported = 1 ORDER BY line",
3156
3163
  )
3157
- .all(fn.file, fn.file)
3158
- .map((r) => r.target_id),
3159
- );
3160
-
3161
- const exported = symbols.filter((s) => exportedIds.has(s.id));
3164
+ .all(fn.file);
3165
+ } else {
3166
+ // Fallback: symbols that have incoming calls from other files
3167
+ const exportedIds = new Set(
3168
+ db
3169
+ .prepare(
3170
+ `SELECT DISTINCT e.target_id FROM edges e
3171
+ JOIN nodes caller ON e.source_id = caller.id
3172
+ JOIN nodes target ON e.target_id = target.id
3173
+ WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`,
3174
+ )
3175
+ .all(fn.file, fn.file)
3176
+ .map((r) => r.target_id),
3177
+ );
3178
+ exported = symbols.filter((s) => exportedIds.has(s.id));
3179
+ }
3162
3180
  const internalCount = symbols.length - exported.length;
3163
3181
 
3164
3182
  const results = exported.map((s) => {
@@ -3185,6 +3203,8 @@ function exportsFileImpl(db, target, noTests, getFileLines) {
3185
3203
  };
3186
3204
  });
3187
3205
 
3206
+ const totalUnused = results.filter((r) => r.consumerCount === 0).length;
3207
+
3188
3208
  // Files that re-export this file (barrel → this file)
3189
3209
  const reexports = db
3190
3210
  .prepare(
@@ -3194,12 +3214,18 @@ function exportsFileImpl(db, target, noTests, getFileLines) {
3194
3214
  .all(fn.id)
3195
3215
  .map((r) => ({ file: r.file }));
3196
3216
 
3217
+ let filteredResults = results;
3218
+ if (unused) {
3219
+ filteredResults = results.filter((r) => r.consumerCount === 0);
3220
+ }
3221
+
3197
3222
  return {
3198
3223
  file: fn.file,
3199
- results,
3224
+ results: filteredResults,
3200
3225
  reexports,
3201
3226
  totalExported: exported.length,
3202
3227
  totalInternal: internalCount,
3228
+ totalUnused,
3203
3229
  };
3204
3230
  });
3205
3231
  }
@@ -3229,12 +3255,13 @@ export function exportsData(file, customDbPath, opts = {}) {
3229
3255
  }
3230
3256
  }
3231
3257
 
3232
- const fileResults = exportsFileImpl(db, file, noTests, getFileLines);
3258
+ const unused = opts.unused || false;
3259
+ const fileResults = exportsFileImpl(db, file, noTests, getFileLines, unused);
3233
3260
  db.close();
3234
3261
 
3235
3262
  if (fileResults.length === 0) {
3236
3263
  return paginateResult(
3237
- { file, results: [], reexports: [], totalExported: 0, totalInternal: 0 },
3264
+ { file, results: [], reexports: [], totalExported: 0, totalInternal: 0, totalUnused: 0 },
3238
3265
  'results',
3239
3266
  { limit: opts.limit, offset: opts.offset },
3240
3267
  );
@@ -3248,6 +3275,7 @@ export function exportsData(file, customDbPath, opts = {}) {
3248
3275
  reexports: first.reexports,
3249
3276
  totalExported: first.totalExported,
3250
3277
  totalInternal: first.totalInternal,
3278
+ totalUnused: first.totalUnused,
3251
3279
  };
3252
3280
  return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
3253
3281
  }
@@ -3264,13 +3292,24 @@ export function fileExports(file, customDbPath, opts = {}) {
3264
3292
  }
3265
3293
 
3266
3294
  if (data.results.length === 0) {
3267
- console.log(`No exported symbols found for "${file}". Run "codegraph build" first.`);
3295
+ if (opts.unused) {
3296
+ console.log(`No unused exports found for "${file}".`);
3297
+ } else {
3298
+ console.log(`No exported symbols found for "${file}". Run "codegraph build" first.`);
3299
+ }
3268
3300
  return;
3269
3301
  }
3270
3302
 
3271
- console.log(
3272
- `\n# ${data.file} — ${data.totalExported} exported, ${data.totalInternal} internal\n`,
3273
- );
3303
+ if (opts.unused) {
3304
+ console.log(
3305
+ `\n# ${data.file} — ${data.totalUnused} unused export${data.totalUnused !== 1 ? 's' : ''} (of ${data.totalExported} exported)\n`,
3306
+ );
3307
+ } else {
3308
+ const unusedNote = data.totalUnused > 0 ? ` (${data.totalUnused} unused)` : '';
3309
+ console.log(
3310
+ `\n# ${data.file} — ${data.totalExported} exported${unusedNote}, ${data.totalInternal} internal\n`,
3311
+ );
3312
+ }
3274
3313
 
3275
3314
  for (const sym of data.results) {
3276
3315
  const icon = kindIcon(sym.kind);
package/src/resolve.js CHANGED
@@ -146,8 +146,12 @@ export function computeConfidence(callerFile, targetFile, importedFrom) {
146
146
  /**
147
147
  * Batch resolve multiple imports in a single native call.
148
148
  * Returns Map<"fromFile|importSource", resolvedPath> or null when native unavailable.
149
+ * @param {Array} inputs - Array of { fromFile, importSource }
150
+ * @param {string} rootDir - Project root
151
+ * @param {object} aliases - Path aliases
152
+ * @param {string[]} [knownFiles] - Optional file paths for FS cache (avoids syscalls)
149
153
  */
150
- export function resolveImportsBatch(inputs, rootDir, aliases) {
154
+ export function resolveImportsBatch(inputs, rootDir, aliases, knownFiles) {
151
155
  const native = loadNative();
152
156
  if (!native) return null;
153
157
 
@@ -156,7 +160,12 @@ export function resolveImportsBatch(inputs, rootDir, aliases) {
156
160
  fromFile,
157
161
  importSource,
158
162
  }));
159
- const results = native.resolveImports(nativeInputs, rootDir, convertAliasesForNative(aliases));
163
+ const results = native.resolveImports(
164
+ nativeInputs,
165
+ rootDir,
166
+ convertAliasesForNative(aliases),
167
+ knownFiles || null,
168
+ );
160
169
  const map = new Map();
161
170
  for (const r of results) {
162
171
  map.set(`${r.fromFile}|${r.importSource}`, normalizePath(path.normalize(r.resolvedPath)));