webfuscator 1.2.0 → 1.3.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/dist/analysis/constant.js +3 -4
- package/dist/analysis/constant.js.map +1 -1
- package/dist/analysis/inlineability.d.ts.map +1 -1
- package/dist/analysis/inlineability.js +37 -25
- package/dist/analysis/inlineability.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/obfuscator.d.ts +2 -1
- package/dist/obfuscator.d.ts.map +1 -1
- package/dist/obfuscator.js +47 -12
- package/dist/obfuscator.js.map +1 -1
- package/dist/options.d.ts +24 -4
- package/dist/options.d.ts.map +1 -1
- package/dist/preparation/expand-destructuring.d.ts +3 -3
- package/dist/preparation/expand-destructuring.js +6 -6
- package/dist/preparation/expand-destructuring.js.map +1 -1
- package/dist/transforms/arrow-to-function.js +3 -5
- package/dist/transforms/arrow-to-function.js.map +1 -1
- package/dist/transforms/conceal-strings.d.ts +57 -0
- package/dist/transforms/conceal-strings.d.ts.map +1 -0
- package/dist/transforms/conceal-strings.js +361 -0
- package/dist/transforms/conceal-strings.js.map +1 -0
- package/dist/transforms/const-let-to-var.d.ts +3 -3
- package/dist/transforms/const-let-to-var.d.ts.map +1 -1
- package/dist/transforms/const-let-to-var.js +36 -9
- package/dist/transforms/const-let-to-var.js.map +1 -1
- package/dist/transforms/const-to-let.d.ts +2 -3
- package/dist/transforms/const-to-let.d.ts.map +1 -1
- package/dist/transforms/const-to-let.js +3 -5
- package/dist/transforms/const-to-let.js.map +1 -1
- package/dist/transforms/fold-builtin-methods.d.ts.map +1 -1
- package/dist/transforms/fold-builtin-methods.js +48 -10
- package/dist/transforms/fold-builtin-methods.js.map +1 -1
- package/dist/transforms/fold-constants.js +3 -0
- package/dist/transforms/fold-constants.js.map +1 -1
- package/dist/transforms/hoist-variables.d.ts +30 -0
- package/dist/transforms/hoist-variables.d.ts.map +1 -0
- package/dist/transforms/hoist-variables.js +224 -0
- package/dist/transforms/hoist-variables.js.map +1 -0
- package/dist/transforms/inline-functions.js +3 -4
- package/dist/transforms/inline-functions.js.map +1 -1
- package/dist/transforms/mangle-properties.js +7 -4
- package/dist/transforms/mangle-properties.js.map +1 -1
- package/dist/transforms/merge-variable-declarations.d.ts +17 -0
- package/dist/transforms/merge-variable-declarations.d.ts.map +1 -0
- package/dist/transforms/merge-variable-declarations.js +68 -0
- package/dist/transforms/merge-variable-declarations.js.map +1 -0
- package/dist/transforms/numbers-to-strings.d.ts.map +1 -1
- package/dist/transforms/numbers-to-strings.js +1 -8
- package/dist/transforms/numbers-to-strings.js.map +1 -1
- package/dist/transforms/pack-declarations-into-parameters.js +1 -5
- package/dist/transforms/pack-declarations-into-parameters.js.map +1 -1
- package/dist/transforms/pack.d.ts +5 -7
- package/dist/transforms/pack.d.ts.map +1 -1
- package/dist/transforms/pack.js +194 -32
- package/dist/transforms/pack.js.map +1 -1
- package/dist/transforms/remove-unused-code.d.ts.map +1 -1
- package/dist/transforms/remove-unused-code.js +7 -39
- package/dist/transforms/remove-unused-code.js.map +1 -1
- package/dist/transforms/rename-identifiers.d.ts.map +1 -1
- package/dist/transforms/rename-identifiers.js +29 -17
- package/dist/transforms/rename-identifiers.js.map +1 -1
- package/dist/transforms/reorder-function-declarations.d.ts +25 -0
- package/dist/transforms/reorder-function-declarations.d.ts.map +1 -0
- package/dist/transforms/reorder-function-declarations.js +64 -0
- package/dist/transforms/reorder-function-declarations.js.map +1 -0
- package/dist/transforms/specials-to-strings.js +4 -3
- package/dist/transforms/specials-to-strings.js.map +1 -1
- package/dist/transforms/update-to-assignment.js +1 -16
- package/dist/transforms/update-to-assignment.js.map +1 -1
- package/dist/utils/ast.d.ts +1 -2
- package/dist/utils/ast.d.ts.map +1 -1
- package/dist/utils/ast.js +12 -42
- package/dist/utils/ast.js.map +1 -1
- package/dist/utils/literal.d.ts.map +1 -1
- package/dist/utils/literal.js +74 -2
- package/dist/utils/literal.js.map +1 -1
- package/dist/utils/statement-lists.d.ts +9 -0
- package/dist/utils/statement-lists.d.ts.map +1 -0
- package/dist/utils/statement-lists.js +27 -0
- package/dist/utils/statement-lists.js.map +1 -0
- package/package.json +12 -13
- package/src/analysis/constant.ts +3 -6
- package/src/analysis/inlineability.ts +40 -32
- package/src/index.ts +1 -0
- package/src/obfuscator.ts +51 -14
- package/src/options.ts +28 -3
- package/src/preparation/expand-destructuring.ts +6 -6
- package/src/transforms/arrow-to-function.ts +3 -5
- package/src/transforms/conceal-strings.ts +530 -0
- package/src/transforms/const-let-to-var.ts +37 -9
- package/src/transforms/const-to-let.ts +3 -5
- package/src/transforms/fold-builtin-methods.ts +67 -12
- package/src/transforms/fold-constants.ts +3 -0
- package/src/transforms/hoist-variables.ts +272 -0
- package/src/transforms/inline-functions.ts +3 -4
- package/src/transforms/mangle-properties.ts +7 -7
- package/src/transforms/merge-variable-declarations.ts +91 -0
- package/src/transforms/numbers-to-strings.ts +2 -12
- package/src/transforms/pack-declarations-into-parameters.ts +1 -5
- package/src/transforms/pack.ts +202 -35
- package/src/transforms/remove-unused-code.ts +9 -64
- package/src/transforms/rename-identifiers.ts +42 -17
- package/src/transforms/reorder-function-declarations.ts +80 -0
- package/src/transforms/specials-to-strings.ts +4 -3
- package/src/transforms/update-to-assignment.ts +1 -17
- package/src/utils/ast.ts +15 -51
- package/src/utils/literal.ts +91 -2
- package/src/utils/statement-lists.ts +45 -0
- package/DISCLOSURE +0 -9
- package/README.md +0 -62
package/src/transforms/pack.ts
CHANGED
|
@@ -10,6 +10,103 @@ import { isCalleeOrTagOf, isInStrictContext, referencesOrWritesVariable } from '
|
|
|
10
10
|
import { mulberry32 } from '../utils/random'
|
|
11
11
|
import { StringGenerator } from '../utils/string-generator'
|
|
12
12
|
|
|
13
|
+
// The ECMAScript intrinsics and the common host globals. A detached Function
|
|
14
|
+
// body reaches every one of them and gets the same value, so pack can leave them
|
|
15
|
+
// bare. The CommonJS wrapper names (require, module, exports, __dirname,
|
|
16
|
+
// __filename) are left out on purpose. They are function parameters, not
|
|
17
|
+
// properties of the global object, so a Function body cannot see them and pack
|
|
18
|
+
// routes them like any other free name. `skipGlobals: true` opts into this list.
|
|
19
|
+
const STANDARD_GLOBALS: readonly string[] = [
|
|
20
|
+
'globalThis',
|
|
21
|
+
'self',
|
|
22
|
+
'Infinity',
|
|
23
|
+
'NaN',
|
|
24
|
+
'undefined',
|
|
25
|
+
'parseInt',
|
|
26
|
+
'parseFloat',
|
|
27
|
+
'isNaN',
|
|
28
|
+
'isFinite',
|
|
29
|
+
'decodeURI',
|
|
30
|
+
'decodeURIComponent',
|
|
31
|
+
'encodeURI',
|
|
32
|
+
'encodeURIComponent',
|
|
33
|
+
'escape',
|
|
34
|
+
'unescape',
|
|
35
|
+
'btoa',
|
|
36
|
+
'atob',
|
|
37
|
+
'structuredClone',
|
|
38
|
+
'fetch',
|
|
39
|
+
'queueMicrotask',
|
|
40
|
+
'setTimeout',
|
|
41
|
+
'clearTimeout',
|
|
42
|
+
'setInterval',
|
|
43
|
+
'clearInterval',
|
|
44
|
+
'setImmediate',
|
|
45
|
+
'clearImmediate',
|
|
46
|
+
'Object',
|
|
47
|
+
'Function',
|
|
48
|
+
'Boolean',
|
|
49
|
+
'Symbol',
|
|
50
|
+
'BigInt',
|
|
51
|
+
'Number',
|
|
52
|
+
'Math',
|
|
53
|
+
'Date',
|
|
54
|
+
'String',
|
|
55
|
+
'RegExp',
|
|
56
|
+
'JSON',
|
|
57
|
+
'Promise',
|
|
58
|
+
'Proxy',
|
|
59
|
+
'Reflect',
|
|
60
|
+
'Error',
|
|
61
|
+
'EvalError',
|
|
62
|
+
'RangeError',
|
|
63
|
+
'ReferenceError',
|
|
64
|
+
'SyntaxError',
|
|
65
|
+
'TypeError',
|
|
66
|
+
'URIError',
|
|
67
|
+
'AggregateError',
|
|
68
|
+
'Array',
|
|
69
|
+
'Map',
|
|
70
|
+
'Set',
|
|
71
|
+
'WeakMap',
|
|
72
|
+
'WeakSet',
|
|
73
|
+
'WeakRef',
|
|
74
|
+
'FinalizationRegistry',
|
|
75
|
+
'ArrayBuffer',
|
|
76
|
+
'SharedArrayBuffer',
|
|
77
|
+
'DataView',
|
|
78
|
+
'Atomics',
|
|
79
|
+
'Int8Array',
|
|
80
|
+
'Uint8Array',
|
|
81
|
+
'Uint8ClampedArray',
|
|
82
|
+
'Int16Array',
|
|
83
|
+
'Uint16Array',
|
|
84
|
+
'Int32Array',
|
|
85
|
+
'Uint32Array',
|
|
86
|
+
'Float32Array',
|
|
87
|
+
'Float64Array',
|
|
88
|
+
'BigInt64Array',
|
|
89
|
+
'BigUint64Array',
|
|
90
|
+
'console',
|
|
91
|
+
'crypto',
|
|
92
|
+
'performance',
|
|
93
|
+
'Intl',
|
|
94
|
+
'URL',
|
|
95
|
+
'URLSearchParams',
|
|
96
|
+
'TextEncoder',
|
|
97
|
+
'TextDecoder',
|
|
98
|
+
'WebAssembly',
|
|
99
|
+
'window',
|
|
100
|
+
'document',
|
|
101
|
+
'location',
|
|
102
|
+
'postMessage',
|
|
103
|
+
'alert',
|
|
104
|
+
'confirm',
|
|
105
|
+
'global',
|
|
106
|
+
'process',
|
|
107
|
+
'Buffer',
|
|
108
|
+
]
|
|
109
|
+
|
|
13
110
|
/**
|
|
14
111
|
* Serializes the whole program to a string and rebuilds it at runtime through
|
|
15
112
|
* the `Function` constructor. A detached function body only reaches true globals
|
|
@@ -17,13 +114,11 @@ import { StringGenerator } from '../utils/string-generator'
|
|
|
17
114
|
* bundler-injected binding, or a live import, none reachable from the body and
|
|
18
115
|
* none distinguishable from a global at build time, so free names are routed
|
|
19
116
|
* through a parameter object whose accessors run in the original top-level scope
|
|
20
|
-
* and resolve each name there.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* top-level await, dynamic `import()`, or a top-level `arguments` will not run
|
|
26
|
-
* once moved into a Function body.
|
|
117
|
+
* and resolve each name there. Imports remain above the call. Names inside
|
|
118
|
+
* `with` stay bare so the object still controls lookup. A Function body cannot
|
|
119
|
+
* contain exports, so pack throws when it finds one. Code that needs
|
|
120
|
+
* `import.meta`, top-level await, dynamic `import()`, or top-level `arguments`
|
|
121
|
+
* will not run after the move.
|
|
27
122
|
*
|
|
28
123
|
* @example
|
|
29
124
|
* // ◀️ before
|
|
@@ -94,14 +189,19 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
|
|
|
94
189
|
const readProperties = new Map<string, string>()
|
|
95
190
|
const typeofProperties = new Map<string, string>()
|
|
96
191
|
const settable = new Set<string>()
|
|
192
|
+
const freshProperty = (): string => {
|
|
193
|
+
let property: string
|
|
194
|
+
do {
|
|
195
|
+
// `__proto__` as an object-literal key would set the prototype rather
|
|
196
|
+
// than name an accessor.
|
|
197
|
+
property = generator.next()
|
|
198
|
+
} while (property === '__proto__')
|
|
199
|
+
return property
|
|
200
|
+
}
|
|
97
201
|
const propertyFor = (registry: Map<string, string>, name: string): string => {
|
|
98
202
|
let property = registry.get(name)
|
|
99
203
|
if (property === undefined) {
|
|
100
|
-
|
|
101
|
-
// `__proto__` as an object-literal key would set the prototype rather
|
|
102
|
-
// than name an accessor.
|
|
103
|
-
property = generator.next()
|
|
104
|
-
} while (property === '__proto__')
|
|
204
|
+
property = freshProperty()
|
|
105
205
|
registry.set(name, property)
|
|
106
206
|
}
|
|
107
207
|
return property
|
|
@@ -109,12 +209,28 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
|
|
|
109
209
|
const memberFor = (property: string): t.MemberExpression =>
|
|
110
210
|
t.memberExpression(t.identifier(objectName), t.stringLiteral(property), true)
|
|
111
211
|
|
|
212
|
+
// Import locals read as free once detached but are unreachable from a Function
|
|
213
|
+
// body, so they route even when the caller lists them in skipGlobals.
|
|
214
|
+
const importedNames = new Set<string>()
|
|
215
|
+
for (const declaration of importDeclarations) {
|
|
216
|
+
for (const specifier of declaration.specifiers) {
|
|
217
|
+
importedNames.add(specifier.local.name)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const skipOption = ctx.pack?.skipGlobals ?? false
|
|
221
|
+
let skipNames: readonly string[] = []
|
|
222
|
+
if (skipOption === true) {
|
|
223
|
+
skipNames = STANDARD_GLOBALS
|
|
224
|
+
} else if (skipOption !== false) {
|
|
225
|
+
skipNames = skipOption
|
|
226
|
+
}
|
|
227
|
+
const skip = new Set<string>(skipNames)
|
|
228
|
+
|
|
112
229
|
const isRoutable = (path: NodePath<t.Identifier>): boolean => {
|
|
113
230
|
const name = path.node.name
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
if (name === objectName || name === 'arguments' || name === 'eval') {
|
|
231
|
+
// Keep the wrapper parameter and `arguments` bare. The latter must bind to
|
|
232
|
+
// the wrapper function's own arguments object.
|
|
233
|
+
if (name === objectName || name === 'arguments') {
|
|
118
234
|
return false
|
|
119
235
|
}
|
|
120
236
|
// Inside `with`, a free name may resolve to a property of the with object at
|
|
@@ -125,10 +241,28 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
|
|
|
125
241
|
if (!referencesOrWritesVariable(path)) {
|
|
126
242
|
return false
|
|
127
243
|
}
|
|
128
|
-
|
|
244
|
+
if (path.scope.getBinding(name)) {
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
return importedNames.has(name) || !skip.has(name)
|
|
129
248
|
}
|
|
130
249
|
|
|
250
|
+
// A strict script's top-level `this` is the global object, but a strict
|
|
251
|
+
// Function called plainly sees `undefined`. Rather than reach for `.call`,
|
|
252
|
+
// route top-level `this` through a captured property like any free name.
|
|
253
|
+
const routeTopLevelThis = bodyStrict && !wasModule
|
|
254
|
+
let thisProperty: string | null = null
|
|
255
|
+
|
|
131
256
|
const routingVisitor: Visitor = {
|
|
257
|
+
ThisExpression(path) {
|
|
258
|
+
if (!routeTopLevelThis || !isTopLevelThis(path)) {
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
if (thisProperty === null) {
|
|
262
|
+
thisProperty = freshProperty()
|
|
263
|
+
}
|
|
264
|
+
path.replaceWith(memberFor(thisProperty))
|
|
265
|
+
},
|
|
132
266
|
UnaryExpression(path) {
|
|
133
267
|
if (path.node.operator !== 'typeof') {
|
|
134
268
|
return
|
|
@@ -161,18 +295,13 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
|
|
|
161
295
|
}
|
|
162
296
|
programPath.traverse(routingVisitor)
|
|
163
297
|
|
|
164
|
-
// The script completion value is its trailing expression; returning it keeps
|
|
165
|
-
// `eval` of the packed output producing the same value.
|
|
166
298
|
const body = programNode.body
|
|
167
|
-
const lastStatement = body.at(-1)
|
|
168
|
-
if (lastStatement && t.isExpressionStatement(lastStatement)) {
|
|
169
|
-
body[body.length - 1] = t.returnStatement(lastStatement.expression)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
299
|
const innerDirectives = bodyStrict ? [t.directive(t.directiveLiteral('use strict'))] : []
|
|
173
300
|
const innerCode = generate(t.program(body, innerDirectives, 'script'), {
|
|
174
301
|
minified: true,
|
|
175
302
|
comments: false,
|
|
303
|
+
// `comments: false` still allows @license and @preserve.
|
|
304
|
+
shouldPrintComment: () => false,
|
|
176
305
|
}).code
|
|
177
306
|
|
|
178
307
|
const objectExpression = buildAccessorObject({
|
|
@@ -181,6 +310,7 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
|
|
|
181
310
|
settable,
|
|
182
311
|
setterParam,
|
|
183
312
|
strict: bodyStrict,
|
|
313
|
+
thisProperty,
|
|
184
314
|
})
|
|
185
315
|
|
|
186
316
|
const constructorReference = shadowedConstructor
|
|
@@ -193,16 +323,7 @@ function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): b
|
|
|
193
323
|
t.stringLiteral(objectName),
|
|
194
324
|
t.stringLiteral(innerCode),
|
|
195
325
|
])
|
|
196
|
-
|
|
197
|
-
// called strict function would instead see `undefined`, so pass the global
|
|
198
|
-
// `this` of the sloppy output script.
|
|
199
|
-
const invocation =
|
|
200
|
-
bodyStrict && !wasModule
|
|
201
|
-
? t.callExpression(t.memberExpression(wrapper, t.identifier('call')), [
|
|
202
|
-
t.thisExpression(),
|
|
203
|
-
objectExpression,
|
|
204
|
-
])
|
|
205
|
-
: t.callExpression(wrapper, [objectExpression])
|
|
326
|
+
const invocation = t.callExpression(wrapper, [objectExpression])
|
|
206
327
|
|
|
207
328
|
programNode.body = [...importDeclarations, t.expressionStatement(invocation)]
|
|
208
329
|
programNode.directives = []
|
|
@@ -238,6 +359,45 @@ function bindsFunctionConstructor(declaration: t.ImportDeclaration): boolean {
|
|
|
238
359
|
return declaration.specifiers.some((specifier) => specifier.local.name === 'Function')
|
|
239
360
|
}
|
|
240
361
|
|
|
362
|
+
// A `this` refers to the top-level `this` when no enclosing non-arrow function,
|
|
363
|
+
// class field, or static block captures it before the program does. A computed
|
|
364
|
+
// member key is the exception: it evaluates in the scope surrounding its method
|
|
365
|
+
// or field, not that member's `this`, so a `this` reached through a boundary's
|
|
366
|
+
// `key` skips it and keeps looking outward. `thisBinding` keeps every path
|
|
367
|
+
// predicate off the walked `current`, whose narrowed union would otherwise trip
|
|
368
|
+
// NodePath's invariant generic when reassigned to `child`.
|
|
369
|
+
function isTopLevelThis(path: NodePath<t.ThisExpression>): boolean {
|
|
370
|
+
let child: NodePath = path
|
|
371
|
+
let current: NodePath | null = path.parentPath
|
|
372
|
+
while (current) {
|
|
373
|
+
const binding = thisBinding(current)
|
|
374
|
+
if (binding === 'program') {
|
|
375
|
+
return true
|
|
376
|
+
}
|
|
377
|
+
if (binding === 'captured' && child.key !== 'key') {
|
|
378
|
+
return false
|
|
379
|
+
}
|
|
380
|
+
child = current
|
|
381
|
+
current = current.parentPath
|
|
382
|
+
}
|
|
383
|
+
return false
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function thisBinding(boundary: NodePath): 'captured' | 'program' | null {
|
|
387
|
+
if (boundary.isProgram()) {
|
|
388
|
+
return 'program'
|
|
389
|
+
}
|
|
390
|
+
if (
|
|
391
|
+
(boundary.isFunction() && !boundary.isArrowFunctionExpression()) ||
|
|
392
|
+
boundary.isClassProperty() ||
|
|
393
|
+
boundary.isClassPrivateProperty() ||
|
|
394
|
+
boundary.isStaticBlock()
|
|
395
|
+
) {
|
|
396
|
+
return 'captured'
|
|
397
|
+
}
|
|
398
|
+
return null
|
|
399
|
+
}
|
|
400
|
+
|
|
241
401
|
function isBareDeleteArgument(path: NodePath<t.Identifier>): boolean {
|
|
242
402
|
const parent = path.parentPath
|
|
243
403
|
return (
|
|
@@ -291,11 +451,12 @@ interface AccessorObjectSpec {
|
|
|
291
451
|
setterParam: string
|
|
292
452
|
settable: Set<string>
|
|
293
453
|
strict: boolean
|
|
454
|
+
thisProperty: string | null
|
|
294
455
|
typeofProperties: Map<string, string>
|
|
295
456
|
}
|
|
296
457
|
|
|
297
458
|
function buildAccessorObject(spec: AccessorObjectSpec): t.ObjectExpression {
|
|
298
|
-
const properties: t.ObjectMethod[] = []
|
|
459
|
+
const properties: (t.ObjectMethod | t.ObjectProperty)[] = []
|
|
299
460
|
for (const [name, property] of spec.readProperties) {
|
|
300
461
|
properties.push(
|
|
301
462
|
t.objectMethod(
|
|
@@ -336,5 +497,11 @@ function buildAccessorObject(spec: AccessorObjectSpec): t.ObjectExpression {
|
|
|
336
497
|
),
|
|
337
498
|
)
|
|
338
499
|
}
|
|
500
|
+
// This is a data property. Its `this` value evaluates in the output script's
|
|
501
|
+
// top-level scope, where `this` is the global object. A getter would bind
|
|
502
|
+
// `this` to the accessor object instead.
|
|
503
|
+
if (spec.thisProperty !== null) {
|
|
504
|
+
properties.push(t.objectProperty(t.stringLiteral(spec.thisProperty), t.thisExpression()))
|
|
505
|
+
}
|
|
339
506
|
return t.objectExpression(properties)
|
|
340
507
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import traverse from '@babel/traverse'
|
|
2
|
-
import type { NodePath,
|
|
2
|
+
import type { NodePath, Visitor } from '@babel/traverse'
|
|
3
3
|
import * as t from '@babel/types'
|
|
4
4
|
import type { File } from '@babel/types'
|
|
5
5
|
|
|
6
6
|
import { isSideEffectFree } from '../analysis/purity'
|
|
7
|
-
import { isDirectEvalCall } from '../utils/ast'
|
|
8
7
|
import type { ChangeState } from '../utils/change-tracking'
|
|
9
8
|
import { hasAnnexBFunctionAlias } from '../utils/paths'
|
|
10
9
|
|
|
@@ -28,10 +27,7 @@ export function removeUnusedCode(ast: File): boolean {
|
|
|
28
27
|
// then maintain counts during removal.
|
|
29
28
|
crawlProgramScope(ast)
|
|
30
29
|
let anyChanged = false
|
|
31
|
-
const state:
|
|
32
|
-
changed: true,
|
|
33
|
-
evalReachableScopes: collectEvalReachableScopes(ast),
|
|
34
|
-
}
|
|
30
|
+
const state: ChangeState = { changed: true }
|
|
35
31
|
while (state.changed) {
|
|
36
32
|
state.changed = false
|
|
37
33
|
traverse(ast, sweepVisitor, undefined, state)
|
|
@@ -51,49 +47,19 @@ function crawlProgramScope(ast: File): void {
|
|
|
51
47
|
})
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
// Sweep state keeps this set local to one pass.
|
|
56
|
-
interface SweepState extends ChangeState {
|
|
57
|
-
evalReachableScopes: Set<t.Node>
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function collectEvalReachableScopes(ast: File): Set<t.Node> {
|
|
61
|
-
const reachable = new Set<t.Node>()
|
|
62
|
-
traverse(ast, {
|
|
63
|
-
CallExpression(path) {
|
|
64
|
-
if (!isDirectEvalCall(path.node)) {
|
|
65
|
-
return
|
|
66
|
-
}
|
|
67
|
-
let scope: Scope | undefined = path.scope
|
|
68
|
-
while (scope) {
|
|
69
|
-
reachable.add(scope.block)
|
|
70
|
-
scope = scope.parent
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
})
|
|
74
|
-
return reachable
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function bindingReachableByEval(
|
|
78
|
-
binding: { scope: Scope },
|
|
79
|
-
evalReachableScopes: Set<t.Node>,
|
|
80
|
-
): boolean {
|
|
81
|
-
return evalReachableScopes.has(binding.scope.block)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const sweepVisitor: Visitor<SweepState> = {
|
|
50
|
+
const sweepVisitor: Visitor<ChangeState> = {
|
|
85
51
|
VariableDeclarator(path, state) {
|
|
86
|
-
if (tryRemoveDeclarator(path
|
|
52
|
+
if (tryRemoveDeclarator(path)) {
|
|
87
53
|
state.changed = true
|
|
88
54
|
}
|
|
89
55
|
},
|
|
90
56
|
FunctionDeclaration(path, state) {
|
|
91
|
-
if (tryRemoveFunctionDecl(path
|
|
57
|
+
if (tryRemoveFunctionDecl(path)) {
|
|
92
58
|
state.changed = true
|
|
93
59
|
}
|
|
94
60
|
},
|
|
95
61
|
ClassDeclaration(path, state) {
|
|
96
|
-
if (tryRemoveClassDecl(path
|
|
62
|
+
if (tryRemoveClassDecl(path)) {
|
|
97
63
|
state.changed = true
|
|
98
64
|
}
|
|
99
65
|
},
|
|
@@ -126,10 +92,7 @@ function dereferenceSubtree(rootPath: NodePath): void {
|
|
|
126
92
|
rootPath.traverse(dereferenceVisitor)
|
|
127
93
|
}
|
|
128
94
|
|
|
129
|
-
function tryRemoveDeclarator(
|
|
130
|
-
path: NodePath<t.VariableDeclarator>,
|
|
131
|
-
evalReachableScopes: Set<t.Node>,
|
|
132
|
-
): boolean {
|
|
95
|
+
function tryRemoveDeclarator(path: NodePath<t.VariableDeclarator>): boolean {
|
|
133
96
|
const declaration = path.parentPath
|
|
134
97
|
if (!declaration || !declaration.isVariableDeclaration()) {
|
|
135
98
|
return false
|
|
@@ -170,10 +133,6 @@ function tryRemoveDeclarator(
|
|
|
170
133
|
if (binding.constantViolations.length > 0) {
|
|
171
134
|
return false
|
|
172
135
|
}
|
|
173
|
-
if (bindingReachableByEval(binding, evalReachableScopes)) {
|
|
174
|
-
return false
|
|
175
|
-
}
|
|
176
|
-
|
|
177
136
|
if (path.node.init) {
|
|
178
137
|
dereferenceSubtree(path.get('init') as NodePath)
|
|
179
138
|
}
|
|
@@ -185,10 +144,7 @@ function tryRemoveDeclarator(
|
|
|
185
144
|
return true
|
|
186
145
|
}
|
|
187
146
|
|
|
188
|
-
function tryRemoveFunctionDecl(
|
|
189
|
-
path: NodePath<t.FunctionDeclaration>,
|
|
190
|
-
evalReachableScopes: Set<t.Node>,
|
|
191
|
-
): boolean {
|
|
147
|
+
function tryRemoveFunctionDecl(path: NodePath<t.FunctionDeclaration>): boolean {
|
|
192
148
|
if (isExported(path)) {
|
|
193
149
|
return false
|
|
194
150
|
}
|
|
@@ -211,20 +167,13 @@ function tryRemoveFunctionDecl(
|
|
|
211
167
|
if (binding.constantViolations.length > 0) {
|
|
212
168
|
return false
|
|
213
169
|
}
|
|
214
|
-
if (bindingReachableByEval(binding, evalReachableScopes)) {
|
|
215
|
-
return false
|
|
216
|
-
}
|
|
217
|
-
|
|
218
170
|
dereferenceSubtree(path.get('body') as NodePath)
|
|
219
171
|
path.scope.removeBinding(id.name)
|
|
220
172
|
path.remove()
|
|
221
173
|
return true
|
|
222
174
|
}
|
|
223
175
|
|
|
224
|
-
function tryRemoveClassDecl(
|
|
225
|
-
path: NodePath<t.ClassDeclaration>,
|
|
226
|
-
evalReachableScopes: Set<t.Node>,
|
|
227
|
-
): boolean {
|
|
176
|
+
function tryRemoveClassDecl(path: NodePath<t.ClassDeclaration>): boolean {
|
|
228
177
|
if (isExported(path)) {
|
|
229
178
|
return false
|
|
230
179
|
}
|
|
@@ -265,10 +214,6 @@ function tryRemoveClassDecl(
|
|
|
265
214
|
if (binding.constantViolations.length > 0) {
|
|
266
215
|
return false
|
|
267
216
|
}
|
|
268
|
-
if (bindingReachableByEval(binding, evalReachableScopes)) {
|
|
269
|
-
return false
|
|
270
|
-
}
|
|
271
|
-
|
|
272
217
|
dereferenceSubtree(path.get('body') as NodePath)
|
|
273
218
|
if (path.node.superClass) {
|
|
274
219
|
dereferenceSubtree(path.get('superClass') as NodePath)
|
|
@@ -4,7 +4,7 @@ import * as t from '@babel/types'
|
|
|
4
4
|
import type { File } from '@babel/types'
|
|
5
5
|
|
|
6
6
|
import type { TransformContext } from '../options'
|
|
7
|
-
import {
|
|
7
|
+
import { isInsideWith } from '../utils/ast'
|
|
8
8
|
import { hasAnnexBFunctionAlias, referencesOrWritesVariable } from '../utils/paths'
|
|
9
9
|
import { mulberry32 } from '../utils/random'
|
|
10
10
|
import { StringGenerator } from '../utils/string-generator'
|
|
@@ -31,6 +31,7 @@ import { StringGenerator } from '../utils/string-generator'
|
|
|
31
31
|
*/
|
|
32
32
|
export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
|
|
33
33
|
const generator = new StringGenerator(ctx.stringGeneratorMode, mulberry32(ctx.seed))
|
|
34
|
+
const separatedDirectExports = separateDirectExports(ast.program)
|
|
34
35
|
|
|
35
36
|
let programScope: Scope | undefined
|
|
36
37
|
traverse(ast, {
|
|
@@ -40,14 +41,14 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
|
|
|
40
41
|
},
|
|
41
42
|
})
|
|
42
43
|
if (!programScope) {
|
|
43
|
-
return
|
|
44
|
+
return separatedDirectExports
|
|
44
45
|
}
|
|
45
46
|
programScope.crawl()
|
|
46
47
|
|
|
47
48
|
const liveAncestors = new Map<Scope, Set<Binding>>()
|
|
48
49
|
// Reserve unbound names so generated bindings cannot capture them.
|
|
49
50
|
const freeNames = new Set<string>()
|
|
50
|
-
// `with`
|
|
51
|
+
// A reference inside `with` pins its binding to the source name.
|
|
51
52
|
const unrenamable = new Set<Binding>()
|
|
52
53
|
// Reserve pinned source names globally, including before their binding is seen.
|
|
53
54
|
const pinnedNames = new Set<string>()
|
|
@@ -82,19 +83,6 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
|
|
|
82
83
|
scope = scope.parent
|
|
83
84
|
}
|
|
84
85
|
},
|
|
85
|
-
CallExpression(path) {
|
|
86
|
-
// Direct eval can read every visible binding by its source name.
|
|
87
|
-
if (!isDirectEvalCall(path.node)) {
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
let scope: Scope | undefined = path.scope
|
|
91
|
-
while (scope) {
|
|
92
|
-
for (const name of Object.keys(scope.bindings)) {
|
|
93
|
-
pin(scope.bindings[name]!, name)
|
|
94
|
-
}
|
|
95
|
-
scope = scope.parent
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
86
|
FunctionDeclaration(path) {
|
|
99
87
|
// ECMA-262 B.3.3 gives sloppy block functions an alias Babel does not model.
|
|
100
88
|
if (!hasAnnexBFunctionAlias(path)) {
|
|
@@ -169,7 +157,7 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
|
|
|
169
157
|
})
|
|
170
158
|
|
|
171
159
|
if (bindingToName.size === 0) {
|
|
172
|
-
return renameLabels(ast, generator)
|
|
160
|
+
return renameLabels(ast, generator) || separatedDirectExports
|
|
173
161
|
}
|
|
174
162
|
|
|
175
163
|
traverse(ast, {
|
|
@@ -199,6 +187,43 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
|
|
|
199
187
|
return true
|
|
200
188
|
}
|
|
201
189
|
|
|
190
|
+
function separateDirectExports(program: t.Program): boolean {
|
|
191
|
+
const body: t.Program['body'] = []
|
|
192
|
+
let changed = false
|
|
193
|
+
|
|
194
|
+
for (const statement of program.body) {
|
|
195
|
+
const declaration = t.isExportNamedDeclaration(statement) ? statement.declaration : null
|
|
196
|
+
if (
|
|
197
|
+
!t.isVariableDeclaration(declaration) &&
|
|
198
|
+
!t.isFunctionDeclaration(declaration) &&
|
|
199
|
+
!t.isClassDeclaration(declaration)
|
|
200
|
+
) {
|
|
201
|
+
body.push(statement)
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const names = Object.keys(t.getBindingIdentifiers(declaration))
|
|
206
|
+
if (names.length === 0) {
|
|
207
|
+
body.push(statement)
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
body.push(
|
|
212
|
+
declaration,
|
|
213
|
+
t.exportNamedDeclaration(
|
|
214
|
+
null,
|
|
215
|
+
names.map((name) => t.exportSpecifier(t.identifier(name), t.identifier(name))),
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
changed = true
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (changed) {
|
|
222
|
+
program.body = body
|
|
223
|
+
}
|
|
224
|
+
return changed
|
|
225
|
+
}
|
|
226
|
+
|
|
202
227
|
function isNonBindingPosition(path: { node: t.Identifier; parent: t.Node }): boolean {
|
|
203
228
|
const node = path.node
|
|
204
229
|
const parent = path.parent
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { NodePath } from '@babel/traverse'
|
|
2
|
+
import type * as t from '@babel/types'
|
|
3
|
+
import type { File } from '@babel/types'
|
|
4
|
+
|
|
5
|
+
import { hasAnnexBFunctionAlias } from '../utils/paths'
|
|
6
|
+
import { rewriteStatementLists } from '../utils/statement-lists'
|
|
7
|
+
import type { StatementListContainer } from '../utils/statement-lists'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Moves hoisted function declarations to the end of each program, block, switch
|
|
11
|
+
* case, or static block. It keeps declaration order, including duplicate names,
|
|
12
|
+
* and leaves sloppy Annex B block functions in place.
|
|
13
|
+
*
|
|
14
|
+
* If moving every declaration would turn a string expression into a directive,
|
|
15
|
+
* one declaration stays before the string. Direct exports stay in place.
|
|
16
|
+
* Indirect exports move with their local declaration.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // ◀️ before
|
|
20
|
+
* function area(r) {
|
|
21
|
+
* return r * r;
|
|
22
|
+
* }
|
|
23
|
+
* log(area(2));
|
|
24
|
+
*
|
|
25
|
+
* // ▶️ after
|
|
26
|
+
* log(area(2));
|
|
27
|
+
* function area(r) {
|
|
28
|
+
* return r * r;
|
|
29
|
+
* }
|
|
30
|
+
*/
|
|
31
|
+
export function reorderFunctionDeclarations(ast: File): boolean {
|
|
32
|
+
return rewriteStatementLists(ast, (statements, container) =>
|
|
33
|
+
reorderedStatements(statements, containerAllowsDirective(container)),
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Only programs and function bodies recognize directives, so only they need the guard.
|
|
38
|
+
function containerAllowsDirective(container: NodePath<StatementListContainer>): boolean {
|
|
39
|
+
if (container.isProgram()) {
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
return container.isBlockStatement() && container.parentPath.isFunction()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function reorderedStatements(
|
|
46
|
+
children: NodePath<t.Statement>[],
|
|
47
|
+
canCreateDirective: boolean,
|
|
48
|
+
): t.Statement[] | null {
|
|
49
|
+
const rest: t.Statement[] = []
|
|
50
|
+
const moved: t.Statement[] = []
|
|
51
|
+
for (const child of children) {
|
|
52
|
+
if (isMovable(child)) {
|
|
53
|
+
moved.push(child.node)
|
|
54
|
+
} else {
|
|
55
|
+
rest.push(child.node)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (moved.length === 0) {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
if (canCreateDirective && createsDirective(rest)) {
|
|
63
|
+
rest.unshift(moved.shift()!)
|
|
64
|
+
}
|
|
65
|
+
const reordered = [...rest, ...moved]
|
|
66
|
+
return reordered.every((node, index) => node === children[index]?.node) ? null : reordered
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createsDirective(statements: readonly t.Statement[]): boolean {
|
|
70
|
+
const first = statements[0]
|
|
71
|
+
return (
|
|
72
|
+
first !== undefined &&
|
|
73
|
+
first.type === 'ExpressionStatement' &&
|
|
74
|
+
first.expression.type === 'StringLiteral'
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isMovable(child: NodePath<t.Statement>): boolean {
|
|
79
|
+
return child.isFunctionDeclaration() && child.node.id !== null && !hasAnnexBFunctionAlias(child)
|
|
80
|
+
}
|
|
@@ -4,7 +4,7 @@ import * as t from '@babel/types'
|
|
|
4
4
|
import type { File } from '@babel/types'
|
|
5
5
|
|
|
6
6
|
import type { TransformContext } from '../options'
|
|
7
|
-
import {
|
|
7
|
+
import { isInsideWith } from '../utils/ast'
|
|
8
8
|
import type { ChangeState } from '../utils/change-tracking'
|
|
9
9
|
import { mulberry32 } from '../utils/random'
|
|
10
10
|
import { StringGenerator } from '../utils/string-generator'
|
|
@@ -69,8 +69,9 @@ const visitor: Visitor<State> = {
|
|
|
69
69
|
if (path.scope.getBinding(name)) {
|
|
70
70
|
return
|
|
71
71
|
}
|
|
72
|
-
//
|
|
73
|
-
|
|
72
|
+
// A replacement cannot occupy a write target. `with` may also shadow `NaN`,
|
|
73
|
+
// `Infinity`, or `undefined`.
|
|
74
|
+
if (isWriteTarget(path) || isInsideWith(path)) {
|
|
74
75
|
return
|
|
75
76
|
}
|
|
76
77
|
const str = t.stringLiteral(state.generator.next())
|