@chappibunny/repolens 1.16.2 → 1.16.4

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.
@@ -0,0 +1,921 @@
1
+ // TypeScript import elision.
2
+ //
3
+ // `tsc`, esbuild, Babel, swc, and ts-node drop an import from the compiled
4
+ // JavaScript when none of its bindings is used as a value. An interface
5
+ // imported without `import type`, or a class used only in annotations
6
+ // (`constructor(private gen: MetadataGenerator)`), produces no runtime
7
+ // `require`/`import`. Counting those imports as runtime edges reports cycles
8
+ // that do not exist when the program runs.
9
+ //
10
+ // This module answers two questions without a TypeScript dependency:
11
+ // 1. Where is each identifier used: as a value, or only in type positions?
12
+ // (`usageByName`, a small tokenizer plus a type-position scanner)
13
+ // 2. Which names does a file declare only as types (interface / type alias)?
14
+ // (`declaredKinds`)
15
+ // and reads the tsconfig options that turn elision off (`elisionSettings`).
16
+ //
17
+ // The scanner is conservative. When a position is not clearly a type position,
18
+ // the identifier counts as a value use and the import stays a runtime edge.
19
+
20
+ import fs from "node:fs/promises";
21
+ import path from "node:path";
22
+ import { stripJsComments } from "../utils/js-lexer.js";
23
+
24
+ const KEYWORDS = new Set([
25
+ "abstract", "as", "async", "await", "break", "case", "catch", "class", "const",
26
+ "continue", "debugger", "declare", "default", "delete", "do", "else", "enum",
27
+ "export", "extends", "false", "finally", "for", "from", "function", "get", "if",
28
+ "implements", "import", "in", "instanceof", "interface", "let", "new", "null",
29
+ "of", "override", "private", "protected", "public", "readonly", "return",
30
+ "satisfies", "set", "static", "super", "switch", "this", "throw", "true", "try",
31
+ "type", "typeof", "var", "void", "while", "with", "yield", "keyof", "infer",
32
+ "is", "asserts", "unique", "accessor", "namespace", "module", "global",
33
+ "undefined",
34
+ ]);
35
+
36
+ // Reserved words can never be a binding. Contextual words (`of`, `from`, `get`,
37
+ // `readonly`, `type`, ...) can be, so a use of an import named `readonly` counts.
38
+ const RESERVED = new Set([
39
+ "break", "case", "catch", "class", "const", "continue", "debugger", "default",
40
+ "delete", "do", "else", "enum", "export", "extends", "false", "finally", "for",
41
+ "function", "if", "import", "in", "instanceof", "new", "null", "return", "super",
42
+ "switch", "this", "throw", "true", "try", "typeof", "var", "void", "while", "with",
43
+ "implements", "interface", "let", "package", "private", "protected", "public",
44
+ "static", "yield", "await",
45
+ ]);
46
+
47
+ const REGEX_PREFIX_WORDS = new Set([
48
+ "return", "typeof", "instanceof", "in", "of", "new", "delete", "void", "throw",
49
+ "case", "do", "else", "yield", "await",
50
+ ]);
51
+
52
+ // Words that may continue a type expression after a complete type.
53
+ const TYPE_CONTINUE_WORDS = new Set(["extends", "is", "keyof", "typeof", "infer", "readonly", "unique", "asserts"]);
54
+
55
+ const PUNCT = [
56
+ "...", "===", "!==", "**=", "<<=", "??=", "&&=", "||=",
57
+ "=>", "==", "!=", "&&", "||", "??", "?.", "++", "--", "+=", "-=", "*=", "/=", "%=",
58
+ "&=", "|=", "^=", "**",
59
+ ];
60
+
61
+ function isIdentStart(ch) {
62
+ return /[A-Za-z_$\u00c0-\uffff]/.test(ch || "");
63
+ }
64
+
65
+ function isIdentPart(ch) {
66
+ return /[A-Za-z0-9_$\u00c0-\uffff]/.test(ch || "");
67
+ }
68
+
69
+ function regexAllowed(prev) {
70
+ if (!prev) return true;
71
+ if (prev.t === "id") return REGEX_PREFIX_WORDS.has(prev.v);
72
+ if (prev.t === "punc") return !(prev.v === "<" || prev.v === ")" || prev.v === "]" || prev.v === "}" || prev.v === "++" || prev.v === "--");
73
+ return false;
74
+ }
75
+
76
+ /**
77
+ * Tokens: { t: "id" | "str" | "num" | "punc" | "regex", v, nl, i }
78
+ * `nl` is true when a line break precedes the token. Template literals become
79
+ * a "str" token per text chunk, with `${` and `}` punctuation around each
80
+ * expression, so identifiers inside `${...}` are tokens too.
81
+ */
82
+ export function tokenize(source) {
83
+ const src = String(source ?? "");
84
+ const n = src.length;
85
+ const tokens = [];
86
+ // Set when a string, template, or regex literal does not close: valid source
87
+ // never does that, so the tokenizer misread something (for example a regex
88
+ // after `)` read as division) and some code may be hidden inside a literal.
89
+ tokens.malformed = false;
90
+ // Brace stack: "{" for code braces, "${" for template expressions.
91
+ const braces = [];
92
+ let i = 0;
93
+ let nl = false;
94
+
95
+ const push = (t, v, at) => {
96
+ tokens.push({ t, v, nl, i: at });
97
+ nl = false;
98
+ };
99
+
100
+ function readTemplate(at) {
101
+ // `at` is just after a backtick or a closing `}` of `${`.
102
+ let j = at;
103
+ while (j < n) {
104
+ const ch = src[j];
105
+ if (ch === "\\") {
106
+ j += 2;
107
+ continue;
108
+ }
109
+ if (ch === "`") {
110
+ push("str", "`", at);
111
+ return j + 1;
112
+ }
113
+ if (ch === "$" && src[j + 1] === "{") {
114
+ push("str", "`", at);
115
+ push("punc", "${", j);
116
+ braces.push("${");
117
+ return j + 2;
118
+ }
119
+ j += 1;
120
+ }
121
+ push("str", "`", at);
122
+ tokens.malformed = true;
123
+ return n;
124
+ }
125
+
126
+ while (i < n) {
127
+ const ch = src[i];
128
+ if (ch === "\n" || ch === "\r" || ch === "\u2028" || ch === "\u2029") {
129
+ nl = true;
130
+ i += 1;
131
+ continue;
132
+ }
133
+ if (ch === " " || ch === "\t" || ch === "\f" || ch === "\v" || ch === "\ufeff" || ch === "\u00a0") {
134
+ i += 1;
135
+ continue;
136
+ }
137
+ if (ch === "/" && src[i + 1] === "/") {
138
+ const end = src.indexOf("\n", i);
139
+ i = end === -1 ? n : end;
140
+ continue;
141
+ }
142
+ if (ch === "/" && src[i + 1] === "*") {
143
+ const end = src.indexOf("*/", i + 2);
144
+ const stop = end === -1 ? n : end + 2;
145
+ if (src.slice(i, stop).includes("\n")) nl = true;
146
+ i = stop;
147
+ continue;
148
+ }
149
+ if (ch === "#" && i === 0 && src[1] === "!") {
150
+ const end = src.indexOf("\n", i);
151
+ i = end === -1 ? n : end;
152
+ continue;
153
+ }
154
+ if (ch === "'" || ch === '"') {
155
+ let j = i + 1;
156
+ while (j < n && src[j] !== ch && src[j] !== "\n") j += src[j] === "\\" ? 2 : 1;
157
+ if (src[j] !== ch) tokens.malformed = true;
158
+ push("str", src.slice(i, Math.min(j + 1, n)), i);
159
+ i = Math.min(j + 1, n);
160
+ continue;
161
+ }
162
+ if (ch === "`") {
163
+ i = readTemplate(i + 1);
164
+ continue;
165
+ }
166
+ if (ch === "}" && braces.length > 0 && braces[braces.length - 1] === "${") {
167
+ braces.pop();
168
+ push("punc", "}", i);
169
+ i = readTemplate(i + 1);
170
+ continue;
171
+ }
172
+ if (isIdentStart(ch) || (ch === "#" && isIdentStart(src[i + 1]))) {
173
+ let j = i + 1;
174
+ while (j < n && isIdentPart(src[j])) j += 1;
175
+ push("id", src.slice(i, j), i);
176
+ i = j;
177
+ continue;
178
+ }
179
+ if (/[0-9]/.test(ch) || (ch === "." && /[0-9]/.test(src[i + 1] || ""))) {
180
+ let j = i + 1;
181
+ while (j < n && /[0-9A-Za-z_.]/.test(src[j])) j += 1;
182
+ push("num", src.slice(i, j), i);
183
+ i = j;
184
+ continue;
185
+ }
186
+ if (ch === "/" && regexAllowed(tokens[tokens.length - 1])) {
187
+ let j = i + 1;
188
+ let inClass = false;
189
+ while (j < n && src[j] !== "\n") {
190
+ if (src[j] === "\\") {
191
+ j += 2;
192
+ continue;
193
+ }
194
+ if (src[j] === "[") inClass = true;
195
+ else if (src[j] === "]") inClass = false;
196
+ else if (src[j] === "/" && !inClass) break;
197
+ j += 1;
198
+ }
199
+ if (src[j] !== "/") tokens.malformed = true;
200
+ j += 1;
201
+ while (j < n && isIdentPart(src[j])) j += 1;
202
+ push("regex", src.slice(i, j), i);
203
+ i = j;
204
+ continue;
205
+ }
206
+ // `<` and `>` are always single tokens so generic brackets nest cleanly.
207
+ if (ch === "<" || ch === ">") {
208
+ push("punc", ch, i);
209
+ i += 1;
210
+ continue;
211
+ }
212
+ if (ch === "{") braces.push("{");
213
+ else if (ch === "}") braces.pop();
214
+ let matched = null;
215
+ for (const p of PUNCT) {
216
+ if (src.startsWith(p, i)) {
217
+ matched = p;
218
+ break;
219
+ }
220
+ }
221
+ if (matched === "?." && /[0-9]/.test(src[i + 2] || "")) matched = null;
222
+ const value = matched || ch;
223
+ push("punc", value, i);
224
+ i += value.length;
225
+ }
226
+ return tokens;
227
+ }
228
+
229
+ function isPunc(tok, value) {
230
+ return Boolean(tok) && tok.t === "punc" && tok.v === value;
231
+ }
232
+
233
+ function isWord(tok, value) {
234
+ return Boolean(tok) && tok.t === "id" && tok.v === value;
235
+ }
236
+
237
+ function isName(tok) {
238
+ return Boolean(tok) && tok.t === "id" && !KEYWORDS.has(tok.v);
239
+ }
240
+
241
+ // A token that can end a complete type or a complete value expression.
242
+ function endsOperand(tok) {
243
+ if (!tok) return false;
244
+ if (tok.t === "str" || tok.t === "num" || tok.t === "regex") return true;
245
+ if (tok.t === "id") return !KEYWORDS.has(tok.v) || VALUE_WORDS.has(tok.v);
246
+ return tok.v === ")" || tok.v === "]" || tok.v === "}" || tok.v === ">";
247
+ }
248
+
249
+ const VALUE_WORDS = new Set(["this", "true", "false", "null", "undefined", "super", "void"]);
250
+
251
+ const OPENERS = { "(": ")", "[": "]", "{": "}", "${": "}", "<": ">" };
252
+
253
+ /**
254
+ * Find the `>` that closes type arguments starting at tokens[start] === "<".
255
+ * Returns its index, or -1 when the tokens cannot be a type argument list
256
+ * (for example `a < b && c > d`).
257
+ */
258
+ function matchTypeArguments(tokens, start) {
259
+ const stack = [];
260
+ for (let j = start; j < tokens.length && j < start + 400; j += 1) {
261
+ const tok = tokens[j];
262
+ if (tok.t === "punc") {
263
+ if (tok.v === "<" || tok.v === "(" || tok.v === "[" || tok.v === "{" || tok.v === "${") {
264
+ stack.push(tok.v);
265
+ continue;
266
+ }
267
+ if (tok.v === ">" || tok.v === ")" || tok.v === "]" || tok.v === "}") {
268
+ const open = stack.pop();
269
+ if (!open || OPENERS[open] !== tok.v) return -1;
270
+ if (stack.length === 0) return tok.v === ">" ? j : -1;
271
+ continue;
272
+ }
273
+ const inAngleOnly = stack[stack.length - 1] === "<";
274
+ if (inAngleOnly && ["&&", "||", "??", "==", "===", "!=", "!==", "+", "*", "/", "%", "!", ";", "++", "--", "+=", "-="].includes(tok.v)) return -1;
275
+ continue;
276
+ }
277
+ if (tok.t === "regex") return -1;
278
+ if (tok.t === "id" && stack[stack.length - 1] === "<" && ["return", "if", "for", "while", "const", "let", "var", "function", "class", "new", "await", "yield", "throw", "instanceof"].includes(tok.v)) return -1;
279
+ }
280
+ return -1;
281
+ }
282
+
283
+ /**
284
+ * Scan one type expression starting at tokens[start]. Marks every identifier as
285
+ * a type position and returns the index of the first token after the type.
286
+ * `returnType`: a function return annotation, where `=>` or `{` ends the type.
287
+ */
288
+ function scanType(tokens, start, marks, { returnType = false } = {}) {
289
+ const stack = [];
290
+ let prev = null;
291
+ let started = false;
292
+ let conditional = 0;
293
+ let lastParenClose = -1;
294
+ let lastParenOpen = -1;
295
+ let parenOpenAt = -1;
296
+ let j = start;
297
+ for (; j < tokens.length; j += 1) {
298
+ const tok = tokens[j];
299
+ if (stack.length === 0) {
300
+ if (tok.t === "punc") {
301
+ const v = tok.v;
302
+ if (v === "," || v === ")" || v === "]" || v === "}" || v === ";" || v === ">") break;
303
+ if (v === "=" || v === "==" || v === "===" || v === "!=" || v === "!==" || v === "&&" || v === "||" || v === "??" || v === "+=" || v === "-=") break;
304
+ if ((v === "+" || v === "*" || v === "/" || v === "%" || v === "-" || v === "!") && started && endsOperand(prev)) break;
305
+ if (v === ":") {
306
+ if (conditional > 0) {
307
+ conditional -= 1;
308
+ prev = tok;
309
+ continue;
310
+ }
311
+ break;
312
+ }
313
+ if (v === "?") {
314
+ if (!started) break;
315
+ conditional += 1;
316
+ prev = tok;
317
+ continue;
318
+ }
319
+ if (v === "=>") {
320
+ // `(a: T) => R` inside a type continues the type. A return type
321
+ // annotation ends at the arrow of the function it annotates, unless
322
+ // the return type is itself a function type (`(): (x: T) => R`).
323
+ if (lastParenClose === j - 1 && (!returnType || (startedWithParen(tokens, start) && looksLikeParams(tokens, lastParenOpen, lastParenClose)))) {
324
+ prev = tok;
325
+ started = false;
326
+ continue;
327
+ }
328
+ break;
329
+ }
330
+ if (v === "{") {
331
+ const operandExpected = !started || (prev && prev.t === "punc" && ["|", "&", "=>", "?", ":", "<", ","].includes(prev.v)) || (prev && prev.t === "id" && TYPE_CONTINUE_WORDS.has(prev.v));
332
+ if (!operandExpected) break;
333
+ }
334
+ }
335
+ if (tok.nl && started && endsOperand(prev)) {
336
+ if (tok.t === "id" && !TYPE_CONTINUE_WORDS.has(tok.v)) break;
337
+ if (tok.t === "str" || tok.t === "num") break;
338
+ if (isPunc(tok, "@") || isPunc(tok, "[") || isPunc(tok, "(") || isPunc(tok, "*")) break;
339
+ }
340
+ if (tok.t === "id" && started && endsOperand(prev) && !TYPE_CONTINUE_WORDS.has(tok.v) && !(prev.t === "id" && TYPE_CONTINUE_WORDS.has(prev.v))) {
341
+ // Two operands in a row: the type ended (`x as Foo then` cannot happen;
342
+ // this is `foo: Bar` followed by an unrelated statement).
343
+ if (!(prev.t === "id" && (prev.v === "asserts" || prev.v === "unique"))) break;
344
+ }
345
+ }
346
+ if (tok.t === "punc") {
347
+ if (tok.v === "(" || tok.v === "[" || tok.v === "{" || tok.v === "${" || tok.v === "<") {
348
+ if (tok.v === "(" && stack.length === 0) parenOpenAt = j;
349
+ stack.push(tok.v);
350
+ } else if (tok.v === ")" || tok.v === "]" || tok.v === "}" || tok.v === ">") {
351
+ const open = stack.pop();
352
+ if (open === "(" && stack.length === 0) {
353
+ lastParenClose = j;
354
+ lastParenOpen = parenOpenAt;
355
+ }
356
+ }
357
+ }
358
+ if (tok.t === "id") marks[j] = "type";
359
+ started = true;
360
+ prev = tok;
361
+ }
362
+ return j;
363
+ }
364
+
365
+ // `(a: T, b) => R` has parameters; `(A | B)` is a parenthesized type.
366
+ function looksLikeParams(tokens, open, close) {
367
+ if (open < 0 || close === open + 1) return true;
368
+ let depth = 0;
369
+ for (let k = open + 1; k < close; k += 1) {
370
+ const tok = tokens[k];
371
+ if (tok.t !== "punc") continue;
372
+ if (OPENERS[tok.v]) depth += 1;
373
+ else if (tok.v === ")" || tok.v === "]" || tok.v === "}" || tok.v === ">") depth -= 1;
374
+ else if (depth === 0 && (tok.v === ":" || tok.v === "," || tok.v === "..." || tok.v === "?")) return true;
375
+ }
376
+ return false;
377
+ }
378
+
379
+ function startedWithParen(tokens, start) {
380
+ return isPunc(tokens[start], "(") || isPunc(tokens[start], "<") || isWord(tokens[start], "new");
381
+ }
382
+
383
+ function matchingClose(tokens, openIndex) {
384
+ const stack = [];
385
+ for (let j = openIndex; j < tokens.length; j += 1) {
386
+ const tok = tokens[j];
387
+ if (tok.t !== "punc") continue;
388
+ if (tok.v === "(" || tok.v === "[" || tok.v === "{" || tok.v === "${") stack.push(tok.v);
389
+ else if (tok.v === ")" || tok.v === "]" || tok.v === "}") {
390
+ stack.pop();
391
+ if (stack.length === 0) return j;
392
+ }
393
+ }
394
+ return tokens.length - 1;
395
+ }
396
+
397
+ function markRange(tokens, marks, from, to) {
398
+ for (let j = from; j <= to && j < tokens.length; j += 1) {
399
+ if (tokens[j].t === "id") marks[j] = "type";
400
+ }
401
+ }
402
+
403
+ // End of an `import ...` / `export ... from` declaration (the module string), or -1.
404
+ function skipModuleDeclaration(tokens, i) {
405
+ for (let j = i + 1; j < tokens.length && j < i + 2000; j += 1) {
406
+ const tok = tokens[j];
407
+ if (tok.t === "str") return j;
408
+ if (isPunc(tok, ";")) return j;
409
+ if (isPunc(tok, "=") ) return -1;
410
+ }
411
+ return -1;
412
+ }
413
+
414
+ /**
415
+ * Classify identifier uses in TypeScript source.
416
+ * Returns { uses: Map<name, { value, type }>, decorators: boolean, malformed: boolean }.
417
+ * `malformed`: the tokenizer misread the file, so uses may be missing; callers
418
+ * must not treat an unseen or type-only name as removable.
419
+ * Import and re-export declarations are not uses. Property names after `.`
420
+ * are not uses of a binding with that name.
421
+ */
422
+ export function usageByName(source, { jsx = false, jsxFactories = ["React"] } = {}) {
423
+ const tokens = tokenize(source);
424
+ const marks = new Array(tokens.length).fill(null);
425
+ const frames = [{ kind: "block", ternary: 0, decl: false, inCase: false }];
426
+ let pendingClass = null; // { depth, implementsDone }
427
+ let decorators = false;
428
+
429
+ const frame = () => frames[frames.length - 1];
430
+
431
+ for (let i = 0; i < tokens.length; i += 1) {
432
+ const tok = tokens[i];
433
+ const prev = tokens[i - 1];
434
+ const next = tokens[i + 1];
435
+ if (marks[i]) continue;
436
+
437
+ if (tok.t === "id") {
438
+ const afterDot = isPunc(prev, ".") || isPunc(prev, "?.");
439
+ if (afterDot) {
440
+ marks[i] = "prop";
441
+ continue;
442
+ }
443
+ const v = tok.v;
444
+ if (v === "import" && !isPunc(next, "(") && !isPunc(next, ".")) {
445
+ // `import x = require("y")` and `import x = A.B` are value uses of the RHS.
446
+ const end = skipModuleDeclaration(tokens, i);
447
+ if (end !== -1) {
448
+ markRange(tokens, marks, i, end);
449
+ for (let j = i; j <= end; j += 1) if (tokens[j].t === "id") marks[j] = "decl";
450
+ i = end;
451
+ continue;
452
+ }
453
+ continue;
454
+ }
455
+ if (v === "export") {
456
+ // `export { a } from "x"`, `export * from "x"`: not local uses.
457
+ // `export type { A }`: type uses. `export { a }`: value uses (kept).
458
+ let j = i + 1;
459
+ if (isWord(tokens[j], "type") && isPunc(tokens[j + 1], "{")) {
460
+ const close = matchingClose(tokens, j + 1);
461
+ markRange(tokens, marks, j, close);
462
+ i = close;
463
+ continue;
464
+ }
465
+ if (isPunc(tokens[j], "*") || isPunc(tokens[j], "{")) {
466
+ const close = isPunc(tokens[j], "{") ? matchingClose(tokens, j) : j;
467
+ let k = close + 1;
468
+ if (isWord(tokens[k], "as")) k += 2;
469
+ if (isWord(tokens[k], "from")) {
470
+ for (let m = i; m <= k + 1; m += 1) if (tokens[m]?.t === "id") marks[m] = "decl";
471
+ i = k + 1;
472
+ continue;
473
+ }
474
+ }
475
+ continue;
476
+ }
477
+ if (v === "interface" && isName(next) && !afterDot) {
478
+ let j = i + 1;
479
+ while (j < tokens.length && !isPunc(tokens[j], "{")) j += 1;
480
+ const close = matchingClose(tokens, j);
481
+ markRange(tokens, marks, i, close);
482
+ i = close;
483
+ continue;
484
+ }
485
+ if (v === "type" && isName(next) && (isPunc(tokens[i + 2], "=") || isPunc(tokens[i + 2], "<"))) {
486
+ let j = i + 2;
487
+ if (isPunc(tokens[j], "<")) {
488
+ const close = matchTypeArguments(tokens, j);
489
+ if (close === -1) continue;
490
+ markRange(tokens, marks, j, close);
491
+ j = close + 1;
492
+ }
493
+ if (!isPunc(tokens[j], "=")) continue;
494
+ marks[i + 1] = "type";
495
+ const end = scanType(tokens, j + 1, marks);
496
+ i = end - 1;
497
+ if (isPunc(tokens[end], ";")) i = end;
498
+ continue;
499
+ }
500
+ if (v === "declare" && next && next.t === "id" && !isPunc(tokens[i + 2], "=") && !isPunc(tokens[i + 2], "(") && !isPunc(next, ".")) {
501
+ // Ambient declarations emit nothing.
502
+ const blockForm = ["module", "namespace", "global", "class", "enum", "interface", "abstract"].includes(next.v);
503
+ let j = i + 1;
504
+ if (blockForm) {
505
+ while (j < tokens.length && !isPunc(tokens[j], "{") && !isPunc(tokens[j], ";")) j += 1;
506
+ const close = isPunc(tokens[j], "{") ? matchingClose(tokens, j) : j;
507
+ markRange(tokens, marks, i, close);
508
+ i = close;
509
+ continue;
510
+ }
511
+ const stack = [];
512
+ for (; j < tokens.length; j += 1) {
513
+ const t = tokens[j];
514
+ if (stack.length === 0 && (isPunc(t, ";") || isPunc(t, "}") || (t.nl && j > i + 2 && endsOperand(tokens[j - 1]) && t.t === "id"))) break;
515
+ if (t.t === "punc" && OPENERS[t.v] && t.v !== "<") stack.push(t.v);
516
+ else if (t.t === "punc" && (t.v === ")" || t.v === "]" || t.v === "}")) stack.pop();
517
+ }
518
+ markRange(tokens, marks, i, j - 1);
519
+ i = isPunc(tokens[j], ";") ? j : j - 1;
520
+ continue;
521
+ }
522
+ if (v === "class" && !afterDot) {
523
+ pendingClass = { depth: frames.length };
524
+ continue;
525
+ }
526
+ if (v === "implements" && pendingClass) {
527
+ let j = i + 1;
528
+ for (;;) {
529
+ j = scanType(tokens, j, marks);
530
+ if (isPunc(tokens[j], ",")) {
531
+ j += 1;
532
+ continue;
533
+ }
534
+ break;
535
+ }
536
+ i = j - 1;
537
+ continue;
538
+ }
539
+ if ((v === "const" || v === "let" || v === "var") && !afterDot) {
540
+ frame().decl = true;
541
+ continue;
542
+ }
543
+ if (v === "case" && !afterDot) {
544
+ frame().inCase = true;
545
+ continue;
546
+ }
547
+ if ((v === "as" || v === "satisfies") && endsOperand(prev) && !(prev.t === "id" && prev.v === "void")) {
548
+ const end = scanType(tokens, i + 1, marks);
549
+ i = end - 1;
550
+ continue;
551
+ }
552
+ if (v === "of" || v === "in") frame().decl = false;
553
+ if (RESERVED.has(v)) continue;
554
+ marks[i] = "value";
555
+ continue;
556
+ }
557
+
558
+ if (tok.t !== "punc") continue;
559
+ const v = tok.v;
560
+
561
+ if (v === "@" && next && next.t === "id") decorators = true;
562
+
563
+ if (v === "<") {
564
+ // `foo<T>(`, `new Map<K, V>()`, `extends Base<T>`. In .tsx a `>` before `<`
565
+ // is the end of a JSX tag, and a name can be JSX text, so only a call,
566
+ // tagged template, `new`, or class heritage counts there.
567
+ const typeArgsByPosition = prev && (isName(prev) || (!jsx && isPunc(prev, ">")) || isWord(prev, "this") || isWord(prev, "super"));
568
+ if (typeArgsByPosition) {
569
+ const close = matchTypeArguments(tokens, i);
570
+ if (close !== -1) {
571
+ const after = tokens[close + 1];
572
+ const prevPrev = tokens[i - 2];
573
+ const inHeritage = pendingClass && frames.length === pendingClass.depth;
574
+ const strong = isPunc(after, "(") || (after && after.t === "str" && after.v.startsWith("`")) || inHeritage || isWord(prevPrev, "new") || isWord(prevPrev, "function") || isWord(tokens[i - 3], "function");
575
+ const weak = (after && after.t === "punc" && [")", ",", ";", "]", "}", "."].includes(after.v)) || !after;
576
+ if (strong || (!jsx && weak)) {
577
+ markRange(tokens, marks, i, close);
578
+ i = close;
579
+ continue;
580
+ }
581
+ }
582
+ continue;
583
+ }
584
+ // `<T>(x: T) => x` and `<Foo>bar` (old-style assertion) in .ts.
585
+ // In .tsx a `<` here opens a JSX element, except `<T,>(` / `<T extends X>(`.
586
+ const close = matchTypeArguments(tokens, i);
587
+ if (close !== -1) {
588
+ if (!jsx) {
589
+ markRange(tokens, marks, i, close);
590
+ i = close;
591
+ continue;
592
+ }
593
+ const inner = tokens.slice(i + 1, close);
594
+ const generic = isPunc(tokens[close + 1], "(") && (inner.some((t) => isPunc(t, ",")) || inner.some((t) => isWord(t, "extends")));
595
+ if (generic) {
596
+ markRange(tokens, marks, i, close);
597
+ i = close;
598
+ continue;
599
+ }
600
+ }
601
+ continue;
602
+ }
603
+
604
+ if (v === "?") {
605
+ if (isPunc(next, ":") || isPunc(next, ")") || isPunc(next, ",") || (isPunc(next, "=") && !isPunc(tokens[i + 2], "="))) continue;
606
+ frame().ternary += 1;
607
+ continue;
608
+ }
609
+
610
+ if (v === ":") {
611
+ const f = frame();
612
+ if (f.ternary > 0) {
613
+ f.ternary -= 1;
614
+ continue;
615
+ }
616
+ if (f.inCase) {
617
+ f.inCase = false;
618
+ continue;
619
+ }
620
+ if (isPunc(prev, ")")) {
621
+ const end = scanType(tokens, i + 1, marks, { returnType: true });
622
+ i = end - 1;
623
+ continue;
624
+ }
625
+ const annotatable = isName(prev) || isPunc(prev, "?") || isPunc(prev, "!") || isPunc(prev, "]") || isPunc(prev, "}") || (prev && prev.t === "id");
626
+ if (annotatable && (f.kind === "paren" || f.kind === "bracket" || f.kind === "class" || f.decl)) {
627
+ const end = scanType(tokens, i + 1, marks);
628
+ i = end - 1;
629
+ continue;
630
+ }
631
+ // Object literal key: `{ Foo: 1 }` names a property, not the binding.
632
+ if (prev && prev.t === "id" && marks[i - 1] === "value" && (isPunc(tokens[i - 2], "{") || isPunc(tokens[i - 2], ","))) marks[i - 1] = "prop";
633
+ continue;
634
+ }
635
+
636
+ if (v === "=") {
637
+ frame().decl = false;
638
+ continue;
639
+ }
640
+ if (v === ";") {
641
+ frame().decl = false;
642
+ frame().inCase = false;
643
+ continue;
644
+ }
645
+
646
+ if (v === "(" || v === "[" || v === "{" || v === "${") {
647
+ let kind = v === "(" ? "paren" : v === "[" ? "bracket" : "brace";
648
+ if (v === "{" && pendingClass && frames.length === pendingClass.depth) {
649
+ kind = "class";
650
+ pendingClass = null;
651
+ }
652
+ frames.push({ kind, ternary: 0, decl: false, inCase: false });
653
+ continue;
654
+ }
655
+ if (v === ")" || v === "]" || v === "}") {
656
+ if (frames.length > 1) frames.pop();
657
+ continue;
658
+ }
659
+ }
660
+
661
+ const uses = new Map();
662
+ for (let i = 0; i < tokens.length; i += 1) {
663
+ const tok = tokens[i];
664
+ if (tok.t !== "id") continue;
665
+ const mark = marks[i];
666
+ if (mark !== "value" && mark !== "type") continue;
667
+ let entry = uses.get(tok.v);
668
+ if (!entry) {
669
+ entry = { value: 0, type: 0 };
670
+ uses.set(tok.v, entry);
671
+ }
672
+ entry[mark] += 1;
673
+ }
674
+
675
+ // JSX text can hide code from the tokenizer (an apostrophe in text reads as
676
+ // a quote). In .tsx/.jsx, any whole-word occurrence the tokenizer did not see
677
+ // counts as a value use.
678
+ if (jsx) {
679
+ const seen = new Map();
680
+ for (const tok of tokens) if (tok.t === "id") seen.set(tok.v, (seen.get(tok.v) || 0) + 1);
681
+ const text = stripJsComments(source);
682
+ const counts = new Map();
683
+ const re = /[A-Za-z_$][\w$]*/g;
684
+ let match;
685
+ while ((match = re.exec(text)) !== null) counts.set(match[0], (counts.get(match[0]) || 0) + 1);
686
+ for (const [name, count] of counts) {
687
+ const hidden = count - (seen.get(name) || 0);
688
+ if (hidden <= 0 || RESERVED.has(name)) continue;
689
+ const entry = uses.get(name) || { value: 0, type: 0 };
690
+ entry.value += hidden;
691
+ uses.set(name, entry);
692
+ }
693
+ }
694
+
695
+ // Classic JSX compiles `<div />` to `React.createElement(...)` (or the
696
+ // configured factory), so the factory import is a value use.
697
+ if (jsx && tokens.some((tok, k) => isPunc(tok, "<") && (tokens[k + 1]?.t === "id" || isPunc(tokens[k + 1], ">")))) {
698
+ for (const name of jsxFactories) {
699
+ const entry = uses.get(name) || { value: 0, type: 0 };
700
+ entry.value += 1;
701
+ uses.set(name, entry);
702
+ }
703
+ }
704
+
705
+ return { uses, decorators, malformed: Boolean(tokens.malformed) };
706
+ }
707
+
708
+ /**
709
+ * Names a TypeScript file declares only as types (interface or type alias),
710
+ * and names it declares as values. A name in both (declaration merging) is a value.
711
+ * Only top-level interface / type alias declarations count as types: a nested
712
+ * one (`namespace N { interface Foo }`, inside a function or `declare module`)
713
+ * is not what an importer of `Foo` gets. Names re-exported from another module
714
+ * (`export { Foo } from "./impl"`, `export * as Foo from`) and import-equals
715
+ * aliases (`import Foo = require()`) are values: their kind is unknown here.
716
+ */
717
+ export function declaredKinds(source) {
718
+ const tokens = tokenize(source);
719
+ const types = new Set();
720
+ const values = new Set();
721
+ let depth = 0;
722
+ for (let i = 0; i < tokens.length; i += 1) {
723
+ const tok = tokens[i];
724
+ if (tok.t === "punc") {
725
+ if (tok.v === "{" || tok.v === "${") depth += 1;
726
+ else if (tok.v === "}") depth = Math.max(0, depth - 1);
727
+ continue;
728
+ }
729
+ if (tok.t !== "id") continue;
730
+ const prev = tokens[i - 1];
731
+ if (isPunc(prev, ".") || isPunc(prev, "?.")) continue;
732
+ const next = tokens[i + 1];
733
+ if (tok.v === "interface" && isName(next)) {
734
+ if (depth === 0) types.add(next.v);
735
+ } else if (tok.v === "type" && isName(next) && (isPunc(tokens[i + 2], "=") || isPunc(tokens[i + 2], "<"))) {
736
+ if (depth === 0) types.add(next.v);
737
+ } else if (["const", "let", "var", "class", "enum", "namespace", "module"].includes(tok.v) && isName(next)) values.add(next.v);
738
+ else if (tok.v === "function" && (isName(next) || (isPunc(next, "*") && isName(tokens[i + 2])))) values.add(isName(next) ? next.v : tokens[i + 2].v);
739
+ else if (tok.v === "as" && isName(next) && (isPunc(tokens[i + 2], ",") || isPunc(tokens[i + 2], "}") || isWord(tokens[i + 2], "from"))) values.add(next.v);
740
+ else if ((tok.v === "const" || tok.v === "let" || tok.v === "var") && (isPunc(next, "{") || isPunc(next, "["))) {
741
+ const close = matchingClose(tokens, i + 1);
742
+ for (let j = i + 2; j < close; j += 1) if (isName(tokens[j])) values.add(tokens[j].v);
743
+ } else if (tok.v === "export" && isPunc(next, "{")) {
744
+ // `export { a, b as c } from "x"`: re-exported names, kind unknown.
745
+ const close = matchingClose(tokens, i + 1);
746
+ if (isWord(tokens[close + 1], "from")) {
747
+ for (let j = i + 2; j < close; j += 1) {
748
+ if (isName(tokens[j]) && !isWord(tokens[j + 1], "as")) values.add(tokens[j].v);
749
+ }
750
+ }
751
+ } else if (tok.v === "import" && isName(next) && isPunc(tokens[i + 2], "=")) {
752
+ values.add(next.v);
753
+ } else if (tok.v === "import" && isWord(next, "type") && isName(tokens[i + 2]) && isPunc(tokens[i + 3], "=")) {
754
+ values.add(tokens[i + 2].v);
755
+ } else if (tok.v === "import") {
756
+ // Imported names re-exported from here: kind unknown, treat as value.
757
+ const end = skipModuleDeclaration(tokens, i);
758
+ if (end !== -1) {
759
+ for (let j = i + 1; j < end; j += 1) {
760
+ if (isName(tokens[j])) values.add(tokens[j].v);
761
+ else if (isPunc(tokens[j], "{")) depth += 1;
762
+ else if (isPunc(tokens[j], "}")) depth = Math.max(0, depth - 1);
763
+ }
764
+ i = end;
765
+ }
766
+ }
767
+ }
768
+ for (const name of values) types.delete(name);
769
+ return { types, values };
770
+ }
771
+
772
+ // ---- tsconfig -----------------------------------------------------------
773
+
774
+ function parseJsonc(text) {
775
+ let out = "";
776
+ let i = 0;
777
+ const n = text.length;
778
+ while (i < n) {
779
+ const ch = text[i];
780
+ if (ch === '"') {
781
+ let j = i + 1;
782
+ while (j < n && text[j] !== '"') j += text[j] === "\\" ? 2 : 1;
783
+ out += text.slice(i, j + 1);
784
+ i = j + 1;
785
+ continue;
786
+ }
787
+ if (ch === "/" && text[i + 1] === "/") {
788
+ while (i < n && text[i] !== "\n") i += 1;
789
+ continue;
790
+ }
791
+ if (ch === "/" && text[i + 1] === "*") {
792
+ const end = text.indexOf("*/", i + 2);
793
+ i = end === -1 ? n : end + 2;
794
+ continue;
795
+ }
796
+ out += ch;
797
+ i += 1;
798
+ }
799
+ out = out.replace(/,(\s*[}\]])/g, "$1");
800
+ return JSON.parse(out);
801
+ }
802
+
803
+ async function readJsonc(repoRoot, rel) {
804
+ try {
805
+ return parseJsonc(await fs.readFile(path.join(repoRoot, rel), "utf8"));
806
+ } catch {
807
+ return undefined;
808
+ }
809
+ }
810
+
811
+ // Candidate repo-relative paths for a package `extends` ("@tsconfig/node18",
812
+ // "@tsconfig/node18/tsconfig.json", "pkg/base"), looked up in every
813
+ // node_modules from the config's folder up to the repository root.
814
+ async function resolvePackageExtends(repoRoot, configRel, parent) {
815
+ const dirs = [];
816
+ let dir = path.posix.dirname(configRel);
817
+ for (;;) {
818
+ dirs.push(dir === "." ? "node_modules" : `${dir}/node_modules`);
819
+ if (dir === "." || dir === "" || dir === "/") break;
820
+ dir = path.posix.dirname(dir);
821
+ }
822
+ for (const modules of dirs) {
823
+ const base = path.posix.join(modules, parent);
824
+ const candidates = [base, `${base}.json`, `${base}/tsconfig.json`];
825
+ const pkg = await readJsonc(repoRoot, `${base}/package.json`);
826
+ if (pkg && typeof pkg.tsconfig === "string") candidates.unshift(path.posix.join(base, pkg.tsconfig));
827
+ for (const candidate of candidates) {
828
+ if (!candidate.endsWith(".json")) continue;
829
+ if ((await readJsonc(repoRoot, candidate)) !== undefined) return candidate;
830
+ }
831
+ }
832
+ return null;
833
+ }
834
+
835
+ /**
836
+ * Compiler options of a tsconfig with its `extends` chain. `unresolved` is true
837
+ * when a parent config is not in the checkout (a package that is not installed,
838
+ * or a generated file such as `.nuxt/tsconfig.json`): its options are unknown
839
+ * and may keep imports (`verbatimModuleSyntax`).
840
+ */
841
+ async function readCompilerOptions(repoRoot, configRel, depth = 0, seen = new Set()) {
842
+ if (depth > 6 || seen.has(configRel)) return { options: {}, unresolved: false };
843
+ seen.add(configRel);
844
+ const parsed = await readJsonc(repoRoot, configRel);
845
+ if (parsed === undefined) return { options: {}, unresolved: true };
846
+ let options = {};
847
+ let unresolved = false;
848
+ const parents = Array.isArray(parsed?.extends) ? parsed.extends : parsed?.extends ? [parsed.extends] : [];
849
+ for (const parent of parents) {
850
+ if (typeof parent !== "string") continue;
851
+ let rel = null;
852
+ if (parent.startsWith(".") || parent.startsWith("/")) {
853
+ rel = path.posix.normalize(path.posix.join(path.posix.dirname(configRel), parent));
854
+ if (!rel.endsWith(".json")) rel += ".json";
855
+ if (rel.startsWith("..") || path.posix.isAbsolute(rel)) rel = null;
856
+ } else {
857
+ rel = await resolvePackageExtends(repoRoot, configRel, parent);
858
+ }
859
+ if (!rel) {
860
+ unresolved = true;
861
+ continue;
862
+ }
863
+ const inherited = await readCompilerOptions(repoRoot, rel, depth + 1, seen);
864
+ if (inherited.unresolved) unresolved = true;
865
+ options = { ...options, ...inherited.options };
866
+ }
867
+ return { options: { ...options, ...(parsed?.compilerOptions || {}) }, unresolved };
868
+ }
869
+
870
+ /**
871
+ * Per-file elision settings from the nearest tsconfig.json (with its `extends`
872
+ * chain). `elide: false` when the compiler keeps import statements
873
+ * (`verbatimModuleSyntax`, `preserveValueImports`, `importsNotUsedAsValues`
874
+ * set to "preserve"/"error", or `erasableSyntaxOnly`, the setting for Node's
875
+ * type stripping, which removes only `import type`). `inferUsage: false` when
876
+ * part of the `extends` chain is not in the checkout: then only names declared
877
+ * as interface / type alias are treated as type-only (code that compiles under
878
+ * `verbatimModuleSyntax` cannot import those without `type`), and a class used
879
+ * only as a type stays a runtime edge. `decoratorMetadata` when
880
+ * `emitDecoratorMetadata` is on: annotations on decorated members are then
881
+ * runtime references.
882
+ */
883
+ export function createElisionSettings(repoRoot) {
884
+ const byDir = new Map();
885
+ async function forDir(dir) {
886
+ if (byDir.has(dir)) return byDir.get(dir);
887
+ const promise = (async () => {
888
+ const configRel = dir === "." || dir === "" ? "tsconfig.json" : `${dir}/tsconfig.json`;
889
+ let exists = false;
890
+ try {
891
+ await fs.access(path.join(repoRoot, configRel));
892
+ exists = true;
893
+ } catch {
894
+ exists = false;
895
+ }
896
+ if (exists) {
897
+ const { options, unresolved } = await readCompilerOptions(repoRoot, configRel);
898
+ const preserve = options.verbatimModuleSyntax === true
899
+ || options.preserveValueImports === true
900
+ || options.erasableSyntaxOnly === true
901
+ || ["preserve", "error"].includes(String(options.importsNotUsedAsValues || "").toLowerCase());
902
+ return {
903
+ elide: !preserve,
904
+ inferUsage: !preserve && !unresolved,
905
+ decoratorMetadata: options.emitDecoratorMetadata === true,
906
+ config: configRel,
907
+ unresolved,
908
+ };
909
+ }
910
+ if (dir === "." || dir === "") return { elide: true, inferUsage: true, decoratorMetadata: false, config: null, unresolved: false };
911
+ return forDir(path.posix.dirname(dir));
912
+ })();
913
+ byDir.set(dir, promise);
914
+ return promise;
915
+ }
916
+ return (file) => forDir(path.posix.dirname(String(file).replace(/\\/g, "/")));
917
+ }
918
+
919
+ export function isTypeScriptFile(file) {
920
+ return /\.(ts|tsx|mts|cts)$/.test(String(file)) && !/\.d\.[cm]?ts$/.test(String(file));
921
+ }