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.
- package/dist/index.cjs +168 -20
- package/dist/index.mjs +167 -20
- package/dist/parse-worker.mjs +2248 -0
- package/package.json +1 -1
|
@@ -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 { };
|