@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,820 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External package detector.
|
|
3
|
+
* Reads dependency manifests (package.json, requirements.txt, go.mod, etc.)
|
|
4
|
+
* to determine if an import target is an external (third-party) package.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { cachedExists } from './fs-cache';
|
|
9
|
+
const depsCache = new Map();
|
|
10
|
+
export function clearExternalCache() {
|
|
11
|
+
depsCache.clear();
|
|
12
|
+
}
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Built-in / stdlib lists
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
const NODE_BUILTINS = new Set([
|
|
17
|
+
'fs',
|
|
18
|
+
'path',
|
|
19
|
+
'os',
|
|
20
|
+
'http',
|
|
21
|
+
'https',
|
|
22
|
+
'http2',
|
|
23
|
+
'net',
|
|
24
|
+
'stream',
|
|
25
|
+
'buffer',
|
|
26
|
+
'url',
|
|
27
|
+
'util',
|
|
28
|
+
'crypto',
|
|
29
|
+
'events',
|
|
30
|
+
'child_process',
|
|
31
|
+
'cluster',
|
|
32
|
+
'dns',
|
|
33
|
+
'readline',
|
|
34
|
+
'repl',
|
|
35
|
+
'tls',
|
|
36
|
+
'vm',
|
|
37
|
+
'zlib',
|
|
38
|
+
'assert',
|
|
39
|
+
'async_hooks',
|
|
40
|
+
'console',
|
|
41
|
+
'constants',
|
|
42
|
+
'dgram',
|
|
43
|
+
'diagnostics_channel',
|
|
44
|
+
'domain',
|
|
45
|
+
'inspector',
|
|
46
|
+
'module',
|
|
47
|
+
'perf_hooks',
|
|
48
|
+
'process',
|
|
49
|
+
'punycode',
|
|
50
|
+
'querystring',
|
|
51
|
+
'string_decoder',
|
|
52
|
+
'timers',
|
|
53
|
+
'tty',
|
|
54
|
+
'v8',
|
|
55
|
+
'wasi',
|
|
56
|
+
'worker_threads',
|
|
57
|
+
]);
|
|
58
|
+
const PYTHON_STDLIB = new Set([
|
|
59
|
+
'os',
|
|
60
|
+
'sys',
|
|
61
|
+
'json',
|
|
62
|
+
'typing',
|
|
63
|
+
'collections',
|
|
64
|
+
'datetime',
|
|
65
|
+
're',
|
|
66
|
+
'math',
|
|
67
|
+
'pathlib',
|
|
68
|
+
'functools',
|
|
69
|
+
'itertools',
|
|
70
|
+
'abc',
|
|
71
|
+
'dataclasses',
|
|
72
|
+
'enum',
|
|
73
|
+
'logging',
|
|
74
|
+
'unittest',
|
|
75
|
+
'io',
|
|
76
|
+
'copy',
|
|
77
|
+
'hashlib',
|
|
78
|
+
'hmac',
|
|
79
|
+
'secrets',
|
|
80
|
+
'socket',
|
|
81
|
+
'http',
|
|
82
|
+
'urllib',
|
|
83
|
+
'email',
|
|
84
|
+
'html',
|
|
85
|
+
'xml',
|
|
86
|
+
'sqlite3',
|
|
87
|
+
'csv',
|
|
88
|
+
'configparser',
|
|
89
|
+
'argparse',
|
|
90
|
+
'subprocess',
|
|
91
|
+
'threading',
|
|
92
|
+
'multiprocessing',
|
|
93
|
+
'asyncio',
|
|
94
|
+
'signal',
|
|
95
|
+
'shutil',
|
|
96
|
+
'tempfile',
|
|
97
|
+
'glob',
|
|
98
|
+
'fnmatch',
|
|
99
|
+
'struct',
|
|
100
|
+
'codecs',
|
|
101
|
+
'pprint',
|
|
102
|
+
'textwrap',
|
|
103
|
+
'difflib',
|
|
104
|
+
'traceback',
|
|
105
|
+
'warnings',
|
|
106
|
+
'contextlib',
|
|
107
|
+
'weakref',
|
|
108
|
+
'types',
|
|
109
|
+
'inspect',
|
|
110
|
+
'dis',
|
|
111
|
+
'importlib',
|
|
112
|
+
'pkgutil',
|
|
113
|
+
'pdb',
|
|
114
|
+
'cProfile',
|
|
115
|
+
'time',
|
|
116
|
+
'calendar',
|
|
117
|
+
'random',
|
|
118
|
+
'statistics',
|
|
119
|
+
'fractions',
|
|
120
|
+
'decimal',
|
|
121
|
+
'operator',
|
|
122
|
+
'string',
|
|
123
|
+
'base64',
|
|
124
|
+
'binascii',
|
|
125
|
+
'zlib',
|
|
126
|
+
'gzip',
|
|
127
|
+
'bz2',
|
|
128
|
+
'lzma',
|
|
129
|
+
'zipfile',
|
|
130
|
+
'tarfile',
|
|
131
|
+
// additional commonly used stdlib modules
|
|
132
|
+
'builtins',
|
|
133
|
+
'array',
|
|
134
|
+
'bisect',
|
|
135
|
+
'heapq',
|
|
136
|
+
'queue',
|
|
137
|
+
'sched',
|
|
138
|
+
'selectors',
|
|
139
|
+
'mmap',
|
|
140
|
+
'ctypes',
|
|
141
|
+
'concurrent',
|
|
142
|
+
'test',
|
|
143
|
+
'profile',
|
|
144
|
+
'cmath',
|
|
145
|
+
'numbers',
|
|
146
|
+
'locale',
|
|
147
|
+
'gettext',
|
|
148
|
+
'unicodedata',
|
|
149
|
+
'stringprep',
|
|
150
|
+
'rlcompleter',
|
|
151
|
+
'code',
|
|
152
|
+
'codeop',
|
|
153
|
+
'compileall',
|
|
154
|
+
'py_compile',
|
|
155
|
+
'zipimport',
|
|
156
|
+
'winreg',
|
|
157
|
+
'winsound',
|
|
158
|
+
'msvcrt',
|
|
159
|
+
'posixpath',
|
|
160
|
+
'ntpath',
|
|
161
|
+
'genericpath',
|
|
162
|
+
'posix',
|
|
163
|
+
'nt',
|
|
164
|
+
'token',
|
|
165
|
+
'tokenize',
|
|
166
|
+
'keyword',
|
|
167
|
+
'linecache',
|
|
168
|
+
'pickle',
|
|
169
|
+
'shelve',
|
|
170
|
+
'marshal',
|
|
171
|
+
'dbm',
|
|
172
|
+
'platform',
|
|
173
|
+
'errno',
|
|
174
|
+
'faulthandler',
|
|
175
|
+
'atexit',
|
|
176
|
+
'site',
|
|
177
|
+
'sysconfig',
|
|
178
|
+
'zipapp',
|
|
179
|
+
'venv',
|
|
180
|
+
'ensurepip',
|
|
181
|
+
'distutils',
|
|
182
|
+
'setuptools',
|
|
183
|
+
'_thread',
|
|
184
|
+
'__future__',
|
|
185
|
+
'colorsys',
|
|
186
|
+
'fileinput',
|
|
187
|
+
'filecmp',
|
|
188
|
+
'stat',
|
|
189
|
+
'grp',
|
|
190
|
+
'pwd',
|
|
191
|
+
'resource',
|
|
192
|
+
'termios',
|
|
193
|
+
'fcntl',
|
|
194
|
+
'pty',
|
|
195
|
+
'pipes',
|
|
196
|
+
'mailbox',
|
|
197
|
+
'mailcap',
|
|
198
|
+
'mimetypes',
|
|
199
|
+
'imaplib',
|
|
200
|
+
'poplib',
|
|
201
|
+
'smtplib',
|
|
202
|
+
'ftplib',
|
|
203
|
+
'telnetlib',
|
|
204
|
+
'xmlrpc',
|
|
205
|
+
'ipaddress',
|
|
206
|
+
'ssl',
|
|
207
|
+
'cgi',
|
|
208
|
+
'cgitb',
|
|
209
|
+
'wsgiref',
|
|
210
|
+
'webbrowser',
|
|
211
|
+
'uuid',
|
|
212
|
+
'getpass',
|
|
213
|
+
'curses',
|
|
214
|
+
'turtle',
|
|
215
|
+
'cmd',
|
|
216
|
+
'shlex',
|
|
217
|
+
'tkinter',
|
|
218
|
+
'idlelib',
|
|
219
|
+
'doctest',
|
|
220
|
+
'pydoc',
|
|
221
|
+
'ast',
|
|
222
|
+
'symtable',
|
|
223
|
+
'tabnanny',
|
|
224
|
+
]);
|
|
225
|
+
const RUBY_STDLIB = new Set([
|
|
226
|
+
'json',
|
|
227
|
+
'net/http',
|
|
228
|
+
'uri',
|
|
229
|
+
'fileutils',
|
|
230
|
+
'set',
|
|
231
|
+
'csv',
|
|
232
|
+
'yaml',
|
|
233
|
+
'openssl',
|
|
234
|
+
'pathname',
|
|
235
|
+
'tempfile',
|
|
236
|
+
'socket',
|
|
237
|
+
'open-uri',
|
|
238
|
+
'erb',
|
|
239
|
+
'cgi',
|
|
240
|
+
'digest',
|
|
241
|
+
'base64',
|
|
242
|
+
'securerandom',
|
|
243
|
+
'optparse',
|
|
244
|
+
'logger',
|
|
245
|
+
'stringio',
|
|
246
|
+
'strscan',
|
|
247
|
+
'date',
|
|
248
|
+
'time',
|
|
249
|
+
'bigdecimal',
|
|
250
|
+
'fiddle',
|
|
251
|
+
'readline',
|
|
252
|
+
'io/console',
|
|
253
|
+
'benchmark',
|
|
254
|
+
'minitest',
|
|
255
|
+
'pp',
|
|
256
|
+
'irb',
|
|
257
|
+
'rdoc',
|
|
258
|
+
'psych',
|
|
259
|
+
'zlib',
|
|
260
|
+
'webrick',
|
|
261
|
+
'rexml',
|
|
262
|
+
'rss',
|
|
263
|
+
'drb',
|
|
264
|
+
'mutex_m',
|
|
265
|
+
'observer',
|
|
266
|
+
'singleton',
|
|
267
|
+
'forwardable',
|
|
268
|
+
'delegate',
|
|
269
|
+
'ostruct',
|
|
270
|
+
'open3',
|
|
271
|
+
'shellwords',
|
|
272
|
+
'abbrev',
|
|
273
|
+
'english',
|
|
274
|
+
'find',
|
|
275
|
+
'resolv',
|
|
276
|
+
'ipaddr',
|
|
277
|
+
'un',
|
|
278
|
+
'mkmf',
|
|
279
|
+
]);
|
|
280
|
+
const JAVA_STDLIB_PREFIXES = ['java.', 'javax.', 'jakarta.', 'sun.', 'com.sun.', 'jdk.'];
|
|
281
|
+
const RUST_STDLIB_CRATES = new Set(['std', 'core', 'alloc']);
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Manifest parsers
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
function safeRead(filePath) {
|
|
286
|
+
if (!cachedExists(filePath)) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
return readFileSync(filePath, 'utf-8');
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function safeParseJson(filePath) {
|
|
297
|
+
const text = safeRead(filePath);
|
|
298
|
+
if (!text) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
return JSON.parse(text);
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function loadNodeDeps(repoRoot) {
|
|
309
|
+
const pkgs = new Set();
|
|
310
|
+
const pkg = safeParseJson(join(repoRoot, 'package.json'));
|
|
311
|
+
if (pkg) {
|
|
312
|
+
for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
313
|
+
const deps = pkg[field];
|
|
314
|
+
if (deps && typeof deps === 'object') {
|
|
315
|
+
for (const name of Object.keys(deps)) {
|
|
316
|
+
pkgs.add(name);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return { packages: pkgs };
|
|
322
|
+
}
|
|
323
|
+
function loadPythonDeps(repoRoot) {
|
|
324
|
+
const pkgs = new Set();
|
|
325
|
+
// requirements.txt
|
|
326
|
+
const reqText = safeRead(join(repoRoot, 'requirements.txt'));
|
|
327
|
+
if (reqText) {
|
|
328
|
+
for (const line of reqText.split('\n')) {
|
|
329
|
+
const trimmed = line.trim();
|
|
330
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
// Strip version specifiers: django>=4.0 -> django
|
|
334
|
+
const name = trimmed
|
|
335
|
+
.split(/[>=<!~;\s[]/)[0]
|
|
336
|
+
.trim()
|
|
337
|
+
.toLowerCase()
|
|
338
|
+
.replace(/-/g, '_');
|
|
339
|
+
if (name) {
|
|
340
|
+
pkgs.add(name);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// pyproject.toml — simple line-based parsing
|
|
345
|
+
const pyproject = safeRead(join(repoRoot, 'pyproject.toml'));
|
|
346
|
+
if (pyproject) {
|
|
347
|
+
let inDeps = false;
|
|
348
|
+
for (const line of pyproject.split('\n')) {
|
|
349
|
+
const trimmed = line.trim();
|
|
350
|
+
if (/^\[(project|tool\.poetry)\.?dependencies\]$/i.test(trimmed) ||
|
|
351
|
+
trimmed === '[project]' ||
|
|
352
|
+
trimmed === '[tool.poetry.dependencies]') {
|
|
353
|
+
inDeps = true;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (trimmed.startsWith('[') && inDeps) {
|
|
357
|
+
inDeps = false;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (inDeps) {
|
|
361
|
+
// TOML key = value or "name>=version" in a list
|
|
362
|
+
const match = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=/);
|
|
363
|
+
if (match) {
|
|
364
|
+
const name = match[1].toLowerCase().replace(/-/g, '_');
|
|
365
|
+
if (name !== 'python') {
|
|
366
|
+
pkgs.add(name);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// List items: "django>=4.0"
|
|
370
|
+
const listMatch = trimmed.match(/^"([a-zA-Z0-9_-]+)/);
|
|
371
|
+
if (listMatch) {
|
|
372
|
+
pkgs.add(listMatch[1].toLowerCase().replace(/-/g, '_'));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return { packages: pkgs };
|
|
378
|
+
}
|
|
379
|
+
function loadGoDeps(repoRoot) {
|
|
380
|
+
const pkgs = new Set();
|
|
381
|
+
const meta = {};
|
|
382
|
+
const gomod = safeRead(join(repoRoot, 'go.mod'));
|
|
383
|
+
if (gomod) {
|
|
384
|
+
// Extract module name
|
|
385
|
+
const modMatch = gomod.match(/^module\s+(.+)$/m);
|
|
386
|
+
if (modMatch) {
|
|
387
|
+
meta.module = modMatch[1].trim();
|
|
388
|
+
}
|
|
389
|
+
// Extract require block
|
|
390
|
+
let inRequire = false;
|
|
391
|
+
for (const line of gomod.split('\n')) {
|
|
392
|
+
const trimmed = line.trim();
|
|
393
|
+
if (trimmed === 'require (') {
|
|
394
|
+
inRequire = true;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (trimmed === ')') {
|
|
398
|
+
inRequire = false;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (inRequire) {
|
|
402
|
+
const match = trimmed.match(/^(\S+)\s+/);
|
|
403
|
+
if (match) {
|
|
404
|
+
pkgs.add(match[1]);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Single-line require
|
|
408
|
+
const singleMatch = trimmed.match(/^require\s+(\S+)\s+/);
|
|
409
|
+
if (singleMatch) {
|
|
410
|
+
pkgs.add(singleMatch[1]);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return { packages: pkgs, meta };
|
|
415
|
+
}
|
|
416
|
+
function loadRustDeps(repoRoot) {
|
|
417
|
+
const pkgs = new Set();
|
|
418
|
+
const cargo = safeRead(join(repoRoot, 'Cargo.toml'));
|
|
419
|
+
if (cargo) {
|
|
420
|
+
let inDeps = false;
|
|
421
|
+
for (const line of cargo.split('\n')) {
|
|
422
|
+
const trimmed = line.trim();
|
|
423
|
+
if (/^\[(.*dependencies.*)\]$/i.test(trimmed)) {
|
|
424
|
+
inDeps = true;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (trimmed.startsWith('[') && inDeps) {
|
|
428
|
+
inDeps = false;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (inDeps) {
|
|
432
|
+
const match = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=/);
|
|
433
|
+
if (match) {
|
|
434
|
+
pkgs.add(match[1]);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return { packages: pkgs };
|
|
440
|
+
}
|
|
441
|
+
function loadJavaDeps(repoRoot) {
|
|
442
|
+
const pkgs = new Set();
|
|
443
|
+
// pom.xml — simple regex-based parsing
|
|
444
|
+
const pom = safeRead(join(repoRoot, 'pom.xml'));
|
|
445
|
+
if (pom) {
|
|
446
|
+
const depRegex = /<dependency>\s*<groupId>([^<]+)<\/groupId>\s*<artifactId>([^<]+)<\/artifactId>/gs;
|
|
447
|
+
let m = depRegex.exec(pom);
|
|
448
|
+
while (m !== null) {
|
|
449
|
+
// Store as "groupId:artifactId" for later matching
|
|
450
|
+
pkgs.add(`${m[1]}:${m[2]}`);
|
|
451
|
+
m = depRegex.exec(pom);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// build.gradle — basic regex
|
|
455
|
+
const gradle = safeRead(join(repoRoot, 'build.gradle'));
|
|
456
|
+
const gradleKts = safeRead(join(repoRoot, 'build.gradle.kts'));
|
|
457
|
+
for (const text of [gradle, gradleKts]) {
|
|
458
|
+
if (!text) {
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
// Matches: implementation 'group:artifact:version' or "group:artifact:version"
|
|
462
|
+
const regex = /(?:implementation|api|compileOnly|runtimeOnly|testImplementation)\s+['"]([^'"]+)['"]/g;
|
|
463
|
+
let gm = regex.exec(text);
|
|
464
|
+
while (gm !== null) {
|
|
465
|
+
const parts = gm[1].split(':');
|
|
466
|
+
if (parts.length >= 2) {
|
|
467
|
+
pkgs.add(`${parts[0]}:${parts[1]}`);
|
|
468
|
+
}
|
|
469
|
+
gm = regex.exec(text);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return { packages: pkgs };
|
|
473
|
+
}
|
|
474
|
+
function loadPhpDeps(repoRoot) {
|
|
475
|
+
const pkgs = new Set();
|
|
476
|
+
const composer = safeParseJson(join(repoRoot, 'composer.json'));
|
|
477
|
+
if (composer) {
|
|
478
|
+
for (const field of ['require', 'require-dev']) {
|
|
479
|
+
const deps = composer[field];
|
|
480
|
+
if (deps && typeof deps === 'object') {
|
|
481
|
+
for (const name of Object.keys(deps)) {
|
|
482
|
+
if (name === 'php') {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
pkgs.add(name);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return { packages: pkgs };
|
|
491
|
+
}
|
|
492
|
+
function loadRubyDeps(repoRoot) {
|
|
493
|
+
const pkgs = new Set();
|
|
494
|
+
const gemfile = safeRead(join(repoRoot, 'Gemfile'));
|
|
495
|
+
if (gemfile) {
|
|
496
|
+
const regex = /gem\s+['"]([^'"]+)['"]/g;
|
|
497
|
+
let m = regex.exec(gemfile);
|
|
498
|
+
while (m !== null) {
|
|
499
|
+
pkgs.add(m[1]);
|
|
500
|
+
m = regex.exec(gemfile);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return { packages: pkgs };
|
|
504
|
+
}
|
|
505
|
+
function loadCsharpDeps(repoRoot) {
|
|
506
|
+
const pkgs = new Set();
|
|
507
|
+
// Find .csproj files at root or one level deep
|
|
508
|
+
const candidates = [];
|
|
509
|
+
try {
|
|
510
|
+
const entries = require('fs').readdirSync(repoRoot);
|
|
511
|
+
for (const e of entries) {
|
|
512
|
+
if (e.endsWith('.csproj')) {
|
|
513
|
+
candidates.push(join(repoRoot, e));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
/* ignore */
|
|
519
|
+
}
|
|
520
|
+
for (const csproj of candidates) {
|
|
521
|
+
const text = safeRead(csproj);
|
|
522
|
+
if (!text) {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
const regex = /<PackageReference\s+Include="([^"]+)"/gi;
|
|
526
|
+
let m = regex.exec(text);
|
|
527
|
+
while (m !== null) {
|
|
528
|
+
pkgs.add(m[1]);
|
|
529
|
+
m = regex.exec(text);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return { packages: pkgs };
|
|
533
|
+
}
|
|
534
|
+
// ---------------------------------------------------------------------------
|
|
535
|
+
// Loader
|
|
536
|
+
// ---------------------------------------------------------------------------
|
|
537
|
+
function loadDeps(repoRoot) {
|
|
538
|
+
const cached = depsCache.get(repoRoot);
|
|
539
|
+
if (cached) {
|
|
540
|
+
return cached;
|
|
541
|
+
}
|
|
542
|
+
const result = new Map();
|
|
543
|
+
// TypeScript / JavaScript
|
|
544
|
+
if (cachedExists(join(repoRoot, 'package.json'))) {
|
|
545
|
+
const nodeDeps = loadNodeDeps(repoRoot);
|
|
546
|
+
result.set('typescript', nodeDeps);
|
|
547
|
+
result.set('javascript', nodeDeps);
|
|
548
|
+
result.set('ts', nodeDeps);
|
|
549
|
+
}
|
|
550
|
+
// Python
|
|
551
|
+
if (cachedExists(join(repoRoot, 'requirements.txt')) || cachedExists(join(repoRoot, 'pyproject.toml'))) {
|
|
552
|
+
result.set('python', loadPythonDeps(repoRoot));
|
|
553
|
+
}
|
|
554
|
+
// Go
|
|
555
|
+
if (cachedExists(join(repoRoot, 'go.mod'))) {
|
|
556
|
+
result.set('go', loadGoDeps(repoRoot));
|
|
557
|
+
}
|
|
558
|
+
// Rust
|
|
559
|
+
if (cachedExists(join(repoRoot, 'Cargo.toml'))) {
|
|
560
|
+
result.set('rust', loadRustDeps(repoRoot));
|
|
561
|
+
}
|
|
562
|
+
// Java
|
|
563
|
+
if (cachedExists(join(repoRoot, 'pom.xml')) ||
|
|
564
|
+
cachedExists(join(repoRoot, 'build.gradle')) ||
|
|
565
|
+
cachedExists(join(repoRoot, 'build.gradle.kts'))) {
|
|
566
|
+
result.set('java', loadJavaDeps(repoRoot));
|
|
567
|
+
}
|
|
568
|
+
// PHP
|
|
569
|
+
if (cachedExists(join(repoRoot, 'composer.json'))) {
|
|
570
|
+
result.set('php', loadPhpDeps(repoRoot));
|
|
571
|
+
}
|
|
572
|
+
// Ruby
|
|
573
|
+
if (cachedExists(join(repoRoot, 'Gemfile'))) {
|
|
574
|
+
result.set('ruby', loadRubyDeps(repoRoot));
|
|
575
|
+
}
|
|
576
|
+
// C#
|
|
577
|
+
result.set('csharp', loadCsharpDeps(repoRoot));
|
|
578
|
+
depsCache.set(repoRoot, result);
|
|
579
|
+
return result;
|
|
580
|
+
}
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
// Public API
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
/**
|
|
585
|
+
* Check if an import is an external (third-party) package.
|
|
586
|
+
* Returns the package name if external, null if not detected as external.
|
|
587
|
+
*/
|
|
588
|
+
export function detectExternal(modulePath, lang, repoRoot) {
|
|
589
|
+
// Normalize language key
|
|
590
|
+
const langKey = lang === 'ts' ? 'typescript' : lang;
|
|
591
|
+
// ----- TypeScript / JavaScript -----
|
|
592
|
+
if (langKey === 'typescript' || langKey === 'javascript') {
|
|
593
|
+
// Relative imports are never external
|
|
594
|
+
if (modulePath.startsWith('.') || modulePath.startsWith('#')) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
// Node builtin (with or without node: prefix)
|
|
598
|
+
if (modulePath.startsWith('node:')) {
|
|
599
|
+
return modulePath;
|
|
600
|
+
}
|
|
601
|
+
if (modulePath.startsWith('bun:')) {
|
|
602
|
+
return modulePath;
|
|
603
|
+
}
|
|
604
|
+
if (NODE_BUILTINS.has(modulePath)) {
|
|
605
|
+
return modulePath;
|
|
606
|
+
}
|
|
607
|
+
// Also handle node:XXX/subpath
|
|
608
|
+
const bareNode = modulePath.split('/')[0];
|
|
609
|
+
if (NODE_BUILTINS.has(bareNode)) {
|
|
610
|
+
return bareNode;
|
|
611
|
+
}
|
|
612
|
+
const deps = loadDeps(repoRoot);
|
|
613
|
+
const langDeps = deps.get('typescript');
|
|
614
|
+
if (!langDeps) {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
// Scoped package: @scope/name or @scope/name/subpath
|
|
618
|
+
if (modulePath.startsWith('@')) {
|
|
619
|
+
const parts = modulePath.split('/');
|
|
620
|
+
const scopedName = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : modulePath;
|
|
621
|
+
if (langDeps.packages.has(scopedName)) {
|
|
622
|
+
return scopedName;
|
|
623
|
+
}
|
|
624
|
+
// Bare specifier not in deps but doesn't start with . or # → likely external
|
|
625
|
+
return scopedName;
|
|
626
|
+
}
|
|
627
|
+
// Non-scoped: bare specifier
|
|
628
|
+
const topLevel = modulePath.split('/')[0];
|
|
629
|
+
if (langDeps.packages.has(topLevel)) {
|
|
630
|
+
return topLevel;
|
|
631
|
+
}
|
|
632
|
+
// Bare specifier not found in deps — still likely external (unlisted dep)
|
|
633
|
+
return topLevel;
|
|
634
|
+
}
|
|
635
|
+
// ----- Python -----
|
|
636
|
+
if (langKey === 'python') {
|
|
637
|
+
// Relative imports start with .
|
|
638
|
+
if (modulePath.startsWith('.')) {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
const topLevel = modulePath.split('.')[0].toLowerCase().replace(/-/g, '_');
|
|
642
|
+
// Python stdlib
|
|
643
|
+
if (PYTHON_STDLIB.has(topLevel)) {
|
|
644
|
+
return topLevel;
|
|
645
|
+
}
|
|
646
|
+
const deps = loadDeps(repoRoot);
|
|
647
|
+
const langDeps = deps.get('python');
|
|
648
|
+
if (!langDeps) {
|
|
649
|
+
// No manifest found — check stdlib only
|
|
650
|
+
return PYTHON_STDLIB.has(topLevel) ? topLevel : null;
|
|
651
|
+
}
|
|
652
|
+
if (langDeps.packages.has(topLevel)) {
|
|
653
|
+
return topLevel;
|
|
654
|
+
}
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
// ----- Go -----
|
|
658
|
+
if (langKey === 'go') {
|
|
659
|
+
// Go stdlib: no dot in first segment
|
|
660
|
+
const firstSegment = modulePath.split('/')[0];
|
|
661
|
+
if (!firstSegment.includes('.')) {
|
|
662
|
+
return modulePath;
|
|
663
|
+
}
|
|
664
|
+
const deps = loadDeps(repoRoot);
|
|
665
|
+
const langDeps = deps.get('go');
|
|
666
|
+
if (!langDeps) {
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
// Check if it's the project's own module
|
|
670
|
+
const ownModule = langDeps.meta?.module;
|
|
671
|
+
if (ownModule && modulePath.startsWith(ownModule)) {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
// Check require list — match prefix
|
|
675
|
+
for (const dep of langDeps.packages) {
|
|
676
|
+
if (modulePath === dep || modulePath.startsWith(`${dep}/`)) {
|
|
677
|
+
return dep;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// Has a dot in first segment but not in require list — still likely external
|
|
681
|
+
return modulePath;
|
|
682
|
+
}
|
|
683
|
+
// ----- Rust -----
|
|
684
|
+
if (langKey === 'rust') {
|
|
685
|
+
const firstSegment = modulePath.split('::')[0];
|
|
686
|
+
// crate:: and super:: and self:: are local
|
|
687
|
+
if (firstSegment === 'crate' || firstSegment === 'super' || firstSegment === 'self') {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
// stdlib crates
|
|
691
|
+
if (RUST_STDLIB_CRATES.has(firstSegment)) {
|
|
692
|
+
return firstSegment;
|
|
693
|
+
}
|
|
694
|
+
const deps = loadDeps(repoRoot);
|
|
695
|
+
const langDeps = deps.get('rust');
|
|
696
|
+
if (!langDeps) {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
// Cargo dependency names use hyphens but Rust uses underscores
|
|
700
|
+
const normalized = firstSegment.replace(/-/g, '_');
|
|
701
|
+
for (const dep of langDeps.packages) {
|
|
702
|
+
if (dep.replace(/-/g, '_') === normalized) {
|
|
703
|
+
return dep;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
// ----- Java -----
|
|
709
|
+
if (langKey === 'java') {
|
|
710
|
+
// Java stdlib
|
|
711
|
+
for (const prefix of JAVA_STDLIB_PREFIXES) {
|
|
712
|
+
if (modulePath.startsWith(prefix)) {
|
|
713
|
+
// Return the first two segments (e.g. java.util)
|
|
714
|
+
const parts = modulePath.split('.');
|
|
715
|
+
return parts.slice(0, 2).join('.');
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const deps = loadDeps(repoRoot);
|
|
719
|
+
const langDeps = deps.get('java');
|
|
720
|
+
if (!langDeps) {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
// Match groupId prefix against import path
|
|
724
|
+
// e.g. groupId "org.springframework.boot" -> import "org.springframework.boot.SpringApplication"
|
|
725
|
+
for (const dep of langDeps.packages) {
|
|
726
|
+
const [groupId, artifactId] = dep.split(':');
|
|
727
|
+
if (modulePath.startsWith(groupId)) {
|
|
728
|
+
return artifactId;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
// ----- PHP -----
|
|
734
|
+
if (langKey === 'php') {
|
|
735
|
+
const deps = loadDeps(repoRoot);
|
|
736
|
+
const langDeps = deps.get('php');
|
|
737
|
+
if (!langDeps) {
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
// Get composer.json autoload info for local namespace detection
|
|
741
|
+
const composer = safeParseJson(join(repoRoot, 'composer.json'));
|
|
742
|
+
if (composer) {
|
|
743
|
+
const autoload = composer.autoload;
|
|
744
|
+
if (autoload) {
|
|
745
|
+
const psr4 = autoload['psr-4'];
|
|
746
|
+
if (psr4) {
|
|
747
|
+
// Normalize import path separators
|
|
748
|
+
const normalized = modulePath.replace(/\//g, '\\');
|
|
749
|
+
for (const ns of Object.keys(psr4)) {
|
|
750
|
+
if (normalized.startsWith(ns)) {
|
|
751
|
+
return null; // local namespace
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
// Check known package → namespace mappings
|
|
758
|
+
// Common Composer package namespace mappings
|
|
759
|
+
const COMPOSER_NS_MAP = {
|
|
760
|
+
'laravel/framework': ['Illuminate\\'],
|
|
761
|
+
'guzzlehttp/guzzle': ['GuzzleHttp\\'],
|
|
762
|
+
'symfony/console': ['Symfony\\Component\\Console\\'],
|
|
763
|
+
'symfony/http-foundation': ['Symfony\\Component\\HttpFoundation\\'],
|
|
764
|
+
'monolog/monolog': ['Monolog\\'],
|
|
765
|
+
'doctrine/orm': ['Doctrine\\ORM\\'],
|
|
766
|
+
'phpunit/phpunit': ['PHPUnit\\'],
|
|
767
|
+
};
|
|
768
|
+
const normalized = modulePath.replace(/\//g, '\\');
|
|
769
|
+
for (const dep of langDeps.packages) {
|
|
770
|
+
const namespaces = COMPOSER_NS_MAP[dep];
|
|
771
|
+
if (namespaces) {
|
|
772
|
+
for (const ns of namespaces) {
|
|
773
|
+
if (normalized.startsWith(ns)) {
|
|
774
|
+
return dep;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
// ----- Ruby -----
|
|
782
|
+
if (langKey === 'ruby') {
|
|
783
|
+
// Ruby stdlib
|
|
784
|
+
if (RUBY_STDLIB.has(modulePath)) {
|
|
785
|
+
return modulePath;
|
|
786
|
+
}
|
|
787
|
+
const deps = loadDeps(repoRoot);
|
|
788
|
+
const langDeps = deps.get('ruby');
|
|
789
|
+
if (!langDeps) {
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
if (langDeps.packages.has(modulePath)) {
|
|
793
|
+
return modulePath;
|
|
794
|
+
}
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
// ----- C# -----
|
|
798
|
+
if (langKey === 'csharp') {
|
|
799
|
+
// Framework namespaces
|
|
800
|
+
if (modulePath.startsWith('System.') ||
|
|
801
|
+
modulePath === 'System' ||
|
|
802
|
+
modulePath.startsWith('Microsoft.') ||
|
|
803
|
+
modulePath === 'Microsoft') {
|
|
804
|
+
return modulePath.split('.').slice(0, 2).join('.');
|
|
805
|
+
}
|
|
806
|
+
const deps = loadDeps(repoRoot);
|
|
807
|
+
const langDeps = deps.get('csharp');
|
|
808
|
+
if (!langDeps) {
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
// Match PackageReference names against import namespace
|
|
812
|
+
for (const dep of langDeps.packages) {
|
|
813
|
+
if (modulePath.startsWith(dep)) {
|
|
814
|
+
return dep;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
return null;
|
|
820
|
+
}
|