deslop-js 0.0.17-dev.e47ef34 → 0.0.17-dev.e99b313

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,2248 @@
1
+ import { parentPort } from "node:worker_threads";
2
+ import { parseSync } from "oxc-parser";
3
+ import { readFileSync, statSync } from "node:fs";
4
+
5
+ //#region src/constants.ts
6
+ const DEFAULT_EXTENSIONS = [
7
+ ".ts",
8
+ ".tsx",
9
+ ".js",
10
+ ".jsx",
11
+ ".mts",
12
+ ".mjs",
13
+ ".cts",
14
+ ".cjs",
15
+ ".mdx",
16
+ ".astro",
17
+ ".graphql",
18
+ ".gql",
19
+ ".css",
20
+ ".scss",
21
+ ".vue",
22
+ ".svelte"
23
+ ];
24
+ const RESOLVER_EXTENSIONS = [
25
+ ...DEFAULT_EXTENSIONS,
26
+ ".d.ts",
27
+ ".d.mts",
28
+ ".d.cts",
29
+ ".json",
30
+ ".node",
31
+ ".css",
32
+ ".scss",
33
+ ".less",
34
+ ".svg",
35
+ ".png",
36
+ ".jpg",
37
+ ".graphql",
38
+ ".gql"
39
+ ];
40
+ const MAX_PARSE_FILE_SIZE_BYTES = 2e6;
41
+ const MAX_ERROR_DETAIL_LENGTH = 1e3;
42
+ const BINARY_DETECTION_SAMPLE_BYTES = 2048;
43
+ const MINIFIED_DETECTION_MIN_BYTES = 5e3;
44
+ /**
45
+ * Numeric literals below 1000 are dominated by indices, counters, small
46
+ * ranges, ports, percentages, and array sizes that coincide by accident
47
+ * (every `MAX_RETRIES = 3` is not a duplicate of every `LIMIT = 3`).
48
+ * 1000 admits real shared constants (timeouts in ms, byte sizes, polling
49
+ * intervals) without producing the noise floor that smaller magnitudes do.
50
+ * NOTE: even at 1000, the rule still produces medium-confidence false
51
+ * positives when constants share a value coincidentally with different
52
+ * names (e.g. `STEP_DELAY_MS` vs `MINIMUM_TOKENS`); the report explicitly
53
+ * downgrades those to `confidence: "medium"`.
54
+ */
55
+ const MIN_NUMERIC_LITERAL_MAGNITUDE_FOR_DUPLICATE = 1e3;
56
+
57
+ //#endregion
58
+ //#region src/errors.ts
59
+ const truncateDetail = (text) => {
60
+ if (text.length <= 1e3) return text;
61
+ return `${text.slice(0, MAX_ERROR_DETAIL_LENGTH)}… [truncated ${text.length - MAX_ERROR_DETAIL_LENGTH} chars]`;
62
+ };
63
+ const describeUnknownError = (caughtValue) => {
64
+ let rawText;
65
+ if (caughtValue instanceof Error) rawText = caughtValue.message || caughtValue.name || "unknown error";
66
+ else if (typeof caughtValue === "string") rawText = caughtValue;
67
+ else try {
68
+ rawText = JSON.stringify(caughtValue);
69
+ } catch {
70
+ rawText = String(caughtValue);
71
+ }
72
+ return truncateDetail(rawText ?? "");
73
+ };
74
+ var DeslopError = class DeslopError extends Error {
75
+ constructor(input) {
76
+ super(input.message);
77
+ this.name = "DeslopError";
78
+ this.code = input.code;
79
+ this.module = input.module;
80
+ this.severity = input.severity ?? "warning";
81
+ if (input.path !== void 0) this.path = input.path;
82
+ if (input.detail !== void 0) this.detail = input.detail;
83
+ }
84
+ toJSON() {
85
+ const payload = {
86
+ name: this.name,
87
+ code: this.code,
88
+ module: this.module,
89
+ severity: this.severity,
90
+ message: this.message
91
+ };
92
+ if (this.path !== void 0) payload.path = this.path;
93
+ if (this.detail !== void 0) payload.detail = this.detail;
94
+ return payload;
95
+ }
96
+ static fromCaught(input) {
97
+ return new DeslopError({
98
+ code: input.code,
99
+ module: input.module,
100
+ severity: input.severity,
101
+ message: input.message,
102
+ path: input.path,
103
+ detail: describeUnknownError(input.caught)
104
+ });
105
+ }
106
+ };
107
+ var FileReadError = class extends DeslopError {
108
+ constructor(input) {
109
+ super({
110
+ ...input,
111
+ module: "parse"
112
+ });
113
+ this.name = "FileReadError";
114
+ }
115
+ };
116
+ var ParseError = class extends DeslopError {
117
+ constructor(input) {
118
+ super({
119
+ ...input,
120
+ module: "parse"
121
+ });
122
+ this.name = "ParseError";
123
+ }
124
+ };
125
+
126
+ //#endregion
127
+ //#region src/utils/line-column.ts
128
+ const getLineFromOffset = (source, offset) => {
129
+ let line = 1;
130
+ for (let charIndex = 0; charIndex < offset && charIndex < source.length; charIndex++) if (source[charIndex] === "\n") line++;
131
+ return line;
132
+ };
133
+ const getColumnFromOffset = (source, offset) => {
134
+ let column = 0;
135
+ for (let charIndex = offset - 1; charIndex >= 0; charIndex--) {
136
+ if (source[charIndex] === "\n") break;
137
+ column++;
138
+ }
139
+ return column;
140
+ };
141
+
142
+ //#endregion
143
+ //#region src/utils/extract-default-export-local-name.ts
144
+ const extractIdentifierFromCallArguments = (expression) => {
145
+ if (expression.type !== "CallExpression") return void 0;
146
+ for (const argument of expression.arguments) {
147
+ if (argument.type === "Identifier" && argument.name) return argument.name;
148
+ if (argument.type === "CallExpression") {
149
+ const nestedName = extractIdentifierFromCallArguments(argument);
150
+ if (nestedName) return nestedName;
151
+ }
152
+ }
153
+ if (expression.callee.type === "CallExpression") return extractIdentifierFromCallArguments(expression.callee);
154
+ };
155
+ const extractDefaultExportLocalName = (declaration) => {
156
+ if (!declaration) return void 0;
157
+ if (declaration.type === "Identifier" && declaration.name) return declaration.name;
158
+ if (declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") return declaration.id?.name;
159
+ if (declaration.type === "CallExpression") return extractIdentifierFromCallArguments(declaration);
160
+ };
161
+
162
+ //#endregion
163
+ //#region src/utils/detect-redundant-type-pattern.ts
164
+ const isTypeNode = (value) => Boolean(value) && typeof value === "object" && typeof value.type === "string";
165
+ const isEmptyTypeLiteral = (node) => {
166
+ if (node.type !== "TSTypeLiteral") return false;
167
+ const members = node.members;
168
+ return Array.isArray(members) && members.length === 0;
169
+ };
170
+ const typeReferenceName = (node) => {
171
+ if (node.type !== "TSTypeReference") return void 0;
172
+ const typeName = node.typeName;
173
+ if (!typeName || typeName.type !== "Identifier") return void 0;
174
+ return typeName.name;
175
+ };
176
+ const isKeyofOfType = (candidate, expectedReferenceName) => {
177
+ if (candidate.type !== "TSTypeOperator") return false;
178
+ if (candidate.operator !== "keyof") return false;
179
+ const operand = candidate.typeAnnotation;
180
+ if (!operand) return false;
181
+ return typeReferenceName(operand) === expectedReferenceName;
182
+ };
183
+ const isNeverKeyword = (node) => node.type === "TSNeverKeyword";
184
+ const isLiterallyEqualByJson = (left, right) => {
185
+ const stripPositions = (key, value) => {
186
+ if (key === "start" || key === "end") return void 0;
187
+ return value;
188
+ };
189
+ return JSON.stringify(left, stripPositions) === JSON.stringify(right, stripPositions);
190
+ };
191
+ const detectIntersectionWithEmpty = (node) => {
192
+ if (node.type !== "TSIntersectionType") return void 0;
193
+ const operands = node.types;
194
+ if (!Array.isArray(operands) || operands.length < 2) return void 0;
195
+ if (!operands.some(isEmptyTypeLiteral)) return void 0;
196
+ return {
197
+ kind: "intersection-with-empty-object",
198
+ reason: "intersection with `{}` is a no-op; the empty object type does not constrain anything",
199
+ suggestion: "drop the `& {}` term"
200
+ };
201
+ };
202
+ const detectSelfUnion = (node) => {
203
+ if (node.type !== "TSUnionType") return void 0;
204
+ const operands = node.types;
205
+ if (!Array.isArray(operands) || operands.length < 2) return void 0;
206
+ for (let leftIndex = 0; leftIndex < operands.length; leftIndex++) for (let rightIndex = leftIndex + 1; rightIndex < operands.length; rightIndex++) if (isLiterallyEqualByJson(operands[leftIndex], operands[rightIndex])) return {
207
+ kind: "self-union",
208
+ reason: "union contains the same member twice",
209
+ suggestion: "deduplicate the union members"
210
+ };
211
+ };
212
+ const detectSelfIntersection = (node) => {
213
+ if (node.type !== "TSIntersectionType") return void 0;
214
+ const operands = node.types;
215
+ if (!Array.isArray(operands) || operands.length < 2) return void 0;
216
+ for (let leftIndex = 0; leftIndex < operands.length; leftIndex++) for (let rightIndex = leftIndex + 1; rightIndex < operands.length; rightIndex++) if (isLiterallyEqualByJson(operands[leftIndex], operands[rightIndex])) return {
217
+ kind: "self-intersection",
218
+ reason: "intersection contains the same operand twice",
219
+ suggestion: "deduplicate the intersection operands"
220
+ };
221
+ };
222
+ const detectNestedUtility = (node, utilityName, kind) => {
223
+ if (node.type !== "TSTypeReference") return void 0;
224
+ if (typeReferenceName(node) !== utilityName) return void 0;
225
+ const typeArguments = node.typeArguments;
226
+ if (!typeArguments) return void 0;
227
+ const params = typeArguments.params;
228
+ if (!Array.isArray(params) || params.length === 0) return void 0;
229
+ const firstArg = params[0];
230
+ if (firstArg.type !== "TSTypeReference") return void 0;
231
+ if (typeReferenceName(firstArg) !== utilityName) return void 0;
232
+ return {
233
+ kind,
234
+ reason: `${utilityName}<${utilityName}<T>> collapses to ${utilityName}<T>`,
235
+ suggestion: `flatten the nested ${utilityName}<...>`
236
+ };
237
+ };
238
+ const detectPickAllKeys = (node) => {
239
+ if (node.type !== "TSTypeReference") return void 0;
240
+ if (typeReferenceName(node) !== "Pick") return void 0;
241
+ const typeArguments = node.typeArguments;
242
+ if (!typeArguments) return void 0;
243
+ const params = typeArguments.params;
244
+ if (!Array.isArray(params) || params.length !== 2) return void 0;
245
+ const targetType = params[0];
246
+ const keys = params[1];
247
+ const targetName = typeReferenceName(targetType);
248
+ if (!targetName) return void 0;
249
+ if (!isKeyofOfType(keys, targetName)) return void 0;
250
+ return {
251
+ kind: "pick-all-keys",
252
+ reason: `Pick<${targetName}, keyof ${targetName}> is equivalent to ${targetName} itself`,
253
+ suggestion: `replace with ${targetName}`
254
+ };
255
+ };
256
+ const detectOmitNoKeys = (node) => {
257
+ if (node.type !== "TSTypeReference") return void 0;
258
+ if (typeReferenceName(node) !== "Omit") return void 0;
259
+ const typeArguments = node.typeArguments;
260
+ if (!typeArguments) return void 0;
261
+ const params = typeArguments.params;
262
+ if (!Array.isArray(params) || params.length !== 2) return void 0;
263
+ const targetType = params[0];
264
+ const keys = params[1];
265
+ const targetName = typeReferenceName(targetType);
266
+ if (!targetName) return void 0;
267
+ if (!isNeverKeyword(keys)) return void 0;
268
+ return {
269
+ kind: "omit-no-keys",
270
+ reason: `Omit<${targetName}, never> is equivalent to ${targetName} itself`,
271
+ suggestion: `replace with ${targetName}`
272
+ };
273
+ };
274
+ const isZodInferDeclarationMergingExtension = (parentExpression) => {
275
+ if (!parentExpression || parentExpression.type !== "MemberExpression") return false;
276
+ const propertyNode = parentExpression.property;
277
+ if (!propertyNode || propertyNode.type !== "Identifier") return false;
278
+ return propertyNode.name === "infer";
279
+ };
280
+ const isRadixStylePropsAliasExtension = (parentExpression) => {
281
+ if (!parentExpression || parentExpression.type !== "MemberExpression") return false;
282
+ const propertyNode = parentExpression.property;
283
+ if (!propertyNode || propertyNode.type !== "Identifier") return false;
284
+ return propertyNode.name === "Props";
285
+ };
286
+ const detectEmptyInterfaceExtendsOne = (declarationNode) => {
287
+ if (declarationNode.type !== "TSInterfaceDeclaration") return void 0;
288
+ const body = declarationNode.body;
289
+ if (!body || !Array.isArray(body.body) || body.body.length !== 0) return void 0;
290
+ const extendsClauses = declarationNode.extends;
291
+ if (!Array.isArray(extendsClauses) || extendsClauses.length !== 1) return void 0;
292
+ const declarationName = declarationNode.id?.name;
293
+ const parentExpression = extendsClauses[0]?.expression;
294
+ if (isZodInferDeclarationMergingExtension(parentExpression)) return void 0;
295
+ if (isRadixStylePropsAliasExtension(parentExpression)) return void 0;
296
+ const parentName = parentExpression && parentExpression.type === "Identifier" ? parentExpression.name : void 0;
297
+ return {
298
+ kind: "empty-interface-extends-one",
299
+ reason: `interface ${declarationName ?? "<anon>"} extends ${parentName ?? "<base>"} with no new members`,
300
+ suggestion: `replace with \`type ${declarationName ?? "X"} = ${parentName ?? "Base"}\``
301
+ };
302
+ };
303
+ const detectRedundantTypePatternForTypeAnnotation = (typeAnnotation) => {
304
+ if (!isTypeNode(typeAnnotation)) return void 0;
305
+ return detectIntersectionWithEmpty(typeAnnotation) ?? detectSelfUnion(typeAnnotation) ?? detectSelfIntersection(typeAnnotation) ?? detectNestedUtility(typeAnnotation, "Partial", "nested-partial") ?? detectNestedUtility(typeAnnotation, "Readonly", "nested-readonly") ?? detectNestedUtility(typeAnnotation, "Required", "nested-required") ?? detectPickAllKeys(typeAnnotation) ?? detectOmitNoKeys(typeAnnotation);
306
+ };
307
+ const detectRedundantInterfaceDeclaration = (declarationNode) => {
308
+ if (!isTypeNode(declarationNode)) return void 0;
309
+ return detectEmptyInterfaceExtendsOne(declarationNode);
310
+ };
311
+
312
+ //#endregion
313
+ //#region src/utils/oxc-ast-node.ts
314
+ const isOxcAstNode = (value) => Boolean(value) && typeof value === "object" && typeof value.type === "string";
315
+ const getNodeStringField = (node, key) => {
316
+ const value = node[key];
317
+ return typeof value === "string" ? value : void 0;
318
+ };
319
+ const getIdentifierName = (node) => {
320
+ if (!isOxcAstNode(node)) return void 0;
321
+ if (node.type !== "Identifier") return void 0;
322
+ return getNodeStringField(node, "name");
323
+ };
324
+
325
+ //#endregion
326
+ //#region src/utils/detect-identity-wrapper.ts
327
+ const getCalleeText = (calleeNode) => {
328
+ if (calleeNode.type === "Identifier") return getIdentifierName(calleeNode);
329
+ if (calleeNode.type === "MemberExpression") {
330
+ if (calleeNode.computed) return void 0;
331
+ const objectNode = calleeNode.object;
332
+ const propertyNode = calleeNode.property;
333
+ if (!objectNode || !propertyNode) return void 0;
334
+ const objectText = getCalleeText(objectNode);
335
+ const propertyText = propertyNode.type === "Identifier" ? propertyNode.name : void 0;
336
+ if (!objectText || !propertyText) return void 0;
337
+ return `${objectText}.${propertyText}`;
338
+ }
339
+ };
340
+ const collectParameterNames = (parameters) => {
341
+ const names = [];
342
+ let hasRest = false;
343
+ let hasDefault = false;
344
+ let restName;
345
+ for (const parameter of parameters) {
346
+ if (!isOxcAstNode(parameter)) return {
347
+ names,
348
+ hasRest: true,
349
+ hasDefault,
350
+ restName
351
+ };
352
+ if (parameter.type === "RestElement") {
353
+ const restArgument = parameter.argument;
354
+ if (restArgument && restArgument.type === "Identifier") {
355
+ hasRest = true;
356
+ restName = restArgument.name;
357
+ continue;
358
+ }
359
+ return {
360
+ names,
361
+ hasRest: true,
362
+ hasDefault,
363
+ restName
364
+ };
365
+ }
366
+ if (parameter.type === "AssignmentPattern") {
367
+ hasDefault = true;
368
+ return {
369
+ names,
370
+ hasRest,
371
+ hasDefault,
372
+ restName
373
+ };
374
+ }
375
+ if (parameter.type === "Identifier") {
376
+ names.push(parameter.name);
377
+ continue;
378
+ }
379
+ return {
380
+ names: [],
381
+ hasRest,
382
+ hasDefault,
383
+ restName
384
+ };
385
+ }
386
+ return {
387
+ names,
388
+ hasRest,
389
+ hasDefault,
390
+ restName
391
+ };
392
+ };
393
+ const argumentsMatchParameters = (callArguments, parameterNames, restName) => {
394
+ if (restName !== void 0) {
395
+ if (callArguments.length !== 1) return false;
396
+ const onlyArgument = callArguments[0];
397
+ if (!isOxcAstNode(onlyArgument)) return false;
398
+ if (onlyArgument.type !== "SpreadElement") return false;
399
+ const spreadArgumentNode = onlyArgument.argument;
400
+ return Boolean(spreadArgumentNode && spreadArgumentNode.type === "Identifier" && spreadArgumentNode.name === restName);
401
+ }
402
+ if (callArguments.length !== parameterNames.length) return false;
403
+ for (let argumentIndex = 0; argumentIndex < callArguments.length; argumentIndex++) {
404
+ const argumentNode = callArguments[argumentIndex];
405
+ if (!isOxcAstNode(argumentNode)) return false;
406
+ if (argumentNode.type !== "Identifier") return false;
407
+ if (argumentNode.name !== parameterNames[argumentIndex]) return false;
408
+ }
409
+ return true;
410
+ };
411
+ const extractCallExpressionFromBody = (bodyNode) => {
412
+ if (bodyNode.type === "CallExpression") return bodyNode;
413
+ if (bodyNode.type === "BlockStatement") {
414
+ const blockBody = bodyNode.body;
415
+ if (!Array.isArray(blockBody) || blockBody.length !== 1) return void 0;
416
+ const onlyStatement = blockBody[0];
417
+ if (!isOxcAstNode(onlyStatement)) return void 0;
418
+ if (onlyStatement.type !== "ReturnStatement") return void 0;
419
+ const returnedExpression = onlyStatement.argument;
420
+ if (!returnedExpression) return void 0;
421
+ if (returnedExpression.type !== "CallExpression") return void 0;
422
+ return returnedExpression;
423
+ }
424
+ };
425
+ const detectIdentityWrapperFromInitializer = (initializerNode, wrapperName) => {
426
+ if (!isOxcAstNode(initializerNode)) return void 0;
427
+ if (initializerNode.type !== "ArrowFunctionExpression" && initializerNode.type !== "FunctionExpression") return;
428
+ if (initializerNode.async) return void 0;
429
+ if (initializerNode.generator) return void 0;
430
+ const { names: parameterNames, hasRest, hasDefault, restName } = collectParameterNames(initializerNode.params ?? []);
431
+ if (hasDefault) return void 0;
432
+ const bodyNode = initializerNode.body;
433
+ if (!bodyNode) return void 0;
434
+ const callExpression = extractCallExpressionFromBody(bodyNode);
435
+ if (!callExpression) return void 0;
436
+ const calleeNode = callExpression.callee;
437
+ if (!calleeNode) return void 0;
438
+ const calleeText = getCalleeText(calleeNode);
439
+ if (!calleeText) return void 0;
440
+ if (calleeText === wrapperName) return void 0;
441
+ if (!argumentsMatchParameters(callExpression.arguments ?? [], parameterNames, hasRest ? restName : void 0)) return;
442
+ return { wrappedExpression: calleeText };
443
+ };
444
+
445
+ //#endregion
446
+ //#region src/utils/normalize-type-hash.ts
447
+ const POSITION_KEYS = new Set([
448
+ "start",
449
+ "end",
450
+ "loc",
451
+ "range"
452
+ ]);
453
+ const NOISY_KEYS = new Set([
454
+ "decorators",
455
+ "leadingComments",
456
+ "trailingComments",
457
+ "innerComments",
458
+ "directive",
459
+ "optional",
460
+ "computed",
461
+ "static",
462
+ "accessibility",
463
+ "declare",
464
+ "readonly"
465
+ ]);
466
+ const NAME_KEYS_TO_STRIP = new Set(["id"]);
467
+ const sanitizeNode = (input) => {
468
+ if (input === null || input === void 0) return input;
469
+ if (Array.isArray(input)) return input.map((element) => sanitizeNode(element));
470
+ if (typeof input !== "object") return input;
471
+ const record = input;
472
+ const cleaned = {};
473
+ for (const key of Object.keys(record)) {
474
+ if (POSITION_KEYS.has(key)) continue;
475
+ if (NOISY_KEYS.has(key)) continue;
476
+ if (NAME_KEYS_TO_STRIP.has(key)) continue;
477
+ cleaned[key] = sanitizeNode(record[key]);
478
+ }
479
+ if (cleaned.type === "TSTypeLiteral" && Array.isArray(cleaned.members)) cleaned.members = sortMembersByKey(cleaned.members);
480
+ if (cleaned.type === "TSInterfaceBody" && Array.isArray(cleaned.body)) cleaned.body = sortMembersByKey(cleaned.body);
481
+ return cleaned;
482
+ };
483
+ const extractMemberKey = (member) => {
484
+ if (!member || typeof member !== "object") return "";
485
+ const record = member;
486
+ if (record.key) {
487
+ const candidate = record.key.name ?? record.key.value;
488
+ if (candidate === void 0 || candidate === null) return "";
489
+ return String(candidate);
490
+ }
491
+ return `__${record.type ?? ""}__`;
492
+ };
493
+ const sortMembersByKey = (members) => {
494
+ const tagged = members.map((member) => ({
495
+ key: extractMemberKey(member),
496
+ member
497
+ }));
498
+ tagged.sort((leftEntry, rightEntry) => {
499
+ if (leftEntry.key < rightEntry.key) return -1;
500
+ if (leftEntry.key > rightEntry.key) return 1;
501
+ return 0;
502
+ });
503
+ return tagged.map((entry) => entry.member);
504
+ };
505
+ const normalizeTypeAstHash = (typeAnnotation) => {
506
+ const sanitized = sanitizeNode(typeAnnotation);
507
+ return JSON.stringify(sanitized);
508
+ };
509
+
510
+ //#endregion
511
+ //#region src/utils/collect-inline-type-literals.ts
512
+ const isTypeLiteralNode = (node) => node.type === "TSTypeLiteral";
513
+ const buildPreview = (typeLiteralNode) => {
514
+ const members = typeLiteralNode.members ?? [];
515
+ const propertyKeys = [];
516
+ for (const memberCandidate of members) {
517
+ if (!isOxcAstNode(memberCandidate)) continue;
518
+ if (memberCandidate.type !== "TSPropertySignature") continue;
519
+ const keyNode = memberCandidate.key;
520
+ const keyName = keyNode?.name ?? keyNode?.value;
521
+ if (keyName) propertyKeys.push(String(keyName));
522
+ }
523
+ propertyKeys.sort();
524
+ const truncatedKeys = propertyKeys.slice(0, 4);
525
+ const suffix = propertyKeys.length > 4 ? `, +${propertyKeys.length - 4} more` : "";
526
+ return `{ ${truncatedKeys.join(", ")}${suffix} }`;
527
+ };
528
+ const countPropertySignatures = (typeLiteralNode) => {
529
+ const members = typeLiteralNode.members ?? [];
530
+ let signatureCount = 0;
531
+ for (const memberCandidate of members) {
532
+ if (!isOxcAstNode(memberCandidate)) continue;
533
+ if (memberCandidate.type === "TSPropertySignature") signatureCount++;
534
+ }
535
+ return signatureCount;
536
+ };
537
+ const captureIfTypeLiteral = (candidateNode, captures, context, nearestName) => {
538
+ if (!isOxcAstNode(candidateNode)) return;
539
+ if (!isTypeLiteralNode(candidateNode)) return;
540
+ const memberCount = countPropertySignatures(candidateNode);
541
+ if (memberCount < 3) return;
542
+ captures.push({
543
+ structuralHash: `inline:${normalizeTypeAstHash(candidateNode)}`,
544
+ memberCount,
545
+ preview: buildPreview(candidateNode),
546
+ context,
547
+ nearestName,
548
+ startOffset: candidateNode.start ?? 0
549
+ });
550
+ };
551
+ const GENERIC_WRAPPERS_TO_RECURSE = new Set([
552
+ "Array",
553
+ "ReadonlyArray",
554
+ "Promise",
555
+ "Set",
556
+ "ReadonlySet",
557
+ "Map",
558
+ "ReadonlyMap",
559
+ "Record",
560
+ "Partial",
561
+ "Required",
562
+ "Readonly",
563
+ "NonNullable",
564
+ "Awaited"
565
+ ]);
566
+ const inspectAnyTypeNode = (candidateNode, captures, context, nearestName, recursionDepth) => {
567
+ if (!isOxcAstNode(candidateNode)) return;
568
+ if (recursionDepth > 6) return;
569
+ if (isTypeLiteralNode(candidateNode)) {
570
+ captureIfTypeLiteral(candidateNode, captures, context, nearestName);
571
+ const members = candidateNode.members ?? [];
572
+ for (const memberCandidate of members) {
573
+ if (!isOxcAstNode(memberCandidate)) continue;
574
+ if (memberCandidate.type !== "TSPropertySignature") continue;
575
+ const memberKey = memberCandidate.key?.name;
576
+ const nested = memberCandidate.typeAnnotation;
577
+ inspectAnyTypeNode(nested, captures, "interface-property", memberKey ?? nearestName, recursionDepth + 1);
578
+ }
579
+ return;
580
+ }
581
+ if (candidateNode.type === "TSTypeAnnotation") {
582
+ inspectAnyTypeNode(candidateNode.typeAnnotation, captures, context, nearestName, recursionDepth + 1);
583
+ return;
584
+ }
585
+ if (candidateNode.type === "TSArrayType") {
586
+ inspectAnyTypeNode(candidateNode.elementType, captures, context, nearestName, recursionDepth + 1);
587
+ return;
588
+ }
589
+ if (candidateNode.type === "TSUnionType" || candidateNode.type === "TSIntersectionType") {
590
+ const operands = candidateNode.types ?? [];
591
+ for (const operand of operands) inspectAnyTypeNode(operand, captures, context, nearestName, recursionDepth + 1);
592
+ return;
593
+ }
594
+ if (candidateNode.type === "TSTupleType") {
595
+ const elements = candidateNode.elementTypes ?? [];
596
+ for (const element of elements) inspectAnyTypeNode(element, captures, context, nearestName, recursionDepth + 1);
597
+ return;
598
+ }
599
+ if (candidateNode.type === "TSTypeReference") {
600
+ const referenceTypeName = candidateNode.typeName?.name;
601
+ const typeArguments = candidateNode.typeArguments;
602
+ if (referenceTypeName && typeArguments?.params && GENERIC_WRAPPERS_TO_RECURSE.has(referenceTypeName)) for (const param of typeArguments.params) inspectAnyTypeNode(param, captures, context, nearestName, recursionDepth + 1);
603
+ }
604
+ };
605
+ const inspectTypeAnnotation = (typeAnnotationNode, captures, context, nearestName) => {
606
+ inspectAnyTypeNode(typeAnnotationNode, captures, context, nearestName, 0);
607
+ };
608
+ const visitFunctionParameters = (parameters, captures, functionName) => {
609
+ if (!parameters) return;
610
+ for (const parameter of parameters) {
611
+ if (!isOxcAstNode(parameter)) continue;
612
+ const parameterIdentifierName = getIdentifierName(parameter);
613
+ inspectTypeAnnotation(parameter.typeAnnotation, captures, "function-parameter", functionName ? `${functionName}(${parameterIdentifierName ?? "?"})` : parameterIdentifierName);
614
+ }
615
+ };
616
+ const visitFunctionLike = (functionNode, captures, functionName) => {
617
+ const parameters = functionNode.params;
618
+ visitFunctionParameters(parameters, captures, functionName);
619
+ const returnTypeNode = functionNode.returnType;
620
+ if (returnTypeNode) inspectTypeAnnotation(returnTypeNode, captures, "function-return", functionName);
621
+ const bodyNode = functionNode.body;
622
+ if (bodyNode) walkBodyForInlineTypes(bodyNode, captures, functionName);
623
+ };
624
+ const visitVariableDeclaration = (declarationNode, captures, enclosingName) => {
625
+ const declarators = declarationNode.declarations ?? [];
626
+ for (const declarator of declarators) {
627
+ if (!isOxcAstNode(declarator)) continue;
628
+ const declarationName = getIdentifierName(declarator.id);
629
+ inspectTypeAnnotation(declarator.typeAnnotation ?? (declarator.id && isOxcAstNode(declarator.id) ? declarator.id.typeAnnotation : void 0), captures, "variable-annotation", declarationName);
630
+ const initializerNode = declarator.init;
631
+ if (isOxcAstNode(initializerNode)) if (initializerNode.type === "ArrowFunctionExpression" || initializerNode.type === "FunctionExpression") visitFunctionLike(initializerNode, captures, declarationName ?? enclosingName);
632
+ else walkExpressionForInlineTypes(initializerNode, captures, declarationName ?? enclosingName);
633
+ }
634
+ };
635
+ const walkBodyForInlineTypes = (bodyNode, captures, enclosingName, recursionDepth = 0) => {
636
+ if (recursionDepth > 200) return;
637
+ if (!isOxcAstNode(bodyNode)) return;
638
+ const statements = bodyNode.body ?? [];
639
+ if (!Array.isArray(statements)) return;
640
+ for (const statement of statements) {
641
+ if (!isOxcAstNode(statement)) continue;
642
+ if (statement.type === "VariableDeclaration") visitVariableDeclaration(statement, captures, enclosingName);
643
+ else if (statement.type === "FunctionDeclaration") visitFunctionLike(statement, captures, getIdentifierName(statement.id) ?? enclosingName);
644
+ else if (statement.type === "TSTypeAliasDeclaration") {
645
+ const typeAliasName = getIdentifierName(statement.id);
646
+ captureIfTypeLiteral(statement.typeAnnotation, captures, "local-type-alias", typeAliasName);
647
+ } else if (statement.type === "ReturnStatement") walkExpressionForInlineTypes(statement.argument, captures, enclosingName, recursionDepth + 1);
648
+ else if (statement.type === "BlockStatement") walkBodyForInlineTypes(statement, captures, enclosingName, recursionDepth + 1);
649
+ else if (statement.type === "ExpressionStatement") walkExpressionForInlineTypes(statement.expression, captures, enclosingName, recursionDepth + 1);
650
+ }
651
+ };
652
+ const walkExpressionForInlineTypes = (expressionNode, captures, enclosingName, recursionDepth = 0) => {
653
+ if (recursionDepth > 200) return;
654
+ if (!isOxcAstNode(expressionNode)) return;
655
+ if (expressionNode.type === "ArrowFunctionExpression" || expressionNode.type === "FunctionExpression") {
656
+ visitFunctionLike(expressionNode, captures, enclosingName);
657
+ return;
658
+ }
659
+ for (const value of Object.values(expressionNode)) if (Array.isArray(value)) for (const element of value) walkExpressionForInlineTypes(element, captures, enclosingName, recursionDepth + 1);
660
+ else if (isOxcAstNode(value)) walkExpressionForInlineTypes(value, captures, enclosingName, recursionDepth + 1);
661
+ };
662
+ const visitTopLevelStatement = (statementNode, captures) => {
663
+ if (!isOxcAstNode(statementNode)) return;
664
+ const innerNode = statementNode.type === "ExportNamedDeclaration" || statementNode.type === "ExportDefaultDeclaration" ? statementNode.declaration ?? statementNode : statementNode;
665
+ const targetNode = isOxcAstNode(innerNode) ? innerNode : statementNode;
666
+ if (targetNode.type === "FunctionDeclaration") {
667
+ visitFunctionLike(targetNode, captures, getIdentifierName(targetNode.id));
668
+ return;
669
+ }
670
+ if (targetNode.type === "VariableDeclaration") {
671
+ visitVariableDeclaration(targetNode, captures, void 0);
672
+ return;
673
+ }
674
+ if (targetNode.type === "ClassDeclaration") {
675
+ const className = getIdentifierName(targetNode.id);
676
+ const members = targetNode.body?.body ?? [];
677
+ for (const memberCandidate of members) {
678
+ if (!isOxcAstNode(memberCandidate)) continue;
679
+ const memberKeyName = getIdentifierName(memberCandidate.key);
680
+ const qualifiedName = className && memberKeyName ? `${className}.${memberKeyName}` : memberKeyName;
681
+ if (memberCandidate.type === "PropertyDefinition") {
682
+ inspectTypeAnnotation(memberCandidate.typeAnnotation, captures, "class-property", qualifiedName);
683
+ continue;
684
+ }
685
+ if (memberCandidate.type === "MethodDefinition" || memberCandidate.type === "TSAbstractMethodDefinition") {
686
+ const methodValue = memberCandidate.value;
687
+ if (isOxcAstNode(methodValue)) visitFunctionLike(methodValue, captures, qualifiedName);
688
+ }
689
+ }
690
+ return;
691
+ }
692
+ if (targetNode.type === "TSInterfaceDeclaration") {
693
+ const interfaceName = getIdentifierName(targetNode.id);
694
+ const interfaceMembers = targetNode.body?.body ?? [];
695
+ for (const memberCandidate of interfaceMembers) {
696
+ if (!isOxcAstNode(memberCandidate)) continue;
697
+ if (memberCandidate.type !== "TSPropertySignature") continue;
698
+ const memberKeyName = getIdentifierName(memberCandidate.key);
699
+ const qualifiedName = interfaceName && memberKeyName ? `${interfaceName}.${memberKeyName}` : memberKeyName;
700
+ inspectTypeAnnotation(memberCandidate.typeAnnotation, captures, "interface-property", qualifiedName);
701
+ }
702
+ }
703
+ };
704
+ const collectInlineTypeLiterals = (programBody) => {
705
+ const captures = [];
706
+ for (const statement of programBody) visitTopLevelStatement(statement, captures);
707
+ return captures;
708
+ };
709
+
710
+ //#endregion
711
+ //#region src/utils/detect-simplifiable-function.ts
712
+ const containsAwaitExpression = (node, recursionDepth = 0) => {
713
+ if (recursionDepth > 30) return false;
714
+ if (!isOxcAstNode(node)) return false;
715
+ if (node.type === "AwaitExpression") return true;
716
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") return false;
717
+ for (const value of Object.values(node)) if (Array.isArray(value)) {
718
+ for (const element of value) if (containsAwaitExpression(element, recursionDepth + 1)) return true;
719
+ } else if (isOxcAstNode(value)) {
720
+ if (containsAwaitExpression(value, recursionDepth + 1)) return true;
721
+ }
722
+ return false;
723
+ };
724
+ const containsCallOrPromiseSurface = (node, recursionDepth = 0) => {
725
+ if (recursionDepth > 30) return false;
726
+ if (!isOxcAstNode(node)) return false;
727
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") return false;
728
+ if (node.type === "CallExpression" || node.type === "NewExpression" || node.type === "TaggedTemplateExpression" || node.type === "ThrowStatement" || node.type === "YieldExpression") return true;
729
+ if (node.type === "MemberExpression") {
730
+ const objectNode = node.object;
731
+ if (objectNode && isOxcAstNode(objectNode) && objectNode.type === "Identifier") {
732
+ if (objectNode.name === "Promise") return true;
733
+ }
734
+ }
735
+ for (const value of Object.values(node)) if (Array.isArray(value)) {
736
+ for (const element of value) if (containsCallOrPromiseSurface(element, recursionDepth + 1)) return true;
737
+ } else if (isOxcAstNode(value)) {
738
+ if (containsCallOrPromiseSurface(value, recursionDepth + 1)) return true;
739
+ }
740
+ return false;
741
+ };
742
+ const unwrapParenthesizedExpression = (node) => {
743
+ let current = node;
744
+ while (current.type === "ParenthesizedExpression") {
745
+ const inner = current.expression;
746
+ if (!inner || !isOxcAstNode(inner)) return current;
747
+ current = inner;
748
+ }
749
+ return current;
750
+ };
751
+ const isSimpleReturnArgument = (argumentNode) => {
752
+ if (!isOxcAstNode(argumentNode)) return false;
753
+ const unwrapped = unwrapParenthesizedExpression(argumentNode);
754
+ if (unwrapped.type === "BlockStatement") return false;
755
+ if (unwrapped.type === "ObjectExpression") return false;
756
+ if (unwrapped.type === "JSXElement") return false;
757
+ if (unwrapped.type === "JSXFragment") return false;
758
+ return true;
759
+ };
760
+ const detectBlockArrowSingleReturn = (functionNode) => {
761
+ if (functionNode.type !== "ArrowFunctionExpression") return void 0;
762
+ if (functionNode.async) return void 0;
763
+ const bodyNode = functionNode.body;
764
+ if (!bodyNode || bodyNode.type !== "BlockStatement") return void 0;
765
+ const statements = bodyNode.body ?? [];
766
+ if (statements.length !== 1) return void 0;
767
+ const onlyStatement = statements[0];
768
+ if (!isOxcAstNode(onlyStatement)) return void 0;
769
+ if (onlyStatement.type !== "ReturnStatement") return void 0;
770
+ const returnArgument = onlyStatement.argument;
771
+ if (!returnArgument) return void 0;
772
+ if (!isSimpleReturnArgument(returnArgument)) return void 0;
773
+ return {
774
+ kind: "block-arrow-single-return",
775
+ startOffset: functionNode.start ?? 0,
776
+ reason: "arrow body is a single `return` statement; the block can be replaced by the expression directly",
777
+ suggestion: "rewrite as `() => expression` without `{}`"
778
+ };
779
+ };
780
+ const detectRedundantAwaitReturn = (functionNode) => {
781
+ const bodyNode = functionNode.body;
782
+ if (!bodyNode || bodyNode.type !== "BlockStatement") return void 0;
783
+ const statements = bodyNode.body ?? [];
784
+ if (statements.length < 2) return void 0;
785
+ const penultimate = statements[statements.length - 2];
786
+ const last = statements[statements.length - 1];
787
+ if (!isOxcAstNode(penultimate) || !isOxcAstNode(last)) return void 0;
788
+ if (penultimate.type !== "VariableDeclaration") return void 0;
789
+ if (last.type !== "ReturnStatement") return void 0;
790
+ const declarators = penultimate.declarations ?? [];
791
+ if (declarators.length !== 1) return void 0;
792
+ const declarator = declarators[0];
793
+ if (!isOxcAstNode(declarator)) return void 0;
794
+ const declaredIdentifier = declarator.id;
795
+ const initializer = declarator.init;
796
+ if (!declaredIdentifier?.name) return void 0;
797
+ if (!isOxcAstNode(initializer)) return void 0;
798
+ if (initializer.type !== "AwaitExpression") return void 0;
799
+ const returnedArgument = last.argument;
800
+ if (!isOxcAstNode(returnedArgument)) return void 0;
801
+ if (returnedArgument.type !== "Identifier") return void 0;
802
+ if (returnedArgument.name !== declaredIdentifier.name) return void 0;
803
+ return {
804
+ kind: "redundant-await-return",
805
+ startOffset: penultimate.start ?? 0,
806
+ reason: `\`const ${declaredIdentifier.name} = await …; return ${declaredIdentifier.name};\` can be \`return …;\` (the await is preserved by the implicit promise chain)`,
807
+ suggestion: `replace the await/assign/return sequence with a single \`return await …\` or \`return …\` if no try/catch wraps it`
808
+ };
809
+ };
810
+ const isAsyncFunction = (functionNode) => Boolean(functionNode.async);
811
+ const containsPromiseTypeReference = (node, recursionDepth = 0) => {
812
+ if (recursionDepth > 30) return false;
813
+ if (!isOxcAstNode(node)) return false;
814
+ if (node.type === "TSTypeReference") {
815
+ const typeName = node.typeName;
816
+ if (typeName?.name === "Promise") return true;
817
+ if (typeName?.right?.name === "Promise") return true;
818
+ }
819
+ for (const value of Object.values(node)) if (Array.isArray(value)) {
820
+ for (const element of value) if (containsPromiseTypeReference(element, recursionDepth + 1)) return true;
821
+ } else if (isOxcAstNode(value)) {
822
+ if (containsPromiseTypeReference(value, recursionDepth + 1)) return true;
823
+ }
824
+ return false;
825
+ };
826
+ const hasExplicitPromiseReturnType = (functionNode) => {
827
+ const returnType = functionNode.returnType;
828
+ if (!returnType || !isOxcAstNode(returnType)) return false;
829
+ const annotation = returnType.typeAnnotation;
830
+ if (!annotation || !isOxcAstNode(annotation)) return false;
831
+ return containsPromiseTypeReference(annotation);
832
+ };
833
+ const detectUselessAsync = (functionNode, context) => {
834
+ if (!isAsyncFunction(functionNode)) return void 0;
835
+ if (functionNode.type === "ClassDeclaration" || functionNode.type === "MethodDefinition") return;
836
+ if (context.isMethodContext) return void 0;
837
+ if (context.isInlineCallback) return void 0;
838
+ if (hasExplicitPromiseReturnType(functionNode)) return void 0;
839
+ const bodyNode = functionNode.body;
840
+ if (!isOxcAstNode(bodyNode)) return void 0;
841
+ if (containsAwaitExpression(bodyNode)) return void 0;
842
+ if (containsCallOrPromiseSurface(bodyNode)) return void 0;
843
+ return {
844
+ kind: "useless-async-no-await",
845
+ startOffset: functionNode.start ?? 0,
846
+ reason: "async function body contains no `await`, no function calls, and no Promise surface — the implicit Promise wrap is purely decorative",
847
+ suggestion: "drop `async` (caller's existing `await` keeps the type identical) or add an explicit return type"
848
+ };
849
+ };
850
+ const detectSimplifiableFunctionPatterns = (functionNode, context = {}) => {
851
+ if (!isOxcAstNode(functionNode)) return [];
852
+ const findings = [];
853
+ const blockArrow = detectBlockArrowSingleReturn(functionNode);
854
+ if (blockArrow) findings.push(blockArrow);
855
+ const awaitReturn = detectRedundantAwaitReturn(functionNode);
856
+ if (awaitReturn) findings.push(awaitReturn);
857
+ const uselessAsync = detectUselessAsync(functionNode, context);
858
+ if (uselessAsync) findings.push(uselessAsync);
859
+ return findings;
860
+ };
861
+
862
+ //#endregion
863
+ //#region src/utils/collect-simplifiable-functions.ts
864
+ const looksLikeFunction = (node) => node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
865
+ const inferFunctionName = (functionNode, parentContext) => {
866
+ const declaredId = functionNode.id;
867
+ if (declaredId?.name) return declaredId.name;
868
+ return parentContext;
869
+ };
870
+ const visitFunctionAndDescend = (functionNode, captures, contextName, recursionDepth, isMethodContext, isInlineCallback) => {
871
+ const functionName = inferFunctionName(functionNode, contextName);
872
+ const detections = detectSimplifiableFunctionPatterns(functionNode, {
873
+ isMethodContext,
874
+ isInlineCallback
875
+ });
876
+ for (const detection of detections) captures.push({
877
+ kind: detection.kind,
878
+ functionName,
879
+ startOffset: detection.startOffset,
880
+ reason: detection.reason,
881
+ suggestion: detection.suggestion
882
+ });
883
+ const bodyNode = functionNode.body;
884
+ if (isOxcAstNode(bodyNode)) walkForFunctions(bodyNode, captures, functionName, recursionDepth + 1);
885
+ const parameters = functionNode.params ?? [];
886
+ for (const parameter of parameters) if (isOxcAstNode(parameter)) walkForFunctions(parameter, captures, functionName, recursionDepth + 1);
887
+ };
888
+ const isObjectMethodShorthand = (node) => (node.type === "Property" || node.type === "ObjectProperty") && node.method === true;
889
+ const isObjectPropertyAssignment = (node) => (node.type === "Property" || node.type === "ObjectProperty") && node.method !== true;
890
+ const isCallOrNewExpression = (node) => node.type === "CallExpression" || node.type === "NewExpression";
891
+ const walkForFunctions = (node, captures, contextName, recursionDepth = 0) => {
892
+ if (recursionDepth > 200) return;
893
+ if (looksLikeFunction(node)) {
894
+ visitFunctionAndDescend(node, captures, contextName, recursionDepth, false, false);
895
+ return;
896
+ }
897
+ let nextContext = contextName;
898
+ if (node.type === "VariableDeclarator") {
899
+ const declaredName = getIdentifierName(node.id);
900
+ if (declaredName) nextContext = declaredName;
901
+ }
902
+ if (node.type === "MethodDefinition" || node.type === "PropertyDefinition") {
903
+ const propertyKeyName = getIdentifierName(node.key);
904
+ if (propertyKeyName) nextContext = propertyKeyName;
905
+ }
906
+ if (node.type === "ClassDeclaration") {
907
+ const className = getIdentifierName(node.id);
908
+ if (className) nextContext = className;
909
+ }
910
+ if (node.type === "MethodDefinition" || isObjectMethodShorthand(node)) {
911
+ const methodValue = node.value;
912
+ if (methodValue && isOxcAstNode(methodValue) && looksLikeFunction(methodValue)) {
913
+ visitFunctionAndDescend(methodValue, captures, getIdentifierName(node.key) ?? nextContext, recursionDepth + 1, true, false);
914
+ const keyNode = node.key;
915
+ if (keyNode && isOxcAstNode(keyNode) && node.computed) walkForFunctions(keyNode, captures, nextContext, recursionDepth + 1);
916
+ return;
917
+ }
918
+ }
919
+ if (isObjectPropertyAssignment(node)) {
920
+ const propertyValue = node.value;
921
+ if (propertyValue && isOxcAstNode(propertyValue) && looksLikeFunction(propertyValue)) {
922
+ visitFunctionAndDescend(propertyValue, captures, getIdentifierName(node.key) ?? nextContext, recursionDepth + 1, false, true);
923
+ const keyNode = node.key;
924
+ if (keyNode && isOxcAstNode(keyNode) && node.computed) walkForFunctions(keyNode, captures, nextContext, recursionDepth + 1);
925
+ return;
926
+ }
927
+ }
928
+ if (isCallOrNewExpression(node)) {
929
+ const callee = node.callee;
930
+ if (callee && isOxcAstNode(callee)) walkForFunctions(callee, captures, nextContext, recursionDepth + 1);
931
+ const callArguments = node.arguments ?? [];
932
+ for (const argument of callArguments) {
933
+ if (!isOxcAstNode(argument)) continue;
934
+ if (looksLikeFunction(argument)) visitFunctionAndDescend(argument, captures, nextContext, recursionDepth + 1, false, true);
935
+ else walkForFunctions(argument, captures, nextContext, recursionDepth + 1);
936
+ }
937
+ return;
938
+ }
939
+ for (const value of Object.values(node)) if (Array.isArray(value)) {
940
+ for (const element of value) if (isOxcAstNode(element)) walkForFunctions(element, captures, nextContext, recursionDepth + 1);
941
+ } else if (isOxcAstNode(value)) walkForFunctions(value, captures, nextContext, recursionDepth + 1);
942
+ };
943
+ const collectSimplifiableFunctions = (programBody) => {
944
+ const captures = [];
945
+ for (const statement of programBody) if (isOxcAstNode(statement)) walkForFunctions(statement, captures, void 0, 0);
946
+ return captures;
947
+ };
948
+
949
+ //#endregion
950
+ //#region src/utils/collect-simplifiable-expressions.ts
951
+ const memberAccessText = (node, depth = 0) => {
952
+ if (depth > 6) return void 0;
953
+ if (node.type === "Identifier") return node.name;
954
+ if (node.type === "ThisExpression") return "this";
955
+ if (node.type === "MemberExpression") {
956
+ if (node.computed) return void 0;
957
+ const objectNode = node.object;
958
+ const propertyNode = node.property;
959
+ if (!objectNode || !propertyNode) return void 0;
960
+ const objectText = memberAccessText(objectNode, depth + 1);
961
+ const propertyText = propertyNode.type === "Identifier" ? propertyNode.name : void 0;
962
+ if (!objectText || !propertyText) return void 0;
963
+ return `${objectText}.${propertyText}`;
964
+ }
965
+ };
966
+ const isBooleanLiteral = (node, expected) => {
967
+ if (node.type !== "Literal") return false;
968
+ return node.value === expected;
969
+ };
970
+ const detectSelfFallbackTernary = (conditionalNode) => {
971
+ if (conditionalNode.type !== "ConditionalExpression") return void 0;
972
+ const testNode = conditionalNode.test;
973
+ const consequentNode = conditionalNode.consequent;
974
+ if (!testNode || !consequentNode) return void 0;
975
+ const testText = memberAccessText(testNode);
976
+ const consequentText = memberAccessText(consequentNode);
977
+ if (!testText || !consequentText) return void 0;
978
+ if (testText !== consequentText) return void 0;
979
+ return {
980
+ kind: "self-fallback-ternary",
981
+ snippet: `${testText} ? ${consequentText} : ...`,
982
+ startOffset: conditionalNode.start ?? 0,
983
+ reason: `\`${testText} ? ${testText} : x\` is a self-fallback ternary`,
984
+ suggestion: `use \`${testText} ?? x\` (nullish-only) or \`${testText} || x\` (falsy fallback) depending on intent`
985
+ };
986
+ };
987
+ const detectTernaryReturnsBoolean = (conditionalNode) => {
988
+ if (conditionalNode.type !== "ConditionalExpression") return void 0;
989
+ const consequentNode = conditionalNode.consequent;
990
+ const alternateNode = conditionalNode.alternate;
991
+ if (!consequentNode || !alternateNode) return void 0;
992
+ const isTrueFalse = isBooleanLiteral(consequentNode, true) && isBooleanLiteral(alternateNode, false);
993
+ const isFalseTrue = isBooleanLiteral(consequentNode, false) && isBooleanLiteral(alternateNode, true);
994
+ if (!isTrueFalse && !isFalseTrue) return void 0;
995
+ return {
996
+ kind: "ternary-returns-boolean",
997
+ snippet: isTrueFalse ? "cond ? true : false" : "cond ? false : true",
998
+ startOffset: conditionalNode.start ?? 0,
999
+ reason: isTrueFalse ? "`cond ? true : false` collapses to `Boolean(cond)`" : "`cond ? false : true` collapses to `!cond`",
1000
+ suggestion: isTrueFalse ? "replace with `Boolean(cond)` or just `cond` when types match" : "replace with `!cond`"
1001
+ };
1002
+ };
1003
+ const isNullLiteral = (node) => node.type === "Literal" && node.value === null;
1004
+ const isUndefinedIdentifier = (node) => node.type === "Identifier" && node.name === "undefined";
1005
+ const detectNullishCoalescingWithNullish = (logicalNode) => {
1006
+ if (logicalNode.type !== "LogicalExpression") return void 0;
1007
+ if (logicalNode.operator !== "??") return void 0;
1008
+ const rightNode = logicalNode.right;
1009
+ if (!rightNode) return void 0;
1010
+ if (!(isNullLiteral(rightNode) || isUndefinedIdentifier(rightNode))) return void 0;
1011
+ const leftNode = logicalNode.left;
1012
+ const leftText = leftNode ? memberAccessText(leftNode) ?? "expr" : "expr";
1013
+ const rightLabel = isNullLiteral(rightNode) ? "null" : "undefined";
1014
+ return {
1015
+ kind: "nullish-coalescing-with-nullish",
1016
+ snippet: `${leftText} ?? ${rightLabel}`,
1017
+ startOffset: logicalNode.start ?? 0,
1018
+ reason: `\`x ?? ${rightLabel}\` looks like a no-op — but may be intentional when a caller's signature requires \`${rightLabel}\` (PropTypes, form-control onChange, etc.)`,
1019
+ suggestion: `if \`x\` is already \`T | ${rightLabel}\`, drop the \`?? ${rightLabel}\`; otherwise keep — the coercion changes the resolved type`
1020
+ };
1021
+ };
1022
+ const detectRedundantNullAndUndefinedCheck = (logicalNode) => {
1023
+ if (logicalNode.type !== "LogicalExpression") return void 0;
1024
+ if (logicalNode.operator !== "&&") return void 0;
1025
+ const leftNode = logicalNode.left;
1026
+ const rightNode = logicalNode.right;
1027
+ if (!leftNode || !rightNode) return void 0;
1028
+ if (leftNode.type !== "BinaryExpression" || rightNode.type !== "BinaryExpression") return void 0;
1029
+ const leftOp = leftNode.operator;
1030
+ const rightOp = rightNode.operator;
1031
+ if (leftOp !== "!==" || rightOp !== "!==") return void 0;
1032
+ const leftLeft = leftNode.left;
1033
+ const leftRight = leftNode.right;
1034
+ const rightLeft = rightNode.left;
1035
+ const rightRight = rightNode.right;
1036
+ if (!leftLeft || !leftRight || !rightLeft || !rightRight) return void 0;
1037
+ const leftLeftText = memberAccessText(leftLeft);
1038
+ const rightLeftText = memberAccessText(rightLeft);
1039
+ if (!leftLeftText || leftLeftText !== rightLeftText) return void 0;
1040
+ const leftRhsIsNull = isNullLiteral(leftRight);
1041
+ const leftRhsIsUndefined = isUndefinedIdentifier(leftRight);
1042
+ const rightRhsIsNull = isNullLiteral(rightRight);
1043
+ const rightRhsIsUndefined = isUndefinedIdentifier(rightRight);
1044
+ if (!(leftRhsIsNull && rightRhsIsUndefined || leftRhsIsUndefined && rightRhsIsNull)) return void 0;
1045
+ return {
1046
+ kind: "redundant-null-and-undefined-check",
1047
+ snippet: `${leftLeftText} !== null && ${leftLeftText} !== undefined`,
1048
+ startOffset: logicalNode.start ?? 0,
1049
+ reason: `\`x !== null && x !== undefined\` is equivalent to \`x != null\` (loose comparison checks both)`,
1050
+ suggestion: `replace with \`${leftLeftText} != null\``
1051
+ };
1052
+ };
1053
+ const detectDoubleBangBoolean = (unaryNode) => {
1054
+ if (unaryNode.type !== "UnaryExpression") return void 0;
1055
+ if (unaryNode.operator !== "!") return void 0;
1056
+ const inner = unaryNode.argument;
1057
+ if (!inner || inner.type !== "UnaryExpression") return void 0;
1058
+ if (inner.operator !== "!") return void 0;
1059
+ const coerced = inner.argument;
1060
+ if (!coerced) return void 0;
1061
+ const coercedText = memberAccessText(coerced) ?? "expr";
1062
+ return {
1063
+ kind: "double-bang-boolean",
1064
+ snippet: `!!${coercedText}`,
1065
+ startOffset: unaryNode.start ?? 0,
1066
+ reason: "`!!x` is a double-negation boolean coercion",
1067
+ suggestion: `replace with \`Boolean(${coercedText})\``
1068
+ };
1069
+ };
1070
+ const visit = (node, captures, depth) => {
1071
+ if (depth > 100) return;
1072
+ const conditionalCapture = detectSelfFallbackTernary(node) ?? detectTernaryReturnsBoolean(node);
1073
+ if (conditionalCapture) captures.push(conditionalCapture);
1074
+ const doubleBangCapture = detectDoubleBangBoolean(node);
1075
+ if (doubleBangCapture) captures.push(doubleBangCapture);
1076
+ const logicalCapture = detectNullishCoalescingWithNullish(node) ?? detectRedundantNullAndUndefinedCheck(node);
1077
+ if (logicalCapture) captures.push(logicalCapture);
1078
+ for (const value of Object.values(node)) if (Array.isArray(value)) {
1079
+ for (const element of value) if (isOxcAstNode(element)) visit(element, captures, depth + 1);
1080
+ } else if (isOxcAstNode(value)) visit(value, captures, depth + 1);
1081
+ };
1082
+ const collectSimplifiableExpressions = (programBody) => {
1083
+ const captures = [];
1084
+ for (const statement of programBody) if (isOxcAstNode(statement)) visit(statement, captures, 0);
1085
+ return captures;
1086
+ };
1087
+
1088
+ //#endregion
1089
+ //#region src/utils/collect-duplicate-constants.ts
1090
+ const FRAMEWORK_RESERVED_CONSTANT_NAMES = new Set([
1091
+ "dynamic",
1092
+ "dynamicParams",
1093
+ "revalidate",
1094
+ "runtime",
1095
+ "fetchCache",
1096
+ "preferredRegion",
1097
+ "maxDuration",
1098
+ "metadata",
1099
+ "viewport",
1100
+ "generateStaticParams",
1101
+ "generateMetadata",
1102
+ "config",
1103
+ "loader",
1104
+ "action",
1105
+ "links",
1106
+ "meta",
1107
+ "headers",
1108
+ "handle",
1109
+ "shouldRevalidate",
1110
+ "ErrorBoundary",
1111
+ "HydrateFallback",
1112
+ "Layout"
1113
+ ]);
1114
+ const isLiteralCandidate = (node) => {
1115
+ if (node.type === "Literal") {
1116
+ const value = node.value;
1117
+ if (typeof value === "string") {
1118
+ if (value.length < 8) return false;
1119
+ return true;
1120
+ }
1121
+ if (typeof value === "number") {
1122
+ if (!Number.isFinite(value)) return false;
1123
+ if (Math.abs(value) < 1e3) return false;
1124
+ return true;
1125
+ }
1126
+ return false;
1127
+ }
1128
+ if (node.type === "TemplateLiteral") {
1129
+ const expressions = node.expressions;
1130
+ if (Array.isArray(expressions) && expressions.length > 0) return false;
1131
+ const quasis = node.quasis;
1132
+ if (!Array.isArray(quasis) || quasis.length === 0) return false;
1133
+ return (quasis[0].value?.cooked ?? "").length >= 8;
1134
+ }
1135
+ if (node.type === "ArrayExpression") {
1136
+ const elements = node.elements ?? [];
1137
+ if (elements.length === 0) return false;
1138
+ for (const element of elements) {
1139
+ if (!isOxcAstNode(element)) return false;
1140
+ if (element.type !== "Literal") return false;
1141
+ }
1142
+ return true;
1143
+ }
1144
+ return false;
1145
+ };
1146
+ const hashLiteralNode = (node) => {
1147
+ if (node.type === "Literal") return `lit:${typeof node.value}:${JSON.stringify(node.value)}`;
1148
+ if (node.type === "TemplateLiteral") {
1149
+ const quasis = node.quasis ?? [];
1150
+ return `tpl:${JSON.stringify(quasis[0]?.value?.cooked ?? "")}`;
1151
+ }
1152
+ if (node.type === "ArrayExpression") return `arr:[${(node.elements ?? []).map((element) => {
1153
+ if (!isOxcAstNode(element)) return "?";
1154
+ if (element.type !== "Literal") return "?";
1155
+ return JSON.stringify(element.value);
1156
+ }).join(",")}]`;
1157
+ return "?";
1158
+ };
1159
+ const previewLiteralNode = (node) => {
1160
+ if (node.type === "Literal") {
1161
+ const value = node.value;
1162
+ if (typeof value === "string") return `"${value.length > 60 ? value.slice(0, 57) + "..." : value}"`;
1163
+ return String(value);
1164
+ }
1165
+ if (node.type === "TemplateLiteral") {
1166
+ const cooked = (node.quasis ?? [])[0]?.value?.cooked ?? "";
1167
+ return `\`${cooked.length > 60 ? cooked.slice(0, 57) + "..." : cooked}\``;
1168
+ }
1169
+ if (node.type === "ArrayExpression") {
1170
+ const elements = node.elements ?? [];
1171
+ return `[${elements.slice(0, 3).map((element) => isOxcAstNode(element) && element.type === "Literal" ? JSON.stringify(element.value) : "?").join(", ")}${elements.length > 3 ? `, +${elements.length - 3} more` : ""}]`;
1172
+ }
1173
+ return "<literal>";
1174
+ };
1175
+ const visitForConstants = (statementNode, candidates) => {
1176
+ if (!isOxcAstNode(statementNode)) return;
1177
+ const inner = (statementNode.type === "ExportNamedDeclaration" || statementNode.type === "ExportDefaultDeclaration") && statementNode.declaration ? statementNode.declaration : statementNode;
1178
+ if (!isOxcAstNode(inner)) return;
1179
+ if (inner.type !== "VariableDeclaration") return;
1180
+ if (inner.kind !== "const") return;
1181
+ const declarators = inner.declarations ?? [];
1182
+ for (const declarator of declarators) {
1183
+ if (!isOxcAstNode(declarator)) continue;
1184
+ const idNode = declarator.id;
1185
+ const initializerNode = declarator.init;
1186
+ if (!idNode || !initializerNode) continue;
1187
+ if (idNode.type !== "Identifier") continue;
1188
+ const constantName = idNode.name;
1189
+ if (!constantName) continue;
1190
+ if (FRAMEWORK_RESERVED_CONSTANT_NAMES.has(constantName)) continue;
1191
+ if (!isLiteralCandidate(initializerNode)) continue;
1192
+ candidates.push({
1193
+ constantName,
1194
+ literalHash: hashLiteralNode(initializerNode),
1195
+ literalPreview: previewLiteralNode(initializerNode),
1196
+ startOffset: declarator.start ?? inner.start ?? 0
1197
+ });
1198
+ }
1199
+ };
1200
+ const collectDuplicateConstantCandidates = (programBody) => {
1201
+ const candidates = [];
1202
+ for (const statement of programBody) visitForConstants(statement, candidates);
1203
+ return candidates;
1204
+ };
1205
+
1206
+ //#endregion
1207
+ //#region src/collect/parse.ts
1208
+ const extractMdxImportsExports = (sourceText) => {
1209
+ const statements = [];
1210
+ let isInMultiline = false;
1211
+ let braceDepth = 0;
1212
+ for (const line of sourceText.split("\n")) {
1213
+ const trimmedLine = line.trim();
1214
+ if (isInMultiline) {
1215
+ statements.push(line);
1216
+ for (const character of trimmedLine) {
1217
+ if (character === "{") braceDepth++;
1218
+ if (character === "}") braceDepth--;
1219
+ }
1220
+ const hasFromClause = trimmedLine.includes(" from ") || trimmedLine.includes(" from'") || trimmedLine.includes(" from\"");
1221
+ if (braceDepth <= 0 || trimmedLine.endsWith(";") || hasFromClause) {
1222
+ isInMultiline = false;
1223
+ braceDepth = 0;
1224
+ }
1225
+ } else if (trimmedLine.startsWith("import ") || trimmedLine.startsWith("import{") || trimmedLine.startsWith("export ") || trimmedLine.startsWith("export{")) {
1226
+ statements.push(line);
1227
+ for (const character of trimmedLine) {
1228
+ if (character === "{") braceDepth++;
1229
+ if (character === "}") braceDepth--;
1230
+ }
1231
+ if (braceDepth > 0 && !trimmedLine.includes(" from ")) isInMultiline = true;
1232
+ }
1233
+ }
1234
+ return statements.join("\n");
1235
+ };
1236
+ const ASTRO_FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
1237
+ const ASTRO_SCRIPT_TAG_PATTERN = /<script\b([^>]*?)\/>|<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
1238
+ const ASTRO_SCRIPT_SRC_ATTRIBUTE_PATTERN = /\bsrc\s*=\s*["']([^"']+)["']/i;
1239
+ const extractAstroSources = (sourceText) => {
1240
+ const sections = [];
1241
+ const frontmatterMatch = sourceText.match(ASTRO_FRONTMATTER_PATTERN);
1242
+ if (frontmatterMatch) sections.push(frontmatterMatch[1]);
1243
+ ASTRO_SCRIPT_TAG_PATTERN.lastIndex = 0;
1244
+ let scriptMatch;
1245
+ while ((scriptMatch = ASTRO_SCRIPT_TAG_PATTERN.exec(sourceText)) !== null) {
1246
+ const selfClosingAttributes = scriptMatch[1];
1247
+ const pairedAttributes = scriptMatch[2];
1248
+ const attributes = selfClosingAttributes ?? pairedAttributes ?? "";
1249
+ const body = selfClosingAttributes === void 0 ? scriptMatch[3] ?? "" : "";
1250
+ const srcMatch = attributes.match(ASTRO_SCRIPT_SRC_ATTRIBUTE_PATTERN);
1251
+ if (srcMatch) sections.push(`import ${JSON.stringify(srcMatch[1])};`);
1252
+ if (body) sections.push(body);
1253
+ }
1254
+ return sections.join("\n");
1255
+ };
1256
+ const VUE_SCRIPT_PATTERN = /<script[^>]*(?:lang=["'](?:ts|tsx)["'][^>]*)?>([\s\S]*?)<\/script>/gi;
1257
+ const extractVueScriptContent = (sourceText) => {
1258
+ const scriptBlocks = [];
1259
+ let scriptMatch;
1260
+ VUE_SCRIPT_PATTERN.lastIndex = 0;
1261
+ while ((scriptMatch = VUE_SCRIPT_PATTERN.exec(sourceText)) !== null) if (scriptMatch[1]) scriptBlocks.push(scriptMatch[1]);
1262
+ return scriptBlocks.join("\n");
1263
+ };
1264
+ const SVELTE_SCRIPT_PATTERN = /<script[^>]*>([\s\S]*?)<\/script>/gi;
1265
+ const extractSvelteScriptContent = (sourceText) => {
1266
+ const scriptBlocks = [];
1267
+ let scriptMatch;
1268
+ SVELTE_SCRIPT_PATTERN.lastIndex = 0;
1269
+ while ((scriptMatch = SVELTE_SCRIPT_PATTERN.exec(sourceText)) !== null) if (scriptMatch[1]) scriptBlocks.push(scriptMatch[1]);
1270
+ return scriptBlocks.join("\n");
1271
+ };
1272
+ const getModuleExportNameValue = (exportName) => {
1273
+ if (exportName.type === "Identifier") return exportName.name;
1274
+ if (exportName.type === "Literal") return exportName.value;
1275
+ return "default";
1276
+ };
1277
+ const CSS_EXTENSIONS = [
1278
+ ".css",
1279
+ ".scss",
1280
+ ".less",
1281
+ ".sass"
1282
+ ];
1283
+ const CSS_IMPORT_PATTERN = /@import\s+(?:url\()?['"]([^'"]+)['"]\)?/g;
1284
+ const SCSS_USE_FORWARD_PATTERN = /@(?:use|forward)\s+['"]([^'"]+)['"]/g;
1285
+ const TAILWIND_PLUGIN_REFERENCE_PATTERN = /@(?:plugin|reference|config)\s+['"]([^'"]+)['"]/g;
1286
+ const parseCssImports = (filePath) => {
1287
+ const sourceText = readFileSync(filePath, "utf-8");
1288
+ const imports = [];
1289
+ const patterns = [
1290
+ CSS_IMPORT_PATTERN,
1291
+ SCSS_USE_FORWARD_PATTERN,
1292
+ TAILWIND_PLUGIN_REFERENCE_PATTERN
1293
+ ];
1294
+ for (const pattern of patterns) {
1295
+ let match;
1296
+ pattern.lastIndex = 0;
1297
+ while ((match = pattern.exec(sourceText)) !== null) {
1298
+ const specifier = match[1];
1299
+ if (specifier && !specifier.startsWith("http")) imports.push({
1300
+ specifier,
1301
+ importedNames: [],
1302
+ isTypeOnly: false,
1303
+ isDynamic: false,
1304
+ isSideEffect: true,
1305
+ line: sourceText.substring(0, match.index).split("\n").length,
1306
+ column: 0
1307
+ });
1308
+ }
1309
+ }
1310
+ return {
1311
+ imports,
1312
+ exports: [],
1313
+ memberAccesses: [],
1314
+ wholeObjectUses: [],
1315
+ localIdentifierReferences: [],
1316
+ referencedFilenames: [],
1317
+ redundantTypePatterns: [],
1318
+ identityWrappers: [],
1319
+ typeDefinitionHashes: [],
1320
+ inlineTypeLiterals: [],
1321
+ simplifiableFunctions: [],
1322
+ simplifiableExpressions: [],
1323
+ duplicateConstantCandidates: [],
1324
+ errors: []
1325
+ };
1326
+ };
1327
+ const NON_JS_EXTENSIONS = [".graphql", ".gql"];
1328
+ const collectLocalIdentifierReferences = (statements) => {
1329
+ const references = [];
1330
+ const seenNames = /* @__PURE__ */ new Set();
1331
+ const visitNode = (node) => {
1332
+ if (!node || typeof node !== "object") return;
1333
+ const record = node;
1334
+ if (record.type === "Identifier" && typeof record.name === "string") {
1335
+ if (!seenNames.has(record.name)) {
1336
+ seenNames.add(record.name);
1337
+ references.push(record.name);
1338
+ }
1339
+ return;
1340
+ }
1341
+ for (const value of Object.values(record)) if (Array.isArray(value)) for (const innerValue of value) visitNode(innerValue);
1342
+ else if (value && typeof value === "object") visitNode(value);
1343
+ };
1344
+ for (const statement of statements) {
1345
+ if (statement.type === "ImportDeclaration" || statement.type === "ExportNamedDeclaration" || statement.type === "ExportDefaultDeclaration" || statement.type === "ExportAllDeclaration") continue;
1346
+ visitNode(statement);
1347
+ }
1348
+ return references;
1349
+ };
1350
+ const createEmptyParsedSource = () => ({
1351
+ imports: [],
1352
+ exports: [],
1353
+ memberAccesses: [],
1354
+ wholeObjectUses: [],
1355
+ localIdentifierReferences: [],
1356
+ referencedFilenames: [],
1357
+ redundantTypePatterns: [],
1358
+ identityWrappers: [],
1359
+ typeDefinitionHashes: [],
1360
+ inlineTypeLiterals: [],
1361
+ simplifiableFunctions: [],
1362
+ simplifiableExpressions: [],
1363
+ duplicateConstantCandidates: [],
1364
+ errors: []
1365
+ });
1366
+ const stripByteOrderMark = (sourceText) => {
1367
+ if (sourceText.charCodeAt(0) === 65279) return sourceText.slice(1);
1368
+ return sourceText;
1369
+ };
1370
+ const looksLikeBinaryContent = (sourceText) => {
1371
+ const sampleLength = Math.min(sourceText.length, BINARY_DETECTION_SAMPLE_BYTES);
1372
+ let nullByteCount = 0;
1373
+ for (let scanIndex = 0; scanIndex < sampleLength; scanIndex++) {
1374
+ if (sourceText.charCodeAt(scanIndex) === 0) nullByteCount++;
1375
+ if (nullByteCount > 4) return true;
1376
+ }
1377
+ return false;
1378
+ };
1379
+ const looksLikeMinifiedSource = (sourceText) => {
1380
+ if (sourceText.length < 5e3) return false;
1381
+ let newlineCount = 0;
1382
+ for (let scanIndex = 0; scanIndex < sourceText.length; scanIndex++) if (sourceText.charCodeAt(scanIndex) === 10) newlineCount++;
1383
+ return sourceText.length / (newlineCount + 1) > 500;
1384
+ };
1385
+ const safeReadSourceFile = (filePath, errors) => {
1386
+ try {
1387
+ const stats = statSync(filePath);
1388
+ if (stats.size === 0) {
1389
+ errors.push(new FileReadError({
1390
+ code: "file-empty",
1391
+ severity: "info",
1392
+ message: "file is empty — nothing to analyze",
1393
+ path: filePath
1394
+ }));
1395
+ return;
1396
+ }
1397
+ if (stats.size > 2e6) {
1398
+ errors.push(new FileReadError({
1399
+ code: "file-too-large",
1400
+ message: `file size ${stats.size}B exceeds MAX_PARSE_FILE_SIZE_BYTES (${MAX_PARSE_FILE_SIZE_BYTES})`,
1401
+ path: filePath
1402
+ }));
1403
+ return;
1404
+ }
1405
+ } catch (statError) {
1406
+ errors.push(new FileReadError({
1407
+ code: "file-read-failed",
1408
+ message: "could not stat source file",
1409
+ path: filePath,
1410
+ detail: describeUnknownError(statError)
1411
+ }));
1412
+ return;
1413
+ }
1414
+ try {
1415
+ const sourceText = stripByteOrderMark(readFileSync(filePath, "utf-8"));
1416
+ if (looksLikeBinaryContent(sourceText)) {
1417
+ errors.push(new FileReadError({
1418
+ code: "file-binary",
1419
+ severity: "info",
1420
+ message: "file appears to be binary — skipping",
1421
+ path: filePath
1422
+ }));
1423
+ return;
1424
+ }
1425
+ if (looksLikeMinifiedSource(sourceText)) {
1426
+ errors.push(new FileReadError({
1427
+ code: "file-minified",
1428
+ severity: "info",
1429
+ message: "file appears to be a minified/bundled artifact — skipping redundancy analysis",
1430
+ path: filePath
1431
+ }));
1432
+ return;
1433
+ }
1434
+ return sourceText;
1435
+ } catch (readError) {
1436
+ errors.push(new FileReadError({
1437
+ code: "file-read-failed",
1438
+ message: "could not read source file",
1439
+ path: filePath,
1440
+ detail: describeUnknownError(readError)
1441
+ }));
1442
+ return;
1443
+ }
1444
+ };
1445
+ const parseSourceFile = (filePath) => {
1446
+ if (CSS_EXTENSIONS.some((ext) => filePath.endsWith(ext))) try {
1447
+ return parseCssImports(filePath);
1448
+ } catch (cssError) {
1449
+ return {
1450
+ ...createEmptyParsedSource(),
1451
+ errors: [new ParseError({
1452
+ code: "parse-failed",
1453
+ message: "CSS import parsing crashed",
1454
+ path: filePath,
1455
+ detail: describeUnknownError(cssError)
1456
+ })]
1457
+ };
1458
+ }
1459
+ if (NON_JS_EXTENSIONS.some((ext) => filePath.endsWith(ext))) return createEmptyParsedSource();
1460
+ const earlyErrors = [];
1461
+ const sourceText = safeReadSourceFile(filePath, earlyErrors);
1462
+ if (sourceText === void 0) return {
1463
+ ...createEmptyParsedSource(),
1464
+ errors: earlyErrors
1465
+ };
1466
+ const imports = [];
1467
+ const exports = [];
1468
+ const isMdx = filePath.endsWith(".mdx");
1469
+ const isAstro = filePath.endsWith(".astro");
1470
+ const isVue = filePath.endsWith(".vue");
1471
+ const isSvelte = filePath.endsWith(".svelte");
1472
+ const isPreprocessed = isMdx || isAstro || isVue || isSvelte;
1473
+ const textToParse = isMdx ? extractMdxImportsExports(sourceText) : isAstro ? extractAstroSources(sourceText) : isVue ? extractVueScriptContent(sourceText) : isSvelte ? extractSvelteScriptContent(sourceText) : sourceText;
1474
+ const parseFileName = isMdx || isAstro || isVue || isSvelte ? filePath.replace(/\.(mdx|astro|vue|svelte)$/, ".tsx") : filePath;
1475
+ let result;
1476
+ try {
1477
+ result = parseSync(parseFileName, textToParse);
1478
+ } catch (parseError) {
1479
+ return {
1480
+ ...createEmptyParsedSource(),
1481
+ errors: [...earlyErrors, new ParseError({
1482
+ code: "parse-failed",
1483
+ message: "oxc-parser threw during initial parse",
1484
+ path: filePath,
1485
+ detail: describeUnknownError(parseError)
1486
+ })]
1487
+ };
1488
+ }
1489
+ if ((parseFileName.endsWith(".js") || parseFileName.endsWith(".mjs") || parseFileName.endsWith(".cjs")) && result.errors.length > 0) try {
1490
+ const jsxResult = parseSync(parseFileName.replace(/\.(m?js|cjs)$/, ".jsx"), textToParse);
1491
+ if (jsxResult.errors.length === 0) result = jsxResult;
1492
+ else {
1493
+ const tsxResult = parseSync(parseFileName.replace(/\.(m?js|cjs)$/, ".tsx"), textToParse);
1494
+ if (tsxResult.errors.length === 0) result = tsxResult;
1495
+ }
1496
+ } catch {}
1497
+ if (result.errors.length > 0 && !isPreprocessed) return {
1498
+ ...createEmptyParsedSource(),
1499
+ imports,
1500
+ exports,
1501
+ referencedFilenames: extractReferencedFilenames(sourceText),
1502
+ errors: [...earlyErrors, new ParseError({
1503
+ code: "parse-recovered",
1504
+ severity: "info",
1505
+ message: `oxc-parser reported ${result.errors.length} syntax issue(s); skipping deep analysis for this file`,
1506
+ path: filePath
1507
+ })]
1508
+ };
1509
+ if (result.errors.length > 0) earlyErrors.push(new ParseError({
1510
+ code: "parse-recovered-partial",
1511
+ severity: "info",
1512
+ message: `oxc-parser reported ${result.errors.length} syntax issue(s) in extracted ${isAstro ? "Astro" : isVue ? "Vue" : isSvelte ? "Svelte" : "MDX"} sources; continuing with partial AST`,
1513
+ path: filePath
1514
+ }));
1515
+ const program = result.program;
1516
+ if (!program?.body) return {
1517
+ ...createEmptyParsedSource(),
1518
+ imports,
1519
+ exports,
1520
+ referencedFilenames: extractReferencedFilenames(sourceText),
1521
+ errors: [...earlyErrors, new ParseError({
1522
+ code: "parse-failed",
1523
+ message: "oxc-parser returned no program body",
1524
+ path: filePath
1525
+ })]
1526
+ };
1527
+ const detectorErrors = [];
1528
+ const safeWalk = (walkerName, walker, fallback) => {
1529
+ try {
1530
+ return walker();
1531
+ } catch (walkError) {
1532
+ detectorErrors.push(new ParseError({
1533
+ code: "ast-walk-failed",
1534
+ message: `${walkerName} threw during AST traversal`,
1535
+ path: filePath,
1536
+ detail: describeUnknownError(walkError)
1537
+ }));
1538
+ return fallback;
1539
+ }
1540
+ };
1541
+ safeWalk("extractImportsAndExports", () => {
1542
+ for (const node of program.body) switch (node.type) {
1543
+ case "ImportDeclaration":
1544
+ extractImportDeclaration(node, sourceText, imports);
1545
+ break;
1546
+ case "ExportNamedDeclaration":
1547
+ extractNamedExportDeclaration(node, sourceText, exports);
1548
+ break;
1549
+ case "ExportDefaultDeclaration":
1550
+ extractDefaultExportDeclaration(node, sourceText, exports);
1551
+ break;
1552
+ case "ExportAllDeclaration":
1553
+ extractExportAllDeclaration(node, sourceText, exports);
1554
+ break;
1555
+ }
1556
+ }, void 0);
1557
+ safeWalk("collectDynamicImports", () => {
1558
+ collectDynamicImports(program.body, sourceText, imports);
1559
+ }, void 0);
1560
+ const namespaceLocalNames = collectNamespaceLocalNames(imports);
1561
+ const memberAccesses = [];
1562
+ const wholeObjectUses = [];
1563
+ if (namespaceLocalNames.size > 0) safeWalk("collectMemberAccesses", () => {
1564
+ collectMemberAccesses(program.body, namespaceLocalNames, memberAccesses, wholeObjectUses);
1565
+ }, void 0);
1566
+ const localIdentifierReferences = safeWalk("collectLocalIdentifierReferences", () => collectLocalIdentifierReferences(program.body), []);
1567
+ const redundantTypePatterns = [];
1568
+ const identityWrappers = [];
1569
+ const typeDefinitionHashes = [];
1570
+ safeWalk("collectDryPatterns", () => {
1571
+ collectDryPatterns(program.body, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes);
1572
+ }, void 0);
1573
+ const inlineTypeLiterals = safeWalk("collectInlineTypeLiterals", () => collectInlineTypeLiterals(program.body), []).map((capture) => ({
1574
+ structuralHash: capture.structuralHash,
1575
+ memberCount: capture.memberCount,
1576
+ preview: capture.preview,
1577
+ context: capture.context,
1578
+ nearestName: capture.nearestName,
1579
+ line: getLineFromOffset(sourceText, capture.startOffset),
1580
+ column: getColumnFromOffset(sourceText, capture.startOffset)
1581
+ }));
1582
+ const simplifiableFunctions = safeWalk("collectSimplifiableFunctions", () => collectSimplifiableFunctions(program.body), []).map((capture) => ({
1583
+ kind: capture.kind,
1584
+ functionName: capture.functionName,
1585
+ line: getLineFromOffset(sourceText, capture.startOffset),
1586
+ column: getColumnFromOffset(sourceText, capture.startOffset),
1587
+ reason: capture.reason,
1588
+ suggestion: capture.suggestion
1589
+ }));
1590
+ const simplifiableExpressions = safeWalk("collectSimplifiableExpressions", () => collectSimplifiableExpressions(program.body), []).map((capture) => ({
1591
+ kind: capture.kind,
1592
+ snippet: capture.snippet,
1593
+ line: getLineFromOffset(sourceText, capture.startOffset),
1594
+ column: getColumnFromOffset(sourceText, capture.startOffset),
1595
+ reason: capture.reason,
1596
+ suggestion: capture.suggestion
1597
+ }));
1598
+ const duplicateConstantCandidates = safeWalk("collectDuplicateConstantCandidates", () => collectDuplicateConstantCandidates(program.body), []).map((capture) => ({
1599
+ constantName: capture.constantName,
1600
+ literalHash: capture.literalHash,
1601
+ literalPreview: capture.literalPreview,
1602
+ line: getLineFromOffset(sourceText, capture.startOffset),
1603
+ column: getColumnFromOffset(sourceText, capture.startOffset)
1604
+ }));
1605
+ return {
1606
+ imports,
1607
+ exports,
1608
+ memberAccesses,
1609
+ wholeObjectUses,
1610
+ localIdentifierReferences,
1611
+ referencedFilenames: extractReferencedFilenames(sourceText),
1612
+ redundantTypePatterns,
1613
+ identityWrappers,
1614
+ typeDefinitionHashes,
1615
+ inlineTypeLiterals,
1616
+ simplifiableFunctions,
1617
+ simplifiableExpressions,
1618
+ duplicateConstantCandidates,
1619
+ errors: [...earlyErrors, ...detectorErrors]
1620
+ };
1621
+ };
1622
+ const REFERENCED_FILENAME_LITERAL_PATTERN = /(?<![./@\w-])(?:["'`])([a-z][\w-]*\.(?:ts|tsx|js|jsx|mts|mjs|cts|cjs))(?:["'`])/g;
1623
+ const extractReferencedFilenames = (sourceText) => {
1624
+ const captured = /* @__PURE__ */ new Set();
1625
+ REFERENCED_FILENAME_LITERAL_PATTERN.lastIndex = 0;
1626
+ let match;
1627
+ while ((match = REFERENCED_FILENAME_LITERAL_PATTERN.exec(sourceText)) !== null) captured.add(match[1]);
1628
+ return [...captured];
1629
+ };
1630
+ const collectDryPatterns = (bodyNodes, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes) => {
1631
+ for (const statement of bodyNodes) inspectStatement(statement, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes);
1632
+ };
1633
+ const inspectStatement = (statementNode, sourceText, redundantTypePatterns, identityWrappers, typeDefinitionHashes) => {
1634
+ let declarationOfInterest = statementNode;
1635
+ if (statementNode.type === "ExportNamedDeclaration" && statementNode.declaration) declarationOfInterest = statementNode.declaration;
1636
+ if (declarationOfInterest && typeof declarationOfInterest === "object") {
1637
+ const declarationNode = declarationOfInterest;
1638
+ if (declarationNode.type === "TSTypeAliasDeclaration") {
1639
+ const typeAliasName = declarationNode.id?.name;
1640
+ const typeAnnotation = declarationNode.typeAnnotation;
1641
+ const startOffset = declarationNode.start ?? 0;
1642
+ if (typeAliasName && typeAnnotation) {
1643
+ const redundantPattern = detectRedundantTypePatternForTypeAnnotation(typeAnnotation);
1644
+ if (redundantPattern) redundantTypePatterns.push({
1645
+ typeName: typeAliasName,
1646
+ kind: redundantPattern.kind,
1647
+ line: getLineFromOffset(sourceText, startOffset),
1648
+ column: getColumnFromOffset(sourceText, startOffset),
1649
+ reason: redundantPattern.reason,
1650
+ suggestion: redundantPattern.suggestion
1651
+ });
1652
+ typeDefinitionHashes.push({
1653
+ typeName: typeAliasName,
1654
+ structuralHash: `alias:${normalizeTypeAstHash(typeAnnotation)}`,
1655
+ line: getLineFromOffset(sourceText, startOffset),
1656
+ column: getColumnFromOffset(sourceText, startOffset)
1657
+ });
1658
+ }
1659
+ } else if (declarationNode.type === "TSInterfaceDeclaration") {
1660
+ const interfaceName = declarationNode.id?.name;
1661
+ const startOffset = declarationNode.start ?? 0;
1662
+ if (interfaceName) {
1663
+ const redundantPattern = detectRedundantInterfaceDeclaration(declarationNode);
1664
+ if (redundantPattern) redundantTypePatterns.push({
1665
+ typeName: interfaceName,
1666
+ kind: redundantPattern.kind,
1667
+ line: getLineFromOffset(sourceText, startOffset),
1668
+ column: getColumnFromOffset(sourceText, startOffset),
1669
+ reason: redundantPattern.reason,
1670
+ suggestion: redundantPattern.suggestion
1671
+ });
1672
+ const declarationCopy = {
1673
+ ...declarationNode,
1674
+ id: void 0
1675
+ };
1676
+ typeDefinitionHashes.push({
1677
+ typeName: interfaceName,
1678
+ structuralHash: `interface:${normalizeTypeAstHash(declarationCopy)}`,
1679
+ line: getLineFromOffset(sourceText, startOffset),
1680
+ column: getColumnFromOffset(sourceText, startOffset)
1681
+ });
1682
+ }
1683
+ } else if (declarationNode.type === "VariableDeclaration") for (const declarator of declarationNode.declarations ?? []) {
1684
+ const wrapperName = declarator.id?.name;
1685
+ const initializerNode = declarator.init;
1686
+ const startOffset = declarator.start ?? declarationNode.start ?? 0;
1687
+ if (!wrapperName || !initializerNode) continue;
1688
+ const wrapperDetection = detectIdentityWrapperFromInitializer(initializerNode, wrapperName);
1689
+ if (wrapperDetection) identityWrappers.push({
1690
+ wrapperName,
1691
+ wrappedExpression: wrapperDetection.wrappedExpression,
1692
+ line: getLineFromOffset(sourceText, startOffset),
1693
+ column: getColumnFromOffset(sourceText, startOffset)
1694
+ });
1695
+ }
1696
+ }
1697
+ };
1698
+ const WHOLE_OBJECT_FUNCTION_NAMES = new Set([
1699
+ "keys",
1700
+ "values",
1701
+ "entries",
1702
+ "assign",
1703
+ "freeze",
1704
+ "getOwnPropertyNames",
1705
+ "getOwnPropertyDescriptors"
1706
+ ]);
1707
+ const collectNamespaceLocalNames = (imports) => {
1708
+ const namespaceNames = /* @__PURE__ */ new Set();
1709
+ for (const importInfo of imports) for (const importedName of importInfo.importedNames) if (importedName.isNamespace && importedName.alias) namespaceNames.add(importedName.alias);
1710
+ return namespaceNames;
1711
+ };
1712
+ const collectMemberAccesses = (bodyNodes, namespaceLocalNames, memberAccesses, wholeObjectUses) => {
1713
+ const walkForMemberAccesses = (node) => {
1714
+ if (node.type === "MemberExpression" && !node.computed) {
1715
+ const memberExpression = node;
1716
+ if (memberExpression.object.type === "Identifier" && namespaceLocalNames.has(memberExpression.object.name)) {
1717
+ const objectName = memberExpression.object.name;
1718
+ const memberName = memberExpression.property.name;
1719
+ if (memberName) memberAccesses.push({
1720
+ objectName,
1721
+ memberName
1722
+ });
1723
+ }
1724
+ }
1725
+ if (node.type === "MemberExpression" && Boolean(node.computed)) {
1726
+ const computedExpression = node;
1727
+ if (computedExpression.object.type === "Identifier" && namespaceLocalNames.has(computedExpression.object.name)) {
1728
+ const objectName = computedExpression.object.name;
1729
+ const expressionNode = node.expression;
1730
+ if (expressionNode?.type === "Literal") {
1731
+ const literalValue = expressionNode.value;
1732
+ if (typeof literalValue === "string") memberAccesses.push({
1733
+ objectName,
1734
+ memberName: literalValue
1735
+ });
1736
+ else wholeObjectUses.push(objectName);
1737
+ } else wholeObjectUses.push(objectName);
1738
+ }
1739
+ }
1740
+ if (node.type === "SpreadElement") {
1741
+ const spreadArgument = node.argument;
1742
+ if (spreadArgument?.type === "Identifier" && namespaceLocalNames.has(spreadArgument.name)) wholeObjectUses.push(spreadArgument.name);
1743
+ }
1744
+ if (node.type === "ForInStatement") {
1745
+ const forInRight = node.right;
1746
+ if (forInRight?.type === "Identifier" && namespaceLocalNames.has(forInRight.name)) wholeObjectUses.push(forInRight.name);
1747
+ }
1748
+ if (node.type === "CallExpression") {
1749
+ const callExpression = node;
1750
+ if (callExpression.callee.type === "MemberExpression" && !callExpression.callee.computed) {
1751
+ const calleeMember = callExpression.callee;
1752
+ if (calleeMember.object.type === "Identifier" && calleeMember.object.name === "Object" && WHOLE_OBJECT_FUNCTION_NAMES.has(calleeMember.property.name)) {
1753
+ const firstArgument = callExpression.arguments[0];
1754
+ if (firstArgument && firstArgument.type !== "SpreadElement" && firstArgument.type === "Identifier" && namespaceLocalNames.has(firstArgument.name)) wholeObjectUses.push(firstArgument.name);
1755
+ }
1756
+ }
1757
+ }
1758
+ for (const value of Object.values(node)) if (Array.isArray(value)) {
1759
+ for (const element of value) if (isWalkableNode(element)) walkForMemberAccesses(element);
1760
+ } else if (isWalkableNode(value)) walkForMemberAccesses(value);
1761
+ };
1762
+ for (const topLevelNode of bodyNodes) if (isWalkableNode(topLevelNode)) walkForMemberAccesses(topLevelNode);
1763
+ };
1764
+ const extractImportDeclaration = (node, sourceText, imports) => {
1765
+ const specifier = node.source.value;
1766
+ if (!specifier) return;
1767
+ const isTypeOnly = node.importKind === "type";
1768
+ const importedNames = [];
1769
+ for (const specifierNode of node.specifiers) switch (specifierNode.type) {
1770
+ case "ImportDefaultSpecifier":
1771
+ importedNames.push({
1772
+ name: "default",
1773
+ alias: specifierNode.local.name,
1774
+ isNamespace: false,
1775
+ isDefault: true,
1776
+ isTypeOnly
1777
+ });
1778
+ break;
1779
+ case "ImportNamespaceSpecifier":
1780
+ importedNames.push({
1781
+ name: "*",
1782
+ alias: specifierNode.local.name,
1783
+ isNamespace: true,
1784
+ isDefault: false,
1785
+ isTypeOnly
1786
+ });
1787
+ break;
1788
+ case "ImportSpecifier": {
1789
+ const importedName = getModuleExportNameValue(specifierNode.imported);
1790
+ const localName = specifierNode.local.name;
1791
+ const isSelfAlias = localName === importedName && specifierNode.imported.type === "Identifier" && specifierNode.imported.start !== specifierNode.local.start;
1792
+ importedNames.push({
1793
+ name: importedName,
1794
+ alias: localName !== importedName ? localName : void 0,
1795
+ isNamespace: false,
1796
+ isDefault: importedName === "default",
1797
+ isTypeOnly: isTypeOnly || specifierNode.importKind === "type",
1798
+ isRedundantAlias: isSelfAlias || void 0
1799
+ });
1800
+ break;
1801
+ }
1802
+ }
1803
+ const isSideEffectImport = importedNames.length === 0;
1804
+ if (isSideEffectImport) importedNames.push({
1805
+ name: "*",
1806
+ alias: void 0,
1807
+ isNamespace: false,
1808
+ isDefault: false,
1809
+ isTypeOnly: false
1810
+ });
1811
+ imports.push({
1812
+ specifier,
1813
+ importedNames,
1814
+ isTypeOnly,
1815
+ isDynamic: false,
1816
+ isSideEffect: isSideEffectImport,
1817
+ line: getLineFromOffset(sourceText, node.start),
1818
+ column: getColumnFromOffset(sourceText, node.start)
1819
+ });
1820
+ };
1821
+ const extractNamedExportDeclaration = (node, sourceText, exports) => {
1822
+ const isTypeOnly = node.exportKind === "type";
1823
+ const reExportSource = node.source?.value;
1824
+ if (node.declaration) extractDeclarationNames(node.declaration, isTypeOnly, sourceText, exports, node.start);
1825
+ for (const specifierNode of node.specifiers) {
1826
+ const exportedName = getModuleExportNameValue(specifierNode.exported);
1827
+ const localName = getModuleExportNameValue(specifierNode.local);
1828
+ const isSelfAlias = exportedName === localName && specifierNode.exported.type === "Identifier" && specifierNode.local.type === "Identifier" && specifierNode.exported.start !== specifierNode.local.start;
1829
+ exports.push({
1830
+ name: exportedName,
1831
+ isDefault: exportedName === "default",
1832
+ isTypeOnly: isTypeOnly || specifierNode.exportKind === "type",
1833
+ isReExport: reExportSource !== void 0,
1834
+ isSynthetic: false,
1835
+ reExportSource,
1836
+ reExportOriginalName: reExportSource !== void 0 ? localName : void 0,
1837
+ isNamespaceReExport: false,
1838
+ line: getLineFromOffset(sourceText, specifierNode.start ?? node.start),
1839
+ column: getColumnFromOffset(sourceText, specifierNode.start ?? node.start),
1840
+ isRedundantAlias: isSelfAlias || void 0
1841
+ });
1842
+ }
1843
+ };
1844
+ const extractDefaultExportDeclaration = (node, sourceText, exports) => {
1845
+ const defaultExportLocalName = extractDefaultExportLocalName(node.declaration);
1846
+ exports.push({
1847
+ name: "default",
1848
+ isDefault: true,
1849
+ isTypeOnly: false,
1850
+ isReExport: false,
1851
+ isSynthetic: false,
1852
+ reExportSource: void 0,
1853
+ reExportOriginalName: void 0,
1854
+ isNamespaceReExport: false,
1855
+ line: getLineFromOffset(sourceText, node.start),
1856
+ column: getColumnFromOffset(sourceText, node.start),
1857
+ defaultExportLocalName
1858
+ });
1859
+ };
1860
+ const extractExportAllDeclaration = (node, sourceText, exports) => {
1861
+ const reExportSource = node.source.value;
1862
+ if (!reExportSource) return;
1863
+ const exportedName = node.exported ? getModuleExportNameValue(node.exported) : void 0;
1864
+ exports.push({
1865
+ name: exportedName ?? "*",
1866
+ isDefault: false,
1867
+ isTypeOnly: node.exportKind === "type",
1868
+ isReExport: true,
1869
+ isSynthetic: false,
1870
+ reExportSource,
1871
+ reExportOriginalName: "*",
1872
+ isNamespaceReExport: !exportedName,
1873
+ line: getLineFromOffset(sourceText, node.start),
1874
+ column: getColumnFromOffset(sourceText, node.start)
1875
+ });
1876
+ };
1877
+ const extractDeclarationNames = (declaration, isTypeOnly, sourceText, exports, fallbackStart) => {
1878
+ const declarationType = declaration.type;
1879
+ if (declarationType === "FunctionDeclaration" || declarationType === "ClassDeclaration" || declarationType === "TSEnumDeclaration") {
1880
+ const declarationName = declaration.id?.name;
1881
+ if (declarationName) exports.push({
1882
+ name: declarationName,
1883
+ isDefault: false,
1884
+ isTypeOnly,
1885
+ isReExport: false,
1886
+ isSynthetic: false,
1887
+ reExportSource: void 0,
1888
+ reExportOriginalName: void 0,
1889
+ isNamespaceReExport: false,
1890
+ line: getLineFromOffset(sourceText, declaration.start ?? fallbackStart),
1891
+ column: getColumnFromOffset(sourceText, declaration.start ?? fallbackStart)
1892
+ });
1893
+ return;
1894
+ }
1895
+ if (declarationType === "TSTypeAliasDeclaration" || declarationType === "TSInterfaceDeclaration") {
1896
+ const declarationName = declaration.id.name;
1897
+ if (declarationName) exports.push({
1898
+ name: declarationName,
1899
+ isDefault: false,
1900
+ isTypeOnly: true,
1901
+ isReExport: false,
1902
+ isSynthetic: false,
1903
+ reExportSource: void 0,
1904
+ reExportOriginalName: void 0,
1905
+ isNamespaceReExport: false,
1906
+ line: getLineFromOffset(sourceText, declaration.start ?? fallbackStart),
1907
+ column: getColumnFromOffset(sourceText, declaration.start ?? fallbackStart)
1908
+ });
1909
+ return;
1910
+ }
1911
+ if (declarationType === "VariableDeclaration") {
1912
+ const variableDeclaration = declaration;
1913
+ for (const declarator of variableDeclaration.declarations) {
1914
+ const bindingNames = extractBindingPatternNames(declarator.id);
1915
+ for (const bindingName of bindingNames) exports.push({
1916
+ name: bindingName,
1917
+ isDefault: false,
1918
+ isTypeOnly,
1919
+ isReExport: false,
1920
+ isSynthetic: false,
1921
+ reExportSource: void 0,
1922
+ reExportOriginalName: void 0,
1923
+ isNamespaceReExport: false,
1924
+ line: getLineFromOffset(sourceText, declarator.start ?? fallbackStart),
1925
+ column: getColumnFromOffset(sourceText, declarator.start ?? fallbackStart)
1926
+ });
1927
+ }
1928
+ }
1929
+ };
1930
+ const extractBindingPatternNames = (pattern) => {
1931
+ if (!pattern) return [];
1932
+ if (pattern.type === "Identifier") return pattern.name ? [pattern.name] : [];
1933
+ if (pattern.type === "ObjectPattern") {
1934
+ const names = [];
1935
+ for (const property of pattern.properties) if (property.type === "RestElement") names.push(...extractBindingPatternNames(property.argument));
1936
+ else names.push(...extractBindingPatternNames(property.value));
1937
+ return names;
1938
+ }
1939
+ if (pattern.type === "ArrayPattern") {
1940
+ const names = [];
1941
+ for (const element of pattern.elements) {
1942
+ if (!element) continue;
1943
+ if (element.type === "RestElement") names.push(...extractBindingPatternNames(element.argument));
1944
+ else names.push(...extractBindingPatternNames(element));
1945
+ }
1946
+ return names;
1947
+ }
1948
+ if (pattern.type === "AssignmentPattern") return extractBindingPatternNames(pattern.left);
1949
+ return [];
1950
+ };
1951
+ const createNamespaceImportBinding = () => ({
1952
+ name: "*",
1953
+ alias: void 0,
1954
+ isNamespace: true,
1955
+ isDefault: false,
1956
+ isTypeOnly: false
1957
+ });
1958
+ const isWalkableNode = (value) => Boolean(value) && typeof value === "object" && typeof value.type === "string";
1959
+ const extractStringLiteralFromArgument = (callArguments) => {
1960
+ const firstArgument = callArguments[0];
1961
+ if (!firstArgument) return void 0;
1962
+ if (firstArgument.type === "SpreadElement") return void 0;
1963
+ if (firstArgument.type !== "Literal") return void 0;
1964
+ const literalValue = firstArgument.value;
1965
+ return typeof literalValue === "string" ? literalValue : void 0;
1966
+ };
1967
+ const extractGlobPatterns = (callArguments) => {
1968
+ const firstArgument = callArguments[0];
1969
+ if (!firstArgument || firstArgument.type === "SpreadElement") return [];
1970
+ if (firstArgument.type === "Literal") {
1971
+ const literalValue = firstArgument.value;
1972
+ if (typeof literalValue === "string" && (literalValue.startsWith("./") || literalValue.startsWith("../"))) return [literalValue];
1973
+ return [];
1974
+ }
1975
+ if (firstArgument.type === "ArrayExpression") return firstArgument.elements.filter((element) => element.type === "Literal" && typeof element.value === "string" && (element.value.startsWith("./") || element.value.startsWith("../"))).map((element) => element.value);
1976
+ return [];
1977
+ };
1978
+ const extractRegexGlobSuffix = (callArguments) => {
1979
+ const thirdArgument = callArguments[2];
1980
+ if (!thirdArgument || thirdArgument.type === "SpreadElement") return void 0;
1981
+ if (thirdArgument.type !== "Literal") return void 0;
1982
+ const regExpValue = thirdArgument.regex;
1983
+ if (!regExpValue) return void 0;
1984
+ const extensionMatch = regExpValue.pattern.match(/^\\\.([\w|]+)\$$/);
1985
+ if (extensionMatch) {
1986
+ const extensions = extensionMatch[1].split("|");
1987
+ if (extensions.length === 1) return `*.${extensions[0]}`;
1988
+ return `*.{${extensions.join(",")}}`;
1989
+ }
1990
+ };
1991
+ const hasMockFactoryArgument = (callExpression) => {
1992
+ const secondArgument = callExpression.arguments[1];
1993
+ if (!secondArgument) return false;
1994
+ if (secondArgument.type === "SpreadElement") return false;
1995
+ return secondArgument.type === "ArrowFunctionExpression" || secondArgument.type === "FunctionExpression";
1996
+ };
1997
+ const synthesizeAutoMockSibling = (mockSource) => {
1998
+ if (!mockSource || mockSource.includes("://") || mockSource.startsWith("data:") || mockSource.split("/").some((segment) => segment === "__mocks__")) return;
1999
+ const lastSlashIndex = mockSource.lastIndexOf("/");
2000
+ if (lastSlashIndex === -1) return void 0;
2001
+ const directory = mockSource.slice(0, lastSlashIndex);
2002
+ const fileName = mockSource.slice(lastSlashIndex + 1);
2003
+ if (!fileName) return void 0;
2004
+ return `${directory}/__mocks__/${fileName}`;
2005
+ };
2006
+ const collectDynamicImports = (bodyNodes, sourceText, imports) => {
2007
+ const walkNode = (node) => {
2008
+ if (node.type === "ImportExpression") {
2009
+ const importExpression = node;
2010
+ const sourceExpression = importExpression.source;
2011
+ if (sourceExpression.type === "Literal") {
2012
+ const specifierValue = sourceExpression.value;
2013
+ if (specifierValue) imports.push({
2014
+ specifier: specifierValue,
2015
+ importedNames: [createNamespaceImportBinding()],
2016
+ isTypeOnly: false,
2017
+ isDynamic: true,
2018
+ isSideEffect: false,
2019
+ line: getLineFromOffset(sourceText, importExpression.start),
2020
+ column: getColumnFromOffset(sourceText, importExpression.start)
2021
+ });
2022
+ } else if (sourceExpression.type === "TemplateLiteral") {
2023
+ const templateLiteral = sourceExpression;
2024
+ if (templateLiteral.quasis.length >= 2) {
2025
+ const globPattern = templateLiteral.quasis.map((quasi) => quasi.value.cooked).join("*");
2026
+ if (globPattern.startsWith("./") || globPattern.startsWith("../")) imports.push({
2027
+ specifier: globPattern,
2028
+ importedNames: [createNamespaceImportBinding()],
2029
+ isTypeOnly: false,
2030
+ isDynamic: true,
2031
+ isSideEffect: false,
2032
+ isGlob: true,
2033
+ line: getLineFromOffset(sourceText, importExpression.start),
2034
+ column: getColumnFromOffset(sourceText, importExpression.start)
2035
+ });
2036
+ }
2037
+ }
2038
+ return;
2039
+ }
2040
+ if (node.type === "CallExpression") {
2041
+ const callExpression = node;
2042
+ if (callExpression.callee.type === "Identifier" && callExpression.callee.name === "require") {
2043
+ const requireSpecifier = extractStringLiteralFromArgument(callExpression.arguments);
2044
+ if (requireSpecifier) imports.push({
2045
+ specifier: requireSpecifier,
2046
+ importedNames: [createNamespaceImportBinding()],
2047
+ isTypeOnly: false,
2048
+ isDynamic: true,
2049
+ isSideEffect: false,
2050
+ line: getLineFromOffset(sourceText, callExpression.start),
2051
+ column: getColumnFromOffset(sourceText, callExpression.start)
2052
+ });
2053
+ }
2054
+ if (callExpression.callee.type === "MemberExpression" && !callExpression.callee.computed) {
2055
+ const memberExpression = callExpression.callee;
2056
+ if (memberExpression.object.type === "Identifier" && memberExpression.object.name === "require" && memberExpression.property.name === "resolve") {
2057
+ const resolveSpecifier = extractStringLiteralFromArgument(callExpression.arguments);
2058
+ if (resolveSpecifier) imports.push({
2059
+ specifier: resolveSpecifier,
2060
+ importedNames: [createNamespaceImportBinding()],
2061
+ isTypeOnly: false,
2062
+ isDynamic: true,
2063
+ isSideEffect: false,
2064
+ line: getLineFromOffset(sourceText, callExpression.start),
2065
+ column: getColumnFromOffset(sourceText, callExpression.start)
2066
+ });
2067
+ }
2068
+ if (memberExpression.object.type === "Identifier" && (memberExpression.object.name === "vi" || memberExpression.object.name === "jest") && memberExpression.property.name === "mock") {
2069
+ const mockSpecifier = extractStringLiteralFromArgument(callExpression.arguments);
2070
+ if (mockSpecifier) {
2071
+ imports.push({
2072
+ specifier: mockSpecifier,
2073
+ importedNames: [createNamespaceImportBinding()],
2074
+ isTypeOnly: false,
2075
+ isDynamic: true,
2076
+ isSideEffect: true,
2077
+ line: getLineFromOffset(sourceText, callExpression.start),
2078
+ column: getColumnFromOffset(sourceText, callExpression.start)
2079
+ });
2080
+ const hasFactoryArgument = hasMockFactoryArgument(callExpression);
2081
+ const autoMockSibling = synthesizeAutoMockSibling(mockSpecifier);
2082
+ if (!hasFactoryArgument && autoMockSibling) imports.push({
2083
+ specifier: autoMockSibling,
2084
+ importedNames: [createNamespaceImportBinding()],
2085
+ isTypeOnly: false,
2086
+ isDynamic: true,
2087
+ isSideEffect: true,
2088
+ line: getLineFromOffset(sourceText, callExpression.start),
2089
+ column: getColumnFromOffset(sourceText, callExpression.start)
2090
+ });
2091
+ }
2092
+ }
2093
+ if (memberExpression.object.type === "MetaProperty" && memberExpression.property.name === "glob") {
2094
+ const globPatterns = extractGlobPatterns(callExpression.arguments);
2095
+ for (const globPattern of globPatterns) imports.push({
2096
+ specifier: globPattern,
2097
+ importedNames: [createNamespaceImportBinding()],
2098
+ isTypeOnly: false,
2099
+ isDynamic: true,
2100
+ isSideEffect: false,
2101
+ isGlob: true,
2102
+ line: getLineFromOffset(sourceText, callExpression.start),
2103
+ column: getColumnFromOffset(sourceText, callExpression.start)
2104
+ });
2105
+ }
2106
+ if (memberExpression.object.type === "Identifier" && memberExpression.object.name === "require" && memberExpression.property.name === "context") {
2107
+ const directoryArgument = extractStringLiteralFromArgument(callExpression.arguments);
2108
+ if (directoryArgument && (directoryArgument.startsWith("./") || directoryArgument.startsWith("../"))) {
2109
+ const hasRegexArgument = callExpression.arguments.length >= 3 && callExpression.arguments[2].type !== "SpreadElement";
2110
+ const regexSuffix = extractRegexGlobSuffix(callExpression.arguments);
2111
+ if (!hasRegexArgument || Boolean(regexSuffix)) {
2112
+ const contextGlobPrefix = callExpression.arguments[1]?.type === "Literal" && callExpression.arguments[1].value === true ? `${directoryArgument}/**/` : `${directoryArgument}/`;
2113
+ const contextGlobPattern = regexSuffix ? `${contextGlobPrefix}${regexSuffix}` : `${contextGlobPrefix}*`;
2114
+ imports.push({
2115
+ specifier: contextGlobPattern,
2116
+ importedNames: [createNamespaceImportBinding()],
2117
+ isTypeOnly: false,
2118
+ isDynamic: true,
2119
+ isSideEffect: false,
2120
+ isGlob: true,
2121
+ line: getLineFromOffset(sourceText, callExpression.start),
2122
+ column: getColumnFromOffset(sourceText, callExpression.start)
2123
+ });
2124
+ }
2125
+ }
2126
+ }
2127
+ }
2128
+ }
2129
+ if (node.type === "NewExpression") {
2130
+ const newExpression = node;
2131
+ if (newExpression.callee.type === "Identifier" && newExpression.callee.name === "URL" && newExpression.arguments.length >= 2) {
2132
+ const secondArgument = newExpression.arguments[1];
2133
+ if (secondArgument.type === "MemberExpression" && secondArgument.object.type === "MetaProperty" && secondArgument.property.name === "url") {
2134
+ const urlSpecifier = extractStringLiteralFromArgument(newExpression.arguments);
2135
+ if (urlSpecifier) imports.push({
2136
+ specifier: urlSpecifier,
2137
+ importedNames: [createNamespaceImportBinding()],
2138
+ isTypeOnly: false,
2139
+ isDynamic: true,
2140
+ isSideEffect: true,
2141
+ line: getLineFromOffset(sourceText, newExpression.start),
2142
+ column: getColumnFromOffset(sourceText, newExpression.start)
2143
+ });
2144
+ }
2145
+ }
2146
+ }
2147
+ if (node.type === "Decorator") {
2148
+ const expression = node.expression;
2149
+ if (expression?.type === "CallExpression") {
2150
+ const callNode = expression;
2151
+ const callee = callNode.callee;
2152
+ if (callee.type === "Identifier" && callee.name === "Component") {
2153
+ const objectArgument = callNode.arguments[0];
2154
+ if (objectArgument?.type === "ObjectExpression") {
2155
+ const objectProperties = objectArgument.properties;
2156
+ for (const property of objectProperties) {
2157
+ if (property.type !== "ObjectProperty" && property.type !== "Property") continue;
2158
+ const propertyKey = property.key;
2159
+ const propertyName = propertyKey?.name ?? propertyKey?.value;
2160
+ const propertyValue = property.value;
2161
+ if (propertyName === "templateUrl" && propertyValue?.type === "Literal") {
2162
+ const templatePath = propertyValue.value;
2163
+ if (templatePath) imports.push({
2164
+ specifier: templatePath.startsWith(".") ? templatePath : `./${templatePath}`,
2165
+ importedNames: [],
2166
+ isTypeOnly: false,
2167
+ isDynamic: false,
2168
+ isSideEffect: true,
2169
+ line: getLineFromOffset(sourceText, property.start),
2170
+ column: getColumnFromOffset(sourceText, property.start)
2171
+ });
2172
+ }
2173
+ if ((propertyName === "styleUrl" || propertyName === "styleUrls") && propertyValue) {
2174
+ const styleUrlValues = [];
2175
+ if (propertyValue.type === "Literal") {
2176
+ const singleValue = propertyValue.value;
2177
+ if (singleValue) styleUrlValues.push(singleValue);
2178
+ } else if (propertyValue.type === "ArrayExpression") {
2179
+ const arrayElements = propertyValue.elements;
2180
+ for (const element of arrayElements) if (element?.type === "Literal") {
2181
+ const elementValue = element.value;
2182
+ if (elementValue) styleUrlValues.push(elementValue);
2183
+ }
2184
+ }
2185
+ for (const styleUrl of styleUrlValues) imports.push({
2186
+ specifier: styleUrl.startsWith(".") ? styleUrl : `./${styleUrl}`,
2187
+ importedNames: [],
2188
+ isTypeOnly: false,
2189
+ isDynamic: false,
2190
+ isSideEffect: true,
2191
+ line: getLineFromOffset(sourceText, property.start),
2192
+ column: getColumnFromOffset(sourceText, property.start)
2193
+ });
2194
+ }
2195
+ }
2196
+ }
2197
+ }
2198
+ }
2199
+ }
2200
+ for (const value of Object.values(node)) if (Array.isArray(value)) {
2201
+ for (const element of value) if (isWalkableNode(element)) walkNode(element);
2202
+ } else if (isWalkableNode(value)) walkNode(value);
2203
+ };
2204
+ for (const topLevelNode of bodyNodes) if (isWalkableNode(topLevelNode)) walkNode(topLevelNode);
2205
+ };
2206
+
2207
+ //#endregion
2208
+ //#region src/collect/parse-worker.ts
2209
+ const port = parentPort;
2210
+ port.on("message", (message) => {
2211
+ if (message.type === "parse") try {
2212
+ const parsed = parseSourceFile(message.filePath);
2213
+ const response = {
2214
+ type: "result",
2215
+ fileIndex: message.fileIndex,
2216
+ filePath: message.filePath,
2217
+ parsed: {
2218
+ imports: parsed.imports,
2219
+ exports: parsed.exports,
2220
+ memberAccesses: parsed.memberAccesses,
2221
+ wholeObjectUses: parsed.wholeObjectUses,
2222
+ localIdentifierReferences: parsed.localIdentifierReferences,
2223
+ referencedFilenames: parsed.referencedFilenames,
2224
+ redundantTypePatterns: parsed.redundantTypePatterns,
2225
+ identityWrappers: parsed.identityWrappers,
2226
+ typeDefinitionHashes: parsed.typeDefinitionHashes,
2227
+ inlineTypeLiterals: parsed.inlineTypeLiterals,
2228
+ simplifiableFunctions: parsed.simplifiableFunctions,
2229
+ simplifiableExpressions: parsed.simplifiableExpressions,
2230
+ duplicateConstantCandidates: parsed.duplicateConstantCandidates,
2231
+ errors: parsed.errors.map((deslopError) => deslopError.toJSON())
2232
+ }
2233
+ };
2234
+ port.postMessage(response);
2235
+ } catch (taskError) {
2236
+ const response = {
2237
+ type: "error",
2238
+ fileIndex: message.fileIndex,
2239
+ filePath: message.filePath,
2240
+ errorMessage: taskError instanceof Error ? taskError.message : String(taskError)
2241
+ };
2242
+ port.postMessage(response);
2243
+ }
2244
+ });
2245
+ port.postMessage({ type: "ready" });
2246
+
2247
+ //#endregion
2248
+ export { };