@optave/codegraph 1.1.0 → 1.4.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.
package/src/builder.js CHANGED
@@ -1,17 +1,20 @@
1
-
2
- import fs from 'fs';
3
- import path from 'path';
4
- import { createHash } from 'crypto';
5
- import { openDb, initSchema } from './db.js';
6
- import { createParsers, getParser, extractSymbols, extractHCLSymbols, extractPythonSymbols } from './parser.js';
7
- import { IGNORE_DIRS, EXTENSIONS, normalizePath } from './constants.js';
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
8
4
  import { loadConfig } from './config.js';
9
- import { warn, debug, info } from './logger.js';
5
+ import { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
6
+ import { initSchema, openDb } from './db.js';
7
+ import { warn } from './logger.js';
8
+ import { getActiveEngine, parseFilesAuto } from './parser.js';
9
+ import { computeConfidence, resolveImportPath, resolveImportsBatch } from './resolve.js';
10
+
11
+ export { resolveImportPath } from './resolve.js';
10
12
 
11
13
  export function collectFiles(dir, files = [], config = {}) {
12
14
  let entries;
13
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
14
- catch (err) {
15
+ try {
16
+ entries = fs.readdirSync(dir, { withFileTypes: true });
17
+ } catch (err) {
15
18
  warn(`Cannot read directory ${dir}: ${err.message}`);
16
19
  return files;
17
20
  }
@@ -25,7 +28,7 @@ export function collectFiles(dir, files = [], config = {}) {
25
28
  if (entry.isDirectory()) continue;
26
29
  }
27
30
  if (IGNORE_DIRS.has(entry.name)) continue;
28
- if (extraIgnore && extraIgnore.has(entry.name)) continue;
31
+ if (extraIgnore?.has(entry.name)) continue;
29
32
 
30
33
  const full = path.join(dir, entry.name);
31
34
  if (entry.isDirectory()) {
@@ -43,7 +46,8 @@ export function loadPathAliases(rootDir) {
43
46
  const configPath = path.join(rootDir, configName);
44
47
  if (!fs.existsSync(configPath)) continue;
45
48
  try {
46
- const raw = fs.readFileSync(configPath, 'utf-8')
49
+ const raw = fs
50
+ .readFileSync(configPath, 'utf-8')
47
51
  .replace(/\/\/.*$/gm, '')
48
52
  .replace(/\/\*[\s\S]*?\*\//g, '')
49
53
  .replace(/,\s*([\]}])/g, '$1');
@@ -52,7 +56,7 @@ export function loadPathAliases(rootDir) {
52
56
  if (opts.baseUrl) aliases.baseUrl = path.resolve(rootDir, opts.baseUrl);
53
57
  if (opts.paths) {
54
58
  for (const [pattern, targets] of Object.entries(opts.paths)) {
55
- aliases.paths[pattern] = targets.map(t => path.resolve(aliases.baseUrl || rootDir, t));
59
+ aliases.paths[pattern] = targets.map((t) => path.resolve(aliases.baseUrl || rootDir, t));
56
60
  }
57
61
  }
58
62
  break;
@@ -63,70 +67,6 @@ export function loadPathAliases(rootDir) {
63
67
  return aliases;
64
68
  }
65
69
 
66
- function resolveViaAlias(importSource, aliases, rootDir) {
67
- if (aliases.baseUrl && !importSource.startsWith('.') && !importSource.startsWith('/')) {
68
- const candidate = path.resolve(aliases.baseUrl, importSource);
69
- for (const ext of ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js']) {
70
- const full = candidate + ext;
71
- if (fs.existsSync(full)) return full;
72
- }
73
- }
74
-
75
- for (const [pattern, targets] of Object.entries(aliases.paths)) {
76
- const prefix = pattern.replace(/\*$/, '');
77
- if (!importSource.startsWith(prefix)) continue;
78
- const rest = importSource.slice(prefix.length);
79
- for (const target of targets) {
80
- const resolved = target.replace(/\*$/, rest);
81
- for (const ext of ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js']) {
82
- const full = resolved + ext;
83
- if (fs.existsSync(full)) return full;
84
- }
85
- }
86
- }
87
- return null;
88
- }
89
-
90
- export function resolveImportPath(fromFile, importSource, rootDir, aliases) {
91
- if (!importSource.startsWith('.') && aliases) {
92
- const aliasResolved = resolveViaAlias(importSource, aliases, rootDir);
93
- if (aliasResolved) return normalizePath(path.relative(rootDir, aliasResolved));
94
- }
95
- if (!importSource.startsWith('.')) return importSource;
96
- const dir = path.dirname(fromFile);
97
- let resolved = path.resolve(dir, importSource);
98
-
99
- if (resolved.endsWith('.js')) {
100
- const tsCandidate = resolved.replace(/\.js$/, '.ts');
101
- if (fs.existsSync(tsCandidate)) return normalizePath(path.relative(rootDir, tsCandidate));
102
- const tsxCandidate = resolved.replace(/\.js$/, '.tsx');
103
- if (fs.existsSync(tsxCandidate)) return normalizePath(path.relative(rootDir, tsxCandidate));
104
- }
105
-
106
- for (const ext of ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.py', '/index.ts', '/index.tsx', '/index.js', '/__init__.py']) {
107
- const candidate = resolved + ext;
108
- if (fs.existsSync(candidate)) {
109
- return normalizePath(path.relative(rootDir, candidate));
110
- }
111
- }
112
- if (fs.existsSync(resolved)) return normalizePath(path.relative(rootDir, resolved));
113
- return normalizePath(path.relative(rootDir, resolved));
114
- }
115
-
116
- /**
117
- * Compute proximity-based confidence for call resolution.
118
- */
119
- function computeConfidence(callerFile, targetFile, importedFrom) {
120
- if (!targetFile || !callerFile) return 0.3;
121
- if (callerFile === targetFile) return 1.0;
122
- if (importedFrom === targetFile) return 1.0;
123
- if (path.dirname(callerFile) === path.dirname(targetFile)) return 0.7;
124
- const callerParent = path.dirname(path.dirname(callerFile));
125
- const targetParent = path.dirname(path.dirname(targetFile));
126
- if (callerParent === targetParent) return 0.5;
127
- return 0.3;
128
- }
129
-
130
70
  /**
131
71
  * Compute MD5 hash of file contents for incremental builds.
132
72
  */
@@ -143,20 +83,24 @@ function getChangedFiles(db, allFiles, rootDir) {
143
83
  try {
144
84
  db.prepare('SELECT 1 FROM file_hashes LIMIT 1').get();
145
85
  hasTable = true;
146
- } catch { /* table doesn't exist */ }
86
+ } catch {
87
+ /* table doesn't exist */
88
+ }
147
89
 
148
90
  if (!hasTable) {
149
91
  // No hash table = first build, everything is new
150
92
  return {
151
- changed: allFiles.map(f => ({ file: f })),
93
+ changed: allFiles.map((f) => ({ file: f })),
152
94
  removed: [],
153
- isFullBuild: true
95
+ isFullBuild: true,
154
96
  };
155
97
  }
156
98
 
157
99
  const existing = new Map(
158
- db.prepare('SELECT file, hash FROM file_hashes').all()
159
- .map(r => [r.file, r.hash])
100
+ db
101
+ .prepare('SELECT file, hash FROM file_hashes')
102
+ .all()
103
+ .map((r) => [r.file, r.hash]),
160
104
  );
161
105
 
162
106
  const changed = [];
@@ -167,7 +111,11 @@ function getChangedFiles(db, allFiles, rootDir) {
167
111
  currentFiles.add(relPath);
168
112
 
169
113
  let content;
170
- try { content = fs.readFileSync(file, 'utf-8'); } catch { continue; }
114
+ try {
115
+ content = fs.readFileSync(file, 'utf-8');
116
+ } catch {
117
+ continue;
118
+ }
171
119
  const hash = fileHash(content);
172
120
 
173
121
  if (existing.get(relPath) !== hash) {
@@ -191,21 +139,28 @@ export async function buildGraph(rootDir, opts = {}) {
191
139
  initSchema(db);
192
140
 
193
141
  const config = loadConfig(rootDir);
194
- const incremental = opts.incremental !== false && config.build && config.build.incremental !== false;
142
+ const incremental =
143
+ opts.incremental !== false && config.build && config.build.incremental !== false;
144
+
145
+ // Engine selection: 'native', 'wasm', or 'auto' (default)
146
+ const engineOpts = { engine: opts.engine || 'auto' };
147
+ const { name: engineName, version: engineVersion } = getActiveEngine(engineOpts);
148
+ console.log(`Using ${engineName} engine${engineVersion ? ` (v${engineVersion})` : ''}`);
195
149
 
196
- const parsers = await createParsers();
197
150
  const aliases = loadPathAliases(rootDir);
198
151
  // Merge config aliases
199
152
  if (config.aliases) {
200
153
  for (const [key, value] of Object.entries(config.aliases)) {
201
- const pattern = key.endsWith('/') ? key + '*' : key;
154
+ const pattern = key.endsWith('/') ? `${key}*` : key;
202
155
  const target = path.resolve(rootDir, value);
203
- aliases.paths[pattern] = [target.endsWith('/') ? target + '*' : target + '/*'];
156
+ aliases.paths[pattern] = [target.endsWith('/') ? `${target}*` : `${target}/*`];
204
157
  }
205
158
  }
206
159
 
207
160
  if (aliases.baseUrl || Object.keys(aliases.paths).length > 0) {
208
- console.log(`Loaded path aliases: baseUrl=${aliases.baseUrl || 'none'}, ${Object.keys(aliases.paths).length} path mappings`);
161
+ console.log(
162
+ `Loaded path aliases: baseUrl=${aliases.baseUrl || 'none'}, ${Object.keys(aliases.paths).length} path mappings`,
163
+ );
209
164
  }
210
165
 
211
166
  const files = collectFiles(rootDir, [], config);
@@ -214,7 +169,7 @@ export async function buildGraph(rootDir, opts = {}) {
214
169
  // Check for incremental build
215
170
  const { changed, removed, isFullBuild } = incremental
216
171
  ? getChangedFiles(db, files, rootDir)
217
- : { changed: files.map(f => ({ file: f })), removed: [], isFullBuild: true };
172
+ : { changed: files.map((f) => ({ file: f })), removed: [], isFullBuild: true };
218
173
 
219
174
  if (!isFullBuild && changed.length === 0 && removed.length === 0) {
220
175
  console.log('No changes detected. Graph is up to date.');
@@ -223,7 +178,9 @@ export async function buildGraph(rootDir, opts = {}) {
223
178
  }
224
179
 
225
180
  if (isFullBuild) {
226
- db.exec('PRAGMA foreign_keys = OFF; DELETE FROM edges; DELETE FROM nodes; PRAGMA foreign_keys = ON;');
181
+ db.exec(
182
+ 'PRAGMA foreign_keys = OFF; DELETE FROM edges; DELETE FROM nodes; PRAGMA foreign_keys = ON;',
183
+ );
227
184
  } else {
228
185
  console.log(`Incremental: ${changed.length} changed, ${removed.length} removed`);
229
186
  // Remove nodes/edges for changed and removed files
@@ -243,86 +200,88 @@ export async function buildGraph(rootDir, opts = {}) {
243
200
  }
244
201
  }
245
202
 
246
- const insertNode = db.prepare('INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line) VALUES (?, ?, ?, ?, ?)');
247
- const getNodeId = db.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?');
248
- const insertEdge = db.prepare('INSERT INTO edges (source_id, target_id, kind, confidence, dynamic) VALUES (?, ?, ?, ?, ?)');
203
+ const insertNode = db.prepare(
204
+ 'INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line) VALUES (?, ?, ?, ?, ?)',
205
+ );
206
+ const getNodeId = db.prepare(
207
+ 'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?',
208
+ );
209
+ const insertEdge = db.prepare(
210
+ 'INSERT INTO edges (source_id, target_id, kind, confidence, dynamic) VALUES (?, ?, ?, ?, ?)',
211
+ );
249
212
 
250
213
  // Prepare hash upsert
251
214
  let upsertHash;
252
215
  try {
253
- upsertHash = db.prepare('INSERT OR REPLACE INTO file_hashes (file, hash, mtime) VALUES (?, ?, ?)');
254
- } catch { upsertHash = null; }
216
+ upsertHash = db.prepare(
217
+ 'INSERT OR REPLACE INTO file_hashes (file, hash, mtime) VALUES (?, ?, ?)',
218
+ );
219
+ } catch {
220
+ upsertHash = null;
221
+ }
255
222
 
256
223
  // First pass: parse files and insert nodes
257
224
  const fileSymbols = new Map();
258
- let parsed = 0, skipped = 0;
259
225
 
260
226
  // For incremental builds, also load existing symbols that aren't changing
261
227
  if (!isFullBuild) {
262
228
  // We need to reload ALL file symbols for edge building
263
- const allExistingFiles = db.prepare("SELECT DISTINCT file FROM nodes WHERE kind = 'file'").all();
229
+ const _allExistingFiles = db
230
+ .prepare("SELECT DISTINCT file FROM nodes WHERE kind = 'file'")
231
+ .all();
264
232
  // We'll fill these in during the parse pass + edge pass
265
233
  }
266
234
 
267
- const filesToParse = isFullBuild
268
- ? files.map(f => ({ file: f }))
269
- : changed;
270
-
271
- const insertMany = db.transaction(() => {
272
- for (const item of filesToParse) {
273
- const filePath = item.file;
274
- const parser = getParser(parsers, filePath);
275
- if (!parser) { skipped++; continue; }
276
-
277
- let code;
278
- if (item.content) {
279
- code = item.content;
280
- } else {
281
- try { code = fs.readFileSync(filePath, 'utf-8'); }
282
- catch (err) {
283
- warn(`Skipping ${path.relative(rootDir, filePath)}: ${err.message}`);
284
- skipped++;
285
- continue;
286
- }
287
- }
235
+ const filesToParse = isFullBuild ? files.map((f) => ({ file: f })) : changed;
288
236
 
289
- let tree;
290
- try { tree = parser.parse(code); }
291
- catch (e) {
292
- warn(`Parse error in ${path.relative(rootDir, filePath)}: ${e.message}`);
293
- skipped++;
294
- continue;
295
- }
237
+ // ── Unified parse via parseFilesAuto ───────────────────────────────
238
+ const filePaths = filesToParse.map((item) => item.file);
239
+ const allSymbols = await parseFilesAuto(filePaths, rootDir, engineOpts);
240
+
241
+ // Build a hash lookup from incremental data (changed items may carry pre-computed hashes)
242
+ const precomputedHashes = new Map();
243
+ for (const item of filesToParse) {
244
+ if (item.hash && item.relPath) {
245
+ precomputedHashes.set(item.relPath, item.hash);
246
+ }
247
+ }
296
248
 
297
- const relPath = normalizePath(path.relative(rootDir, filePath));
298
- const isHCL = filePath.endsWith('.tf') || filePath.endsWith('.hcl');
299
- const isPython = filePath.endsWith('.py');
300
- const symbols = isHCL ? extractHCLSymbols(tree, filePath)
301
- : isPython ? extractPythonSymbols(tree, filePath)
302
- : extractSymbols(tree, filePath);
249
+ const insertAll = db.transaction(() => {
250
+ for (const [relPath, symbols] of allSymbols) {
303
251
  fileSymbols.set(relPath, symbols);
304
252
 
305
253
  insertNode.run(relPath, 'file', relPath, 0, null);
306
-
307
254
  for (const def of symbols.definitions) {
308
255
  insertNode.run(def.name, def.kind, relPath, def.line, def.endLine || null);
309
256
  }
310
-
311
257
  for (const exp of symbols.exports) {
312
258
  insertNode.run(exp.name, exp.kind, relPath, exp.line, null);
313
259
  }
314
260
 
315
261
  // Update file hash for incremental builds
316
262
  if (upsertHash) {
317
- const hash = item.hash || fileHash(code);
318
- upsertHash.run(relPath, hash, Date.now());
263
+ const existingHash = precomputedHashes.get(relPath);
264
+ if (existingHash) {
265
+ upsertHash.run(relPath, existingHash, Date.now());
266
+ } else {
267
+ const absPath = path.join(rootDir, relPath);
268
+ let code;
269
+ try {
270
+ code = fs.readFileSync(absPath, 'utf-8');
271
+ } catch {
272
+ code = null;
273
+ }
274
+ if (code !== null) {
275
+ upsertHash.run(relPath, fileHash(code), Date.now());
276
+ }
277
+ }
319
278
  }
320
-
321
- parsed++;
322
- if (parsed % 100 === 0) process.stdout.write(` Parsed ${parsed}/${filesToParse.length} files\r`);
323
279
  }
324
280
  });
325
- insertMany();
281
+ insertAll();
282
+
283
+ const parsed = allSymbols.size;
284
+ const skipped = filesToParse.length - parsed;
326
285
  console.log(`Parsed ${parsed} files (${skipped} skipped)`);
327
286
 
328
287
  // Clean up removed file hashes
@@ -333,23 +292,46 @@ export async function buildGraph(rootDir, opts = {}) {
333
292
  }
334
293
  }
335
294
 
295
+ // ── Batch import resolution ────────────────────────────────────────
296
+ // Collect all (fromFile, importSource) pairs and resolve in one native call
297
+ const batchInputs = [];
298
+ for (const [relPath, symbols] of fileSymbols) {
299
+ const absFile = path.join(rootDir, relPath);
300
+ for (const imp of symbols.imports) {
301
+ batchInputs.push({ fromFile: absFile, importSource: imp.source });
302
+ }
303
+ }
304
+ const batchResolved = resolveImportsBatch(batchInputs, rootDir, aliases);
305
+
306
+ function getResolved(absFile, importSource) {
307
+ if (batchResolved) {
308
+ const key = `${absFile}|${importSource}`;
309
+ const hit = batchResolved.get(key);
310
+ if (hit !== undefined) return hit;
311
+ }
312
+ return resolveImportPath(absFile, importSource, rootDir, aliases);
313
+ }
314
+
336
315
  // Build re-export map for barrel resolution
337
316
  const reexportMap = new Map();
338
317
  for (const [relPath, symbols] of fileSymbols) {
339
- const reexports = symbols.imports.filter(imp => imp.reexport);
318
+ const reexports = symbols.imports.filter((imp) => imp.reexport);
340
319
  if (reexports.length > 0) {
341
- reexportMap.set(relPath, reexports.map(imp => ({
342
- source: resolveImportPath(path.join(rootDir, relPath), imp.source, rootDir, aliases),
343
- names: imp.names,
344
- wildcardReexport: imp.wildcardReexport || false
345
- })));
320
+ reexportMap.set(
321
+ relPath,
322
+ reexports.map((imp) => ({
323
+ source: getResolved(path.join(rootDir, relPath), imp.source),
324
+ names: imp.names,
325
+ wildcardReexport: imp.wildcardReexport || false,
326
+ })),
327
+ );
346
328
  }
347
329
  }
348
330
 
349
331
  function isBarrelFile(relPath) {
350
332
  const symbols = fileSymbols.get(relPath);
351
333
  if (!symbols) return false;
352
- const reexports = symbols.imports.filter(imp => imp.reexport);
334
+ const reexports = symbols.imports.filter((imp) => imp.reexport);
353
335
  if (reexports.length === 0) return false;
354
336
  const ownDefs = symbols.definitions.length;
355
337
  return reexports.length >= ownDefs;
@@ -366,7 +348,7 @@ export async function buildGraph(rootDir, opts = {}) {
366
348
  if (re.names.includes(symbolName)) {
367
349
  const targetSymbols = fileSymbols.get(re.source);
368
350
  if (targetSymbols) {
369
- const hasDef = targetSymbols.definitions.some(d => d.name === symbolName);
351
+ const hasDef = targetSymbols.definitions.some((d) => d.name === symbolName);
370
352
  if (hasDef) return re.source;
371
353
  const deeper = resolveBarrelExport(re.source, symbolName, visited);
372
354
  if (deeper) return deeper;
@@ -378,7 +360,7 @@ export async function buildGraph(rootDir, opts = {}) {
378
360
  if (re.wildcardReexport || re.names.length === 0) {
379
361
  const targetSymbols = fileSymbols.get(re.source);
380
362
  if (targetSymbols) {
381
- const hasDef = targetSymbols.definitions.some(d => d.name === symbolName);
363
+ const hasDef = targetSymbols.definitions.some((d) => d.name === symbolName);
382
364
  if (hasDef) return re.source;
383
365
  const deeper = resolveBarrelExport(re.source, symbolName, visited);
384
366
  if (deeper) return deeper;
@@ -389,9 +371,11 @@ export async function buildGraph(rootDir, opts = {}) {
389
371
  }
390
372
 
391
373
  // N+1 optimization: pre-load all nodes into a lookup map for edge building
392
- const allNodes = db.prepare(
393
- `SELECT id, name, kind, file FROM nodes WHERE kind IN ('function','method','class','interface')`
394
- ).all();
374
+ const allNodes = db
375
+ .prepare(
376
+ `SELECT id, name, kind, file FROM nodes WHERE kind IN ('function','method','class','interface')`,
377
+ )
378
+ .all();
395
379
  const nodesByName = new Map();
396
380
  for (const node of allNodes) {
397
381
  if (!nodesByName.has(node.name)) nodesByName.set(node.name, []);
@@ -414,7 +398,7 @@ export async function buildGraph(rootDir, opts = {}) {
414
398
 
415
399
  // Import edges
416
400
  for (const imp of symbols.imports) {
417
- const resolvedPath = resolveImportPath(path.join(rootDir, relPath), imp.source, rootDir, aliases);
401
+ const resolvedPath = getResolved(path.join(rootDir, relPath), imp.source);
418
402
  const targetRow = getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
419
403
  if (targetRow) {
420
404
  const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports';
@@ -426,11 +410,21 @@ export async function buildGraph(rootDir, opts = {}) {
426
410
  for (const name of imp.names) {
427
411
  const cleanName = name.replace(/^\*\s+as\s+/, '');
428
412
  const actualSource = resolveBarrelExport(resolvedPath, cleanName);
429
- if (actualSource && actualSource !== resolvedPath && !resolvedSources.has(actualSource)) {
413
+ if (
414
+ actualSource &&
415
+ actualSource !== resolvedPath &&
416
+ !resolvedSources.has(actualSource)
417
+ ) {
430
418
  resolvedSources.add(actualSource);
431
419
  const actualRow = getNodeId.get(actualSource, 'file', actualSource, 0);
432
420
  if (actualRow) {
433
- insertEdge.run(fileNodeId, actualRow.id, edgeKind === 'imports-type' ? 'imports-type' : 'imports', 0.9, 0);
421
+ insertEdge.run(
422
+ fileNodeId,
423
+ actualRow.id,
424
+ edgeKind === 'imports-type' ? 'imports-type' : 'imports',
425
+ 0.9,
426
+ 0,
427
+ );
434
428
  edgeCount++;
435
429
  }
436
430
  }
@@ -442,7 +436,7 @@ export async function buildGraph(rootDir, opts = {}) {
442
436
  // Build import name -> target file mapping
443
437
  const importedNames = new Map();
444
438
  for (const imp of symbols.imports) {
445
- const resolvedPath = resolveImportPath(path.join(rootDir, relPath), imp.source, rootDir, aliases);
439
+ const resolvedPath = getResolved(path.join(rootDir, relPath), imp.source);
446
440
  for (const name of imp.names) {
447
441
  const cleanName = name.replace(/^\*\s+as\s+/, '');
448
442
  importedNames.set(cleanName, resolvedPath);
@@ -480,8 +474,8 @@ export async function buildGraph(rootDir, opts = {}) {
480
474
  targets = nodesByNameAndFile.get(`${call.name}|${relPath}`) || [];
481
475
  if (targets.length === 0) {
482
476
  // Method name match (e.g. ClassName.methodName)
483
- const methodCandidates = (nodesByName.get(call.name) || []).filter(n =>
484
- n.name.endsWith(`.${call.name}`) && n.kind === 'method'
477
+ const methodCandidates = (nodesByName.get(call.name) || []).filter(
478
+ (n) => n.name.endsWith(`.${call.name}`) && n.kind === 'method',
485
479
  );
486
480
  if (methodCandidates.length > 0) {
487
481
  targets = methodCandidates;
@@ -512,9 +506,11 @@ export async function buildGraph(rootDir, opts = {}) {
512
506
  // Class extends edges
513
507
  for (const cls of symbols.classes) {
514
508
  if (cls.extends) {
515
- const sourceRow = db.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ?').get(cls.name, 'class', relPath);
509
+ const sourceRow = db
510
+ .prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ?')
511
+ .get(cls.name, 'class', relPath);
516
512
  const targetCandidates = nodesByName.get(cls.extends) || [];
517
- const targetRows = targetCandidates.filter(n => n.kind === 'class');
513
+ const targetRows = targetCandidates.filter((n) => n.kind === 'class');
518
514
  if (sourceRow) {
519
515
  for (const t of targetRows) {
520
516
  insertEdge.run(sourceRow.id, t.id, 'extends', 1.0, 0);
@@ -524,9 +520,13 @@ export async function buildGraph(rootDir, opts = {}) {
524
520
  }
525
521
 
526
522
  if (cls.implements) {
527
- const sourceRow = db.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ?').get(cls.name, 'class', relPath);
523
+ const sourceRow = db
524
+ .prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ?')
525
+ .get(cls.name, 'class', relPath);
528
526
  const targetCandidates = nodesByName.get(cls.implements) || [];
529
- const targetRows = targetCandidates.filter(n => n.kind === 'interface' || n.kind === 'class');
527
+ const targetRows = targetCandidates.filter(
528
+ (n) => n.kind === 'interface' || n.kind === 'class',
529
+ );
530
530
  if (sourceRow) {
531
531
  for (const t of targetRows) {
532
532
  insertEdge.run(sourceRow.id, t.id, 'implements', 1.0, 0);
@@ -544,4 +544,3 @@ export async function buildGraph(rootDir, opts = {}) {
544
544
  console.log(`Stored in ${dbPath}`);
545
545
  db.close();
546
546
  }
547
-