optipack 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +231 -2
- package/bin/optipack.js +12 -0
- package/example/manifest.json +27 -0
- package/example/scripts/drops.js +24 -0
- package/example/scripts/main.js +67 -0
- package/example/scripts/util.js +30 -0
- package/package.json +34 -6
- package/src/cli.js +180 -0
- package/src/config.js +141 -0
- package/src/diff.js +108 -0
- package/src/hotpath.js +158 -0
- package/src/lint.js +150 -0
- package/src/pack-walker.js +50 -0
- package/src/pass-utils.js +419 -0
- package/src/passes/b1-cache-getter-chains.js +406 -0
- package/src/passes/b2-closure-lift.js +316 -0
- package/src/passes/b3-string-build.js +167 -0
- package/src/passes/b4-dynprop-hoist.js +177 -0
- package/src/passes/b5-null-chain-flatten.js +137 -0
- package/src/passes/index.js +43 -0
- package/src/passes/r1-cross-tick-cache.js +192 -0
- package/src/passes/r2-json-fold.js +201 -0
- package/src/passes/r3-try-catch-strip.js +61 -0
- package/src/passes/r4-getter-cache-cross-barrier.js +29 -0
- package/src/passes/s1-const-fold.js +130 -0
- package/src/passes/s2-dead-code.js +183 -0
- package/src/passes/s3-bool-simplify.js +82 -0
- package/src/passes/s4-for-of-to-indexed.js +140 -0
- package/src/passes/s5-array-methods-to-loops.js +357 -0
- package/src/passes/s6-destructure-to-direct.js +186 -0
- package/src/passes/s7-hoist-invariant-members.js +327 -0
- package/src/pipeline.js +186 -0
- package/src/pragma.js +115 -0
- package/src/report.js +98 -0
- package/test/array-methods-to-loops/expected.js +58 -0
- package/test/array-methods-to-loops/expects.json +6 -0
- package/test/array-methods-to-loops/input.js +23 -0
- package/test/array-methods-to-loops/scenario.json +22 -0
- package/test/array-methods-to-loops-bail/expected.js +10 -0
- package/test/array-methods-to-loops-bail/input.js +24 -0
- package/test/bool-simplify/expected.js +14 -0
- package/test/bool-simplify/expects.json +11 -0
- package/test/bool-simplify/input.js +18 -0
- package/test/cache-getter-chains/expected.js +20 -0
- package/test/cache-getter-chains/expects.json +11 -0
- package/test/cache-getter-chains/input.js +20 -0
- package/test/cache-getter-chains/scenario.json +35 -0
- package/test/closure-lift/expected.js +58 -0
- package/test/closure-lift/expects.json +1 -0
- package/test/closure-lift/input.js +50 -0
- package/test/closure-lift/scenario.json +9 -0
- package/test/const-fold/expected.js +12 -0
- package/test/const-fold/expects.json +1 -0
- package/test/const-fold/input.js +12 -0
- package/test/const-fold-bail/expected.js +7 -0
- package/test/const-fold-bail/expects.json +3 -0
- package/test/const-fold-bail/input.js +7 -0
- package/test/cross-tick-cache/config.json +1 -0
- package/test/cross-tick-cache/expected.js +38 -0
- package/test/cross-tick-cache/expects.json +6 -0
- package/test/cross-tick-cache/input.js +13 -0
- package/test/cross-tick-cache/scenario.json +33 -0
- package/test/dead-code/expected.js +14 -0
- package/test/dead-code/expects.json +5 -0
- package/test/dead-code/input.js +23 -0
- package/test/destructure-to-direct/expected.js +37 -0
- package/test/destructure-to-direct/expects.json +11 -0
- package/test/destructure-to-direct/input.js +28 -0
- package/test/destructure-to-direct/scenario.json +22 -0
- package/test/destructure-to-direct-bail/expected.js +36 -0
- package/test/destructure-to-direct-bail/expects.json +11 -0
- package/test/destructure-to-direct-bail/input.js +25 -0
- package/test/differential.test.mjs +267 -0
- package/test/dynprop-hoist/expected.js +24 -0
- package/test/dynprop-hoist/expects.json +11 -0
- package/test/dynprop-hoist/input.js +25 -0
- package/test/dynprop-hoist/scenario.json +24 -0
- package/test/example-report-snapshot.json +107 -0
- package/test/example.test.mjs +115 -0
- package/test/for-of-to-indexed/expected.js +59 -0
- package/test/for-of-to-indexed/expects.json +11 -0
- package/test/for-of-to-indexed/input.js +43 -0
- package/test/for-of-to-indexed-bail/expected.js +25 -0
- package/test/for-of-to-indexed-bail/expects.json +11 -0
- package/test/for-of-to-indexed-bail/input.js +29 -0
- package/test/gen-expected.mjs +33 -0
- package/test/getter-cache-cross-barrier/config.json +1 -0
- package/test/getter-cache-cross-barrier/expected.js +10 -0
- package/test/getter-cache-cross-barrier/expects.json +6 -0
- package/test/getter-cache-cross-barrier/input.js +11 -0
- package/test/getter-cache-cross-barrier/scenario.json +28 -0
- package/test/harness/fixture.js +117 -0
- package/test/harness/setup.js +198 -0
- package/test/hoist-invariant-members/expected.js +37 -0
- package/test/hoist-invariant-members/expects.json +11 -0
- package/test/hoist-invariant-members/input.js +34 -0
- package/test/hoist-invariant-members/scenario.json +35 -0
- package/test/hoist-invariant-members-bail/expected.js +19 -0
- package/test/hoist-invariant-members-bail/expects.json +5 -0
- package/test/hoist-invariant-members-bail/input.js +22 -0
- package/test/infra.test.mjs +260 -0
- package/test/json-fold/config.json +1 -0
- package/test/json-fold/expected.js +14 -0
- package/test/json-fold/expects.json +12 -0
- package/test/json-fold/input.js +17 -0
- package/test/json-fold-bail/config.json +1 -0
- package/test/json-fold-bail/expected.js +10 -0
- package/test/json-fold-bail/expects.json +5 -0
- package/test/json-fold-bail/input.js +14 -0
- package/test/loader-hooks.mjs +38 -0
- package/test/mock/@minecraft/server.mjs +274 -0
- package/test/null-chain-flatten/expected.js +22 -0
- package/test/null-chain-flatten/expects.json +5 -0
- package/test/null-chain-flatten/input.js +22 -0
- package/test/null-chain-flatten/scenario.json +24 -0
- package/test/string-build/expected.js +29 -0
- package/test/string-build/expects.json +5 -0
- package/test/string-build/input.js +29 -0
- package/test/string-build/scenario.json +27 -0
- package/test/string-build-bail/expected.js +10 -0
- package/test/string-build-bail/input.js +21 -0
- package/test/transforms.test.mjs +43 -0
- package/test/try-catch-strip/config.json +1 -0
- package/test/try-catch-strip/expected.js +57 -0
- package/test/try-catch-strip/expects.json +1 -0
- package/test/try-catch-strip/input.js +34 -0
- package/test/try-catch-strip/scenario.json +28 -0
- package/test/update-example-snapshot.mjs +19 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared AST analysis helpers used by the passes.
|
|
3
|
+
* All checks are conservative: when in doubt, the answer is "no".
|
|
4
|
+
*/
|
|
5
|
+
import * as t from '@babel/types';
|
|
6
|
+
|
|
7
|
+
const SKIP_KEYS = new Set([
|
|
8
|
+
'loc', 'start', 'end', 'range', 'leadingComments', 'trailingComments',
|
|
9
|
+
'innerComments', 'comments', 'extra', 'tokens', 'regex', 'value',
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
/** Generic DFS over an AST node. enter() returning false skips the subtree. */
|
|
13
|
+
export function walk(node, enter) {
|
|
14
|
+
if (!node || typeof node !== 'object') return;
|
|
15
|
+
if (Array.isArray(node)) {
|
|
16
|
+
for (const item of node) walk(item, enter);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (typeof node.type !== 'string') return;
|
|
20
|
+
if (enter(node) === false) return;
|
|
21
|
+
for (const key of Object.keys(node)) {
|
|
22
|
+
if (SKIP_KEYS.has(key)) continue;
|
|
23
|
+
const v = node[key];
|
|
24
|
+
if (v && typeof v === 'object') {
|
|
25
|
+
if (Array.isArray(v)) {
|
|
26
|
+
for (const item of v) {
|
|
27
|
+
if (item && typeof item === 'object') walk(item, enter);
|
|
28
|
+
}
|
|
29
|
+
} else if (typeof v.type === 'string') {
|
|
30
|
+
walk(v, enter);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const FUNCTION_TYPES = new Set([
|
|
37
|
+
'FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression',
|
|
38
|
+
'ClassMethod', 'ObjectMethod', 'ClassPrivateMethod',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/** Like walk() but stops at function boundaries (does not descend into them). */
|
|
42
|
+
export function walkSameFunction(node, enter) {
|
|
43
|
+
return walk(node, (n) => {
|
|
44
|
+
if (FUNCTION_TYPES.has(n.type) && n !== node) return false;
|
|
45
|
+
return enter(n);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* DFS with parent tracking; stops at function boundaries like walkSameFunction.
|
|
51
|
+
* enter(node, parent) — parent is the directly containing node (or null).
|
|
52
|
+
*/
|
|
53
|
+
export function walkWithParents(node, enter) {
|
|
54
|
+
const rec = (n, parent) => {
|
|
55
|
+
if (!n || typeof n !== 'object' || typeof n.type !== 'string') return true;
|
|
56
|
+
if (FUNCTION_TYPES.has(n.type) && n !== node) return true; // skip, don't enter
|
|
57
|
+
if (enter(n, parent) === false) return false;
|
|
58
|
+
for (const key of Object.keys(n)) {
|
|
59
|
+
if (SKIP_KEYS.has(key)) continue;
|
|
60
|
+
const v = n[key];
|
|
61
|
+
if (v && typeof v === 'object') {
|
|
62
|
+
if (Array.isArray(v)) {
|
|
63
|
+
for (const item of v) {
|
|
64
|
+
if (item && typeof item === 'object' && typeof item.type === 'string') {
|
|
65
|
+
if (!rec(item, n)) return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} else if (typeof v.type === 'string') {
|
|
69
|
+
if (!rec(v, n)) return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
};
|
|
75
|
+
return rec(node, null);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const BARRIER_KINDS = {
|
|
79
|
+
call: new Set(['CallExpression', 'NewExpression', 'TaggedTemplateExpression']),
|
|
80
|
+
assign: new Set(['AssignmentExpression', 'UpdateExpression', 'ForInStatement', 'ForOfStatement']),
|
|
81
|
+
awaitYield: new Set(['AwaitExpression', 'YieldExpression']),
|
|
82
|
+
loop: new Set(['ForStatement', 'ForInStatement', 'ForOfStatement', 'WhileStatement', 'DoWhileStatement']),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function kindsHas(set, node) {
|
|
86
|
+
return set.has(node.type);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Does `node`'s subtree contain a barrier?
|
|
91
|
+
* opts: { calls=true, assigns=true, awaitYield=true, loops=false, skipFunctions=false }
|
|
92
|
+
*/
|
|
93
|
+
export function containsBarrier(node, opts = {}) {
|
|
94
|
+
const { calls = true, assigns = true, awaitYield = true, loops = false, skipFunctions = false } = opts;
|
|
95
|
+
let found = false;
|
|
96
|
+
walkSameFunctionAll(node, skipFunctions, (n) => {
|
|
97
|
+
if (found) return false;
|
|
98
|
+
if (calls && kindsHas(BARRIER_KINDS.call, n)) { found = true; return false; }
|
|
99
|
+
if (assigns && kindsHas(BARRIER_KINDS.assign, n)) { found = true; return false; }
|
|
100
|
+
if (awaitYield && kindsHas(BARRIER_KINDS.awaitYield, n)) { found = true; return false; }
|
|
101
|
+
if (loops && kindsHas(BARRIER_KINDS.loop, n)) { found = true; return false; }
|
|
102
|
+
if (n.type === 'UnaryExpression' && n.operator === 'delete') { found = true; return false; }
|
|
103
|
+
if (n.type === 'OptionalCallExpression') { found = true; return false; }
|
|
104
|
+
return !found;
|
|
105
|
+
});
|
|
106
|
+
return found;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function walkSameFunctionAll(node, skipFunctions, enter) {
|
|
110
|
+
if (skipFunctions) {
|
|
111
|
+
walkSameFunction(node, enter);
|
|
112
|
+
} else {
|
|
113
|
+
walk(node, enter);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** True when the expression subtree provably cannot execute arbitrary code. */
|
|
118
|
+
export function isPureExpression(node, opts = {}) {
|
|
119
|
+
const { allowMemberReads = true } = opts;
|
|
120
|
+
let pure = true;
|
|
121
|
+
walk(node, (n) => {
|
|
122
|
+
if (!pure) return false;
|
|
123
|
+
switch (n.type) {
|
|
124
|
+
case 'CallExpression':
|
|
125
|
+
case 'NewExpression':
|
|
126
|
+
case 'AwaitExpression':
|
|
127
|
+
case 'YieldExpression':
|
|
128
|
+
case 'AssignmentExpression':
|
|
129
|
+
case 'UpdateExpression':
|
|
130
|
+
case 'TaggedTemplateExpression':
|
|
131
|
+
case 'OptionalCallExpression':
|
|
132
|
+
pure = false;
|
|
133
|
+
return false;
|
|
134
|
+
case 'MemberExpression':
|
|
135
|
+
case 'OptionalMemberExpression':
|
|
136
|
+
// Reading a property can hit a getter; spec sanctions member reads as
|
|
137
|
+
// side-effect-free for dead-code purposes.
|
|
138
|
+
if (!allowMemberReads) { pure = false; return false; }
|
|
139
|
+
return true;
|
|
140
|
+
case 'UnaryExpression':
|
|
141
|
+
if (n.operator === 'delete') { pure = false; return false; }
|
|
142
|
+
return true;
|
|
143
|
+
default:
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return pure;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Is this node a Literal we can reason about statically? */
|
|
151
|
+
export function isStaticLiteral(node) {
|
|
152
|
+
return (
|
|
153
|
+
t.isStringLiteral(node) ||
|
|
154
|
+
t.isNumericLiteral(node) ||
|
|
155
|
+
t.isBooleanLiteral(node) ||
|
|
156
|
+
t.isNullLiteral(node)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Extract a primitive value from a literal-ish node (unwraps unary minus). */
|
|
161
|
+
export function literalValue(node) {
|
|
162
|
+
if (t.isStringLiteral(node)) return { kind: 'string', value: node.value };
|
|
163
|
+
if (t.isNumericLiteral(node)) return { kind: 'number', value: node.value };
|
|
164
|
+
if (t.isBooleanLiteral(node)) return { kind: 'boolean', value: node.value };
|
|
165
|
+
if (t.isNullLiteral(node)) return { kind: 'null', value: null };
|
|
166
|
+
if (t.isUnaryExpression(node) && (node.operator === '-' || node.operator === '+') && t.isNumericLiteral(node.argument)) {
|
|
167
|
+
const v = node.operator === '-' ? -node.argument.value : node.argument.value;
|
|
168
|
+
return { kind: 'number', value: v };
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function isNumericResultSafe(v) {
|
|
174
|
+
return typeof v === 'number' && Number.isFinite(v) && !Number.isNaN(v) && !Object.is(v, -0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Compute a binary operation over two static literal values. Returns value or undefined (unfoldable). */
|
|
178
|
+
export function evalBinary(op, lv, rv) {
|
|
179
|
+
const a = lv.value;
|
|
180
|
+
const b = rv.value;
|
|
181
|
+
try {
|
|
182
|
+
switch (op) {
|
|
183
|
+
case '+': return a + b;
|
|
184
|
+
case '-': return a - b;
|
|
185
|
+
case '*': return a * b;
|
|
186
|
+
case '/': return a / b;
|
|
187
|
+
case '%': return a % b;
|
|
188
|
+
case '**': return a ** b;
|
|
189
|
+
case '==': return a == b; // eslint-disable-line eqeqeq
|
|
190
|
+
case '!=': return a != b; // eslint-disable-line eqeqeq
|
|
191
|
+
case '===': return a === b;
|
|
192
|
+
case '!==': return a !== b;
|
|
193
|
+
case '<': return a < b;
|
|
194
|
+
case '>': return a > b;
|
|
195
|
+
case '<=': return a <= b;
|
|
196
|
+
case '>=': return a >= b;
|
|
197
|
+
case '&': return a & b;
|
|
198
|
+
case '|': return a | b;
|
|
199
|
+
case '^': return a ^ b;
|
|
200
|
+
case '<<': return a << b;
|
|
201
|
+
case '>>': return a >> b;
|
|
202
|
+
case '>>>': return a >>> b;
|
|
203
|
+
default: return undefined;
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function valueToLiteralNode(v) {
|
|
211
|
+
if (typeof v === 'string') return t.stringLiteral(v);
|
|
212
|
+
if (typeof v === 'number') return t.numericLiteral(v);
|
|
213
|
+
if (typeof v === 'boolean') return t.booleanLiteral(v);
|
|
214
|
+
if (v === null) return t.nullLiteral();
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Provably-array analysis for `for...of` conversion.
|
|
220
|
+
* Returns { array: boolean, holey: boolean } — holey=true means the array may
|
|
221
|
+
* contain holes (sparse), which changes forEach/map semantics.
|
|
222
|
+
*/
|
|
223
|
+
export function provablyArray(node, path, seen = new Set()) {
|
|
224
|
+
const no = { array: false, holey: true };
|
|
225
|
+
if (!node) return no;
|
|
226
|
+
if (t.isArrayExpression(node)) {
|
|
227
|
+
const holey = node.elements.some((e) => e === null || t.isSpreadElement(e));
|
|
228
|
+
return { array: !holey, holey };
|
|
229
|
+
}
|
|
230
|
+
if (t.isIdentifier(node)) {
|
|
231
|
+
if (node.name === 'arguments' || node.name === 'eval') return no;
|
|
232
|
+
const binding = path.scope.getBinding(node.name);
|
|
233
|
+
if (!binding) return no;
|
|
234
|
+
if (seen.has(node.name)) return no; // cycle
|
|
235
|
+
seen.add(node.name);
|
|
236
|
+
const sources = [];
|
|
237
|
+
if (binding.path.isVariableDeclarator()) sources.push(binding.path.node.init);
|
|
238
|
+
for (const viol of binding.constantViolations) {
|
|
239
|
+
if (viol.isAssignmentExpression()) sources.push(viol.node.right);
|
|
240
|
+
else if (viol.isUpdateExpression()) return no;
|
|
241
|
+
else return no;
|
|
242
|
+
}
|
|
243
|
+
let any = sources.length > 0;
|
|
244
|
+
let all = true;
|
|
245
|
+
let holey = false;
|
|
246
|
+
for (const src of sources) {
|
|
247
|
+
const r = provablyArray(src, path, seen);
|
|
248
|
+
if (!r.array) { all = false; break; }
|
|
249
|
+
if (r.holey) holey = true;
|
|
250
|
+
}
|
|
251
|
+
seen.delete(node.name);
|
|
252
|
+
if (any && all) return { array: true, holey };
|
|
253
|
+
return no;
|
|
254
|
+
}
|
|
255
|
+
if (t.isCallExpression(node) && t.isMemberExpression(node.callee)) {
|
|
256
|
+
const prop = node.callee.property;
|
|
257
|
+
if (!t.isIdentifier(prop)) return no;
|
|
258
|
+
const alwaysArray = new Set([
|
|
259
|
+
'split', 'map', 'filter', 'concat', 'flat', 'flatMap', 'fill',
|
|
260
|
+
'reverse', 'sort', 'from', 'of', 'keys', 'values', 'entries', 'splice',
|
|
261
|
+
]);
|
|
262
|
+
const arrayReceiverOnly = new Set(['slice']);
|
|
263
|
+
if (alwaysArray.has(prop.name)) {
|
|
264
|
+
// These methods throw unless invoked on the right receiver, so on any
|
|
265
|
+
// executing path they return a real Array.
|
|
266
|
+
const holey = prop.name === 'from' && node.arguments.length > 0 && t.isArrayExpression(node.arguments[0])
|
|
267
|
+
? node.arguments[0].elements.some((e) => e === null)
|
|
268
|
+
: false;
|
|
269
|
+
const holey2 = prop.name === 'concat' && node.arguments.some((a) => t.isArrayExpression(a) && a.elements.some((e) => e === null));
|
|
270
|
+
return { array: true, holey: holey || holey2 };
|
|
271
|
+
}
|
|
272
|
+
if (arrayReceiverOnly.has(prop.name)) {
|
|
273
|
+
const recv = provablyArray(node.callee.object, path, seen);
|
|
274
|
+
return recv.array ? { array: true, holey: recv.holey } : no;
|
|
275
|
+
}
|
|
276
|
+
return no;
|
|
277
|
+
}
|
|
278
|
+
if (t.isNewExpression(node) && t.isIdentifier(node.callee) && node.callee.name === 'Array') {
|
|
279
|
+
// new Array(n) is holey; new Array(a, b) is dense.
|
|
280
|
+
if (node.arguments.length === 1) return { array: true, holey: true };
|
|
281
|
+
if (node.arguments.length === 0) return { array: true, holey: false };
|
|
282
|
+
return { array: true, holey: false };
|
|
283
|
+
}
|
|
284
|
+
return no;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Is the value of this expression statically guaranteed to be a boolean? */
|
|
288
|
+
export function isBooleanTyped(node, depth = 0) {
|
|
289
|
+
if (depth > 3) return false;
|
|
290
|
+
if (t.isBooleanLiteral(node)) return true;
|
|
291
|
+
if (t.isUnaryExpression(node) && node.operator === '!') return true;
|
|
292
|
+
if (t.isBinaryExpression(node)) {
|
|
293
|
+
return ['==', '===', '!=', '!==', '<', '>', '<=', '>=', 'in', 'instanceof'].includes(node.operator);
|
|
294
|
+
}
|
|
295
|
+
if (t.isLogicalExpression(node) && node.operator !== '??') {
|
|
296
|
+
return isBooleanTyped(node.left, depth + 1) && isBooleanTyped(node.right, depth + 1);
|
|
297
|
+
}
|
|
298
|
+
if (t.isCallExpression(node) && t.isIdentifier(node.callee) && node.callee.name === 'Boolean' && node.arguments.length === 1) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Computes member-access chains for grouping repeated host-object reads.
|
|
306
|
+
* A chain is a sequence of static (non-computed) property accesses rooted at
|
|
307
|
+
* an Identifier or `this`. Chains stop below any computed access or call.
|
|
308
|
+
*/
|
|
309
|
+
export class ChainAnalyzer {
|
|
310
|
+
constructor() {
|
|
311
|
+
// Map<binding object, numeric id> so shadowed roots never group together.
|
|
312
|
+
this.bindingIds = new WeakMap();
|
|
313
|
+
this.nextId = 1;
|
|
314
|
+
this.syntheticRoots = new Set(); // names of hoisted consts created by passes
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
markSynthetic(name) {
|
|
318
|
+
this.syntheticRoots.add(name);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
bindingId(binding) {
|
|
322
|
+
if (!binding) return 'noglob';
|
|
323
|
+
let id = this.bindingIds.get(binding);
|
|
324
|
+
if (id === undefined) {
|
|
325
|
+
id = this.nextId++;
|
|
326
|
+
this.bindingIds.set(binding, id);
|
|
327
|
+
}
|
|
328
|
+
return 'b' + id;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @param memberPath a NodePath to a MemberExpression
|
|
333
|
+
* @returns { root, rootId, isThis, synthetic, links, key } or null
|
|
334
|
+
*/
|
|
335
|
+
chainOf(memberPath) {
|
|
336
|
+
const links = [];
|
|
337
|
+
let cur = memberPath.node;
|
|
338
|
+
let root = null;
|
|
339
|
+
let isThis = false;
|
|
340
|
+
while (true) {
|
|
341
|
+
if (!t.isMemberExpression(cur)) break;
|
|
342
|
+
if (cur.computed || !t.isIdentifier(cur.property)) return null;
|
|
343
|
+
links.unshift(cur.property.name);
|
|
344
|
+
const obj = cur.object;
|
|
345
|
+
if (t.isMemberExpression(obj)) {
|
|
346
|
+
cur = obj;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (t.isIdentifier(obj)) root = obj;
|
|
350
|
+
else if (t.isThisExpression(obj)) { isThis = true; root = null; }
|
|
351
|
+
else return null;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
if (!root && !isThis) return null;
|
|
355
|
+
let rootId;
|
|
356
|
+
if (isThis) {
|
|
357
|
+
rootId = '@this';
|
|
358
|
+
} else {
|
|
359
|
+
const binding = memberPath.scope.getBinding(root.name);
|
|
360
|
+
rootId = binding ? this.bindingId(binding) : 'global:' + root.name;
|
|
361
|
+
}
|
|
362
|
+
const synthetic = !isThis && this.syntheticRoots.has(root.name);
|
|
363
|
+
return { root, rootId, isThis, synthetic, links, key: rootId + '#' + links.join('.') };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Property names that mark a chain as host-boundary crossing. */
|
|
368
|
+
export const HOST_MARKERS = new Set([
|
|
369
|
+
'world', 'dimension', 'location', 'getComponent', 'getEntities',
|
|
370
|
+
'getAllPlayers', 'getDynamicProperty', 'setDynamicProperty', 'getEntity',
|
|
371
|
+
'spawnEntity', 'runCommand', 'players', 'velocity', 'headLocation',
|
|
372
|
+
'viewDirection', 'getEntitiesFromViewDirection', 'getBlockFromViewDirection',
|
|
373
|
+
'target', 'dimension',
|
|
374
|
+
]);
|
|
375
|
+
|
|
376
|
+
/** All call-family nodes in a statement (excluding nested functions). */
|
|
377
|
+
export function callsIn(stmt) {
|
|
378
|
+
const calls = [];
|
|
379
|
+
walkSameFunction(stmt, (n) => {
|
|
380
|
+
if (t.isFunctionExpression(n) || t.isArrowFunctionExpression(n)) return false;
|
|
381
|
+
if (t.isCallExpression(n) || t.isNewExpression(n) || t.isTaggedTemplateExpression(n) ||
|
|
382
|
+
t.isOptionalCallExpression(n)) {
|
|
383
|
+
calls.push(n);
|
|
384
|
+
}
|
|
385
|
+
return true;
|
|
386
|
+
});
|
|
387
|
+
return calls;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const nodePos = (n) => (n.start !== undefined ? n.start : 0);
|
|
391
|
+
const nodeEnd = (n) => (n.end !== undefined ? n.end : 0);
|
|
392
|
+
const contains = (outer, inner) => nodePos(outer) <= nodePos(inner) && nodeEnd(inner) <= nodeEnd(outer);
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Corridor check: would a value captured BEFORE `stmt` executes still match
|
|
396
|
+
* what each use in `sortedUses` (Node nodes, sorted by position) reads?
|
|
397
|
+
* A call executes before a use when it does not contain the use and starts
|
|
398
|
+
* earlier (argument/object sub-expressions evaluate before the call itself).
|
|
399
|
+
* Returns false when any call runs between the capture point and a use, or
|
|
400
|
+
* between two uses of the same captured value.
|
|
401
|
+
*/
|
|
402
|
+
export function corridorSafe(stmt, sortedUses) {
|
|
403
|
+
const calls = callsIn(stmt);
|
|
404
|
+
for (let i = 0; i < sortedUses.length; i++) {
|
|
405
|
+
const cur = sortedUses[i];
|
|
406
|
+
const prev = i > 0 ? sortedUses[i - 1] : null;
|
|
407
|
+
for (const C of calls) {
|
|
408
|
+
if (contains(C, cur)) continue; // cur evaluates before C runs
|
|
409
|
+
const cStart = nodePos(C);
|
|
410
|
+
if (cStart < nodePos(cur)) {
|
|
411
|
+
if (prev === null) return false; // between capture point and first use
|
|
412
|
+
const containsPrev = contains(C, prev);
|
|
413
|
+
const afterPrev = containsPrev || cStart >= nodePos(prev);
|
|
414
|
+
if (afterPrev) return false; // C runs between prev and cur
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return true;
|
|
419
|
+
}
|