@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/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 {
|
|
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 {
|
|
14
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
159
|
-
.
|
|
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 {
|
|
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 =
|
|
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
|
|
154
|
+
const pattern = key.endsWith('/') ? `${key}*` : key;
|
|
202
155
|
const target = path.resolve(rootDir, value);
|
|
203
|
-
aliases.paths[pattern] = [target.endsWith('/') ? 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(
|
|
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(
|
|
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(
|
|
247
|
-
|
|
248
|
-
|
|
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(
|
|
254
|
-
|
|
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
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
|
318
|
-
|
|
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
|
-
|
|
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(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
393
|
-
|
|
394
|
-
|
|
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 =
|
|
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 (
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|