@monoes/monograph 1.5.0 → 1.5.2
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/.monodesign/hook.cache.json +1 -0
- package/__tests__/storage/stores.test.ts +66 -1
- package/dist/src/analysis/cycles.d.ts.map +1 -1
- package/dist/src/analysis/cycles.js +5 -3
- package/dist/src/analysis/cycles.js.map +1 -1
- package/dist/src/analysis/trace.d.ts.map +1 -1
- package/dist/src/analysis/trace.js +1 -19
- package/dist/src/analysis/trace.js.map +1 -1
- package/dist/src/analysis/worker-pool.d.ts.map +1 -1
- package/dist/src/analysis/worker-pool.js +9 -7
- package/dist/src/analysis/worker-pool.js.map +1 -1
- package/dist/src/cli/skill-gen.d.ts.map +1 -1
- package/dist/src/cli/skill-gen.js +16 -25
- package/dist/src/cli/skill-gen.js.map +1 -1
- package/dist/src/export/html.d.ts.map +1 -1
- package/dist/src/export/html.js +3 -2
- package/dist/src/export/html.js.map +1 -1
- package/dist/src/graph/regex-search.d.ts.map +1 -1
- package/dist/src/graph/regex-search.js +9 -3
- package/dist/src/graph/regex-search.js.map +1 -1
- package/dist/src/pipeline/phases/call-site-extractors.d.ts +17 -0
- package/dist/src/pipeline/phases/call-site-extractors.d.ts.map +1 -0
- package/dist/src/pipeline/phases/call-site-extractors.js +132 -0
- package/dist/src/pipeline/phases/call-site-extractors.js.map +1 -0
- package/dist/src/pipeline/phases/module-resolution.d.ts +10 -0
- package/dist/src/pipeline/phases/module-resolution.d.ts.map +1 -0
- package/dist/src/pipeline/phases/module-resolution.js +301 -0
- package/dist/src/pipeline/phases/module-resolution.js.map +1 -0
- package/dist/src/pipeline/phases/orm.js +1 -2
- package/dist/src/pipeline/phases/orm.js.map +1 -1
- package/dist/src/pipeline/phases/scope-resolution.d.ts +2 -25
- package/dist/src/pipeline/phases/scope-resolution.d.ts.map +1 -1
- package/dist/src/pipeline/phases/scope-resolution.js +18 -580
- package/dist/src/pipeline/phases/scope-resolution.js.map +1 -1
- package/dist/src/pipeline/phases/wildcard-phase.d.ts.map +1 -1
- package/dist/src/pipeline/phases/wildcard-phase.js +6 -1
- package/dist/src/pipeline/phases/wildcard-phase.js.map +1 -1
- package/dist/src/search/hybrid-query.js +1 -1
- package/dist/src/search/hybrid-query.js.map +1 -1
- package/dist/src/storage/fts-store.d.ts.map +1 -1
- package/dist/src/storage/fts-store.js +7 -4
- package/dist/src/storage/fts-store.js.map +1 -1
- package/dist/src/watch/watcher.d.ts +2 -0
- package/dist/src/watch/watcher.d.ts.map +1 -1
- package/dist/src/watch/watcher.js +23 -1
- package/dist/src/watch/watcher.js.map +1 -1
- package/dist/src/web/api.d.ts.map +1 -1
- package/dist/src/web/api.js +17 -14
- package/dist/src/web/api.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/__tests__/pipeline/phases/wildcard-phase.test.ts +39 -4
- package/src/analysis/cycles.ts +7 -3
- package/src/analysis/trace.ts +1 -21
- package/src/analysis/worker-pool.ts +8 -7
- package/src/cli/skill-gen.ts +6 -14
- package/src/export/html.ts +3 -2
- package/src/graph/regex-search.ts +9 -5
- package/src/pipeline/phases/call-site-extractors.ts +169 -0
- package/src/pipeline/phases/module-resolution.ts +294 -0
- package/src/pipeline/phases/orm.ts +1 -2
- package/src/pipeline/phases/scope-resolution.ts +25 -618
- package/src/pipeline/phases/wildcard-phase.ts +5 -1
- package/src/search/hybrid-query.ts +1 -1
- package/src/storage/fts-store.ts +7 -3
- package/src/watch/watcher.ts +23 -1
- package/src/web/api.ts +17 -13
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import { readFileSync
|
|
2
|
-
import { join, extname
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
3
|
import type { PipelinePhase, PipelineContext } from '../types.js';
|
|
4
4
|
import type { MonographNode } from '../../types.js';
|
|
5
5
|
import { makeId } from '../../types.js';
|
|
6
6
|
import type { ParseOutput } from './parse.js';
|
|
7
|
+
import type { CallSite } from './call-site-extractors.js';
|
|
8
|
+
import { TS_JS_EXTS, CJS_MJS_EXTS, isSupportedExt, extractCallSites, extractConstructorAssignments } from './call-site-extractors.js';
|
|
9
|
+
import { buildWorkspacePackageMap, resolveModuleSpecifier, extractImportNames, buildAllImportMapsFromSource, REEXPORT_RE } from './module-resolution.js';
|
|
10
|
+
|
|
11
|
+
// Re-export for external consumers
|
|
12
|
+
export { clearWorkspacePackageMapCache, buildWorkspacePackageMap, resolveModuleSpecifier } from './module-resolution.js';
|
|
13
|
+
export { extractGoCallSites, extractJavaCallSites, extractRustCallSites } from './call-site-extractors.js';
|
|
7
14
|
|
|
8
15
|
// ── Output ────────────────────────────────────────────────────────────────────
|
|
9
16
|
|
|
@@ -16,581 +23,8 @@ export interface ScopeResolutionOutput {
|
|
|
16
23
|
importsReconstructed: number;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
|
-
// ──
|
|
20
|
-
|
|
21
|
-
interface CallSite {
|
|
22
|
-
callerFileNodeId: string;
|
|
23
|
-
callerFilePath: string;
|
|
24
|
-
calleeRaw: string;
|
|
25
|
-
form: 'method' | 'direct' | 'dynamic';
|
|
26
|
-
receiverName?: string;
|
|
27
|
-
methodName?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// ── JS/TS keywords to skip for direct calls ───────────────────────────────────
|
|
31
|
-
|
|
32
|
-
const JS_KEYWORDS = new Set([
|
|
33
|
-
'if', 'for', 'while', 'switch', 'return', 'function', 'class', 'new', 'typeof',
|
|
34
|
-
'await', 'catch', 'throw', 'delete', 'void', 'instanceof', 'in', 'of',
|
|
35
|
-
'import', 'export', 'yield', 'async', 'super', 'this', 'const', 'let', 'var',
|
|
36
|
-
'try', 'finally', 'break', 'continue', 'debugger', 'with', 'do',
|
|
37
|
-
]);
|
|
38
|
-
|
|
39
|
-
const PY_KEYWORDS = new Set([
|
|
40
|
-
'if', 'for', 'while', 'with', 'return', 'def', 'class', 'import', 'from',
|
|
41
|
-
'raise', 'yield', 'lambda', 'await', 'async', 'del', 'pass', 'assert',
|
|
42
|
-
'except', 'elif', 'else', 'not', 'and', 'or', 'in', 'is', 'print',
|
|
43
|
-
]);
|
|
44
|
-
|
|
45
|
-
const GO_KEYWORDS = new Set([
|
|
46
|
-
'if', 'for', 'range', 'return', 'func', 'type', 'var', 'const', 'import', 'package',
|
|
47
|
-
'go', 'defer', 'select', 'case', 'default', 'break', 'continue', 'goto', 'fallthrough',
|
|
48
|
-
'chan', 'map', 'struct', 'interface', 'make', 'new', 'len', 'cap', 'append', 'delete',
|
|
49
|
-
'panic', 'recover', 'close', 'switch', 'else',
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
|
-
const JAVA_KEYWORDS = new Set([
|
|
53
|
-
'if', 'for', 'while', 'do', 'switch', 'case', 'return', 'class', 'interface', 'enum',
|
|
54
|
-
'new', 'extends', 'implements', 'import', 'package', 'throw', 'throws', 'catch', 'try',
|
|
55
|
-
'finally', 'static', 'final', 'abstract', 'public', 'private', 'protected', 'void',
|
|
56
|
-
'break', 'continue', 'default', 'else', 'instanceof', 'this', 'super',
|
|
57
|
-
]);
|
|
58
|
-
|
|
59
|
-
const RUST_KEYWORDS = new Set([
|
|
60
|
-
'if', 'let', 'for', 'while', 'loop', 'match', 'return', 'fn', 'struct', 'enum', 'trait',
|
|
61
|
-
'impl', 'use', 'mod', 'pub', 'super', 'self', 'type', 'where', 'in', 'as', 'mut',
|
|
62
|
-
'ref', 'move', 'async', 'await', 'dyn', 'extern', 'crate', 'static', 'const', 'unsafe',
|
|
63
|
-
'break', 'continue', 'else',
|
|
64
|
-
]);
|
|
65
|
-
|
|
66
|
-
// ── Supported extensions ──────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
const TS_JS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
69
|
-
const PY_EXTS = new Set(['.py']);
|
|
70
|
-
const GO_EXTS = new Set(['.go']);
|
|
71
|
-
const JAVA_EXTS = new Set(['.java']);
|
|
72
|
-
const RUST_EXTS = new Set(['.rs']);
|
|
73
|
-
|
|
74
|
-
const CJS_MJS_EXTS = new Set(['.cjs', '.mjs']);
|
|
75
|
-
|
|
76
|
-
function isSupportedExt(ext: string): boolean {
|
|
77
|
-
return TS_JS_EXTS.has(ext) || CJS_MJS_EXTS.has(ext) || PY_EXTS.has(ext) || GO_EXTS.has(ext) || JAVA_EXTS.has(ext) || RUST_EXTS.has(ext);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ── Language-specific extractors ──────────────────────────────────────────────
|
|
81
|
-
|
|
82
|
-
export function extractGoCallSites(source: string, filePath: string, fileNodeId: string): CallSite[] {
|
|
83
|
-
const sites: CallSite[] = [];
|
|
84
|
-
const methodPattern = /(\w+)\.(\w+)\s*\(/g;
|
|
85
|
-
let m: RegExpExecArray | null;
|
|
86
|
-
while ((m = methodPattern.exec(source)) !== null) {
|
|
87
|
-
sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: `${m[1]}.${m[2]}`, form: 'method', receiverName: m[1], methodName: m[2] });
|
|
88
|
-
}
|
|
89
|
-
const directPattern = /(?<![.[\w])([A-Za-z_][\w]*)\s*\(/g;
|
|
90
|
-
while ((m = directPattern.exec(source)) !== null) {
|
|
91
|
-
const name = m[1]!;
|
|
92
|
-
if (GO_KEYWORDS.has(name)) continue;
|
|
93
|
-
sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: name, form: 'direct' });
|
|
94
|
-
}
|
|
95
|
-
return sites;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function extractJavaCallSites(source: string, filePath: string, fileNodeId: string): CallSite[] {
|
|
99
|
-
const sites: CallSite[] = [];
|
|
100
|
-
const methodPattern = /(\w+)\.(\w+)\s*\(/g;
|
|
101
|
-
let m: RegExpExecArray | null;
|
|
102
|
-
while ((m = methodPattern.exec(source)) !== null) {
|
|
103
|
-
sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: `${m[1]}.${m[2]}`, form: 'method', receiverName: m[1], methodName: m[2] });
|
|
104
|
-
}
|
|
105
|
-
const directPattern = /(?<![.[\w])([A-Za-z_$][\w$]*)\s*\(/g;
|
|
106
|
-
while ((m = directPattern.exec(source)) !== null) {
|
|
107
|
-
const name = m[1]!;
|
|
108
|
-
if (JAVA_KEYWORDS.has(name)) continue;
|
|
109
|
-
sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: name, form: 'direct' });
|
|
110
|
-
}
|
|
111
|
-
return sites;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function extractRustCallSites(source: string, filePath: string, fileNodeId: string): CallSite[] {
|
|
115
|
-
const sites: CallSite[] = [];
|
|
116
|
-
const methodPattern = /(\w+)\.(\w+)\s*\(/g;
|
|
117
|
-
let m: RegExpExecArray | null;
|
|
118
|
-
while ((m = methodPattern.exec(source)) !== null) {
|
|
119
|
-
sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: `${m[1]}.${m[2]}`, form: 'method', receiverName: m[1], methodName: m[2] });
|
|
120
|
-
}
|
|
121
|
-
const directPattern = /(?<![.[\w])([A-Za-z_][\w]*)\s*\(/g;
|
|
122
|
-
while ((m = directPattern.exec(source)) !== null) {
|
|
123
|
-
const name = m[1]!;
|
|
124
|
-
if (RUST_KEYWORDS.has(name)) continue;
|
|
125
|
-
sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: name, form: 'direct' });
|
|
126
|
-
}
|
|
127
|
-
return sites;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// ── Stage 0.5: Track constructor assignments for type-aware method resolution ─
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Scan source for `const/let/var x = new ClassName(...)` patterns.
|
|
134
|
-
* Returns a map: localVarName → ClassName (e.g. "svc" → "MyService").
|
|
135
|
-
*/
|
|
136
|
-
function extractConstructorAssignments(source: string): Map<string, string> {
|
|
137
|
-
const result = new Map<string, string>();
|
|
138
|
-
const pattern = /(?:const|let|var)\s+(\w+)\s*=\s*new\s+([A-Z][\w$]*)\s*(?:<[^>]*>\s*)?\(/g;
|
|
139
|
-
let m: RegExpExecArray | null;
|
|
140
|
-
while ((m = pattern.exec(source)) !== null) {
|
|
141
|
-
result.set(m[1], m[2]);
|
|
142
|
-
}
|
|
143
|
-
return result;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// ── Stage 1 + 2: Extract and classify call sites ──────────────────────────────
|
|
147
|
-
|
|
148
|
-
function extractCallSites(
|
|
149
|
-
source: string,
|
|
150
|
-
filePath: string,
|
|
151
|
-
fileNodeId: string,
|
|
152
|
-
ext: string,
|
|
153
|
-
): CallSite[] {
|
|
154
|
-
const sites: CallSite[] = [];
|
|
155
|
-
|
|
156
|
-
if (TS_JS_EXTS.has(ext) || CJS_MJS_EXTS.has(ext)) {
|
|
157
|
-
// Method calls: word.word( or word.word<T>(
|
|
158
|
-
const methodPattern = /(\w+)\.(\w+)\s*(?:<[^>]*>\s*)?\(/g;
|
|
159
|
-
let m: RegExpExecArray | null;
|
|
160
|
-
while ((m = methodPattern.exec(source)) !== null) {
|
|
161
|
-
sites.push({
|
|
162
|
-
callerFileNodeId: fileNodeId,
|
|
163
|
-
callerFilePath: filePath,
|
|
164
|
-
calleeRaw: `${m[1]}.${m[2]}`,
|
|
165
|
-
form: 'method',
|
|
166
|
-
receiverName: m[1],
|
|
167
|
-
methodName: m[2],
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Chained method calls: ).method( or ].method( — receiver unknown, resolve by method name
|
|
172
|
-
const chainedPattern = /[)\]]\s*\.(\w+)\s*(?:<[^>]*>\s*)?\(/g;
|
|
173
|
-
while ((m = chainedPattern.exec(source)) !== null) {
|
|
174
|
-
const name = m[1];
|
|
175
|
-
if (JS_KEYWORDS.has(name)) continue;
|
|
176
|
-
sites.push({
|
|
177
|
-
callerFileNodeId: fileNodeId,
|
|
178
|
-
callerFilePath: filePath,
|
|
179
|
-
calleeRaw: `?.${name}`,
|
|
180
|
-
form: 'method',
|
|
181
|
-
methodName: name,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Direct calls: word( or word<T>( — not preceded by . or word char
|
|
186
|
-
const directPattern = /(?<![.[\w])([A-Za-z_$][\w$]*)\s*(?:<[^>]*>\s*)?\(/g;
|
|
187
|
-
while ((m = directPattern.exec(source)) !== null) {
|
|
188
|
-
const name = m[1];
|
|
189
|
-
if (JS_KEYWORDS.has(name)) continue;
|
|
190
|
-
sites.push({
|
|
191
|
-
callerFileNodeId: fileNodeId,
|
|
192
|
-
callerFilePath: filePath,
|
|
193
|
-
calleeRaw: name,
|
|
194
|
-
form: 'direct',
|
|
195
|
-
methodName: name,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Constructor calls: new ClassName( or new ClassName<T>(
|
|
200
|
-
const newPattern = /\bnew\s+([A-Z][\w$]*)\s*(?:<[^>]*>\s*)?\(/g;
|
|
201
|
-
while ((m = newPattern.exec(source)) !== null) {
|
|
202
|
-
sites.push({
|
|
203
|
-
callerFileNodeId: fileNodeId,
|
|
204
|
-
callerFilePath: filePath,
|
|
205
|
-
calleeRaw: `new ${m[1]}`,
|
|
206
|
-
form: 'direct',
|
|
207
|
-
methodName: m[1],
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Dynamic calls: obj[key]( → mark as dynamic
|
|
212
|
-
const dynamicPattern = /\w+\s*\[[\w'"` ]+\]\s*\(/g;
|
|
213
|
-
while ((m = dynamicPattern.exec(source)) !== null) {
|
|
214
|
-
sites.push({
|
|
215
|
-
callerFileNodeId: fileNodeId,
|
|
216
|
-
callerFilePath: filePath,
|
|
217
|
-
calleeRaw: m[0],
|
|
218
|
-
form: 'dynamic',
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
} else if (PY_EXTS.has(ext)) {
|
|
222
|
-
// Method calls: self.method( or obj.method(
|
|
223
|
-
const methodPattern = /(\w+)\.(\w+)\s*\(/g;
|
|
224
|
-
let m: RegExpExecArray | null;
|
|
225
|
-
while ((m = methodPattern.exec(source)) !== null) {
|
|
226
|
-
sites.push({
|
|
227
|
-
callerFileNodeId: fileNodeId,
|
|
228
|
-
callerFilePath: filePath,
|
|
229
|
-
calleeRaw: `${m[1]}.${m[2]}`,
|
|
230
|
-
form: 'method',
|
|
231
|
-
receiverName: m[1],
|
|
232
|
-
methodName: m[2],
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Direct calls: word( not preceded by . or word char
|
|
237
|
-
const directPattern = /(?<![.[\w])([A-Za-z_][\w]*)\s*\(/g;
|
|
238
|
-
while ((m = directPattern.exec(source)) !== null) {
|
|
239
|
-
const name = m[1];
|
|
240
|
-
if (PY_KEYWORDS.has(name)) continue;
|
|
241
|
-
sites.push({
|
|
242
|
-
callerFileNodeId: fileNodeId,
|
|
243
|
-
callerFilePath: filePath,
|
|
244
|
-
calleeRaw: name,
|
|
245
|
-
form: 'direct',
|
|
246
|
-
methodName: name,
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
} else if (GO_EXTS.has(ext)) {
|
|
250
|
-
return extractGoCallSites(source, filePath, fileNodeId);
|
|
251
|
-
} else if (JAVA_EXTS.has(ext)) {
|
|
252
|
-
return extractJavaCallSites(source, filePath, fileNodeId);
|
|
253
|
-
} else if (RUST_EXTS.has(ext)) {
|
|
254
|
-
return extractRustCallSites(source, filePath, fileNodeId);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return sites;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ── Stage 3: Build import maps by parsing source imports ─────────────────────
|
|
261
|
-
|
|
262
|
-
const IMPORT_RE = /import\s+(?:type\s+)?(?:\{([^}]+)\}|(\w+)|\*\s+as\s+(\w+))(?:\s*,\s*(?:\{([^}]+)\}|(\w+)|\*\s+as\s+(\w+)))?\s+from\s+['"]([^'"]+)['"]/g;
|
|
263
|
-
|
|
264
|
-
// CJS require: const x = require('y') or const { a, b } = require('y')
|
|
265
|
-
const REQUIRE_RE = /(?:const|let|var)\s+(?:\{([^}]+)\}|(\w+))\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
266
|
-
|
|
267
|
-
// Python: from x import y, z OR import x
|
|
268
|
-
const PY_FROM_IMPORT_RE = /from\s+([\w.]+)\s+import\s+(.+)/g;
|
|
269
|
-
const PY_IMPORT_RE = /^import\s+([\w.]+)(?:\s+as\s+(\w+))?/gm;
|
|
270
|
-
|
|
271
|
-
// Go: import "path" or import ( "path" )
|
|
272
|
-
const GO_IMPORT_RE = /import\s+(?:"([^"]+)"|(?:\w+\s+)?"([^"]+)"|\(\s*([\s\S]*?)\s*\))/g;
|
|
273
|
-
|
|
274
|
-
// Java: import com.foo.Bar;
|
|
275
|
-
const JAVA_IMPORT_RE = /import\s+(?:static\s+)?([\w.]+)\s*;/g;
|
|
276
|
-
|
|
277
|
-
// Rust: use crate::module::Item; or use super::foo;
|
|
278
|
-
const RUST_USE_RE = /use\s+((?:crate|super|self)(?:::\w+)+)(?:::\{([^}]+)\})?;/g;
|
|
279
|
-
|
|
280
|
-
// TS/JS re-exports: export { foo, bar } from './module'
|
|
281
|
-
const REEXPORT_RE = /export\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
|
|
282
|
-
|
|
283
|
-
const RESOLVE_EXTS = ['.ts', '.tsx', '.js', '.jsx'];
|
|
284
|
-
const PY_RESOLVE_EXTS = ['.py'];
|
|
285
|
-
const GO_RESOLVE_EXTS = ['.go'];
|
|
286
|
-
const JAVA_RESOLVE_EXTS = ['.java'];
|
|
287
|
-
const RUST_RESOLVE_EXTS = ['.rs'];
|
|
288
|
-
|
|
289
|
-
const workspacePackageMapCache = new Map<string, Map<string, string>>();
|
|
290
|
-
|
|
291
|
-
/** Invalidate the cached workspace package map for a repo — call at the start
|
|
292
|
-
* of each build so long-lived processes (watch mode) pick up package.json
|
|
293
|
-
* additions/removals instead of serving a stale map from a prior build. */
|
|
294
|
-
export function clearWorkspacePackageMapCache(repoPath: string): void {
|
|
295
|
-
workspacePackageMapCache.delete(repoPath);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Build package-name → directory map from workspace package.json files.
|
|
300
|
-
* Scans packages/ for package.json and maps npm name to its relative src path.
|
|
301
|
-
* Cached per repoPath — multiple pipeline phases (cross-file, scope-resolution,
|
|
302
|
-
* the latter's own re-export loop) call this per-file/per-edge within the same
|
|
303
|
-
* build, and the workspace's package.json set doesn't change mid-build.
|
|
304
|
-
*/
|
|
305
|
-
export function buildWorkspacePackageMap(repoPath: string): Map<string, string> {
|
|
306
|
-
const cached = workspacePackageMapCache.get(repoPath);
|
|
307
|
-
if (cached) return cached;
|
|
308
|
-
const result = new Map<string, string>();
|
|
309
|
-
const packagesDir = join(repoPath, 'packages');
|
|
310
|
-
try {
|
|
311
|
-
const scanDirs = (base: string, depth: number) => {
|
|
312
|
-
if (depth > 2) return;
|
|
313
|
-
let entries: string[];
|
|
314
|
-
try { entries = require('fs').readdirSync(base); } catch { return; }
|
|
315
|
-
for (const e of entries) {
|
|
316
|
-
const full = join(base, e);
|
|
317
|
-
const pkgJson = join(full, 'package.json');
|
|
318
|
-
try {
|
|
319
|
-
const pkg = JSON.parse(readFileSync(pkgJson, 'utf-8'));
|
|
320
|
-
if (pkg.name) {
|
|
321
|
-
const relDir = full.slice(repoPath.length + 1); // e.g. packages/@monomind/cli
|
|
322
|
-
result.set(pkg.name, relDir);
|
|
323
|
-
}
|
|
324
|
-
} catch {
|
|
325
|
-
// No package.json — recurse for @scoped dirs
|
|
326
|
-
if (e.startsWith('@')) scanDirs(full, depth + 1);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
scanDirs(packagesDir, 0);
|
|
331
|
-
} catch { /* no packages dir */ }
|
|
332
|
-
workspacePackageMapCache.set(repoPath, result);
|
|
333
|
-
return result;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
export function resolveModuleSpecifier(
|
|
337
|
-
importerPath: string,
|
|
338
|
-
specifier: string,
|
|
339
|
-
repoPath: string,
|
|
340
|
-
knownFiles: Set<string>,
|
|
341
|
-
workspaceMap: Map<string, string>,
|
|
342
|
-
): string | null {
|
|
343
|
-
if (specifier.startsWith('.')) {
|
|
344
|
-
const dir = dirname(importerPath);
|
|
345
|
-
const raw = resolvePath('/', dir, specifier).slice(1);
|
|
346
|
-
// Strip .js/.jsx — TS source uses .js extensions but files are .ts
|
|
347
|
-
const base = raw.replace(/\.(js|jsx)$/, '');
|
|
348
|
-
|
|
349
|
-
for (const candidate of [
|
|
350
|
-
raw,
|
|
351
|
-
base,
|
|
352
|
-
...RESOLVE_EXTS.map(e => base + e),
|
|
353
|
-
...RESOLVE_EXTS.map(e => base + '/index' + e),
|
|
354
|
-
]) {
|
|
355
|
-
if (knownFiles.has(candidate)) return candidate;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
for (const ext of RESOLVE_EXTS) {
|
|
359
|
-
if (existsSync(join(repoPath, base + ext))) return base + ext;
|
|
360
|
-
}
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Workspace package specifier: @monoes/hooks → packages/@monomind/hooks/src/index.ts
|
|
365
|
-
// Also handles subpath: @monoes/hooks/src/foo → packages/@monomind/hooks/src/foo.ts
|
|
366
|
-
for (const [pkgName, pkgDir] of workspaceMap) {
|
|
367
|
-
if (specifier === pkgName) {
|
|
368
|
-
// Bare import: resolve to package entry point (src/index.ts)
|
|
369
|
-
for (const entry of RESOLVE_EXTS.map(e => pkgDir + '/src/index' + e)) {
|
|
370
|
-
if (knownFiles.has(entry)) return entry;
|
|
371
|
-
}
|
|
372
|
-
return null;
|
|
373
|
-
}
|
|
374
|
-
if (specifier.startsWith(pkgName + '/')) {
|
|
375
|
-
const subpath = specifier.slice(pkgName.length + 1);
|
|
376
|
-
const base = pkgDir + '/' + subpath;
|
|
377
|
-
for (const candidate of [
|
|
378
|
-
base,
|
|
379
|
-
...RESOLVE_EXTS.map(e => base + e),
|
|
380
|
-
...RESOLVE_EXTS.map(e => base + '/index' + e),
|
|
381
|
-
]) {
|
|
382
|
-
if (knownFiles.has(candidate)) return candidate;
|
|
383
|
-
}
|
|
384
|
-
return null;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
26
|
+
// ── Function index ───────────────────────────────────────────────────────────
|
|
387
27
|
|
|
388
|
-
return null;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function resolvePythonModule(importerPath: string, modulePath: string, knownFiles: Set<string>): string | null {
|
|
392
|
-
// Try relative to importer's directory, then absolute from repo root
|
|
393
|
-
const dir = dirname(importerPath);
|
|
394
|
-
for (const base of [dir + '/' + modulePath, modulePath]) {
|
|
395
|
-
for (const candidate of [base + '.py', base + '/__init__.py']) {
|
|
396
|
-
if (knownFiles.has(candidate)) return candidate;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
return null;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function resolveGoPackage(importerPath: string, goPath: string, knownFiles: Set<string>): string | null {
|
|
403
|
-
for (const f of knownFiles) {
|
|
404
|
-
if (f.startsWith(goPath + '/') && f.endsWith('.go')) return f;
|
|
405
|
-
}
|
|
406
|
-
return null;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function resolveJavaImport(qualifiedName: string, knownFiles: Set<string>): string | null {
|
|
410
|
-
// com.foo.Bar → com/foo/Bar.java or src/main/java/com/foo/Bar.java
|
|
411
|
-
const pathPart = qualifiedName.replace(/\./g, '/');
|
|
412
|
-
for (const candidate of [pathPart + '.java', 'src/main/java/' + pathPart + '.java', 'src/' + pathPart + '.java']) {
|
|
413
|
-
if (knownFiles.has(candidate)) return candidate;
|
|
414
|
-
}
|
|
415
|
-
// Wildcard: com.foo.* → find any .java file in com/foo/
|
|
416
|
-
if (pathPart.endsWith('/*')) {
|
|
417
|
-
const dir = pathPart.slice(0, -2);
|
|
418
|
-
for (const f of knownFiles) {
|
|
419
|
-
if (f.endsWith('.java') && (f.startsWith(dir + '/') || f.includes('/' + dir + '/'))) return f;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
return null;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
function resolveRustUse(usePath: string, importerPath: string, knownFiles: Set<string>): string | null {
|
|
426
|
-
// crate::module::item → src/module.rs or src/module/mod.rs
|
|
427
|
-
const parts = usePath.split('::');
|
|
428
|
-
if (parts[0] === 'crate') parts[0] = 'src';
|
|
429
|
-
else if (parts[0] === 'super') {
|
|
430
|
-
const dir = dirname(importerPath);
|
|
431
|
-
parts[0] = dirname(dir);
|
|
432
|
-
} else if (parts[0] === 'self') {
|
|
433
|
-
parts[0] = dirname(importerPath);
|
|
434
|
-
}
|
|
435
|
-
// Last segment is the item name — try with and without it
|
|
436
|
-
for (let len = parts.length; len >= 2; len--) {
|
|
437
|
-
const base = parts.slice(0, len).join('/');
|
|
438
|
-
for (const candidate of [base + '.rs', base + '/mod.rs']) {
|
|
439
|
-
if (knownFiles.has(candidate)) return candidate;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return null;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function extractImportNames(clause: string): string[] {
|
|
446
|
-
return clause
|
|
447
|
-
.split(',')
|
|
448
|
-
.map(s => s.trim().split(/\s+as\s+/).pop()!.trim())
|
|
449
|
-
.filter(Boolean);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Parse each file's import statements to build importedName → resolvedFilePath maps.
|
|
454
|
-
* Replaces the broken DB-edge approach (IMPORTS edges target Variable nodes, not Files).
|
|
455
|
-
*/
|
|
456
|
-
function buildAllImportMapsFromSource(
|
|
457
|
-
repoPath: string,
|
|
458
|
-
fileNodesByPath: Map<string, MonographNode>,
|
|
459
|
-
fileContents?: Map<string, string>,
|
|
460
|
-
): Map<string, Map<string, string>> {
|
|
461
|
-
const result = new Map<string, Map<string, string>>();
|
|
462
|
-
const knownFiles = new Set(fileNodesByPath.keys());
|
|
463
|
-
const workspaceMap = buildWorkspacePackageMap(repoPath);
|
|
464
|
-
|
|
465
|
-
for (const [filePath, fileNode] of fileNodesByPath) {
|
|
466
|
-
const ext = extname(filePath).toLowerCase();
|
|
467
|
-
const importMap = new Map<string, string>();
|
|
468
|
-
|
|
469
|
-
let source: string | undefined = fileContents?.get(filePath);
|
|
470
|
-
if (!source) {
|
|
471
|
-
try {
|
|
472
|
-
source = readFileSync(join(repoPath, filePath), 'utf-8');
|
|
473
|
-
} catch {
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (TS_JS_EXTS.has(ext) || ext === '.cjs' || ext === '.mjs') {
|
|
479
|
-
// ESM imports
|
|
480
|
-
IMPORT_RE.lastIndex = 0;
|
|
481
|
-
let m: RegExpExecArray | null;
|
|
482
|
-
while ((m = IMPORT_RE.exec(source)) !== null) {
|
|
483
|
-
const specifier = m[7];
|
|
484
|
-
const resolved = resolveModuleSpecifier(filePath, specifier, repoPath, knownFiles, workspaceMap);
|
|
485
|
-
if (!resolved) continue;
|
|
486
|
-
|
|
487
|
-
if (m[1]) for (const n of extractImportNames(m[1])) importMap.set(n, resolved);
|
|
488
|
-
if (m[4]) for (const n of extractImportNames(m[4])) importMap.set(n, resolved);
|
|
489
|
-
if (m[2]) importMap.set(m[2], resolved);
|
|
490
|
-
if (m[5]) importMap.set(m[5], resolved);
|
|
491
|
-
if (m[3]) importMap.set(m[3], resolved);
|
|
492
|
-
if (m[6]) importMap.set(m[6], resolved);
|
|
493
|
-
const baseName = resolved.split('/').pop()!.replace(/\.\w+$/, '');
|
|
494
|
-
importMap.set(baseName, resolved);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// CJS require()
|
|
498
|
-
REQUIRE_RE.lastIndex = 0;
|
|
499
|
-
while ((m = REQUIRE_RE.exec(source)) !== null) {
|
|
500
|
-
const specifier = m[3];
|
|
501
|
-
const resolved = resolveModuleSpecifier(filePath, specifier, repoPath, knownFiles, workspaceMap);
|
|
502
|
-
if (!resolved) continue;
|
|
503
|
-
if (m[1]) for (const n of extractImportNames(m[1])) importMap.set(n, resolved);
|
|
504
|
-
if (m[2]) importMap.set(m[2], resolved);
|
|
505
|
-
const baseName = resolved.split('/').pop()!.replace(/\.\w+$/, '');
|
|
506
|
-
importMap.set(baseName, resolved);
|
|
507
|
-
}
|
|
508
|
-
} else if (PY_EXTS.has(ext)) {
|
|
509
|
-
// Python: from module import name
|
|
510
|
-
PY_FROM_IMPORT_RE.lastIndex = 0;
|
|
511
|
-
let m: RegExpExecArray | null;
|
|
512
|
-
while ((m = PY_FROM_IMPORT_RE.exec(source)) !== null) {
|
|
513
|
-
const modulePath = m[1].replace(/\./g, '/');
|
|
514
|
-
const resolved = resolvePythonModule(filePath, modulePath, knownFiles);
|
|
515
|
-
if (!resolved) continue;
|
|
516
|
-
for (const name of m[2].split(',').map(s => s.trim().split(/\s+as\s+/).pop()!.trim()).filter(Boolean)) {
|
|
517
|
-
importMap.set(name, resolved);
|
|
518
|
-
}
|
|
519
|
-
const baseName = resolved.split('/').pop()!.replace(/\.\w+$/, '');
|
|
520
|
-
importMap.set(baseName, resolved);
|
|
521
|
-
}
|
|
522
|
-
// Python: import module
|
|
523
|
-
PY_IMPORT_RE.lastIndex = 0;
|
|
524
|
-
while ((m = PY_IMPORT_RE.exec(source)) !== null) {
|
|
525
|
-
const modulePath = m[1].replace(/\./g, '/');
|
|
526
|
-
const resolved = resolvePythonModule(filePath, modulePath, knownFiles);
|
|
527
|
-
if (!resolved) continue;
|
|
528
|
-
const alias = m[2] ?? m[1].split('.').pop()!;
|
|
529
|
-
importMap.set(alias, resolved);
|
|
530
|
-
}
|
|
531
|
-
} else if (GO_EXTS.has(ext)) {
|
|
532
|
-
GO_IMPORT_RE.lastIndex = 0;
|
|
533
|
-
let m: RegExpExecArray | null;
|
|
534
|
-
while ((m = GO_IMPORT_RE.exec(source)) !== null) {
|
|
535
|
-
const paths = m[3]
|
|
536
|
-
? m[3].match(/"([^"]+)"/g)?.map(s => s.slice(1, -1)) ?? []
|
|
537
|
-
: [m[1] ?? m[2]].filter(Boolean);
|
|
538
|
-
for (const goPath of paths) {
|
|
539
|
-
const resolved = resolveGoPackage(filePath, goPath, knownFiles);
|
|
540
|
-
if (!resolved) continue;
|
|
541
|
-
const pkgName = goPath.split('/').pop()!;
|
|
542
|
-
importMap.set(pkgName, resolved);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
} else if (JAVA_EXTS.has(ext)) {
|
|
546
|
-
JAVA_IMPORT_RE.lastIndex = 0;
|
|
547
|
-
let m: RegExpExecArray | null;
|
|
548
|
-
while ((m = JAVA_IMPORT_RE.exec(source)) !== null) {
|
|
549
|
-
const resolved = resolveJavaImport(m[1], knownFiles);
|
|
550
|
-
if (!resolved) continue;
|
|
551
|
-
const className = m[1].split('.').pop()!;
|
|
552
|
-
importMap.set(className, resolved);
|
|
553
|
-
}
|
|
554
|
-
} else if (RUST_EXTS.has(ext)) {
|
|
555
|
-
RUST_USE_RE.lastIndex = 0;
|
|
556
|
-
let m: RegExpExecArray | null;
|
|
557
|
-
while ((m = RUST_USE_RE.exec(source)) !== null) {
|
|
558
|
-
if (m[2]) {
|
|
559
|
-
// use crate::module::{Item1, Item2}
|
|
560
|
-
for (const name of m[2].split(',').map(s => s.trim()).filter(Boolean)) {
|
|
561
|
-
const resolved = resolveRustUse(m[1] + '::' + name, filePath, knownFiles);
|
|
562
|
-
if (resolved) importMap.set(name.split('::').pop()!, resolved);
|
|
563
|
-
}
|
|
564
|
-
} else {
|
|
565
|
-
const resolved = resolveRustUse(m[1], filePath, knownFiles);
|
|
566
|
-
if (!resolved) continue;
|
|
567
|
-
const itemName = m[1].split('::').pop()!;
|
|
568
|
-
importMap.set(itemName, resolved);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if (importMap.size > 0) {
|
|
574
|
-
result.set(fileNode.id, importMap);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
return result;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function getImportMap(
|
|
582
|
-
allImportMaps: Map<string, Map<string, string>>,
|
|
583
|
-
fileNodeId: string,
|
|
584
|
-
): Map<string, string> {
|
|
585
|
-
return allImportMaps.get(fileNodeId) ?? new Map();
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// ── Stage 4 helpers: preloaded function/method index ──────────────────────────
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Load all callable nodes once (Function, Method, Constructor, Class).
|
|
592
|
-
* Class is included so `new ClassName()` can resolve to Class nodes.
|
|
593
|
-
*/
|
|
594
28
|
function buildFunctionIndex(ctx: PipelineContext): {
|
|
595
29
|
byFilePath: Map<string, Map<string, string[]>>;
|
|
596
30
|
nameCounts: Map<string, number>;
|
|
@@ -605,7 +39,6 @@ function buildFunctionIndex(ctx: PipelineContext): {
|
|
|
605
39
|
.all() as { id: string; name: string; file_path: string }[];
|
|
606
40
|
|
|
607
41
|
for (const row of rows) {
|
|
608
|
-
// byFilePath index
|
|
609
42
|
let fileMap = byFilePath.get(row.file_path);
|
|
610
43
|
if (!fileMap) {
|
|
611
44
|
fileMap = new Map();
|
|
@@ -617,23 +50,19 @@ function buildFunctionIndex(ctx: PipelineContext): {
|
|
|
617
50
|
fileMap.set(row.name, ids);
|
|
618
51
|
}
|
|
619
52
|
ids.push(row.id);
|
|
620
|
-
|
|
621
|
-
// global count for ambiguity detection
|
|
622
53
|
nameCounts.set(row.name, (nameCounts.get(row.name) ?? 0) + 1);
|
|
623
54
|
}
|
|
624
55
|
|
|
625
56
|
return { byFilePath, nameCounts };
|
|
626
57
|
}
|
|
627
58
|
|
|
628
|
-
// ──
|
|
59
|
+
// ── Target resolution ────────────────────────────────────────────────────────
|
|
629
60
|
|
|
630
61
|
function pickBestId(ids: string[], site: CallSite): string | null {
|
|
631
62
|
if (ids.length === 1) return ids[0];
|
|
632
63
|
if (ids.length === 0) return null;
|
|
633
|
-
// Disambiguate: prefer Function for direct calls, Method for method calls
|
|
634
64
|
const suffix = site.form === 'method' ? '_method' : '_function';
|
|
635
65
|
const match = ids.find(id => id.endsWith(suffix));
|
|
636
|
-
// For `new ClassName()` prefer _class
|
|
637
66
|
if (!match && site.calleeRaw.startsWith('new ')) {
|
|
638
67
|
const classMatch = ids.find(id => id.endsWith('_class'));
|
|
639
68
|
if (classMatch) return classMatch;
|
|
@@ -698,7 +127,7 @@ function resolveTarget(
|
|
|
698
127
|
return null;
|
|
699
128
|
}
|
|
700
129
|
|
|
701
|
-
// ──
|
|
130
|
+
// ── Edge emission ────────────────────────────────────────────────────────────
|
|
702
131
|
|
|
703
132
|
interface PreparedEdgeStmts {
|
|
704
133
|
selectExisting: import('better-sqlite3').Statement;
|
|
@@ -750,6 +179,13 @@ function emitEdge(
|
|
|
750
179
|
|
|
751
180
|
// ── Phase definition ──────────────────────────────────────────────────────────
|
|
752
181
|
|
|
182
|
+
function getImportMap(
|
|
183
|
+
allImportMaps: Map<string, Map<string, string>>,
|
|
184
|
+
fileNodeId: string,
|
|
185
|
+
): Map<string, string> {
|
|
186
|
+
return allImportMaps.get(fileNodeId) ?? new Map();
|
|
187
|
+
}
|
|
188
|
+
|
|
753
189
|
export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
|
|
754
190
|
name: 'scope-resolution',
|
|
755
191
|
deps: ['parse', 'cross-file'],
|
|
@@ -757,15 +193,12 @@ export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
|
|
|
757
193
|
if (ctx.allFilesCached) {
|
|
758
194
|
return { resolvedEdges: 0, skippedDynamic: 0, ambiguous: 0, reexportEdges: 0, orphanImportsRemoved: 0, importsReconstructed: 0 };
|
|
759
195
|
}
|
|
760
|
-
const { symbolNodes } = deps.get('parse') as ParseOutput;
|
|
196
|
+
const { symbolNodes, fileContents } = deps.get('parse') as ParseOutput;
|
|
761
197
|
|
|
762
198
|
let resolvedEdges = 0;
|
|
763
199
|
let skippedDynamic = 0;
|
|
764
200
|
let ambiguous = 0;
|
|
765
201
|
|
|
766
|
-
// ── Batch preload: one query per table instead of one per file/call-site ──
|
|
767
|
-
|
|
768
|
-
// Build a map from file_path → file node id using the parse output
|
|
769
202
|
const fileNodesByPath = new Map<string, MonographNode>();
|
|
770
203
|
for (const node of symbolNodes) {
|
|
771
204
|
if (node.label === 'File' && node.filePath) {
|
|
@@ -773,7 +206,6 @@ export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
|
|
|
773
206
|
}
|
|
774
207
|
}
|
|
775
208
|
|
|
776
|
-
// Also look up File nodes from the DB (includes nodes created by structure phase)
|
|
777
209
|
if (ctx.db) {
|
|
778
210
|
const dbFileNodes = ctx.db
|
|
779
211
|
.prepare(`SELECT id, file_path FROM nodes WHERE label = 'File'`)
|
|
@@ -792,11 +224,7 @@ export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
|
|
|
792
224
|
}
|
|
793
225
|
}
|
|
794
226
|
|
|
795
|
-
// Parse source imports → per-file import maps (replaces broken DB-edge approach)
|
|
796
|
-
const { fileContents } = deps.get('parse') as ParseOutput;
|
|
797
227
|
const allImportMaps = buildAllImportMapsFromSource(ctx.repoPath, fileNodesByPath, fileContents);
|
|
798
|
-
|
|
799
|
-
// Preload all Function/Method/Constructor nodes into indices (one DB query total)
|
|
800
228
|
const { byFilePath: fnIndex, nameCounts } = buildFunctionIndex(ctx);
|
|
801
229
|
|
|
802
230
|
const edgeStmts = ctx.db ? prepareEdgeStmts(ctx.db) : null;
|
|
@@ -805,7 +233,6 @@ export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
|
|
|
805
233
|
const ext = extname(filePath).toLowerCase();
|
|
806
234
|
if (!isSupportedExt(ext)) continue;
|
|
807
235
|
|
|
808
|
-
// Read file source (prefer cached from parse phase)
|
|
809
236
|
let source: string | undefined = fileContents.get(filePath);
|
|
810
237
|
if (!source) {
|
|
811
238
|
try {
|
|
@@ -815,16 +242,9 @@ export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
|
|
|
815
242
|
}
|
|
816
243
|
}
|
|
817
244
|
|
|
818
|
-
// Stage 1+2: Extract call sites
|
|
819
245
|
const callSites = extractCallSites(source, filePath, fileNode.id, ext);
|
|
820
|
-
|
|
821
|
-
// Stage 3: Get pre-built import map for this file (O(1))
|
|
822
246
|
const importMap = getImportMap(allImportMaps, fileNode.id);
|
|
823
|
-
|
|
824
|
-
// Track constructor assignments for type-aware method resolution
|
|
825
247
|
const ctorMap = (TS_JS_EXTS.has(ext) || CJS_MJS_EXTS.has(ext)) ? extractConstructorAssignments(source) : undefined;
|
|
826
|
-
|
|
827
|
-
// Precompute once per file instead of per call site
|
|
828
248
|
const importedFiles = [...new Set(importMap.values())];
|
|
829
249
|
|
|
830
250
|
for (const site of callSites) {
|
|
@@ -833,19 +253,13 @@ export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
|
|
|
833
253
|
continue;
|
|
834
254
|
}
|
|
835
255
|
|
|
836
|
-
// Stage 4+5: Resolve target using preloaded indices (no DB query)
|
|
837
256
|
const resolved = resolveTarget(site, filePath, importMap, fnIndex, ctorMap, importedFiles);
|
|
257
|
+
if (!resolved) continue;
|
|
838
258
|
|
|
839
|
-
if (!resolved) {
|
|
840
|
-
continue;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// Ambiguity check using preloaded name counts (no DB query)
|
|
844
259
|
if (site.methodName && (nameCounts.get(site.methodName) ?? 0) > 1) {
|
|
845
260
|
ambiguous++;
|
|
846
261
|
}
|
|
847
262
|
|
|
848
|
-
// Stage 6: Emit edge (source is the file node of the caller)
|
|
849
263
|
const result = edgeStmts ? emitEdge(edgeStmts, site.callerFileNodeId, resolved.targetId) : 'skipped';
|
|
850
264
|
if (result === 'inserted' || result === 'upgraded') {
|
|
851
265
|
resolvedEdges++;
|
|
@@ -855,14 +269,14 @@ export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
|
|
|
855
269
|
});
|
|
856
270
|
resolveAllCalls?.();
|
|
857
271
|
|
|
858
|
-
// Scan for re-exports: export { foo, bar } from './module'
|
|
859
|
-
// Creates REFERENCES edges so re-exported symbols don't appear as dead code.
|
|
860
272
|
let reexportEdges = 0;
|
|
861
273
|
if (ctx.db) {
|
|
862
274
|
const insertRef = ctx.db.prepare(`
|
|
863
275
|
INSERT OR IGNORE INTO edges (id, source_id, target_id, relation, confidence, confidence_score)
|
|
864
276
|
VALUES (?, ?, ?, 'REFERENCES', 'EXTRACTED', 0.85)
|
|
865
277
|
`);
|
|
278
|
+
const reexportKnownFiles = new Set(fileNodesByPath.keys());
|
|
279
|
+
const reexportWorkspaceMap = buildWorkspacePackageMap(ctx.repoPath);
|
|
866
280
|
const insertReexports = ctx.db.transaction(() => {
|
|
867
281
|
for (const [filePath, fileNode] of fileNodesByPath) {
|
|
868
282
|
const ext = extname(filePath).toLowerCase();
|
|
@@ -871,20 +285,15 @@ export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
|
|
|
871
285
|
let source: string | undefined = fileContents.get(filePath);
|
|
872
286
|
if (!source) { try { source = readFileSync(join(ctx.repoPath, filePath), 'utf-8'); } catch { continue; } }
|
|
873
287
|
|
|
874
|
-
const importMap = getImportMap(allImportMaps, fileNode.id);
|
|
875
288
|
REEXPORT_RE.lastIndex = 0;
|
|
876
289
|
let m: RegExpExecArray | null;
|
|
877
290
|
while ((m = REEXPORT_RE.exec(source)) !== null) {
|
|
878
291
|
const specifier = m[2];
|
|
879
292
|
const names = extractImportNames(m[1]);
|
|
880
|
-
|
|
881
|
-
const knownFiles = new Set(fileNodesByPath.keys());
|
|
882
|
-
const workspaceMap = buildWorkspacePackageMap(ctx.repoPath);
|
|
883
|
-
const resolvedFile = resolveModuleSpecifier(filePath, specifier, ctx.repoPath, knownFiles, workspaceMap);
|
|
293
|
+
const resolvedFile = resolveModuleSpecifier(filePath, specifier, ctx.repoPath, reexportKnownFiles, reexportWorkspaceMap);
|
|
884
294
|
if (!resolvedFile) continue;
|
|
885
295
|
|
|
886
296
|
for (const name of names) {
|
|
887
|
-
// Find the function/class node in the source file
|
|
888
297
|
const ids = fnIndex.get(resolvedFile)?.get(name);
|
|
889
298
|
if (!ids || ids.length === 0) continue;
|
|
890
299
|
const targetId = ids.length === 1 ? ids[0] : (ids.find(id => id.endsWith('_function')) ?? ids[0]);
|
|
@@ -900,7 +309,6 @@ export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
|
|
|
900
309
|
insertReexports();
|
|
901
310
|
}
|
|
902
311
|
|
|
903
|
-
// Clean up orphan IMPORTS edges that target Variable nodes instead of Files.
|
|
904
312
|
let orphanImportsRemoved = 0;
|
|
905
313
|
if (ctx.db) {
|
|
906
314
|
const result = ctx.db
|
|
@@ -915,7 +323,6 @@ export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
|
|
|
915
323
|
orphanImportsRemoved = result.changes;
|
|
916
324
|
}
|
|
917
325
|
|
|
918
|
-
// Reconstruct proper File→File IMPORTS edges from source-parsed import maps.
|
|
919
326
|
let importsReconstructed = 0;
|
|
920
327
|
if (ctx.db) {
|
|
921
328
|
const insertImport = ctx.db.prepare(`
|
|
@@ -935,7 +342,7 @@ export const scopeResolutionPhase: PipelinePhase<ScopeResolutionOutput> = {
|
|
|
935
342
|
try {
|
|
936
343
|
insertImport.run(edgeId, fileNodeId, targetFileId);
|
|
937
344
|
importsReconstructed++;
|
|
938
|
-
} catch { /* duplicate
|
|
345
|
+
} catch { /* duplicate */ }
|
|
939
346
|
}
|
|
940
347
|
}
|
|
941
348
|
});
|