@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/resolve.js ADDED
@@ -0,0 +1,171 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { normalizePath } from './constants.js';
4
+ import { loadNative } from './native.js';
5
+
6
+ // ── Alias format conversion ─────────────────────────────────────────
7
+
8
+ /**
9
+ * Convert JS alias format { baseUrl, paths: { pattern: [targets] } }
10
+ * to native format { baseUrl, paths: [{ pattern, targets }] }.
11
+ */
12
+ export function convertAliasesForNative(aliases) {
13
+ if (!aliases) return null;
14
+ return {
15
+ baseUrl: aliases.baseUrl || '',
16
+ paths: Object.entries(aliases.paths || {}).map(([pattern, targets]) => ({
17
+ pattern,
18
+ targets,
19
+ })),
20
+ };
21
+ }
22
+
23
+ // ── JS fallback implementations ─────────────────────────────────────
24
+
25
+ function resolveViaAlias(importSource, aliases, _rootDir) {
26
+ if (aliases.baseUrl && !importSource.startsWith('.') && !importSource.startsWith('/')) {
27
+ const candidate = path.resolve(aliases.baseUrl, importSource);
28
+ for (const ext of ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js']) {
29
+ const full = candidate + ext;
30
+ if (fs.existsSync(full)) return full;
31
+ }
32
+ }
33
+
34
+ for (const [pattern, targets] of Object.entries(aliases.paths)) {
35
+ const prefix = pattern.replace(/\*$/, '');
36
+ if (!importSource.startsWith(prefix)) continue;
37
+ const rest = importSource.slice(prefix.length);
38
+ for (const target of targets) {
39
+ const resolved = target.replace(/\*$/, rest);
40
+ for (const ext of [
41
+ '',
42
+ '.ts',
43
+ '.tsx',
44
+ '.js',
45
+ '.jsx',
46
+ '/index.ts',
47
+ '/index.tsx',
48
+ '/index.js',
49
+ ]) {
50
+ const full = resolved + ext;
51
+ if (fs.existsSync(full)) return full;
52
+ }
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+
58
+ function resolveImportPathJS(fromFile, importSource, rootDir, aliases) {
59
+ if (!importSource.startsWith('.') && aliases) {
60
+ const aliasResolved = resolveViaAlias(importSource, aliases, rootDir);
61
+ if (aliasResolved) return normalizePath(path.relative(rootDir, aliasResolved));
62
+ }
63
+ if (!importSource.startsWith('.')) return importSource;
64
+ const dir = path.dirname(fromFile);
65
+ const resolved = path.resolve(dir, importSource);
66
+
67
+ if (resolved.endsWith('.js')) {
68
+ const tsCandidate = resolved.replace(/\.js$/, '.ts');
69
+ if (fs.existsSync(tsCandidate)) return normalizePath(path.relative(rootDir, tsCandidate));
70
+ const tsxCandidate = resolved.replace(/\.js$/, '.tsx');
71
+ if (fs.existsSync(tsxCandidate)) return normalizePath(path.relative(rootDir, tsxCandidate));
72
+ }
73
+
74
+ for (const ext of [
75
+ '.ts',
76
+ '.tsx',
77
+ '.js',
78
+ '.jsx',
79
+ '.mjs',
80
+ '.py',
81
+ '/index.ts',
82
+ '/index.tsx',
83
+ '/index.js',
84
+ '/__init__.py',
85
+ ]) {
86
+ const candidate = resolved + ext;
87
+ if (fs.existsSync(candidate)) {
88
+ return normalizePath(path.relative(rootDir, candidate));
89
+ }
90
+ }
91
+ if (fs.existsSync(resolved)) return normalizePath(path.relative(rootDir, resolved));
92
+ return normalizePath(path.relative(rootDir, resolved));
93
+ }
94
+
95
+ function computeConfidenceJS(callerFile, targetFile, importedFrom) {
96
+ if (!targetFile || !callerFile) return 0.3;
97
+ if (callerFile === targetFile) return 1.0;
98
+ if (importedFrom === targetFile) return 1.0;
99
+ if (path.dirname(callerFile) === path.dirname(targetFile)) return 0.7;
100
+ const callerParent = path.dirname(path.dirname(callerFile));
101
+ const targetParent = path.dirname(path.dirname(targetFile));
102
+ if (callerParent === targetParent) return 0.5;
103
+ return 0.3;
104
+ }
105
+
106
+ // ── Public API with native dispatch ─────────────────────────────────
107
+
108
+ /**
109
+ * Resolve a single import path.
110
+ * Tries native, falls back to JS.
111
+ */
112
+ export function resolveImportPath(fromFile, importSource, rootDir, aliases) {
113
+ const native = loadNative();
114
+ if (native) {
115
+ try {
116
+ return native.resolveImport(
117
+ fromFile,
118
+ importSource,
119
+ rootDir,
120
+ convertAliasesForNative(aliases),
121
+ );
122
+ } catch {
123
+ // fall through to JS
124
+ }
125
+ }
126
+ return resolveImportPathJS(fromFile, importSource, rootDir, aliases);
127
+ }
128
+
129
+ /**
130
+ * Compute proximity-based confidence for call resolution.
131
+ * Tries native, falls back to JS.
132
+ */
133
+ export function computeConfidence(callerFile, targetFile, importedFrom) {
134
+ const native = loadNative();
135
+ if (native) {
136
+ try {
137
+ return native.computeConfidence(callerFile, targetFile, importedFrom || null);
138
+ } catch {
139
+ // fall through to JS
140
+ }
141
+ }
142
+ return computeConfidenceJS(callerFile, targetFile, importedFrom);
143
+ }
144
+
145
+ /**
146
+ * Batch resolve multiple imports in a single native call.
147
+ * Returns Map<"fromFile|importSource", resolvedPath> or null when native unavailable.
148
+ */
149
+ export function resolveImportsBatch(inputs, rootDir, aliases) {
150
+ const native = loadNative();
151
+ if (!native) return null;
152
+
153
+ try {
154
+ const nativeInputs = inputs.map(({ fromFile, importSource }) => ({
155
+ fromFile,
156
+ importSource,
157
+ }));
158
+ const results = native.resolveImports(nativeInputs, rootDir, convertAliasesForNative(aliases));
159
+ const map = new Map();
160
+ for (const r of results) {
161
+ map.set(`${r.fromFile}|${r.importSource}`, r.resolvedPath);
162
+ }
163
+ return map;
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ // ── Exported for testing ────────────────────────────────────────────
170
+
171
+ export { resolveImportPathJS, computeConfidenceJS };
package/src/watcher.js CHANGED
@@ -1,15 +1,14 @@
1
-
2
- import fs from 'fs';
3
- import path from 'path';
4
- import { openDb, initSchema } from './db.js';
5
- import { createParsers, getParser, extractSymbols, extractHCLSymbols, extractPythonSymbols } from './parser.js';
6
- import { IGNORE_DIRS, EXTENSIONS, normalizePath } from './constants.js';
7
- import { resolveImportPath } from './builder.js';
8
- import { warn, debug, info } from './logger.js';
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
4
+ import { initSchema, openDb } from './db.js';
5
+ import { info, warn } from './logger.js';
6
+ import { createParseTreeCache, getActiveEngine, parseFileIncremental } from './parser.js';
7
+ import { resolveImportPath } from './resolve.js';
9
8
 
10
9
  function shouldIgnore(filePath) {
11
10
  const parts = filePath.split(path.sep);
12
- return parts.some(p => IGNORE_DIRS.has(p));
11
+ return parts.some((p) => IGNORE_DIRS.has(p));
13
12
  }
14
13
 
15
14
  function isTrackedExt(filePath) {
@@ -19,41 +18,30 @@ function isTrackedExt(filePath) {
19
18
  /**
20
19
  * Parse a single file and update the database incrementally.
21
20
  */
22
- function updateFile(db, rootDir, filePath, parsers, stmts) {
21
+ async function updateFile(_db, rootDir, filePath, stmts, engineOpts, cache) {
23
22
  const relPath = normalizePath(path.relative(rootDir, filePath));
24
23
 
25
24
  const oldNodes = stmts.countNodes.get(relPath)?.c || 0;
26
- const oldEdges = stmts.countEdgesForFile.get(relPath)?.c || 0;
25
+ const _oldEdges = stmts.countEdgesForFile.get(relPath)?.c || 0;
27
26
 
28
27
  stmts.deleteEdgesForFile.run(relPath);
29
28
  stmts.deleteNodes.run(relPath);
30
29
 
31
30
  if (!fs.existsSync(filePath)) {
31
+ if (cache) cache.remove(filePath);
32
32
  return { file: relPath, nodesAdded: 0, nodesRemoved: oldNodes, edgesAdded: 0, deleted: true };
33
33
  }
34
34
 
35
- const parser = getParser(parsers, filePath);
36
- if (!parser) return null;
37
-
38
35
  let code;
39
- try { code = fs.readFileSync(filePath, 'utf-8'); }
40
- catch (err) {
36
+ try {
37
+ code = fs.readFileSync(filePath, 'utf-8');
38
+ } catch (err) {
41
39
  warn(`Cannot read ${relPath}: ${err.message}`);
42
40
  return null;
43
41
  }
44
42
 
45
- let tree;
46
- try { tree = parser.parse(code); }
47
- catch (err) {
48
- warn(`Parse error in ${relPath}: ${err.message}`);
49
- return null;
50
- }
51
-
52
- const isHCL = filePath.endsWith('.tf') || filePath.endsWith('.hcl');
53
- const isPython = filePath.endsWith('.py');
54
- const symbols = isHCL ? extractHCLSymbols(tree, filePath)
55
- : isPython ? extractPythonSymbols(tree, filePath)
56
- : extractSymbols(tree, filePath);
43
+ const symbols = await parseFileIncremental(cache, filePath, code, engineOpts);
44
+ if (!symbols) return null;
57
45
 
58
46
  stmts.insertNode.run(relPath, 'file', relPath, 0, null);
59
47
 
@@ -68,14 +56,20 @@ function updateFile(db, rootDir, filePath, parsers, stmts) {
68
56
 
69
57
  let edgesAdded = 0;
70
58
  const fileNodeRow = stmts.getNodeId.get(relPath, 'file', relPath, 0);
71
- if (!fileNodeRow) return { file: relPath, nodesAdded: newNodes, nodesRemoved: oldNodes, edgesAdded: 0 };
59
+ if (!fileNodeRow)
60
+ return { file: relPath, nodesAdded: newNodes, nodesRemoved: oldNodes, edgesAdded: 0 };
72
61
  const fileNodeId = fileNodeRow.id;
73
62
 
74
63
  // Load aliases for full import resolution
75
64
  const aliases = { baseUrl: null, paths: {} };
76
65
 
77
66
  for (const imp of symbols.imports) {
78
- const resolvedPath = resolveImportPath(path.join(rootDir, relPath), imp.source, rootDir, aliases);
67
+ const resolvedPath = resolveImportPath(
68
+ path.join(rootDir, relPath),
69
+ imp.source,
70
+ rootDir,
71
+ aliases,
72
+ );
79
73
  const targetRow = stmts.getNodeId.get(resolvedPath, 'file', resolvedPath, 0);
80
74
  if (targetRow) {
81
75
  const edgeKind = imp.reexport ? 'reexports' : imp.typeOnly ? 'imports-type' : 'imports';
@@ -86,7 +80,12 @@ function updateFile(db, rootDir, filePath, parsers, stmts) {
86
80
 
87
81
  const importedNames = new Map();
88
82
  for (const imp of symbols.imports) {
89
- const resolvedPath = resolveImportPath(path.join(rootDir, relPath), imp.source, rootDir, aliases);
83
+ const resolvedPath = resolveImportPath(
84
+ path.join(rootDir, relPath),
85
+ imp.source,
86
+ rootDir,
87
+ aliases,
88
+ );
90
89
  for (const name of imp.names) {
91
90
  importedNames.set(name.replace(/^\*\s+as\s+/, ''), resolvedPath);
92
91
  }
@@ -116,7 +115,13 @@ function updateFile(db, rootDir, filePath, parsers, stmts) {
116
115
 
117
116
  for (const t of targets) {
118
117
  if (t.id !== caller.id) {
119
- stmts.insertEdge.run(caller.id, t.id, 'calls', importedFrom ? 1.0 : 0.5, call.dynamic ? 1 : 0);
118
+ stmts.insertEdge.run(
119
+ caller.id,
120
+ t.id,
121
+ 'calls',
122
+ importedFrom ? 1.0 : 0.5,
123
+ call.dynamic ? 1 : 0,
124
+ );
120
125
  edgesAdded++;
121
126
  }
122
127
  }
@@ -127,11 +132,11 @@ function updateFile(db, rootDir, filePath, parsers, stmts) {
127
132
  nodesAdded: newNodes,
128
133
  nodesRemoved: oldNodes,
129
134
  edgesAdded,
130
- deleted: false
135
+ deleted: false,
131
136
  };
132
137
  }
133
138
 
134
- export async function watchProject(rootDir) {
139
+ export async function watchProject(rootDir, opts = {}) {
135
140
  const dbPath = path.join(rootDir, '.codegraph', 'graph.db');
136
141
  if (!fs.existsSync(dbPath)) {
137
142
  console.error('No graph.db found. Run `codegraph build` first.');
@@ -140,23 +145,48 @@ export async function watchProject(rootDir) {
140
145
 
141
146
  const db = openDb(dbPath);
142
147
  initSchema(db);
143
- const parsers = await createParsers();
148
+ const engineOpts = { engine: opts.engine || 'auto' };
149
+ const { name: engineName, version: engineVersion } = getActiveEngine(engineOpts);
150
+ console.log(
151
+ `Watch mode using ${engineName} engine${engineVersion ? ` (v${engineVersion})` : ''}`,
152
+ );
153
+
154
+ const cache = createParseTreeCache();
155
+ console.log(
156
+ cache
157
+ ? 'Incremental parsing enabled (native tree cache)'
158
+ : 'Incremental parsing unavailable (full re-parse)',
159
+ );
144
160
 
145
161
  const stmts = {
146
- insertNode: db.prepare('INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line) VALUES (?, ?, ?, ?, ?)'),
147
- getNodeId: db.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?'),
148
- insertEdge: db.prepare('INSERT INTO edges (source_id, target_id, kind, confidence, dynamic) VALUES (?, ?, ?, ?, ?)'),
162
+ insertNode: db.prepare(
163
+ 'INSERT OR IGNORE INTO nodes (name, kind, file, line, end_line) VALUES (?, ?, ?, ?, ?)',
164
+ ),
165
+ getNodeId: db.prepare(
166
+ 'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?',
167
+ ),
168
+ insertEdge: db.prepare(
169
+ 'INSERT INTO edges (source_id, target_id, kind, confidence, dynamic) VALUES (?, ?, ?, ?, ?)',
170
+ ),
149
171
  deleteNodes: db.prepare('DELETE FROM nodes WHERE file = ?'),
150
172
  deleteEdgesForFile: null,
151
173
  countNodes: db.prepare('SELECT COUNT(*) as c FROM nodes WHERE file = ?'),
152
174
  countEdgesForFile: null,
153
- findNodeInFile: db.prepare('SELECT id, file FROM nodes WHERE name = ? AND kind IN (\'function\', \'method\', \'class\', \'interface\') AND file = ?'),
154
- findNodeByName: db.prepare('SELECT id, file FROM nodes WHERE name = ? AND kind IN (\'function\', \'method\', \'class\', \'interface\')'),
175
+ findNodeInFile: db.prepare(
176
+ "SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface') AND file = ?",
177
+ ),
178
+ findNodeByName: db.prepare(
179
+ "SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface')",
180
+ ),
155
181
  };
156
182
 
157
183
  // Use named params for statements needing the same value twice
158
- const origDeleteEdges = db.prepare(`DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`);
159
- const origCountEdges = db.prepare(`SELECT COUNT(*) as c FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`);
184
+ const origDeleteEdges = db.prepare(
185
+ `DELETE FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`,
186
+ );
187
+ const origCountEdges = db.prepare(
188
+ `SELECT COUNT(*) as c FROM edges WHERE source_id IN (SELECT id FROM nodes WHERE file = @f) OR target_id IN (SELECT id FROM nodes WHERE file = @f)`,
189
+ );
160
190
  stmts.deleteEdgesForFile = { run: (f) => origDeleteEdges.run({ f }) };
161
191
  stmts.countEdgesForFile = { get: (f) => origCountEdges.get({ f }) };
162
192
 
@@ -164,18 +194,16 @@ export async function watchProject(rootDir) {
164
194
  let timer = null;
165
195
  const DEBOUNCE_MS = 300;
166
196
 
167
- function processPending() {
197
+ async function processPending() {
168
198
  const files = [...pending];
169
199
  pending.clear();
170
200
 
171
- const updates = db.transaction(() => {
172
- const results = [];
173
- for (const filePath of files) {
174
- const result = updateFile(db, rootDir, filePath, parsers, stmts);
175
- if (result) results.push(result);
176
- }
177
- return results;
178
- })();
201
+ const results = [];
202
+ for (const filePath of files) {
203
+ const result = await updateFile(db, rootDir, filePath, stmts, engineOpts, cache);
204
+ if (result) results.push(result);
205
+ }
206
+ const updates = results;
179
207
 
180
208
  for (const r of updates) {
181
209
  const nodeDelta = r.nodesAdded - r.nodesRemoved;
@@ -191,7 +219,7 @@ export async function watchProject(rootDir) {
191
219
  console.log(`Watching ${rootDir} for changes...`);
192
220
  console.log('Press Ctrl+C to stop.\n');
193
221
 
194
- const watcher = fs.watch(rootDir, { recursive: true }, (eventType, filename) => {
222
+ const watcher = fs.watch(rootDir, { recursive: true }, (_eventType, filename) => {
195
223
  if (!filename) return;
196
224
  if (shouldIgnore(filename)) return;
197
225
  if (!isTrackedExt(filename)) return;
@@ -206,8 +234,8 @@ export async function watchProject(rootDir) {
206
234
  process.on('SIGINT', () => {
207
235
  console.log('\nStopping watcher...');
208
236
  watcher.close();
237
+ if (cache) cache.clear();
209
238
  db.close();
210
239
  process.exit(0);
211
240
  });
212
241
  }
213
-