@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/LICENSE +190 -190
- package/README.md +498 -311
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-hcl.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +90 -69
- package/src/builder.js +161 -162
- package/src/cli.js +284 -224
- package/src/config.js +103 -55
- package/src/constants.js +41 -28
- package/src/cycles.js +125 -104
- package/src/db.js +129 -117
- package/src/embedder.js +253 -59
- package/src/export.js +150 -138
- package/src/index.js +50 -39
- package/src/logger.js +24 -20
- package/src/mcp.js +311 -139
- package/src/native.js +68 -0
- package/src/parser.js +2214 -573
- package/src/queries.js +334 -128
- package/src/resolve.js +171 -0
- package/src/watcher.js +81 -53
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
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { resolveImportPath } from './
|
|
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(
|
|
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
|
|
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 {
|
|
40
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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)
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
147
|
-
|
|
148
|
-
|
|
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(
|
|
154
|
-
|
|
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(
|
|
159
|
-
|
|
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
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 }, (
|
|
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
|
-
|