@chappibunny/repolens 1.15.1 → 1.15.3
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/CHANGELOG.md +60 -0
- package/README.md +82 -6
- package/llms-full.txt +84 -0
- package/llms-install.md +6 -0
- package/llms.txt +50 -0
- package/package.json +4 -2
- package/src/analyzers/context-builder.js +134 -79
- package/src/analyzers/dependency-graph.js +133 -53
- package/src/analyzers/domain-inference.js +45 -7
- package/src/analyzers/import-parsers.js +810 -0
- package/src/badge.js +274 -0
- package/src/cli-args.js +11 -1
- package/src/cli.js +24 -1
- package/src/core/scan.js +4 -0
- package/src/delivery/architecture-comment.js +480 -0
- package/src/delivery/comment-constants.js +15 -0
- package/src/delivery/comment.js +9 -5
- package/src/docs/generate-doc-set.js +3 -2
- package/src/docs/llms-text.js +180 -0
- package/src/help.js +73 -2
- package/src/renderers/render.js +33 -32
- package/src/renderers/renderAnalysis.js +2 -2
- package/src/watch.js +0 -1
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
// Import extraction for the dependency graph.
|
|
2
|
+
// Comments, strings, and template text are not imports. Type-only
|
|
3
|
+
// TypeScript imports are returned with typeOnly: true so they do not
|
|
4
|
+
// become runtime edges.
|
|
5
|
+
|
|
6
|
+
const PYTHON_STDLIB = new Set([
|
|
7
|
+
"__future__", "abc", "aifc", "argparse", "array", "ast", "asynchat", "asyncio",
|
|
8
|
+
"asyncore", "atexit", "base64", "bdb", "binascii", "binhex", "bisect", "builtins",
|
|
9
|
+
"bz2", "calendar", "cgi", "cgitb", "chunk", "cmath", "cmd", "code", "codecs",
|
|
10
|
+
"codeop", "collections", "colorsys", "compileall", "concurrent", "configparser",
|
|
11
|
+
"contextlib", "contextvars", "copy", "copyreg", "cProfile", "crypt", "csv",
|
|
12
|
+
"ctypes", "curses", "dataclasses", "datetime", "dbm", "decimal", "difflib",
|
|
13
|
+
"dis", "distutils", "doctest", "email", "encodings", "enum", "errno", "faulthandler",
|
|
14
|
+
"fcntl", "filecmp", "fileinput", "fnmatch", "fractions", "ftplib", "functools",
|
|
15
|
+
"gc", "getopt", "getpass", "gettext", "glob", "graphlib", "grp", "gzip", "hashlib",
|
|
16
|
+
"heapq", "hmac", "html", "http", "idlelib", "imaplib", "imghdr", "imp", "importlib",
|
|
17
|
+
"inspect", "io", "ipaddress", "itertools", "json", "keyword", "lib2to3", "linecache",
|
|
18
|
+
"locale", "logging", "lzma", "mailbox", "mailcap", "marshal", "math", "mimetypes",
|
|
19
|
+
"mmap", "modulefinder", "msvcrt", "multiprocessing", "netrc", "nis", "nntplib", "nt", "ntpath", "numbers",
|
|
20
|
+
"operator", "optparse", "os", "ossaudiodev", "pathlib", "pdb", "pickle", "pickletools",
|
|
21
|
+
"pipes", "pkgutil", "platform", "plistlib", "poplib", "posix", "posixpath", "pprint",
|
|
22
|
+
"profile", "pstats", "pty", "pwd", "py_compile", "pyclbr", "pydoc", "queue", "quopri",
|
|
23
|
+
"random", "re", "readline", "reprlib", "resource", "rlcompleter", "runpy", "sched",
|
|
24
|
+
"secrets", "select", "selectors", "shelve", "shlex", "shutil", "signal", "site",
|
|
25
|
+
"smtpd", "smtplib", "sndhdr", "socket", "socketserver", "sqlite3", "ssl", "stat",
|
|
26
|
+
"statistics", "string", "stringprep", "struct", "subprocess", "sunau", "symtable",
|
|
27
|
+
"sys", "sysconfig", "syslog", "tabnanny", "tarfile", "telnetlib", "tempfile", "termios",
|
|
28
|
+
"test", "textwrap", "threading", "time", "timeit", "tkinter", "token", "tokenize",
|
|
29
|
+
"tomllib", "trace", "traceback", "tracemalloc", "tty", "turtle", "turtledemo", "types",
|
|
30
|
+
"typing", "unicodedata", "unittest", "urllib", "uu", "uuid", "venv", "warnings",
|
|
31
|
+
"wave", "weakref", "webbrowser", "winreg", "winsound", "wsgiref", "xdrlib", "xml",
|
|
32
|
+
"xmlrpc", "zipapp", "zipfile", "zipimport", "zlib", "zoneinfo",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function isIdentStart(ch) {
|
|
36
|
+
return /[A-Za-z_$]/.test(ch || "");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isIdentPart(ch) {
|
|
40
|
+
return /[A-Za-z0-9_$]/.test(ch || "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function skipWs(source, i) {
|
|
44
|
+
while (i < source.length && /[\t\n\r \f]/.test(source[i])) i += 1;
|
|
45
|
+
return i;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readWord(source, i) {
|
|
49
|
+
const start = i;
|
|
50
|
+
while (i < source.length && isIdentPart(source[i])) i += 1;
|
|
51
|
+
return { word: source.slice(start, i), next: i };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function skipLineComment(source, i) {
|
|
55
|
+
const nl = source.indexOf("\n", i);
|
|
56
|
+
return nl === -1 ? source.length : nl + 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function skipBlockComment(source, i) {
|
|
60
|
+
const end = source.indexOf("*/", i + 2);
|
|
61
|
+
return end === -1 ? source.length : end + 2;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function skipQuoted(source, i) {
|
|
65
|
+
const quote = source[i];
|
|
66
|
+
i += 1;
|
|
67
|
+
while (i < source.length) {
|
|
68
|
+
if (source[i] === "\\") {
|
|
69
|
+
i += 2;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (source[i] === quote) return i + 1;
|
|
73
|
+
if (source[i] === "\n") return i;
|
|
74
|
+
i += 1;
|
|
75
|
+
}
|
|
76
|
+
return i;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readJsString(source, i) {
|
|
80
|
+
i = skipWs(source, i);
|
|
81
|
+
const quote = source[i];
|
|
82
|
+
if (quote !== "'" && quote !== '"') return null;
|
|
83
|
+
let j = i + 1;
|
|
84
|
+
let value = "";
|
|
85
|
+
while (j < source.length) {
|
|
86
|
+
if (source[j] === "\\") {
|
|
87
|
+
value += source[j + 1] === "n" ? "\n" : (source[j + 1] || "");
|
|
88
|
+
j += 2;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (source[j] === quote) return { value, next: j + 1 };
|
|
92
|
+
if (source[j] === "\n") return null;
|
|
93
|
+
value += source[j];
|
|
94
|
+
j += 1;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function skipRegex(source, i) {
|
|
100
|
+
i += 1;
|
|
101
|
+
let inClass = false;
|
|
102
|
+
while (i < source.length) {
|
|
103
|
+
if (source[i] === "\\") {
|
|
104
|
+
i += 2;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (source[i] === "[") inClass = true;
|
|
108
|
+
else if (source[i] === "]") inClass = false;
|
|
109
|
+
else if (source[i] === "/" && !inClass) return i + 1;
|
|
110
|
+
else if (source[i] === "\n") return i;
|
|
111
|
+
i += 1;
|
|
112
|
+
}
|
|
113
|
+
return i;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function prevCodeChar(source, i) {
|
|
117
|
+
let j = i - 1;
|
|
118
|
+
while (j >= 0 && /[\t\n\r \f]/.test(source[j])) j -= 1;
|
|
119
|
+
return j >= 0 ? source[j] : "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function canStartRegex(ch) {
|
|
123
|
+
return ch === "" || "([{:;,!?&|~^=<>+-*%".includes(ch);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function splitTopCommas(inner) {
|
|
127
|
+
const parts = [];
|
|
128
|
+
let current = "";
|
|
129
|
+
let depth = 0;
|
|
130
|
+
for (const ch of inner) {
|
|
131
|
+
if (ch === "{" || ch === "(" || ch === "[") depth += 1;
|
|
132
|
+
else if (ch === "}" || ch === ")" || ch === "]") depth -= 1;
|
|
133
|
+
if (ch === "," && depth === 0) {
|
|
134
|
+
parts.push(current);
|
|
135
|
+
current = "";
|
|
136
|
+
} else {
|
|
137
|
+
current += ch;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (current.trim()) parts.push(current);
|
|
141
|
+
return parts;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function braceIsTypeOnly(inner) {
|
|
145
|
+
const parts = splitTopCommas(inner).map((part) => part.trim()).filter(Boolean);
|
|
146
|
+
if (parts.length === 0) return true;
|
|
147
|
+
return parts.every((part) => /^type\s+/.test(part) && !/^type\s+as\b/.test(part));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function findMatchingBrace(source, openIndex) {
|
|
151
|
+
let depth = 0;
|
|
152
|
+
for (let i = openIndex; i < source.length; i += 1) {
|
|
153
|
+
if (source[i] === "{") depth += 1;
|
|
154
|
+
else if (source[i] === "}") {
|
|
155
|
+
depth -= 1;
|
|
156
|
+
if (depth === 0) return i;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return -1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function skipAsBinding(source, i) {
|
|
163
|
+
let j = skipWs(source, i);
|
|
164
|
+
const word = readWord(source, j);
|
|
165
|
+
if (word.word !== "as") return i;
|
|
166
|
+
j = skipWs(source, word.next);
|
|
167
|
+
const name = readWord(source, j);
|
|
168
|
+
return name.word ? name.next : j;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function readFromClause(source, i) {
|
|
172
|
+
i = skipAsBinding(source, i);
|
|
173
|
+
i = skipWs(source, i);
|
|
174
|
+
const word = readWord(source, i);
|
|
175
|
+
if (word.word !== "from") return null;
|
|
176
|
+
const lit = readJsString(source, word.next);
|
|
177
|
+
if (!lit) return null;
|
|
178
|
+
return lit;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* @returns {{ specifier: string, typeOnly: boolean }[]}
|
|
183
|
+
*/
|
|
184
|
+
export function extractJsImports(source) {
|
|
185
|
+
const imports = [];
|
|
186
|
+
const n = source.length;
|
|
187
|
+
let i = 0;
|
|
188
|
+
|
|
189
|
+
function add(specifier, typeOnly) {
|
|
190
|
+
if (typeof specifier !== "string") return;
|
|
191
|
+
imports.push({ specifier, typeOnly: Boolean(typeOnly) });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function parseRequire(at) {
|
|
195
|
+
const i0 = skipWs(source, at);
|
|
196
|
+
if (source[i0] !== "(") return at;
|
|
197
|
+
const lit = readJsString(source, i0 + 1);
|
|
198
|
+
if (!lit) return at;
|
|
199
|
+
const after = skipWs(source, lit.next);
|
|
200
|
+
if (source[after] !== ")") return at;
|
|
201
|
+
add(lit.value, false);
|
|
202
|
+
return after + 1;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseImport(at) {
|
|
206
|
+
let j = skipWs(source, at);
|
|
207
|
+
if (source[j] === "(") {
|
|
208
|
+
const lit = readJsString(source, j + 1);
|
|
209
|
+
if (!lit) return at;
|
|
210
|
+
const after = skipWs(source, lit.next);
|
|
211
|
+
if (source[after] !== ")") return at;
|
|
212
|
+
add(lit.value, false);
|
|
213
|
+
return after + 1;
|
|
214
|
+
}
|
|
215
|
+
if (source[j] === ".") return at;
|
|
216
|
+
|
|
217
|
+
let typeOnly = false;
|
|
218
|
+
const first = readWord(source, j);
|
|
219
|
+
if (first.word === "type") {
|
|
220
|
+
typeOnly = true;
|
|
221
|
+
j = skipWs(source, first.next);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (source[j] === "{") {
|
|
225
|
+
const close = findMatchingBrace(source, j);
|
|
226
|
+
if (close === -1) return at;
|
|
227
|
+
const inner = source.slice(j + 1, close);
|
|
228
|
+
const clause = readFromClause(source, close + 1);
|
|
229
|
+
if (!clause) return close + 1;
|
|
230
|
+
const onlyTypes = typeOnly || braceIsTypeOnly(inner);
|
|
231
|
+
add(clause.value, onlyTypes);
|
|
232
|
+
return clause.next;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (source[j] === "*" || isIdentStart(source[j]) || source[j] === "'" || source[j] === '"') {
|
|
236
|
+
if (source[j] === "'" || source[j] === '"') {
|
|
237
|
+
const lit = readJsString(source, j);
|
|
238
|
+
if (lit) add(lit.value, typeOnly);
|
|
239
|
+
return lit ? lit.next : j;
|
|
240
|
+
}
|
|
241
|
+
// default binding, namespace, or `name, { ... }`
|
|
242
|
+
let sawValue = !typeOnly;
|
|
243
|
+
if (source[j] !== "*") {
|
|
244
|
+
const binding = readWord(source, j);
|
|
245
|
+
j = skipWs(source, binding.next);
|
|
246
|
+
if (source[j] === ",") {
|
|
247
|
+
sawValue = !typeOnly;
|
|
248
|
+
j = skipWs(source, j + 1);
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
j = skipWs(source, j + 1);
|
|
252
|
+
j = skipAsBinding(source, j);
|
|
253
|
+
j = skipWs(source, j);
|
|
254
|
+
}
|
|
255
|
+
if (source[j] === "{") {
|
|
256
|
+
const close = findMatchingBrace(source, j);
|
|
257
|
+
if (close === -1) return j;
|
|
258
|
+
const inner = source.slice(j + 1, close);
|
|
259
|
+
const clause = readFromClause(source, close + 1);
|
|
260
|
+
if (!clause) return close + 1;
|
|
261
|
+
const onlyTypes = typeOnly || (!sawValue && braceIsTypeOnly(inner));
|
|
262
|
+
add(clause.value, onlyTypes);
|
|
263
|
+
return clause.next;
|
|
264
|
+
}
|
|
265
|
+
const clause = readFromClause(source, j);
|
|
266
|
+
if (!clause) return at;
|
|
267
|
+
add(clause.value, typeOnly);
|
|
268
|
+
return clause.next;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return at;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function parseExport(at) {
|
|
275
|
+
let j = skipWs(source, at);
|
|
276
|
+
let typeOnly = false;
|
|
277
|
+
const first = readWord(source, j);
|
|
278
|
+
if (first.word === "type") {
|
|
279
|
+
typeOnly = true;
|
|
280
|
+
j = skipWs(source, first.next);
|
|
281
|
+
}
|
|
282
|
+
if (source[j] === "*") {
|
|
283
|
+
const clause = readFromClause(source, j + 1);
|
|
284
|
+
if (!clause) return at;
|
|
285
|
+
add(clause.value, typeOnly);
|
|
286
|
+
return clause.next;
|
|
287
|
+
}
|
|
288
|
+
if (source[j] === "{") {
|
|
289
|
+
const close = findMatchingBrace(source, j);
|
|
290
|
+
if (close === -1) return at;
|
|
291
|
+
const inner = source.slice(j + 1, close);
|
|
292
|
+
const clause = readFromClause(source, close + 1);
|
|
293
|
+
if (!clause) return close + 1;
|
|
294
|
+
add(clause.value, typeOnly || braceIsTypeOnly(inner));
|
|
295
|
+
return clause.next;
|
|
296
|
+
}
|
|
297
|
+
return at;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
while (i < n) {
|
|
301
|
+
const ch = source[i];
|
|
302
|
+
if (ch === "/" && source[i + 1] === "/") {
|
|
303
|
+
i = skipLineComment(source, i);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (ch === "/" && source[i + 1] === "*") {
|
|
307
|
+
i = skipBlockComment(source, i);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (ch === "'" || ch === '"') {
|
|
311
|
+
i = skipQuoted(source, i);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (ch === "`") {
|
|
315
|
+
i = scanTemplate(source, i + 1, (inner, from, to) => {
|
|
316
|
+
// Scan the expression as code by continuing the outer walk on a slice.
|
|
317
|
+
// Handled below by recursion on the expression text with an offset.
|
|
318
|
+
const found = extractJsImports(source.slice(from, to));
|
|
319
|
+
for (const item of found) imports.push(item);
|
|
320
|
+
});
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (ch === "/" && canStartRegex(prevCodeChar(source, i))) {
|
|
324
|
+
i = skipRegex(source, i);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (isIdentStart(ch)) {
|
|
328
|
+
const word = readWord(source, i);
|
|
329
|
+
const prev = prevCodeChar(source, i);
|
|
330
|
+
i = word.next;
|
|
331
|
+
if (prev === ".") continue;
|
|
332
|
+
if (word.word === "import") i = parseImport(i);
|
|
333
|
+
else if (word.word === "export") i = parseExport(i);
|
|
334
|
+
else if (word.word === "require") i = parseRequire(i);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
i += 1;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return imports;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function scanTemplate(source, i, onExpression) {
|
|
344
|
+
while (i < source.length) {
|
|
345
|
+
if (source[i] === "\\") {
|
|
346
|
+
i += 2;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (source[i] === "`") return i + 1;
|
|
350
|
+
if (source[i] === "$" && source[i + 1] === "{") {
|
|
351
|
+
const close = findMatchingBrace(source, i + 1);
|
|
352
|
+
if (close === -1) return source.length;
|
|
353
|
+
onExpression(source, i + 2, close);
|
|
354
|
+
i = close + 1;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
i += 1;
|
|
358
|
+
}
|
|
359
|
+
return i;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const NODE_BUILTINS = new Set([
|
|
363
|
+
"assert", "async_hooks", "buffer", "child_process", "cluster", "console",
|
|
364
|
+
"constants", "crypto", "dgram", "diagnostics_channel", "dns", "domain",
|
|
365
|
+
"events", "fs", "http", "http2", "https", "inspector", "module", "net",
|
|
366
|
+
"os", "path", "perf_hooks", "process", "punycode", "querystring",
|
|
367
|
+
"readline", "repl", "sqlite", "stream", "string_decoder", "sys", "timers",
|
|
368
|
+
"tls", "trace_events", "tty", "url", "util", "v8", "vm", "wasi",
|
|
369
|
+
"worker_threads", "zlib",
|
|
370
|
+
]);
|
|
371
|
+
|
|
372
|
+
export function externalPackageName(specifier) {
|
|
373
|
+
if (typeof specifier !== "string") return null;
|
|
374
|
+
const trimmed = specifier.trim();
|
|
375
|
+
if (!trimmed || trimmed.startsWith(".") || trimmed.startsWith("/") || trimmed.startsWith("node:")) return null;
|
|
376
|
+
if (trimmed.includes("://") || trimmed.startsWith("#")) return null;
|
|
377
|
+
const parts = trimmed.startsWith("@")
|
|
378
|
+
? trimmed.split("/").slice(0, 2)
|
|
379
|
+
: trimmed.split("/").slice(0, 1);
|
|
380
|
+
const name = parts.filter(Boolean).join("/");
|
|
381
|
+
if (!name || name === "." || name === "..") return null;
|
|
382
|
+
const top = name.startsWith("@") ? null : name.split("/")[0];
|
|
383
|
+
if (top && NODE_BUILTINS.has(top)) return null;
|
|
384
|
+
return name;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function stripPythonNoise(source) {
|
|
388
|
+
let out = "";
|
|
389
|
+
let i = 0;
|
|
390
|
+
while (i < source.length) {
|
|
391
|
+
const ch = source[i];
|
|
392
|
+
if (ch === "#") {
|
|
393
|
+
const nl = source.indexOf("\n", i);
|
|
394
|
+
out += " ";
|
|
395
|
+
if (nl === -1) break;
|
|
396
|
+
i = nl;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (ch === '"' || ch === "'") {
|
|
400
|
+
const triple = source.slice(i, i + 3) === ch.repeat(3);
|
|
401
|
+
const quote = triple ? ch.repeat(3) : ch;
|
|
402
|
+
out += " ";
|
|
403
|
+
i += quote.length;
|
|
404
|
+
while (i < source.length) {
|
|
405
|
+
if (source[i] === "\\") {
|
|
406
|
+
out += source[i + 1] === "\n" ? "\n" : " ";
|
|
407
|
+
i += 2;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (source.startsWith(quote, i)) {
|
|
411
|
+
i += quote.length;
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
out += source[i] === "\n" ? "\n" : " ";
|
|
415
|
+
i += 1;
|
|
416
|
+
}
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
out += ch;
|
|
420
|
+
i += 1;
|
|
421
|
+
}
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function leadingIndent(line) {
|
|
426
|
+
const match = String(line).match(/^[ \t]*/);
|
|
427
|
+
return match ? match[0].length : 0;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function noteTypingBindings(statement, aliases, flags) {
|
|
431
|
+
const imported = statement.match(/^import\s+(.+)$/);
|
|
432
|
+
if (imported) {
|
|
433
|
+
for (const part of imported[1].split(",")) {
|
|
434
|
+
const as = part.trim().match(/^([\w.]+)(?:\s+as\s+([A-Za-z_]\w*))?$/);
|
|
435
|
+
if (!as) continue;
|
|
436
|
+
if (as[1] !== "typing" && as[1] !== "typing_extensions") continue;
|
|
437
|
+
aliases.add(as[2] || as[1]);
|
|
438
|
+
}
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const from = statement.match(/^from\s+(typing|typing_extensions)\s+import\s+(.+)$/);
|
|
442
|
+
if (!from) return;
|
|
443
|
+
for (const part of from[2].replace(/[()]/g, " ").split(",")) {
|
|
444
|
+
const as = part.trim().match(/^([A-Za-z_]\w*)(?:\s+as\s+([A-Za-z_]\w*))?$/);
|
|
445
|
+
if (!as || as[1] !== "TYPE_CHECKING") continue;
|
|
446
|
+
flags.add(as[2] || as[1]);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function isTypeCheckingGuard(statement, aliases, flags) {
|
|
451
|
+
const match = statement.match(/^(?:if|elif)\s+(.+?)\s*:\s*$/);
|
|
452
|
+
if (!match) return false;
|
|
453
|
+
const cond = match[1].trim();
|
|
454
|
+
if (cond === "TYPE_CHECKING" || flags.has(cond)) return true;
|
|
455
|
+
if (cond === "typing.TYPE_CHECKING") return true;
|
|
456
|
+
const attr = cond.match(/^([A-Za-z_]\w*)\.TYPE_CHECKING$/);
|
|
457
|
+
return Boolean(attr && aliases.has(attr[1]));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function pushPythonImport(imports, statement, typeOnly) {
|
|
461
|
+
if (statement.startsWith("import ")) {
|
|
462
|
+
const rest = statement.slice("import ".length).replace(/[()]/g, " ");
|
|
463
|
+
for (const part of rest.split(",")) {
|
|
464
|
+
const name = part.trim().split(/\s+/)[0];
|
|
465
|
+
if (name && /^[\w.]+$/.test(name)) imports.push({ module: name, level: 0, typeOnly });
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const relative = statement.match(/^from\s+(\.+)([\w.]*)\s+import\s+(.+)$/);
|
|
470
|
+
if (relative) {
|
|
471
|
+
const level = relative[1].length;
|
|
472
|
+
const base = relative[2] || "";
|
|
473
|
+
if (base) {
|
|
474
|
+
imports.push({ module: base, level, typeOnly });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
for (const part of relative[3].replace(/[()]/g, " ").split(",")) {
|
|
478
|
+
const name = part.trim().split(/\s+/)[0];
|
|
479
|
+
if (name && /^[A-Za-z_]\w*$/.test(name)) imports.push({ module: name, level, typeOnly });
|
|
480
|
+
}
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const match = statement.match(/^from\s+([\w.]+)\s+import\b/);
|
|
484
|
+
if (!match) return;
|
|
485
|
+
imports.push({ module: match[1], level: 0, typeOnly });
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* @returns {{ module: string, level: number, typeOnly: boolean }[]}
|
|
490
|
+
* level 0 is an absolute import. level N is a relative import with N dots.
|
|
491
|
+
* Imports under `if TYPE_CHECKING:` / `if typing.TYPE_CHECKING:` (including
|
|
492
|
+
* `import typing as t`) are typeOnly and must not become runtime edges.
|
|
493
|
+
*/
|
|
494
|
+
export function extractPythonImports(source) {
|
|
495
|
+
const stripped = stripPythonNoise(source).replace(/\\\n/g, " ");
|
|
496
|
+
const lines = stripped.split("\n");
|
|
497
|
+
const imports = [];
|
|
498
|
+
const aliases = new Set(["typing"]);
|
|
499
|
+
const flags = new Set(["TYPE_CHECKING"]);
|
|
500
|
+
const blockIndents = [];
|
|
501
|
+
let buf = "";
|
|
502
|
+
let depth = 0;
|
|
503
|
+
let bufTypeOnly = false;
|
|
504
|
+
let bufIndent = 0;
|
|
505
|
+
|
|
506
|
+
function inTypeChecking(indent) {
|
|
507
|
+
while (blockIndents.length && indent <= blockIndents[blockIndents.length - 1]) {
|
|
508
|
+
blockIndents.pop();
|
|
509
|
+
}
|
|
510
|
+
return blockIndents.length > 0;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function finishStatement() {
|
|
514
|
+
const statement = buf.trim();
|
|
515
|
+
buf = "";
|
|
516
|
+
depth = 0;
|
|
517
|
+
if (!statement) return;
|
|
518
|
+
if (bufTypeOnly) {
|
|
519
|
+
pushPythonImport(imports, statement, true);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (isTypeCheckingGuard(statement, aliases, flags)) {
|
|
523
|
+
blockIndents.push(bufIndent);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
noteTypingBindings(statement, aliases, flags);
|
|
527
|
+
pushPythonImport(imports, statement, false);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
for (const line of lines) {
|
|
531
|
+
const trimmed = line.trim();
|
|
532
|
+
if (!trimmed && depth === 0) continue;
|
|
533
|
+
if (depth === 0) {
|
|
534
|
+
bufTypeOnly = inTypeChecking(leadingIndent(line));
|
|
535
|
+
bufIndent = leadingIndent(line);
|
|
536
|
+
buf = trimmed;
|
|
537
|
+
} else {
|
|
538
|
+
buf = `${buf} ${trimmed}`;
|
|
539
|
+
}
|
|
540
|
+
depth += (line.match(/\(/g) || []).length;
|
|
541
|
+
depth -= (line.match(/\)/g) || []).length;
|
|
542
|
+
if (depth > 0) continue;
|
|
543
|
+
finishStatement();
|
|
544
|
+
}
|
|
545
|
+
if (buf) finishStatement();
|
|
546
|
+
return imports;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function packageRootDirs(files) {
|
|
550
|
+
const initDirs = new Set();
|
|
551
|
+
for (const file of files) {
|
|
552
|
+
const norm = String(file).replace(/\\/g, "/");
|
|
553
|
+
if (!/\/__init__\.pyw?$/.test(norm)) continue;
|
|
554
|
+
initDirs.add(norm.slice(0, norm.lastIndexOf("/")));
|
|
555
|
+
}
|
|
556
|
+
const roots = [];
|
|
557
|
+
for (const dir of initDirs) {
|
|
558
|
+
const parent = dir.includes("/") ? dir.slice(0, dir.lastIndexOf("/")) : "";
|
|
559
|
+
if (!initDirs.has(parent)) roots.push(dir);
|
|
560
|
+
}
|
|
561
|
+
return roots;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export function pythonModuleName(file, roots = []) {
|
|
565
|
+
let rel = String(file).replace(/\\/g, "/");
|
|
566
|
+
const sorted = [...roots].sort((a, b) => b.length - a.length);
|
|
567
|
+
for (const root of sorted) {
|
|
568
|
+
if (rel !== root && !rel.startsWith(`${root}/`)) continue;
|
|
569
|
+
const parent = root.includes("/") ? root.slice(0, root.lastIndexOf("/")) : "";
|
|
570
|
+
rel = parent ? rel.slice(parent.length + 1) : rel;
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
if (rel.startsWith("src/")) rel = rel.slice(4);
|
|
574
|
+
rel = rel.replace(/\.pyw?$/, "");
|
|
575
|
+
if (rel.endsWith("/__init__")) rel = rel.slice(0, -"/__init__".length);
|
|
576
|
+
if (rel === "__init__") return "";
|
|
577
|
+
return rel.replace(/\//g, ".");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export function pythonPackageName(file, roots = []) {
|
|
581
|
+
const rel = String(file).replace(/\\/g, "/");
|
|
582
|
+
const init = rel.endsWith("/__init__.py") || rel.endsWith("/__init__.pyw");
|
|
583
|
+
const mod = pythonModuleName(file, roots);
|
|
584
|
+
if (init) return mod;
|
|
585
|
+
const parts = mod.split(".").filter(Boolean);
|
|
586
|
+
parts.pop();
|
|
587
|
+
return parts.join(".");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export function resolvePythonImport(file, imported, index) {
|
|
591
|
+
const level = imported.level || 0;
|
|
592
|
+
let dotted;
|
|
593
|
+
const roots = index.packageRoots || [];
|
|
594
|
+
if (level > 0) {
|
|
595
|
+
const pkg = pythonPackageName(file, roots);
|
|
596
|
+
const parts = pkg ? pkg.split(".") : [];
|
|
597
|
+
const up = level - 1;
|
|
598
|
+
if (up > parts.length) return { kind: "unresolved" };
|
|
599
|
+
const base = parts.slice(0, parts.length - up);
|
|
600
|
+
if (imported.module) base.push(...imported.module.split(".").filter(Boolean));
|
|
601
|
+
dotted = base.join(".");
|
|
602
|
+
} else {
|
|
603
|
+
dotted = imported.module;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!dotted) return { kind: "unresolved" };
|
|
607
|
+
if (index.has(dotted)) return { kind: "internal", file: index.get(dotted) };
|
|
608
|
+
|
|
609
|
+
if (level > 0) {
|
|
610
|
+
const parent = dotted.split(".").slice(0, -1).join(".");
|
|
611
|
+
if (parent && index.has(parent)) return { kind: "internal", file: index.get(parent) };
|
|
612
|
+
return { kind: "unresolved" };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const top = dotted.split(".")[0];
|
|
616
|
+
if (!PYTHON_STDLIB.has(top)) {
|
|
617
|
+
const nearby = sameDirectoryModule(file, dotted, index);
|
|
618
|
+
if (nearby) return { kind: "internal", file: nearby };
|
|
619
|
+
}
|
|
620
|
+
if (index.has(top) || [...index.keys()].some((name) => name.startsWith(`${top}.`))) {
|
|
621
|
+
return { kind: "unresolved" };
|
|
622
|
+
}
|
|
623
|
+
if (PYTHON_STDLIB.has(top)) return { kind: "stdlib" };
|
|
624
|
+
return { kind: "external", name: top };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function sameDirectoryModule(file, dotted, index) {
|
|
628
|
+
const norm = String(file).replace(/\\/g, "/");
|
|
629
|
+
const slash = norm.lastIndexOf("/");
|
|
630
|
+
const dir = slash === -1 ? "" : norm.slice(0, slash);
|
|
631
|
+
const rel = dotted.replace(/\./g, "/");
|
|
632
|
+
const prefix = dir ? `${dir}/` : "";
|
|
633
|
+
const candidates = new Set([
|
|
634
|
+
`${prefix}${rel}.py`,
|
|
635
|
+
`${prefix}${rel}.pyw`,
|
|
636
|
+
`${prefix}${rel}/__init__.py`,
|
|
637
|
+
`${prefix}${rel}/__init__.pyw`,
|
|
638
|
+
]);
|
|
639
|
+
for (const known of index.values()) {
|
|
640
|
+
if (candidates.has(String(known).replace(/\\/g, "/"))) return known;
|
|
641
|
+
}
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export function indexPythonModules(files) {
|
|
646
|
+
const roots = packageRootDirs(files);
|
|
647
|
+
const index = new Map();
|
|
648
|
+
index.packageRoots = roots;
|
|
649
|
+
const looseStems = new Map();
|
|
650
|
+
for (const file of files) {
|
|
651
|
+
const name = pythonModuleName(file, roots);
|
|
652
|
+
if (name && !index.has(name)) index.set(name, file);
|
|
653
|
+
const norm = String(file).replace(/\\/g, "/");
|
|
654
|
+
const insidePackage = roots.some((root) => norm === `${root}/__init__.py` || norm === `${root}/__init__.pyw` || norm.startsWith(`${root}/`));
|
|
655
|
+
if (insidePackage) continue;
|
|
656
|
+
const stem = norm.split("/").pop().replace(/\.pyw?$/, "");
|
|
657
|
+
if (!stem || PYTHON_STDLIB.has(stem)) continue;
|
|
658
|
+
if (looseStems.has(stem)) looseStems.set(stem, null);
|
|
659
|
+
else looseStems.set(stem, file);
|
|
660
|
+
}
|
|
661
|
+
for (const [stem, file] of looseStems) {
|
|
662
|
+
if (file && !index.has(stem)) index.set(stem, file);
|
|
663
|
+
}
|
|
664
|
+
return index;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function isPythonStdlib(name) {
|
|
668
|
+
return PYTHON_STDLIB.has(name);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* @returns {string[]} import paths
|
|
673
|
+
*/
|
|
674
|
+
export function extractGoImports(source) {
|
|
675
|
+
const paths = [];
|
|
676
|
+
const n = source.length;
|
|
677
|
+
let i = 0;
|
|
678
|
+
|
|
679
|
+
function skipSpaceAndComments(at) {
|
|
680
|
+
let j = at;
|
|
681
|
+
while (j < n) {
|
|
682
|
+
if (/[\t\n\r \f]/.test(source[j])) {
|
|
683
|
+
j += 1;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
if (source[j] === "/" && source[j + 1] === "/") {
|
|
687
|
+
j = skipLineComment(source, j);
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (source[j] === "/" && source[j + 1] === "*") {
|
|
691
|
+
j = skipBlockComment(source, j);
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
return j;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function readGoString(at) {
|
|
700
|
+
at = skipSpaceAndComments(at);
|
|
701
|
+
const quote = source[at];
|
|
702
|
+
if (quote !== '"' && quote !== "`") return null;
|
|
703
|
+
let j = at + 1;
|
|
704
|
+
let value = "";
|
|
705
|
+
if (quote === "`") {
|
|
706
|
+
while (j < n && source[j] !== "`") {
|
|
707
|
+
value += source[j];
|
|
708
|
+
j += 1;
|
|
709
|
+
}
|
|
710
|
+
return source[j] === "`" ? { value, next: j + 1 } : null;
|
|
711
|
+
}
|
|
712
|
+
while (j < n) {
|
|
713
|
+
if (source[j] === "\\") {
|
|
714
|
+
value += source[j + 1] || "";
|
|
715
|
+
j += 2;
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (source[j] === '"') return { value, next: j + 1 };
|
|
719
|
+
if (source[j] === "\n") return null;
|
|
720
|
+
value += source[j];
|
|
721
|
+
j += 1;
|
|
722
|
+
}
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function takeImportPath(at) {
|
|
727
|
+
let j = skipSpaceAndComments(at);
|
|
728
|
+
if (source[j] === ".") j = skipSpaceAndComments(j + 1);
|
|
729
|
+
else if (isIdentStart(source[j]) || source[j] === "_") {
|
|
730
|
+
const word = readWord(source, j);
|
|
731
|
+
const after = skipSpaceAndComments(word.next);
|
|
732
|
+
if (source[after] === '"' || source[after] === "`") j = after;
|
|
733
|
+
}
|
|
734
|
+
const lit = readGoString(j);
|
|
735
|
+
if (!lit || !lit.value) return null;
|
|
736
|
+
return lit;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
while (i < n) {
|
|
740
|
+
if (/[\t\n\r \f]/.test(source[i])) {
|
|
741
|
+
i += 1;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
if (source[i] === "/" && source[i + 1] === "/") {
|
|
745
|
+
i = skipLineComment(source, i);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (source[i] === "/" && source[i + 1] === "*") {
|
|
749
|
+
i = skipBlockComment(source, i);
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (source[i] === '"' || source[i] === "`" || source[i] === "'") {
|
|
753
|
+
i = source[i] === "'" ? skipQuoted(source, i) : (readGoString(i)?.next || i + 1);
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
if (isIdentStart(source[i])) {
|
|
757
|
+
const word = readWord(source, i);
|
|
758
|
+
const prev = prevCodeChar(source, i);
|
|
759
|
+
i = word.next;
|
|
760
|
+
if (word.word !== "import" || prev === ".") continue;
|
|
761
|
+
i = skipSpaceAndComments(i);
|
|
762
|
+
if (source[i] === "(") {
|
|
763
|
+
i += 1;
|
|
764
|
+
while (i < n) {
|
|
765
|
+
i = skipSpaceAndComments(i);
|
|
766
|
+
if (source[i] === ")" || i >= n) {
|
|
767
|
+
i += 1;
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
const lit = takeImportPath(i);
|
|
771
|
+
if (!lit) {
|
|
772
|
+
i += 1;
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
paths.push(lit.value);
|
|
776
|
+
i = lit.next;
|
|
777
|
+
}
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
const lit = takeImportPath(i);
|
|
781
|
+
if (lit) {
|
|
782
|
+
paths.push(lit.value);
|
|
783
|
+
i = lit.next;
|
|
784
|
+
}
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
i += 1;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return paths;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export function goPackageDir(importPath, modulePath, moduleDir) {
|
|
794
|
+
if (!modulePath) return null;
|
|
795
|
+
if (importPath !== modulePath && !importPath.startsWith(`${modulePath}/`)) return null;
|
|
796
|
+
const rel = importPath === modulePath ? "" : importPath.slice(modulePath.length + 1);
|
|
797
|
+
const prefix = moduleDir ? moduleDir.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "") : "";
|
|
798
|
+
if (!prefix) return rel;
|
|
799
|
+
return rel ? `${prefix}/${rel}` : prefix;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export function isGoStdlib(importPath) {
|
|
803
|
+
const first = String(importPath).split("/")[0];
|
|
804
|
+
return Boolean(first) && !first.includes(".");
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export function parseGoModule(text) {
|
|
808
|
+
const match = String(text || "").match(/^\s*module\s+(\S+)/m);
|
|
809
|
+
return match ? match[1] : null;
|
|
810
|
+
}
|