storm-lua-minify 0.3.0 → 0.9.0
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/README.md +119 -41
- package/dist/aggregateSpecialization.js +406 -0
- package/dist/ast2lua.js +156 -68
- package/dist/astWalk.js +162 -0
- package/dist/callGraph.js +372 -0
- package/dist/cli.js +53 -58
- package/dist/cliOptions.js +36 -0
- package/dist/cliProgress.js +87 -0
- package/dist/config.js +73 -0
- package/dist/constantFold.js +798 -0
- package/dist/controlFlow.js +266 -0
- package/dist/functionRewrites.js +580 -0
- package/dist/generatedAst.js +108 -0
- package/dist/generatedNode.js +23 -0
- package/dist/interproceduralAnalysis.js +842 -0
- package/dist/interproceduralConstants.js +120 -0
- package/dist/luaString.js +157 -0
- package/dist/minifier.js +1178 -44
- package/dist/optimizerAnalysis.js +43 -0
- package/dist/optimizerDiagnostics.js +65 -0
- package/dist/optimizerFacts.js +529 -0
- package/dist/optimizerPass.js +96 -0
- package/dist/optimizerTransaction.js +56 -0
- package/dist/optimizerValueDomain.js +180 -0
- package/dist/options.js +233 -0
- package/dist/progress.js +2 -0
- package/dist/removeUnused.js +145 -0
- package/dist/renamer.js +223 -54
- package/dist/resolver.js +28 -11
- package/dist/runtimeEnvironment.js +105 -0
- package/dist/sourceMetadata.js +314 -0
- package/dist/statementDataflow.js +259 -0
- package/dist/statementScheduler.js +595 -0
- package/dist/symbolLiveness.js +92 -0
- package/dist/tableEffects.js +356 -0
- package/dist/transform.js +10 -371
- package/dist/valueFlow.js +409 -0
- package/dist/wholeProgramExports.js +646 -0
- package/dist/wholeProgramFieldRenames.js +583 -0
- package/dist/wholeProgramFields.js +672 -0
- package/dist/wholeProgramObjects.js +783 -0
- package/package.json +11 -2
- package/dist/index.js +0 -27
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.planWholeProgramFieldRenames = planWholeProgramFieldRenames;
|
|
4
|
+
const astWalk_1 = require("./astWalk");
|
|
5
|
+
const renamer_1 = require("./renamer");
|
|
6
|
+
const RUNTIME_RESERVED_KEYS = new Set([
|
|
7
|
+
"__add",
|
|
8
|
+
"__band",
|
|
9
|
+
"__bnot",
|
|
10
|
+
"__bor",
|
|
11
|
+
"__bxor",
|
|
12
|
+
"__call",
|
|
13
|
+
"__concat",
|
|
14
|
+
"__div",
|
|
15
|
+
"__eq",
|
|
16
|
+
"__gc",
|
|
17
|
+
"__idiv",
|
|
18
|
+
"__index",
|
|
19
|
+
"__le",
|
|
20
|
+
"__len",
|
|
21
|
+
"__lt",
|
|
22
|
+
"__metatable",
|
|
23
|
+
"__mod",
|
|
24
|
+
"__mode",
|
|
25
|
+
"__mul",
|
|
26
|
+
"__name",
|
|
27
|
+
"__newindex",
|
|
28
|
+
"__pairs",
|
|
29
|
+
"__pow",
|
|
30
|
+
"__shl",
|
|
31
|
+
"__shr",
|
|
32
|
+
"__sub",
|
|
33
|
+
"__tostring",
|
|
34
|
+
"__unm",
|
|
35
|
+
]);
|
|
36
|
+
function sourceRangeOf(node) {
|
|
37
|
+
return node.range;
|
|
38
|
+
}
|
|
39
|
+
function staticFieldName(expression) {
|
|
40
|
+
if (expression.type !== "StringLiteral")
|
|
41
|
+
return undefined;
|
|
42
|
+
const raw = expression.raw;
|
|
43
|
+
const quote = raw.at(0);
|
|
44
|
+
if ((quote !== '"' && quote !== "'") ||
|
|
45
|
+
raw.at(-1) !== quote ||
|
|
46
|
+
raw.slice(1, -1).includes("\\"))
|
|
47
|
+
return undefined;
|
|
48
|
+
return raw.slice(1, -1);
|
|
49
|
+
}
|
|
50
|
+
class DisjointSet {
|
|
51
|
+
parent = new Map();
|
|
52
|
+
add(value) {
|
|
53
|
+
if (!this.parent.has(value))
|
|
54
|
+
this.parent.set(value, value);
|
|
55
|
+
}
|
|
56
|
+
find(value) {
|
|
57
|
+
this.add(value);
|
|
58
|
+
const parent = this.parent.get(value);
|
|
59
|
+
if (parent === value)
|
|
60
|
+
return value;
|
|
61
|
+
const root = this.find(parent);
|
|
62
|
+
this.parent.set(value, root);
|
|
63
|
+
return root;
|
|
64
|
+
}
|
|
65
|
+
union(left, right) {
|
|
66
|
+
const leftRoot = this.find(left);
|
|
67
|
+
const rightRoot = this.find(right);
|
|
68
|
+
if (leftRoot === rightRoot)
|
|
69
|
+
return false;
|
|
70
|
+
const [root, child] = [leftRoot, rightRoot].sort();
|
|
71
|
+
this.parent.set(child, root);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build one immutable runtime-key plan for the final linked AST generation.
|
|
77
|
+
* The planner never guesses an object or a dynamic key: an unclassified use
|
|
78
|
+
* fixes the complete affected shape instead.
|
|
79
|
+
*/
|
|
80
|
+
function planWholeProgramFieldRenames(objects, fields, exports, entryModule, metadataOf) {
|
|
81
|
+
if (objects.generation !== fields.generation ||
|
|
82
|
+
objects.generation !== exports.generation)
|
|
83
|
+
throw new Error("Field rename planning requires one AST generation");
|
|
84
|
+
const mutable = new Map();
|
|
85
|
+
const union = new DisjointSet();
|
|
86
|
+
const statementOfNode = new WeakMap();
|
|
87
|
+
const diagnostics = [];
|
|
88
|
+
const objectReasons = new Map();
|
|
89
|
+
const unresolvedStaticFields = new Set();
|
|
90
|
+
const safeDynamicIndices = new WeakSet();
|
|
91
|
+
const transferByStatement = new WeakMap();
|
|
92
|
+
const fieldId = (object, field) => `${object.id}\0${field}`;
|
|
93
|
+
const candidateObjectsOf = (expression) => {
|
|
94
|
+
const candidates = objects.objectsOf(expression);
|
|
95
|
+
if (candidates.length > 0)
|
|
96
|
+
return candidates;
|
|
97
|
+
const single = objects.objectOf(expression);
|
|
98
|
+
return single ? [single] : [];
|
|
99
|
+
};
|
|
100
|
+
const getField = (object, field) => {
|
|
101
|
+
const id = fieldId(object, field);
|
|
102
|
+
let value = mutable.get(id);
|
|
103
|
+
if (!value) {
|
|
104
|
+
value = { id, object, field, sites: [], reasons: new Set() };
|
|
105
|
+
mutable.set(id, value);
|
|
106
|
+
union.add(id);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
};
|
|
110
|
+
const keepObject = (object, reason) => {
|
|
111
|
+
const reasons = objectReasons.get(object.id) ?? new Set();
|
|
112
|
+
reasons.add(reason);
|
|
113
|
+
objectReasons.set(object.id, reasons);
|
|
114
|
+
mutable.forEach((field) => {
|
|
115
|
+
if (field.object.id === object.id)
|
|
116
|
+
field.reasons.add(reason);
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
objects.modules.forEach((module) => {
|
|
120
|
+
(0, astWalk_1.walkBlockDeep)(module.chunk.body, {
|
|
121
|
+
onStatement: (statement) => {
|
|
122
|
+
if (statement.type !== "ForGenericStatement")
|
|
123
|
+
return;
|
|
124
|
+
const transfer = keyTransferOf(statement, module.resolved, objects);
|
|
125
|
+
transferByStatement.set(statement, transfer);
|
|
126
|
+
if (transfer?.closed) {
|
|
127
|
+
transfer.indices.forEach((index) => safeDynamicIndices.add(index));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
statement.iterators.forEach((iterator) => {
|
|
131
|
+
if (iterator.type !== "CallExpression")
|
|
132
|
+
return;
|
|
133
|
+
iterator.arguments.forEach((argument) => {
|
|
134
|
+
candidateObjectsOf(argument).forEach((object) => {
|
|
135
|
+
keepObject(object, "iteration-order-observation");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
objects.modules.forEach((module) => {
|
|
143
|
+
const indexBlock = (body) => {
|
|
144
|
+
body.forEach((statement) => {
|
|
145
|
+
statementOfNode.set(statement, statement);
|
|
146
|
+
walkExpressionOwners(statement, statement, statementOfNode);
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
indexBlock(module.chunk.body);
|
|
150
|
+
(0, astWalk_1.walkBlockDeep)(module.chunk.body, {
|
|
151
|
+
onBlock: indexBlock,
|
|
152
|
+
onExpression: (expression) => {
|
|
153
|
+
if (expression.type === "MemberExpression") {
|
|
154
|
+
const candidateObjects = candidateObjectsOf(expression.base);
|
|
155
|
+
if (candidateObjects.length === 0) {
|
|
156
|
+
unresolvedStaticFields.add(expression.identifier.name);
|
|
157
|
+
diagnostics.push({
|
|
158
|
+
moduleName: module.name,
|
|
159
|
+
field: expression.identifier.name,
|
|
160
|
+
reason: "unknown-call",
|
|
161
|
+
accepted: false,
|
|
162
|
+
sourceRange: sourceRangeOf(expression),
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const related = candidateObjects.map((object) => getField(object, expression.identifier.name));
|
|
167
|
+
for (let index = 1; index < related.length; index++)
|
|
168
|
+
union.union(related[0].id, related[index].id);
|
|
169
|
+
related.forEach((field) => {
|
|
170
|
+
field.sites.push({
|
|
171
|
+
node: expression.identifier,
|
|
172
|
+
moduleName: module.name,
|
|
173
|
+
object: field.object,
|
|
174
|
+
field: expression.identifier.name,
|
|
175
|
+
form: "identifier",
|
|
176
|
+
statement: statementOfNode.get(expression),
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (expression.type === "IndexExpression") {
|
|
182
|
+
const candidateObjects = candidateObjectsOf(expression.base);
|
|
183
|
+
if (candidateObjects.length === 0) {
|
|
184
|
+
const key = staticFieldName(expression.index);
|
|
185
|
+
if (key !== undefined) {
|
|
186
|
+
unresolvedStaticFields.add(key);
|
|
187
|
+
diagnostics.push({
|
|
188
|
+
moduleName: module.name,
|
|
189
|
+
field: key,
|
|
190
|
+
reason: "unknown-call",
|
|
191
|
+
accepted: false,
|
|
192
|
+
sourceRange: sourceRangeOf(expression),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const key = staticFieldName(expression.index);
|
|
198
|
+
if (key === undefined) {
|
|
199
|
+
if (!safeDynamicIndices.has(expression))
|
|
200
|
+
candidateObjects.forEach((object) => {
|
|
201
|
+
keepObject(object, "dynamic-key");
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (expression.index.type !== "StringLiteral")
|
|
206
|
+
return;
|
|
207
|
+
const node = expression.index;
|
|
208
|
+
const related = candidateObjects.map((object) => getField(object, key));
|
|
209
|
+
for (let index = 1; index < related.length; index++)
|
|
210
|
+
union.union(related[0].id, related[index].id);
|
|
211
|
+
related.forEach((field) => {
|
|
212
|
+
field.sites.push({
|
|
213
|
+
node,
|
|
214
|
+
moduleName: module.name,
|
|
215
|
+
object: field.object,
|
|
216
|
+
field: key,
|
|
217
|
+
form: "string",
|
|
218
|
+
statement: statementOfNode.get(expression),
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (expression.type !== "TableConstructorExpression")
|
|
224
|
+
return;
|
|
225
|
+
const object = objects.objectOf(expression);
|
|
226
|
+
if (!object)
|
|
227
|
+
return;
|
|
228
|
+
expression.fields.forEach((tableField) => {
|
|
229
|
+
if (tableField.type === "TableValue") {
|
|
230
|
+
keepObject(object, "key-data-observation");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const key = tableField.type === "TableKeyString"
|
|
234
|
+
? tableField.key.name
|
|
235
|
+
: staticFieldName(tableField.key);
|
|
236
|
+
if (key === undefined) {
|
|
237
|
+
keepObject(object, "dynamic-key");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const node = tableField.type === "TableKeyString"
|
|
241
|
+
? tableField.key
|
|
242
|
+
: tableField.key.type === "StringLiteral"
|
|
243
|
+
? tableField.key
|
|
244
|
+
: undefined;
|
|
245
|
+
if (!node)
|
|
246
|
+
return;
|
|
247
|
+
getField(object, key).sites.push({
|
|
248
|
+
node,
|
|
249
|
+
moduleName: module.name,
|
|
250
|
+
object,
|
|
251
|
+
field: key,
|
|
252
|
+
form: tableField.type === "TableKeyString" ? "identifier" : "string",
|
|
253
|
+
statement: statementOfNode.get(expression),
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
// Shared facts contribute fields even when a remaining syntax site exists
|
|
260
|
+
// only on a related prototype or allocation.
|
|
261
|
+
fields.facts.forEach((fact) => getField(fact.object, fact.field));
|
|
262
|
+
exports.fields.forEach((field) => getField(field.object, field.key));
|
|
263
|
+
objects.objects.forEach((object) => {
|
|
264
|
+
object.methods.forEach((_callable, name) => getField(object, name));
|
|
265
|
+
});
|
|
266
|
+
// A derived allocation receives every source key. Equal keys are one runtime
|
|
267
|
+
// relation; different keys still coexist on the derived shape and collide.
|
|
268
|
+
let changed = true;
|
|
269
|
+
while (changed) {
|
|
270
|
+
changed = false;
|
|
271
|
+
objects.objects.forEach((object) => {
|
|
272
|
+
object.sources.forEach((source) => {
|
|
273
|
+
[...mutable.values()]
|
|
274
|
+
.filter((field) => field.object.id === source.id)
|
|
275
|
+
.forEach((sourceField) => {
|
|
276
|
+
const target = getField(object, sourceField.field);
|
|
277
|
+
if (union.union(sourceField.id, target.id))
|
|
278
|
+
changed = true;
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
let keyTransfers = objects.objects.reduce((count, object) => count + object.sources.length, 0);
|
|
284
|
+
objects.modules.forEach((module) => {
|
|
285
|
+
(0, astWalk_1.walkBlockDeep)(module.chunk.body, {
|
|
286
|
+
onStatement: (statement) => {
|
|
287
|
+
if (statement.type !== "ForGenericStatement")
|
|
288
|
+
return;
|
|
289
|
+
const transfer = transferByStatement.get(statement);
|
|
290
|
+
if (!transfer)
|
|
291
|
+
return;
|
|
292
|
+
if (!transfer.closed) {
|
|
293
|
+
transfer.objects.forEach((object) => {
|
|
294
|
+
keepObject(object, transfer.reason);
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
keyTransfers++;
|
|
299
|
+
const names = new Set([...mutable.values()]
|
|
300
|
+
.filter((field) => transfer.objects.some((object) => object.id === field.object.id))
|
|
301
|
+
.map((field) => field.field));
|
|
302
|
+
names.forEach((name) => {
|
|
303
|
+
const related = transfer.objects.map((object) => getField(object, name));
|
|
304
|
+
for (let index = 1; index < related.length; index++) {
|
|
305
|
+
union.union(related[0].id, related[index].id);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
transfer.comparisons.forEach(({ literal, field }) => {
|
|
309
|
+
const comparisonFields = transfer.objects.length > 0
|
|
310
|
+
? transfer.objects.map((object) => getField(object, field))
|
|
311
|
+
: [...mutable.values()].filter((candidate) => candidate.field === field);
|
|
312
|
+
for (let index = 1; index < comparisonFields.length; index++)
|
|
313
|
+
union.union(comparisonFields[0].id, comparisonFields[index].id);
|
|
314
|
+
comparisonFields.slice(0, 1).forEach((comparisonField) => {
|
|
315
|
+
comparisonField.sites.push({
|
|
316
|
+
node: literal,
|
|
317
|
+
moduleName: module.name,
|
|
318
|
+
object: comparisonField.object,
|
|
319
|
+
field,
|
|
320
|
+
form: "string",
|
|
321
|
+
statement,
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
mutable.forEach((field) => {
|
|
329
|
+
objectReasons
|
|
330
|
+
.get(field.object.id)
|
|
331
|
+
?.forEach((reason) => field.reasons.add(reason));
|
|
332
|
+
if (RUNTIME_RESERVED_KEYS.has(field.field) || field.field.startsWith("__"))
|
|
333
|
+
field.reasons.add("reserved-key");
|
|
334
|
+
if (unresolvedStaticFields.has(field.field))
|
|
335
|
+
field.reasons.add("unknown-call");
|
|
336
|
+
field.object.invalidationReasons.forEach((reason) => {
|
|
337
|
+
field.reasons.add(reason === "metatable-mutation"
|
|
338
|
+
? "metatable-observation"
|
|
339
|
+
: reason === "instance-escape" || reason === "prototype-escape"
|
|
340
|
+
? "object-escape"
|
|
341
|
+
: reason === "dynamic-key"
|
|
342
|
+
? "dynamic-key"
|
|
343
|
+
: "unknown-call");
|
|
344
|
+
});
|
|
345
|
+
field.sites.forEach((site) => {
|
|
346
|
+
if (site.statement &&
|
|
347
|
+
metadataOf(site.moduleName).annotationsOf(site.statement).keepName)
|
|
348
|
+
field.reasons.add("keep-name");
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
exports.fields.forEach((field) => {
|
|
352
|
+
if (field.object.moduleName === entryModule ||
|
|
353
|
+
[...field.rootReasons].some((reason) => ["entry-contract", "annotation-root", "export-escape"].includes(reason)))
|
|
354
|
+
getField(field.object, field.key).reasons.add("external-contract");
|
|
355
|
+
});
|
|
356
|
+
const membersByRoot = new Map();
|
|
357
|
+
mutable.forEach((field) => {
|
|
358
|
+
const root = union.find(field.id);
|
|
359
|
+
const members = membersByRoot.get(root) ?? [];
|
|
360
|
+
members.push(field);
|
|
361
|
+
membersByRoot.set(root, members);
|
|
362
|
+
});
|
|
363
|
+
const classes = [...membersByRoot.values()].sort((left, right) => left[0].id.localeCompare(right[0].id));
|
|
364
|
+
const classOfField = new Map();
|
|
365
|
+
classes.forEach((members, index) => {
|
|
366
|
+
members.forEach((member) => {
|
|
367
|
+
classOfField.set(member.id, index);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
const collision = classes.map(() => new Set());
|
|
371
|
+
const classesByObject = new Map();
|
|
372
|
+
mutable.forEach((field) => {
|
|
373
|
+
const index = classOfField.get(field.id);
|
|
374
|
+
const group = classesByObject.get(field.object.id) ?? new Set();
|
|
375
|
+
group.add(index);
|
|
376
|
+
classesByObject.set(field.object.id, group);
|
|
377
|
+
});
|
|
378
|
+
classesByObject.forEach((indices) => {
|
|
379
|
+
indices.forEach((left) => {
|
|
380
|
+
indices.forEach((right) => {
|
|
381
|
+
if (left !== right)
|
|
382
|
+
collision[left].add(right);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
const names = new Map();
|
|
387
|
+
const fixedNames = classes.map((members) => new Set(members.filter((field) => field.reasons.size > 0).map((f) => f.field)));
|
|
388
|
+
const weight = (members) => members.reduce((sum, field) => sum +
|
|
389
|
+
field.sites.reduce((siteSum, site) => siteSum + site.field.length, 0), 0);
|
|
390
|
+
const order = classes
|
|
391
|
+
.map((members, index) => ({ members, index, weight: weight(members) }))
|
|
392
|
+
.sort((left, right) => right.weight - left.weight || left.index - right.index);
|
|
393
|
+
order.forEach(({ members, index }) => {
|
|
394
|
+
if (fixedNames[index].size > 0 ||
|
|
395
|
+
members.every((field) => field.sites.length === 0))
|
|
396
|
+
return;
|
|
397
|
+
const unavailable = new Set();
|
|
398
|
+
collision[index].forEach((other) => {
|
|
399
|
+
const chosen = names.get(other);
|
|
400
|
+
if (chosen)
|
|
401
|
+
unavailable.add(chosen);
|
|
402
|
+
fixedNames[other].forEach((name) => unavailable.add(name));
|
|
403
|
+
classes[other].forEach((field) => unavailable.add(field.field));
|
|
404
|
+
});
|
|
405
|
+
let counter = 0;
|
|
406
|
+
let candidate = (0, renamer_1.generateCandidate)(counter++);
|
|
407
|
+
while (!(0, renamer_1.isAvailable)(candidate, unavailable))
|
|
408
|
+
candidate = (0, renamer_1.generateCandidate)(counter++);
|
|
409
|
+
const originalBytes = members.reduce((sum, field) => sum + field.sites.length * field.field.length, 0);
|
|
410
|
+
const candidateBytes = members.reduce((sum, field) => sum + field.sites.length * candidate.length, 0);
|
|
411
|
+
if (candidateBytes >= originalBytes) {
|
|
412
|
+
members.forEach((field) => field.reasons.add("nonpositive-cost"));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
names.set(index, candidate);
|
|
416
|
+
});
|
|
417
|
+
const renameByNode = new WeakMap();
|
|
418
|
+
const originalByNode = new WeakMap();
|
|
419
|
+
classes.forEach((members, index) => {
|
|
420
|
+
const name = names.get(index);
|
|
421
|
+
members.forEach((field) => {
|
|
422
|
+
diagnostics.push({
|
|
423
|
+
moduleName: field.object.moduleName,
|
|
424
|
+
field: field.field,
|
|
425
|
+
reason: "field-candidate",
|
|
426
|
+
accepted: true,
|
|
427
|
+
sourceRange: sourceRangeOf(field.sites[0]?.node ?? {}),
|
|
428
|
+
});
|
|
429
|
+
[...field.reasons].sort().forEach((reason) => diagnostics.push({
|
|
430
|
+
moduleName: field.object.moduleName,
|
|
431
|
+
field: field.field,
|
|
432
|
+
reason,
|
|
433
|
+
accepted: false,
|
|
434
|
+
sourceRange: sourceRangeOf(field.sites[0]?.node ?? {}),
|
|
435
|
+
}));
|
|
436
|
+
if (!name)
|
|
437
|
+
return;
|
|
438
|
+
field.sites.forEach((site) => {
|
|
439
|
+
renameByNode.set(site.node, name);
|
|
440
|
+
originalByNode.set(site.node, site.field);
|
|
441
|
+
});
|
|
442
|
+
diagnostics.push({
|
|
443
|
+
moduleName: field.object.moduleName,
|
|
444
|
+
field: field.field,
|
|
445
|
+
reason: "field-renamed",
|
|
446
|
+
accepted: true,
|
|
447
|
+
sourceRange: sourceRangeOf(field.sites[0]?.node ?? {}),
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
if (members.length > 1)
|
|
451
|
+
diagnostics.push({
|
|
452
|
+
moduleName: members[0].object.moduleName,
|
|
453
|
+
field: members[0].field,
|
|
454
|
+
reason: "equivalence-class",
|
|
455
|
+
accepted: true,
|
|
456
|
+
sourceRange: sourceRangeOf(members[0].sites[0]?.node ?? {}),
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
const usedNames = [...names.values()];
|
|
460
|
+
const reusedKeys = usedNames.length - new Set(usedNames).size;
|
|
461
|
+
if (keyTransfers > 0)
|
|
462
|
+
diagnostics.push({
|
|
463
|
+
moduleName: entryModule,
|
|
464
|
+
reason: "key-transfer",
|
|
465
|
+
accepted: true,
|
|
466
|
+
});
|
|
467
|
+
if (reusedKeys > 0)
|
|
468
|
+
diagnostics.push({
|
|
469
|
+
moduleName: entryModule,
|
|
470
|
+
reason: "key-reused",
|
|
471
|
+
accepted: true,
|
|
472
|
+
});
|
|
473
|
+
return {
|
|
474
|
+
generation: objects.generation,
|
|
475
|
+
candidateFields: mutable.size,
|
|
476
|
+
equivalenceClasses: classes.length,
|
|
477
|
+
keyTransfers,
|
|
478
|
+
shortenedFields: names.size,
|
|
479
|
+
reusedKeys,
|
|
480
|
+
diagnostics: diagnostics.sort((left, right) => left.moduleName.localeCompare(right.moduleName) ||
|
|
481
|
+
(left.field ?? "").localeCompare(right.field ?? "") ||
|
|
482
|
+
left.reason.localeCompare(right.reason)),
|
|
483
|
+
nameOf: (node) => renameByNode.get(node),
|
|
484
|
+
originalNameOf: (node) => originalByNode.get(node),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function walkExpressionOwners(statement, owner, target) {
|
|
488
|
+
(0, astWalk_1.walkBlockDeep)([statement], {
|
|
489
|
+
onStatement: (nested) => target.set(nested, nested),
|
|
490
|
+
onExpression: (expression) => target.set(expression, owner),
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
function keyTransferOf(statement, resolved, objects) {
|
|
494
|
+
const keyVariable = statement.variables.at(0);
|
|
495
|
+
if (!keyVariable)
|
|
496
|
+
return undefined;
|
|
497
|
+
const keySymbol = resolved.symbolOf(keyVariable);
|
|
498
|
+
if (!keySymbol)
|
|
499
|
+
return undefined;
|
|
500
|
+
let sources = [];
|
|
501
|
+
let recognizedIterator = false;
|
|
502
|
+
const first = statement.iterators.at(0);
|
|
503
|
+
if (first?.type === "CallExpression" &&
|
|
504
|
+
first.base.type === "Identifier" &&
|
|
505
|
+
first.base.name === "pairs") {
|
|
506
|
+
recognizedIterator = true;
|
|
507
|
+
sources = first.arguments[0] ? objects.objectsOf(first.arguments[0]) : [];
|
|
508
|
+
}
|
|
509
|
+
else if (first?.type === "Identifier" &&
|
|
510
|
+
first.name === "next" &&
|
|
511
|
+
statement.iterators[1]) {
|
|
512
|
+
recognizedIterator = true;
|
|
513
|
+
sources = objects.objectsOf(statement.iterators[1]);
|
|
514
|
+
}
|
|
515
|
+
if (!recognizedIterator)
|
|
516
|
+
return undefined;
|
|
517
|
+
const related = new Map();
|
|
518
|
+
sources.forEach((source) => related.set(source.id, source));
|
|
519
|
+
const allowedReferences = new WeakSet();
|
|
520
|
+
const comparisons = [];
|
|
521
|
+
const indices = [];
|
|
522
|
+
const observableOrder = { value: false };
|
|
523
|
+
(0, astWalk_1.walkBlockDeep)(statement.body, {
|
|
524
|
+
onStatement: (nested) => {
|
|
525
|
+
if (nested.type === "CallStatement" ||
|
|
526
|
+
nested.type === "ReturnStatement" ||
|
|
527
|
+
nested.type === "BreakStatement")
|
|
528
|
+
observableOrder.value = true;
|
|
529
|
+
},
|
|
530
|
+
onExpression: (expression) => {
|
|
531
|
+
if (expression.type === "CallExpression" &&
|
|
532
|
+
!(expression.base.type === "Identifier" &&
|
|
533
|
+
expression.base.name === "type"))
|
|
534
|
+
observableOrder.value = true;
|
|
535
|
+
if (expression.type === "IndexExpression") {
|
|
536
|
+
if (expression.index.type === "Identifier" &&
|
|
537
|
+
resolved.symbolOf(expression.index) === keySymbol) {
|
|
538
|
+
const candidateObjects = objects.objectsOf(expression.base);
|
|
539
|
+
allowedReferences.add(expression.index);
|
|
540
|
+
indices.push(expression);
|
|
541
|
+
candidateObjects.forEach((object) => related.set(object.id, object));
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (expression.type === "BinaryExpression" &&
|
|
546
|
+
(expression.operator === "==" || expression.operator === "~=")) {
|
|
547
|
+
const pair = [expression.left, expression.right];
|
|
548
|
+
const identifier = pair.find((item) => item.type === "Identifier" && resolved.symbolOf(item) === keySymbol);
|
|
549
|
+
const literal = pair.find((item) => item.type === "StringLiteral");
|
|
550
|
+
if (identifier && literal) {
|
|
551
|
+
const field = staticFieldName(literal);
|
|
552
|
+
if (field !== undefined) {
|
|
553
|
+
allowedReferences.add(identifier);
|
|
554
|
+
comparisons.push({ literal, field });
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
const unclassified = keySymbol.references.some((reference) => (sourceRangeOf(reference)?.[0] ?? -1) >=
|
|
561
|
+
(sourceRangeOf(statement)?.[0] ?? 0) &&
|
|
562
|
+
(sourceRangeOf(reference)?.[1] ?? Infinity) <=
|
|
563
|
+
(sourceRangeOf(statement)?.[1] ?? 0) &&
|
|
564
|
+
!allowedReferences.has(reference));
|
|
565
|
+
if (observableOrder.value)
|
|
566
|
+
return {
|
|
567
|
+
closed: false,
|
|
568
|
+
objects: [...related.values()],
|
|
569
|
+
reason: "iteration-order-observation",
|
|
570
|
+
};
|
|
571
|
+
if (unclassified)
|
|
572
|
+
return {
|
|
573
|
+
closed: false,
|
|
574
|
+
objects: [...related.values()],
|
|
575
|
+
reason: "key-data-observation",
|
|
576
|
+
};
|
|
577
|
+
return {
|
|
578
|
+
closed: true,
|
|
579
|
+
objects: [...related.values()],
|
|
580
|
+
comparisons,
|
|
581
|
+
indices,
|
|
582
|
+
};
|
|
583
|
+
}
|