@kodus/kodus-graph 0.2.8 → 0.2.10
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 +21 -0
- package/README.md +252 -0
- package/dist/analysis/blast-radius.d.ts +2 -0
- package/dist/analysis/blast-radius.js +55 -0
- package/dist/analysis/communities.d.ts +28 -0
- package/dist/analysis/communities.js +100 -0
- package/dist/analysis/context-builder.d.ts +34 -0
- package/dist/analysis/context-builder.js +92 -0
- package/dist/analysis/diff.d.ts +41 -0
- package/dist/analysis/diff.js +155 -0
- package/dist/analysis/enrich.d.ts +5 -0
- package/dist/analysis/enrich.js +126 -0
- package/dist/analysis/flows.d.ts +27 -0
- package/dist/analysis/flows.js +86 -0
- package/dist/analysis/inheritance.d.ts +3 -0
- package/dist/analysis/inheritance.js +31 -0
- package/dist/analysis/prompt-formatter.d.ts +2 -0
- package/dist/analysis/prompt-formatter.js +173 -0
- package/dist/analysis/risk-score.d.ts +4 -0
- package/dist/analysis/risk-score.js +51 -0
- package/dist/analysis/search.d.ts +11 -0
- package/dist/analysis/search.js +64 -0
- package/dist/analysis/test-gaps.d.ts +2 -0
- package/dist/analysis/test-gaps.js +14 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +210 -0
- package/dist/commands/analyze.d.ts +9 -0
- package/dist/commands/analyze.js +116 -0
- package/dist/commands/communities.d.ts +8 -0
- package/dist/commands/communities.js +9 -0
- package/dist/commands/context.d.ts +12 -0
- package/dist/commands/context.js +130 -0
- package/dist/commands/diff.d.ts +9 -0
- package/dist/commands/diff.js +89 -0
- package/dist/commands/flows.d.ts +8 -0
- package/dist/commands/flows.js +9 -0
- package/dist/commands/parse.d.ts +11 -0
- package/dist/commands/parse.js +101 -0
- package/dist/commands/search.d.ts +12 -0
- package/dist/commands/search.js +27 -0
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.js +154 -0
- package/dist/graph/builder.d.ts +6 -0
- package/dist/graph/builder.js +248 -0
- package/dist/graph/edges.d.ts +23 -0
- package/dist/graph/edges.js +159 -0
- package/dist/graph/json-writer.d.ts +9 -0
- package/dist/graph/json-writer.js +38 -0
- package/dist/graph/loader.d.ts +13 -0
- package/dist/graph/loader.js +101 -0
- package/dist/graph/merger.d.ts +7 -0
- package/dist/graph/merger.js +18 -0
- package/dist/graph/types.d.ts +252 -0
- package/dist/graph/types.js +1 -0
- package/dist/parser/batch.d.ts +5 -0
- package/dist/parser/batch.js +93 -0
- package/dist/parser/discovery.d.ts +7 -0
- package/dist/parser/discovery.js +61 -0
- package/dist/parser/extractor.d.ts +4 -0
- package/dist/parser/extractor.js +33 -0
- package/dist/parser/extractors/generic.d.ts +8 -0
- package/dist/parser/extractors/generic.js +471 -0
- package/dist/parser/extractors/python.d.ts +8 -0
- package/dist/parser/extractors/python.js +133 -0
- package/dist/parser/extractors/ruby.d.ts +8 -0
- package/dist/parser/extractors/ruby.js +153 -0
- package/dist/parser/extractors/typescript.d.ts +10 -0
- package/dist/parser/extractors/typescript.js +365 -0
- package/dist/parser/languages.d.ts +32 -0
- package/dist/parser/languages.js +304 -0
- package/dist/resolver/call-resolver.d.ts +36 -0
- package/dist/resolver/call-resolver.js +178 -0
- package/dist/resolver/external-detector.d.ts +11 -0
- package/dist/resolver/external-detector.js +820 -0
- package/dist/resolver/fs-cache.d.ts +8 -0
- package/dist/resolver/fs-cache.js +36 -0
- package/dist/resolver/import-map.d.ts +12 -0
- package/dist/resolver/import-map.js +21 -0
- package/dist/resolver/import-resolver.d.ts +19 -0
- package/dist/resolver/import-resolver.js +310 -0
- package/dist/resolver/languages/csharp.d.ts +3 -0
- package/dist/resolver/languages/csharp.js +94 -0
- package/dist/resolver/languages/go.d.ts +3 -0
- package/dist/resolver/languages/go.js +197 -0
- package/dist/resolver/languages/java.d.ts +1 -0
- package/dist/resolver/languages/java.js +193 -0
- package/dist/resolver/languages/php.d.ts +3 -0
- package/dist/resolver/languages/php.js +75 -0
- package/dist/resolver/languages/python.d.ts +11 -0
- package/dist/resolver/languages/python.js +127 -0
- package/dist/resolver/languages/ruby.d.ts +24 -0
- package/dist/resolver/languages/ruby.js +110 -0
- package/dist/resolver/languages/rust.d.ts +1 -0
- package/dist/resolver/languages/rust.js +197 -0
- package/dist/resolver/languages/typescript.d.ts +35 -0
- package/dist/resolver/languages/typescript.js +416 -0
- package/dist/resolver/re-export-resolver.d.ts +24 -0
- package/dist/resolver/re-export-resolver.js +57 -0
- package/dist/resolver/symbol-table.d.ts +17 -0
- package/dist/resolver/symbol-table.js +60 -0
- package/dist/shared/extract-calls.d.ts +26 -0
- package/dist/shared/extract-calls.js +57 -0
- package/dist/shared/file-hash.d.ts +3 -0
- package/dist/shared/file-hash.js +10 -0
- package/dist/shared/filters.d.ts +3 -0
- package/dist/shared/filters.js +240 -0
- package/dist/shared/logger.d.ts +6 -0
- package/dist/shared/logger.js +17 -0
- package/dist/shared/qualified-name.d.ts +1 -0
- package/dist/shared/qualified-name.js +9 -0
- package/dist/shared/safe-path.d.ts +6 -0
- package/dist/shared/safe-path.js +29 -0
- package/dist/shared/schemas.d.ts +43 -0
- package/dist/shared/schemas.js +30 -0
- package/dist/shared/temp.d.ts +11 -0
- package/{src/shared/temp.ts → dist/shared/temp.js} +4 -5
- package/package.json +20 -6
- package/src/analysis/blast-radius.ts +0 -54
- package/src/analysis/communities.ts +0 -135
- package/src/analysis/context-builder.ts +0 -130
- package/src/analysis/diff.ts +0 -169
- package/src/analysis/enrich.ts +0 -110
- package/src/analysis/flows.ts +0 -112
- package/src/analysis/inheritance.ts +0 -34
- package/src/analysis/prompt-formatter.ts +0 -175
- package/src/analysis/risk-score.ts +0 -62
- package/src/analysis/search.ts +0 -76
- package/src/analysis/test-gaps.ts +0 -21
- package/src/cli.ts +0 -210
- package/src/commands/analyze.ts +0 -128
- package/src/commands/communities.ts +0 -19
- package/src/commands/context.ts +0 -182
- package/src/commands/diff.ts +0 -96
- package/src/commands/flows.ts +0 -19
- package/src/commands/parse.ts +0 -124
- package/src/commands/search.ts +0 -41
- package/src/commands/update.ts +0 -166
- package/src/graph/builder.ts +0 -209
- package/src/graph/edges.ts +0 -101
- package/src/graph/json-writer.ts +0 -43
- package/src/graph/loader.ts +0 -113
- package/src/graph/merger.ts +0 -25
- package/src/graph/types.ts +0 -283
- package/src/parser/batch.ts +0 -82
- package/src/parser/discovery.ts +0 -75
- package/src/parser/extractor.ts +0 -37
- package/src/parser/extractors/generic.ts +0 -132
- package/src/parser/extractors/python.ts +0 -133
- package/src/parser/extractors/ruby.ts +0 -147
- package/src/parser/extractors/typescript.ts +0 -350
- package/src/parser/languages.ts +0 -122
- package/src/resolver/call-resolver.ts +0 -244
- package/src/resolver/import-map.ts +0 -27
- package/src/resolver/import-resolver.ts +0 -72
- package/src/resolver/languages/csharp.ts +0 -7
- package/src/resolver/languages/go.ts +0 -7
- package/src/resolver/languages/java.ts +0 -7
- package/src/resolver/languages/php.ts +0 -7
- package/src/resolver/languages/python.ts +0 -35
- package/src/resolver/languages/ruby.ts +0 -21
- package/src/resolver/languages/rust.ts +0 -7
- package/src/resolver/languages/typescript.ts +0 -168
- package/src/resolver/re-export-resolver.ts +0 -66
- package/src/resolver/symbol-table.ts +0 -67
- package/src/shared/extract-calls.ts +0 -75
- package/src/shared/file-hash.ts +0 -12
- package/src/shared/filters.ts +0 -243
- package/src/shared/logger.ts +0 -17
- package/src/shared/qualified-name.ts +0 -5
- package/src/shared/safe-path.ts +0 -31
- package/src/shared/schemas.ts +0 -32
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript/JavaScript import resolver.
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Relative imports with extension probing (.ts, .tsx, .js, .jsx)
|
|
6
|
+
* - ESM .js → .ts remapping
|
|
7
|
+
* - Directory index files
|
|
8
|
+
* - tsconfig path aliases
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from 'fs';
|
|
11
|
+
import { dirname, join, resolve as resolvePath } from 'path';
|
|
12
|
+
import { log } from '../../shared/logger';
|
|
13
|
+
import { cachedExists } from '../fs-cache';
|
|
14
|
+
const TS_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
|
|
15
|
+
/**
|
|
16
|
+
* Probe a base path for TS/JS files: try extensions, then index files.
|
|
17
|
+
* Returns the resolved absolute path or null.
|
|
18
|
+
*/
|
|
19
|
+
function probeExtensions(base) {
|
|
20
|
+
for (const ext of TS_EXTENSIONS) {
|
|
21
|
+
const candidate = base + ext;
|
|
22
|
+
if (cachedExists(candidate)) {
|
|
23
|
+
return resolvePath(candidate);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (const ext of TS_EXTENSIONS) {
|
|
27
|
+
const candidate = join(base, `index${ext}`);
|
|
28
|
+
if (cachedExists(candidate)) {
|
|
29
|
+
return resolvePath(candidate);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** Cache for parsed tsconfig.json (keyed by repoRoot). */
|
|
35
|
+
const tsconfigCache = new Map();
|
|
36
|
+
/** Cache for parsed bundler aliases (keyed by repoRoot). */
|
|
37
|
+
const bundlerAliasCache = new Map();
|
|
38
|
+
/**
|
|
39
|
+
* Load aliases from webpack.config.ts/js and vite.config.ts/js.
|
|
40
|
+
* These are NOT in tsconfig — many large projects use bundler aliases instead.
|
|
41
|
+
*
|
|
42
|
+
* Parses simple alias patterns from resolve.alias blocks.
|
|
43
|
+
* Returns Map<prefix, absoluteDir> — same format as tsconfig aliases.
|
|
44
|
+
*/
|
|
45
|
+
export function loadBundlerAliases(repoRoot) {
|
|
46
|
+
const cached = bundlerAliasCache.get(repoRoot);
|
|
47
|
+
if (cached !== undefined) {
|
|
48
|
+
return cached;
|
|
49
|
+
}
|
|
50
|
+
const aliases = new Map();
|
|
51
|
+
const configFiles = ['webpack.config.js', 'webpack.config.ts', 'vite.config.js', 'vite.config.ts'];
|
|
52
|
+
for (const configFile of configFiles) {
|
|
53
|
+
const configPath = join(repoRoot, configFile);
|
|
54
|
+
if (!cachedExists(configPath)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
59
|
+
parseBundlerAliases(content, repoRoot, aliases);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// config file read failed, continue
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
bundlerAliasCache.set(repoRoot, aliases);
|
|
66
|
+
return aliases;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parse alias definitions from a webpack or vite config file content.
|
|
70
|
+
* Handles:
|
|
71
|
+
* - path.join(__dirname, 'a', 'b') and path.resolve(__dirname, 'a', 'b')
|
|
72
|
+
* - Simple string literal values: 'key': '/path/to/dir'
|
|
73
|
+
* - Variable references like path.join(varName, 'sub') where varName is defined
|
|
74
|
+
* earlier as const varName = path.join(__dirname, ...)
|
|
75
|
+
*/
|
|
76
|
+
function parseBundlerAliases(content, repoRoot, aliases) {
|
|
77
|
+
// First, extract top-level variable definitions like:
|
|
78
|
+
// const staticPrefix = path.join(__dirname, 'static')
|
|
79
|
+
const varDefs = new Map();
|
|
80
|
+
const varDefRegex = /(?:const|let|var)\s+(\w+)\s*=\s*path\.(?:join|resolve)\s*\(\s*__dirname\s*,\s*([^)]+)\)/g;
|
|
81
|
+
let varMatch = varDefRegex.exec(content);
|
|
82
|
+
while (varMatch !== null) {
|
|
83
|
+
const varName = varMatch[1];
|
|
84
|
+
const argsStr = varMatch[2];
|
|
85
|
+
const segments = extractStringArgs(argsStr);
|
|
86
|
+
if (segments.length > 0) {
|
|
87
|
+
varDefs.set(varName, join(repoRoot, ...segments));
|
|
88
|
+
}
|
|
89
|
+
varMatch = varDefRegex.exec(content);
|
|
90
|
+
}
|
|
91
|
+
// Find the alias block — look for alias: { ... } or alias: [ ... ]
|
|
92
|
+
// We search for "alias:" or "alias :" possibly inside resolve: { ... }
|
|
93
|
+
const aliasBlockRegex = /alias\s*:\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/gs;
|
|
94
|
+
let aliasMatch = aliasBlockRegex.exec(content);
|
|
95
|
+
while (aliasMatch !== null) {
|
|
96
|
+
const aliasBlock = aliasMatch[1];
|
|
97
|
+
parseAliasEntries(aliasBlock, repoRoot, varDefs, aliases);
|
|
98
|
+
aliasMatch = aliasBlockRegex.exec(content);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Parse individual alias entries from inside an alias block.
|
|
103
|
+
*/
|
|
104
|
+
function parseAliasEntries(block, repoRoot, varDefs, aliases) {
|
|
105
|
+
// Match entries like:
|
|
106
|
+
// key: path.join(__dirname, 'a', 'b'),
|
|
107
|
+
// 'key': path.join(__dirname, 'a', 'b'),
|
|
108
|
+
// "key": path.resolve(__dirname, 'a'),
|
|
109
|
+
// key: path.join(varName, 'sub'),
|
|
110
|
+
// key: 'literal/path',
|
|
111
|
+
// 'key': 'literal/path',
|
|
112
|
+
// Pattern for key (unquoted identifier or quoted string)
|
|
113
|
+
const keyPattern = /(?:'([^']+)'|"([^"]+)"|(\w+))\s*:\s*/g;
|
|
114
|
+
let keyMatch = keyPattern.exec(block);
|
|
115
|
+
while (keyMatch !== null) {
|
|
116
|
+
const key = keyMatch[1] ?? keyMatch[2] ?? keyMatch[3];
|
|
117
|
+
const valueStart = keyMatch.index + keyMatch[0].length;
|
|
118
|
+
const restOfBlock = block.slice(valueStart);
|
|
119
|
+
const resolvedDir = resolveAliasValue(restOfBlock, repoRoot, varDefs);
|
|
120
|
+
if (resolvedDir !== null && !aliases.has(`${key}/`) && !aliases.has(key)) {
|
|
121
|
+
// Use key + '/' as prefix for path-based aliases (like tsconfig aliases)
|
|
122
|
+
// but if the key already ends with special chars like ~, use as-is
|
|
123
|
+
const prefix = key.endsWith('/') ? key : `${key}/`;
|
|
124
|
+
aliases.set(prefix, [resolvedDir]);
|
|
125
|
+
// Also set exact match (for bare imports like 'sentry' → 'sentry/')
|
|
126
|
+
if (!aliases.has(key) && key !== prefix) {
|
|
127
|
+
aliases.set(key, [resolvedDir]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
keyMatch = keyPattern.exec(block);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Try to resolve an alias value expression to an absolute directory.
|
|
135
|
+
*/
|
|
136
|
+
function resolveAliasValue(expr, repoRoot, varDefs) {
|
|
137
|
+
// path.join(__dirname, 'a', 'b') or path.resolve(__dirname, 'a', 'b')
|
|
138
|
+
const pathDirnameRegex = /^path\.(?:join|resolve)\s*\(\s*__dirname\s*,\s*([^)]+)\)/;
|
|
139
|
+
const dirnameMatch = pathDirnameRegex.exec(expr);
|
|
140
|
+
if (dirnameMatch) {
|
|
141
|
+
const segments = extractStringArgs(dirnameMatch[1]);
|
|
142
|
+
if (segments.length > 0) {
|
|
143
|
+
return join(repoRoot, ...segments);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// path.join(varName, 'a', 'b') or path.resolve(varName, 'a')
|
|
147
|
+
const pathVarRegex = /^path\.(?:join|resolve)\s*\(\s*(\w+)\s*(?:,\s*([^)]+))?\)/;
|
|
148
|
+
const varMatch = pathVarRegex.exec(expr);
|
|
149
|
+
if (varMatch) {
|
|
150
|
+
const varName = varMatch[1];
|
|
151
|
+
if (varName !== '__dirname' && varDefs.has(varName)) {
|
|
152
|
+
const baseDir = varDefs.get(varName);
|
|
153
|
+
if (varMatch[2]) {
|
|
154
|
+
const segments = extractStringArgs(varMatch[2]);
|
|
155
|
+
if (segments.length > 0) {
|
|
156
|
+
return join(baseDir, ...segments);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return baseDir;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Simple string literal: 'path/to/dir' or "path/to/dir"
|
|
163
|
+
const stringLiteralRegex = /^['"]([^'"]+)['"]/;
|
|
164
|
+
const strMatch = stringLiteralRegex.exec(expr);
|
|
165
|
+
if (strMatch) {
|
|
166
|
+
return join(repoRoot, strMatch[1]);
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Extract string literal arguments from a comma-separated argument list.
|
|
172
|
+
* e.g. "'static', 'app'" → ['static', 'app']
|
|
173
|
+
*/
|
|
174
|
+
function extractStringArgs(argsStr) {
|
|
175
|
+
const segments = [];
|
|
176
|
+
const argRegex = /['"]([^'"]+)['"]/g;
|
|
177
|
+
let m = argRegex.exec(argsStr);
|
|
178
|
+
while (m !== null) {
|
|
179
|
+
segments.push(m[1]);
|
|
180
|
+
m = argRegex.exec(argsStr);
|
|
181
|
+
}
|
|
182
|
+
return segments;
|
|
183
|
+
}
|
|
184
|
+
function loadTsconfigCompilerOptions(repoRoot) {
|
|
185
|
+
const cached = tsconfigCache.get(repoRoot);
|
|
186
|
+
if (cached !== undefined) {
|
|
187
|
+
return cached;
|
|
188
|
+
}
|
|
189
|
+
const tsconfigPath = join(repoRoot, 'tsconfig.json');
|
|
190
|
+
let result = {};
|
|
191
|
+
if (cachedExists(tsconfigPath)) {
|
|
192
|
+
try {
|
|
193
|
+
const content = readFileSync(tsconfigPath, 'utf-8');
|
|
194
|
+
const cleaned = stripJsonComments(content);
|
|
195
|
+
const config = JSON.parse(cleaned);
|
|
196
|
+
const rootDirs = config?.compilerOptions?.rootDirs;
|
|
197
|
+
if (Array.isArray(rootDirs)) {
|
|
198
|
+
result = { rootDirs };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// ignore parse errors
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
tsconfigCache.set(repoRoot, result);
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Resolve a TypeScript/JavaScript relative import to an absolute file path.
|
|
210
|
+
* Returns null for non-relative (external package) imports.
|
|
211
|
+
*/
|
|
212
|
+
export function resolve(fromAbsFile, modulePath, repoRoot) {
|
|
213
|
+
// Strip Vite-style query suffixes (?raw, ?url, ?worker, etc.)
|
|
214
|
+
const queryIdx = modulePath.indexOf('?');
|
|
215
|
+
if (queryIdx !== -1) {
|
|
216
|
+
modulePath = modulePath.slice(0, queryIdx);
|
|
217
|
+
}
|
|
218
|
+
if (!modulePath.startsWith('.')) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
let base = join(dirname(fromAbsFile), modulePath);
|
|
222
|
+
// If the path has a non-TS/JS extension (e.g. .txt, .svg), try exact match
|
|
223
|
+
if (/\.\w+$/.test(modulePath) && !TS_EXTENSIONS.some((ext) => modulePath.endsWith(ext))) {
|
|
224
|
+
if (cachedExists(base)) {
|
|
225
|
+
return resolvePath(base);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// ESM convention: .js in import -> .ts on disk
|
|
229
|
+
if (modulePath.endsWith('.js')) {
|
|
230
|
+
base = base.slice(0, -3);
|
|
231
|
+
}
|
|
232
|
+
const direct = probeExtensions(base);
|
|
233
|
+
if (direct) {
|
|
234
|
+
return direct;
|
|
235
|
+
}
|
|
236
|
+
// rootDirs fallback: try the same relative import from other root directories
|
|
237
|
+
const { rootDirs } = loadTsconfigCompilerOptions(repoRoot);
|
|
238
|
+
if (rootDirs && rootDirs.length > 0) {
|
|
239
|
+
const fromDir = dirname(fromAbsFile);
|
|
240
|
+
for (const rd of rootDirs) {
|
|
241
|
+
const absRd = resolvePath(repoRoot, rd);
|
|
242
|
+
// Check if fromDir is inside this rootDir
|
|
243
|
+
if (fromDir.startsWith(`${absRd}/`) || fromDir === absRd) {
|
|
244
|
+
const relFromRoot = fromDir.slice(absRd.length); // e.g. "" or "/sub"
|
|
245
|
+
// Try the same relative path under each other rootDir
|
|
246
|
+
for (const otherRd of rootDirs) {
|
|
247
|
+
if (otherRd === rd) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const absOtherRd = resolvePath(repoRoot, otherRd);
|
|
251
|
+
const relModule = modulePath.startsWith('./') ? modulePath.slice(2) : modulePath;
|
|
252
|
+
let otherBase = join(absOtherRd, relFromRoot, relModule);
|
|
253
|
+
if (modulePath.endsWith('.js')) {
|
|
254
|
+
otherBase = otherBase.slice(0, -3);
|
|
255
|
+
}
|
|
256
|
+
const result = probeExtensions(otherBase);
|
|
257
|
+
if (result) {
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Strip comments and trailing commas from JSON (tsconfig-compatible).
|
|
269
|
+
* Handles strings correctly -- won't strip // inside "url://..." etc.
|
|
270
|
+
*/
|
|
271
|
+
function stripJsonComments(str) {
|
|
272
|
+
let result = '';
|
|
273
|
+
let i = 0;
|
|
274
|
+
const len = str.length;
|
|
275
|
+
while (i < len) {
|
|
276
|
+
// String literal -- copy as-is
|
|
277
|
+
if (str[i] === '"') {
|
|
278
|
+
let j = i + 1;
|
|
279
|
+
while (j < len && str[j] !== '"') {
|
|
280
|
+
if (str[j] === '\\') {
|
|
281
|
+
j++; // skip escaped char
|
|
282
|
+
}
|
|
283
|
+
j++;
|
|
284
|
+
}
|
|
285
|
+
result += str.substring(i, j + 1);
|
|
286
|
+
i = j + 1;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
// Single-line comment
|
|
290
|
+
if (str[i] === '/' && str[i + 1] === '/') {
|
|
291
|
+
while (i < len && str[i] !== '\n') {
|
|
292
|
+
i++;
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
// Block comment
|
|
297
|
+
if (str[i] === '/' && str[i + 1] === '*') {
|
|
298
|
+
i += 2;
|
|
299
|
+
while (i < len && !(str[i] === '*' && str[i + 1] === '/')) {
|
|
300
|
+
i++;
|
|
301
|
+
}
|
|
302
|
+
i += 2;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
// Trailing comma: comma followed by optional whitespace + closing bracket
|
|
306
|
+
if (str[i] === ',') {
|
|
307
|
+
let j = i + 1;
|
|
308
|
+
while (j < len && (str[j] === ' ' || str[j] === '\t' || str[j] === '\n' || str[j] === '\r')) {
|
|
309
|
+
j++;
|
|
310
|
+
}
|
|
311
|
+
if (str[j] === '}' || str[j] === ']') {
|
|
312
|
+
i++;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
result += str[i];
|
|
317
|
+
i++;
|
|
318
|
+
}
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Load and parse tsconfig.json path aliases.
|
|
323
|
+
*
|
|
324
|
+
* Tries tsconfig.json first, then tsconfig.base.json.
|
|
325
|
+
* Converts alias patterns like "@libs/*" into prefix → resolved dirs.
|
|
326
|
+
*/
|
|
327
|
+
export function loadTsconfigAliases(repoRoot) {
|
|
328
|
+
const aliases = new Map();
|
|
329
|
+
loadTsconfigPathsInto(repoRoot, aliases);
|
|
330
|
+
return aliases;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Parse a tsconfig.json (and tsconfig.base.json) in the given directory
|
|
334
|
+
* and add its path aliases to the provided map.
|
|
335
|
+
*/
|
|
336
|
+
function loadTsconfigPathsInto(dir, aliases, visited) {
|
|
337
|
+
const seen = visited ?? new Set();
|
|
338
|
+
const absDir = resolvePath(dir);
|
|
339
|
+
if (seen.has(absDir)) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
seen.add(absDir);
|
|
343
|
+
for (const filename of ['tsconfig.json', 'tsconfig.base.json']) {
|
|
344
|
+
const tsconfigPath = join(dir, filename);
|
|
345
|
+
if (!cachedExists(tsconfigPath)) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const content = readFileSync(tsconfigPath, 'utf-8');
|
|
350
|
+
const cleaned = stripJsonComments(content);
|
|
351
|
+
const config = JSON.parse(cleaned);
|
|
352
|
+
const paths = config?.compilerOptions?.paths;
|
|
353
|
+
const baseUrl = config?.compilerOptions?.baseUrl || '.';
|
|
354
|
+
const baseDir = join(dir, baseUrl);
|
|
355
|
+
if (paths) {
|
|
356
|
+
for (const [alias, targets] of Object.entries(paths)) {
|
|
357
|
+
// Convert alias pattern: "@libs/*" -> prefix "@libs/"
|
|
358
|
+
const prefix = alias.replace('/*', '/').replace('*', '');
|
|
359
|
+
if (!aliases.has(prefix)) {
|
|
360
|
+
const resolvedTargets = targets.map((t) => {
|
|
361
|
+
const targetPath = t.replace('/*', '').replace('*', '');
|
|
362
|
+
return join(baseDir, targetPath);
|
|
363
|
+
});
|
|
364
|
+
aliases.set(prefix, resolvedTargets);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Follow project references to discover aliases from referenced projects
|
|
369
|
+
const references = config?.references;
|
|
370
|
+
if (Array.isArray(references)) {
|
|
371
|
+
for (const ref of references) {
|
|
372
|
+
if (ref && typeof ref.path === 'string') {
|
|
373
|
+
const refDir = resolvePath(dir, ref.path);
|
|
374
|
+
if (cachedExists(refDir)) {
|
|
375
|
+
loadTsconfigPathsInto(refDir, aliases, seen);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
log.warn('Failed to parse tsconfig', { file: tsconfigPath, error: String(err) });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Resolve an import path using tsconfig aliases.
|
|
388
|
+
*
|
|
389
|
+
* Tries each alias prefix, and for matches, probes extensions and index files.
|
|
390
|
+
*/
|
|
391
|
+
export function resolveWithAliases(modulePath, aliases, _repoRoot) {
|
|
392
|
+
for (const [prefix, targets] of aliases) {
|
|
393
|
+
if (modulePath.startsWith(prefix)) {
|
|
394
|
+
const rest = modulePath.slice(prefix.length);
|
|
395
|
+
for (const targetBase of targets) {
|
|
396
|
+
const base = join(targetBase, rest);
|
|
397
|
+
for (const ext of TS_EXTENSIONS) {
|
|
398
|
+
if (cachedExists(base + ext)) {
|
|
399
|
+
return resolvePath(base + ext);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
for (const ext of TS_EXTENSIONS) {
|
|
403
|
+
const idx = join(base, `index${ext}`);
|
|
404
|
+
if (cachedExists(idx)) {
|
|
405
|
+
return resolvePath(idx);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// Try exact match (for directories with index)
|
|
409
|
+
if (cachedExists(base)) {
|
|
410
|
+
return resolvePath(base);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export (barrel) resolver.
|
|
3
|
+
*
|
|
4
|
+
* Follows `export { X } from './module'` chains so that the import map
|
|
5
|
+
* points to the file where the symbol is actually defined, not the
|
|
6
|
+
* barrel index file.
|
|
7
|
+
*
|
|
8
|
+
* Without this, an import like `import { Foo } from '@lib'` resolves
|
|
9
|
+
* to `@lib/index.ts`, but `Foo` is defined in `@lib/foo.ts`.
|
|
10
|
+
* The call resolver can't find `Foo` in the barrel's symbol table and
|
|
11
|
+
* falls to lower-confidence tiers.
|
|
12
|
+
*/
|
|
13
|
+
interface RawReExport {
|
|
14
|
+
module: string;
|
|
15
|
+
file: string;
|
|
16
|
+
line: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Build a map: barrel file (relative) → list of resolved re-export target files (relative).
|
|
20
|
+
*
|
|
21
|
+
* Follows one level of re-exports (covers >95% of real-world barrel patterns).
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildReExportMap(reExports: RawReExport[], repoDir: string, tsconfigAliases?: Map<string, string[]>): Map<string, string[]>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-export (barrel) resolver.
|
|
3
|
+
*
|
|
4
|
+
* Follows `export { X } from './module'` chains so that the import map
|
|
5
|
+
* points to the file where the symbol is actually defined, not the
|
|
6
|
+
* barrel index file.
|
|
7
|
+
*
|
|
8
|
+
* Without this, an import like `import { Foo } from '@lib'` resolves
|
|
9
|
+
* to `@lib/index.ts`, but `Foo` is defined in `@lib/foo.ts`.
|
|
10
|
+
* The call resolver can't find `Foo` in the barrel's symbol table and
|
|
11
|
+
* falls to lower-confidence tiers.
|
|
12
|
+
*/
|
|
13
|
+
import { relative, resolve } from 'path';
|
|
14
|
+
import { resolveImport } from './import-resolver';
|
|
15
|
+
/**
|
|
16
|
+
* Build a map: barrel file (relative) → list of resolved re-export target files (relative).
|
|
17
|
+
*
|
|
18
|
+
* Follows one level of re-exports (covers >95% of real-world barrel patterns).
|
|
19
|
+
*/
|
|
20
|
+
export function buildReExportMap(reExports, repoDir, tsconfigAliases) {
|
|
21
|
+
const barrelMap = new Map();
|
|
22
|
+
for (const re of reExports) {
|
|
23
|
+
const absFrom = resolve(repoDir, re.file);
|
|
24
|
+
const resolved = resolveImport(absFrom, re.module, 'typescript', repoDir, tsconfigAliases);
|
|
25
|
+
if (!resolved) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const resolvedRel = relative(repoDir, resolved);
|
|
29
|
+
const list = barrelMap.get(re.file);
|
|
30
|
+
if (list) {
|
|
31
|
+
if (!list.includes(resolvedRel)) {
|
|
32
|
+
list.push(resolvedRel);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
barrelMap.set(re.file, [resolvedRel]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Follow one extra level: if a re-export target is itself a barrel, flatten
|
|
40
|
+
for (const [barrel, targets] of barrelMap) {
|
|
41
|
+
const extra = [];
|
|
42
|
+
for (const target of targets) {
|
|
43
|
+
const nested = barrelMap.get(target);
|
|
44
|
+
if (nested) {
|
|
45
|
+
for (const n of nested) {
|
|
46
|
+
if (n !== barrel && !targets.includes(n) && !extra.includes(n)) {
|
|
47
|
+
extra.push(n);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (extra.length > 0) {
|
|
53
|
+
targets.push(...extra);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return barrelMap;
|
|
57
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol table with dual-index lookup (GitNexus pattern).
|
|
3
|
+
*
|
|
4
|
+
* Provides both exact (file + name) and global (name-only) lookups.
|
|
5
|
+
* The exact lookup is high-confidence; global lookup is lower-confidence
|
|
6
|
+
* but useful when import resolution fails.
|
|
7
|
+
*/
|
|
8
|
+
export interface SymbolTable {
|
|
9
|
+
add(file: string, name: string, qualified: string): void;
|
|
10
|
+
lookupExact(file: string, name: string): string | null;
|
|
11
|
+
lookupInFile(file: string, name: string, className: string): string | null;
|
|
12
|
+
isUnique(name: string): boolean;
|
|
13
|
+
lookupGlobal(name: string): string[];
|
|
14
|
+
readonly size: number;
|
|
15
|
+
readonly fileCount: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function createSymbolTable(): SymbolTable;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol table with dual-index lookup (GitNexus pattern).
|
|
3
|
+
*
|
|
4
|
+
* Provides both exact (file + name) and global (name-only) lookups.
|
|
5
|
+
* The exact lookup is high-confidence; global lookup is lower-confidence
|
|
6
|
+
* but useful when import resolution fails.
|
|
7
|
+
*/
|
|
8
|
+
export function createSymbolTable() {
|
|
9
|
+
const byFile = new Map();
|
|
10
|
+
const byName = new Map();
|
|
11
|
+
return {
|
|
12
|
+
add(file, name, qualified) {
|
|
13
|
+
if (!byFile.has(file)) {
|
|
14
|
+
byFile.set(file, new Map());
|
|
15
|
+
}
|
|
16
|
+
const fileMap = byFile.get(file);
|
|
17
|
+
if (!fileMap.has(name)) {
|
|
18
|
+
fileMap.set(name, []);
|
|
19
|
+
}
|
|
20
|
+
fileMap.get(name).push(qualified);
|
|
21
|
+
if (!byName.has(name)) {
|
|
22
|
+
byName.set(name, []);
|
|
23
|
+
}
|
|
24
|
+
byName.get(name).push(qualified);
|
|
25
|
+
},
|
|
26
|
+
lookupExact(file, name) {
|
|
27
|
+
const candidates = byFile.get(file)?.get(name);
|
|
28
|
+
if (!candidates || candidates.length === 0) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
// Only return if unambiguous within this file
|
|
32
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
33
|
+
},
|
|
34
|
+
lookupInFile(file, name, className) {
|
|
35
|
+
const candidates = byFile.get(file)?.get(name);
|
|
36
|
+
if (!candidates || candidates.length === 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return candidates.find((q) => q.includes(`::${className}.${name}`)) ?? null;
|
|
40
|
+
},
|
|
41
|
+
isUnique(name) {
|
|
42
|
+
return (byName.get(name)?.length ?? 0) === 1;
|
|
43
|
+
},
|
|
44
|
+
lookupGlobal(name) {
|
|
45
|
+
return byName.get(name) ?? [];
|
|
46
|
+
},
|
|
47
|
+
get size() {
|
|
48
|
+
let count = 0;
|
|
49
|
+
for (const m of byFile.values()) {
|
|
50
|
+
for (const arr of m.values()) {
|
|
51
|
+
count += arr.length;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return count;
|
|
55
|
+
},
|
|
56
|
+
get fileCount() {
|
|
57
|
+
return byFile.size;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SgNode } from '@ast-grep/napi';
|
|
2
|
+
import type { RawCallSite } from '../graph/types';
|
|
3
|
+
/**
|
|
4
|
+
* Language-specific configuration for call extraction.
|
|
5
|
+
* Each language provides its self/super patterns and how to find class context in the AST.
|
|
6
|
+
*/
|
|
7
|
+
export interface CallExtractionConfig {
|
|
8
|
+
/** Prefixes indicating a self-reference (e.g., 'self.', 'this.') */
|
|
9
|
+
selfPrefixes: string[];
|
|
10
|
+
/** Prefixes indicating a super-reference (e.g., 'super().', 'super.', 'base.') */
|
|
11
|
+
superPrefixes: string[];
|
|
12
|
+
/** Find the enclosing class/module/impl node from a call site */
|
|
13
|
+
findEnclosingClass: (node: SgNode) => SgNode | null;
|
|
14
|
+
/** Extract the parent class name from a class node (for super resolution) */
|
|
15
|
+
getParentClass?: (classNode: SgNode) => string | undefined;
|
|
16
|
+
/** Skip this callee entirely (e.g., TS skips this.field.method — handled by DI) */
|
|
17
|
+
skipCallee?: (callee: string) => boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Shared call extraction for all languages.
|
|
21
|
+
*
|
|
22
|
+
* Parses `$CALLEE($$$ARGS)` pattern, detects self/super references
|
|
23
|
+
* based on language config, and populates resolveInClass for
|
|
24
|
+
* class-aware resolution downstream.
|
|
25
|
+
*/
|
|
26
|
+
export declare function extractCalls(rootNode: SgNode, fp: string, config: CallExtractionConfig, calls: RawCallSite[]): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { NOISE } from './filters';
|
|
2
|
+
/**
|
|
3
|
+
* Shared call extraction for all languages.
|
|
4
|
+
*
|
|
5
|
+
* Parses `$CALLEE($$$ARGS)` pattern, detects self/super references
|
|
6
|
+
* based on language config, and populates resolveInClass for
|
|
7
|
+
* class-aware resolution downstream.
|
|
8
|
+
*/
|
|
9
|
+
export function extractCalls(rootNode, fp, config, calls) {
|
|
10
|
+
for (const m of rootNode.findAll('$CALLEE($$$ARGS)')) {
|
|
11
|
+
const callee = m.getMatch('CALLEE')?.text();
|
|
12
|
+
if (!callee) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (config.skipCallee?.(callee)) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const callName = callee.includes('.') ? callee.split('.').pop() : callee;
|
|
19
|
+
if (NOISE.has(callName)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
let resolveInClass;
|
|
23
|
+
// Check self-reference: callee must be exactly `prefix + methodName` (no further chaining)
|
|
24
|
+
for (const prefix of config.selfPrefixes) {
|
|
25
|
+
if (!callee.startsWith(prefix)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const rest = callee.substring(prefix.length);
|
|
29
|
+
if (rest.includes('.')) {
|
|
30
|
+
break; // chained access (e.g., this.field.method) — not a self call
|
|
31
|
+
}
|
|
32
|
+
const classNode = config.findEnclosingClass(m);
|
|
33
|
+
resolveInClass = classNode?.field('name')?.text();
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
// Check super-reference if no self match
|
|
37
|
+
if (!resolveInClass) {
|
|
38
|
+
for (const prefix of config.superPrefixes) {
|
|
39
|
+
const matches = callee === prefix || (callee.startsWith(prefix) && !callee.substring(prefix.length).includes('.'));
|
|
40
|
+
if (!matches) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const classNode = config.findEnclosingClass(m);
|
|
44
|
+
if (classNode && config.getParentClass) {
|
|
45
|
+
resolveInClass = config.getParentClass(classNode);
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
calls.push({
|
|
51
|
+
source: fp,
|
|
52
|
+
callName,
|
|
53
|
+
line: m.range().start.line,
|
|
54
|
+
...(resolveInClass ? { resolveInClass } : {}),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
export function computeFileHash(filePath) {
|
|
4
|
+
const content = readFileSync(filePath);
|
|
5
|
+
return createHash('sha256').update(content).digest('hex');
|
|
6
|
+
}
|
|
7
|
+
/** Hash a node's source text (function body, class body, etc.) */
|
|
8
|
+
export function computeContentHash(sourceText) {
|
|
9
|
+
return createHash('sha256').update(sourceText).digest('hex');
|
|
10
|
+
}
|