webfuscator 1.0.1 → 1.2.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 +0 -2
- package/dist/analysis/constant.js +3 -3
- package/dist/analysis/constant.js.map +1 -1
- package/dist/analysis/inlineability.js +2 -2
- package/dist/analysis/inlineability.js.map +1 -1
- package/dist/analysis/purity.js +1 -1
- package/dist/analysis/purity.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/obfuscator.d.ts.map +1 -1
- package/dist/obfuscator.js +14 -0
- package/dist/obfuscator.js.map +1 -1
- package/dist/options.d.ts +14 -2
- package/dist/options.d.ts.map +1 -1
- package/dist/preparation/expand-destructuring.js +4 -3
- package/dist/preparation/expand-destructuring.js.map +1 -1
- package/dist/preparation/expand-template-literals.js +3 -2
- package/dist/preparation/expand-template-literals.js.map +1 -1
- package/dist/preparation/split-sequence-expressions.js +1 -1
- package/dist/preparation/split-sequence-expressions.js.map +1 -1
- package/dist/preparation/split-variable-declarations.js +1 -1
- package/dist/preparation/split-variable-declarations.js.map +1 -1
- package/dist/preparation/stringify-object-keys.js +1 -1
- package/dist/preparation/stringify-object-keys.js.map +1 -1
- package/dist/preparation/wrap-single-statements.js +2 -2
- package/dist/preparation/wrap-single-statements.js.map +1 -1
- package/dist/transforms/arguments-to-parameters.js +1 -1
- package/dist/transforms/arguments-to-parameters.js.map +1 -1
- package/dist/transforms/arrow-to-function.js.map +1 -1
- package/dist/transforms/collapse-single-use-temps.js +7 -7
- package/dist/transforms/collapse-single-use-temps.js.map +1 -1
- package/dist/transforms/const-let-to-var.js +3 -3
- package/dist/transforms/const-let-to-var.js.map +1 -1
- package/dist/transforms/const-to-let.js.map +1 -1
- package/dist/transforms/do-while-to-while.js +3 -3
- package/dist/transforms/do-while-to-while.js.map +1 -1
- package/dist/transforms/drop-console.js +4 -3
- package/dist/transforms/drop-console.js.map +1 -1
- package/dist/transforms/drop-debugger.js.map +1 -1
- package/dist/transforms/expand-binary-assignment.js +5 -3
- package/dist/transforms/expand-binary-assignment.js.map +1 -1
- package/dist/transforms/expand-logical-assignment.js +5 -3
- package/dist/transforms/expand-logical-assignment.js.map +1 -1
- package/dist/transforms/extract-object-properties.js +7 -7
- package/dist/transforms/extract-object-properties.js.map +1 -1
- package/dist/transforms/fold-builtin-methods.js +16 -15
- package/dist/transforms/fold-builtin-methods.js.map +1 -1
- package/dist/transforms/fold-constants.js +2 -2
- package/dist/transforms/fold-constants.js.map +1 -1
- package/dist/transforms/for-to-while.js +5 -5
- package/dist/transforms/for-to-while.js.map +1 -1
- package/dist/transforms/function-declaration-to-expression.js +3 -3
- package/dist/transforms/function-declaration-to-expression.js.map +1 -1
- package/dist/transforms/inline-functions.js +7 -7
- package/dist/transforms/inline-functions.js.map +1 -1
- package/dist/transforms/logical-to-ternary.js +2 -2
- package/dist/transforms/logical-to-ternary.js.map +1 -1
- package/dist/transforms/mangle-properties.d.ts.map +1 -1
- package/dist/transforms/mangle-properties.js +35 -86
- package/dist/transforms/mangle-properties.js.map +1 -1
- package/dist/transforms/nullish-coalescing-to-ternary.js +2 -2
- package/dist/transforms/nullish-coalescing-to-ternary.js.map +1 -1
- package/dist/transforms/numbers-to-strings.js +2 -2
- package/dist/transforms/numbers-to-strings.js.map +1 -1
- package/dist/transforms/object-method-to-property.js +1 -1
- package/dist/transforms/object-method-to-property.js.map +1 -1
- package/dist/transforms/optional-chaining-to-ternary.js +7 -4
- package/dist/transforms/optional-chaining-to-ternary.js.map +1 -1
- package/dist/transforms/pack-declarations-into-parameters.js +2 -2
- package/dist/transforms/pack-declarations-into-parameters.js.map +1 -1
- package/dist/transforms/pack.d.ts +33 -0
- package/dist/transforms/pack.d.ts.map +1 -0
- package/dist/transforms/pack.js +265 -0
- package/dist/transforms/pack.js.map +1 -0
- package/dist/transforms/remove-anonymous-function-names.js +3 -3
- package/dist/transforms/remove-anonymous-function-names.js.map +1 -1
- package/dist/transforms/remove-unreachable-code.js +1 -1
- package/dist/transforms/remove-unreachable-code.js.map +1 -1
- package/dist/transforms/remove-unused-code.js +4 -4
- 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 +4 -3
- package/dist/transforms/rename-identifiers.js.map +1 -1
- package/dist/transforms/specials-to-strings.d.ts.map +1 -1
- package/dist/transforms/specials-to-strings.js +2 -2
- package/dist/transforms/specials-to-strings.js.map +1 -1
- package/dist/transforms/switch-to-if.js +7 -7
- package/dist/transforms/switch-to-if.js.map +1 -1
- package/dist/transforms/ternary-to-if.js +7 -4
- package/dist/transforms/ternary-to-if.js.map +1 -1
- package/dist/transforms/update-to-assignment.js +4 -2
- package/dist/transforms/update-to-assignment.js.map +1 -1
- package/dist/transforms/yodify-conditions.js +3 -3
- package/dist/transforms/yodify-conditions.js.map +1 -1
- package/dist/utils/ast.d.ts.map +1 -1
- package/dist/utils/ast.js +7 -2
- package/dist/utils/ast.js.map +1 -1
- package/dist/utils/evaluation-order.js.map +1 -1
- package/dist/utils/logical-lowering.js +2 -2
- package/dist/utils/logical-lowering.js.map +1 -1
- package/dist/utils/loop-lowering.js +1 -1
- package/dist/utils/loop-lowering.js.map +1 -1
- package/dist/utils/string-generator.js +1 -1
- package/dist/utils/string-generator.js.map +1 -1
- package/package.json +11 -10
- package/src/analysis/constant.ts +4 -4
- package/src/analysis/inlineability.ts +4 -4
- package/src/analysis/purity.ts +3 -3
- package/src/index.ts +9 -2
- package/src/obfuscator.ts +57 -41
- package/src/options.ts +16 -2
- package/src/preparation/expand-destructuring.ts +5 -4
- package/src/preparation/expand-template-literals.ts +4 -3
- package/src/preparation/split-sequence-expressions.ts +2 -2
- package/src/preparation/split-variable-declarations.ts +1 -1
- package/src/preparation/stringify-object-keys.ts +2 -2
- package/src/preparation/wrap-single-statements.ts +2 -2
- package/src/transforms/arguments-to-parameters.ts +5 -5
- package/src/transforms/arrow-to-function.ts +3 -3
- package/src/transforms/collapse-single-use-temps.ts +14 -14
- package/src/transforms/const-let-to-var.ts +6 -6
- package/src/transforms/const-to-let.ts +4 -4
- package/src/transforms/do-while-to-while.ts +6 -6
- package/src/transforms/drop-console.ts +8 -7
- package/src/transforms/drop-debugger.ts +2 -2
- package/src/transforms/expand-binary-assignment.ts +8 -6
- package/src/transforms/expand-logical-assignment.ts +8 -6
- package/src/transforms/extract-object-properties.ts +12 -12
- package/src/transforms/fold-builtin-methods.ts +20 -19
- package/src/transforms/fold-constants.ts +8 -8
- package/src/transforms/for-to-while.ts +8 -8
- package/src/transforms/function-declaration-to-expression.ts +5 -5
- package/src/transforms/inline-functions.ts +14 -14
- package/src/transforms/logical-to-ternary.ts +7 -7
- package/src/transforms/mangle-properties.ts +43 -97
- package/src/transforms/nullish-coalescing-to-ternary.ts +6 -6
- package/src/transforms/numbers-to-strings.ts +4 -4
- package/src/transforms/object-method-to-property.ts +4 -4
- package/src/transforms/optional-chaining-to-ternary.ts +10 -7
- package/src/transforms/pack-declarations-into-parameters.ts +7 -7
- package/src/transforms/pack.ts +340 -0
- package/src/transforms/remove-anonymous-function-names.ts +5 -5
- package/src/transforms/remove-unreachable-code.ts +3 -3
- package/src/transforms/remove-unused-code.ts +8 -8
- package/src/transforms/rename-identifiers.ts +9 -8
- package/src/transforms/specials-to-strings.ts +7 -7
- package/src/transforms/switch-to-if.ts +12 -12
- package/src/transforms/ternary-to-if.ts +12 -9
- package/src/transforms/update-to-assignment.ts +7 -5
- package/src/transforms/yodify-conditions.ts +6 -6
- package/src/utils/ast.ts +9 -2
- package/src/utils/evaluation-order.ts +1 -1
- package/src/utils/logical-lowering.ts +3 -3
- package/src/utils/loop-lowering.ts +1 -1
- package/src/utils/string-generator.ts +1 -1
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import generate from '@babel/generator'
|
|
2
|
+
import traverse from '@babel/traverse'
|
|
3
|
+
import type { NodePath, Visitor } from '@babel/traverse'
|
|
4
|
+
import * as t from '@babel/types'
|
|
5
|
+
import type { File } from '@babel/types'
|
|
6
|
+
|
|
7
|
+
import type { TransformContext } from '../options'
|
|
8
|
+
import { isInsideWith } from '../utils/ast'
|
|
9
|
+
import { isCalleeOrTagOf, isInStrictContext, referencesOrWritesVariable } from '../utils/paths'
|
|
10
|
+
import { mulberry32 } from '../utils/random'
|
|
11
|
+
import { StringGenerator } from '../utils/string-generator'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Serializes the whole program to a string and rebuilds it at runtime through
|
|
15
|
+
* the `Function` constructor. A detached function body only reaches true globals
|
|
16
|
+
* and its own parameters. A free name might instead be a CommonJS `require`, a
|
|
17
|
+
* bundler-injected binding, or a live import, none reachable from the body and
|
|
18
|
+
* none distinguishable from a global at build time, so free names are routed
|
|
19
|
+
* through a parameter object whose accessors run in the original top-level scope
|
|
20
|
+
* and resolve each name there. Import declarations return above the call. `eval`
|
|
21
|
+
* is left un-routed so a direct eval still reads the body scope. Names inside a
|
|
22
|
+
* `with` stay un-routed too, since they may resolve against the with object at
|
|
23
|
+
* runtime. Export statements cannot live inside a Function body, so pack throws
|
|
24
|
+
* on them. It packs everything else as written. Code that needs `import.meta`,
|
|
25
|
+
* top-level await, dynamic `import()`, or a top-level `arguments` will not run
|
|
26
|
+
* once moved into a Function body.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // ◀️ before
|
|
30
|
+
* console.log(value);
|
|
31
|
+
*
|
|
32
|
+
* // ▶️ after
|
|
33
|
+
* Function("M", "return M[\"b\"].log(M[\"_\"]);")({
|
|
34
|
+
* get "b"() {
|
|
35
|
+
* return console;
|
|
36
|
+
* },
|
|
37
|
+
* get "_"() {
|
|
38
|
+
* return value;
|
|
39
|
+
* }
|
|
40
|
+
* });
|
|
41
|
+
*/
|
|
42
|
+
export function pack(ast: File, ctx: TransformContext): boolean {
|
|
43
|
+
let programPath: NodePath<t.Program> | null = null
|
|
44
|
+
traverse(ast, {
|
|
45
|
+
Program(path) {
|
|
46
|
+
programPath = path
|
|
47
|
+
path.stop()
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
if (!programPath) {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
return packProgram(programPath, ctx)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function packProgram(programPath: NodePath<t.Program>, ctx: TransformContext): boolean {
|
|
57
|
+
assertNoExports(programPath)
|
|
58
|
+
const importPaths = collectImportDeclarations(programPath)
|
|
59
|
+
const importDeclarations = importPaths.map((path) => path.node)
|
|
60
|
+
// An import named `Function` shadows the constructor at the output top level,
|
|
61
|
+
// so the wrap reaches the real one through a function instance instead.
|
|
62
|
+
const shadowedConstructor = importDeclarations.some((declaration) =>
|
|
63
|
+
bindsFunctionConstructor(declaration),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const programNode = programPath.node
|
|
67
|
+
const wasModule = programNode.sourceType === 'module'
|
|
68
|
+
// Escaping strict is a deliberate opt-in: drop the directive so the packed
|
|
69
|
+
// body runs sloppy even when the source was strict.
|
|
70
|
+
const escapeStrict = ctx.pack?.escapeStrict ?? false
|
|
71
|
+
const bodyStrict = !escapeStrict && isInStrictContext(programPath)
|
|
72
|
+
|
|
73
|
+
// Detaching imports drops their bindings, so their names read as free and are
|
|
74
|
+
// routed like any other global. The declarations return at the output top
|
|
75
|
+
// level, where the accessors close over them.
|
|
76
|
+
for (const path of importPaths) {
|
|
77
|
+
path.remove()
|
|
78
|
+
}
|
|
79
|
+
programPath.scope.crawl()
|
|
80
|
+
|
|
81
|
+
const usedNames = collectUsedNames(programNode, importDeclarations)
|
|
82
|
+
const generator = new StringGenerator(ctx.stringGeneratorMode, mulberry32(ctx.seed))
|
|
83
|
+
const freshIdentifier = (): string => {
|
|
84
|
+
let name: string
|
|
85
|
+
do {
|
|
86
|
+
name = generator.next()
|
|
87
|
+
} while (usedNames.has(name))
|
|
88
|
+
usedNames.add(name)
|
|
89
|
+
return name
|
|
90
|
+
}
|
|
91
|
+
const objectName = freshIdentifier()
|
|
92
|
+
const setterParam = freshIdentifier()
|
|
93
|
+
|
|
94
|
+
const readProperties = new Map<string, string>()
|
|
95
|
+
const typeofProperties = new Map<string, string>()
|
|
96
|
+
const settable = new Set<string>()
|
|
97
|
+
const propertyFor = (registry: Map<string, string>, name: string): string => {
|
|
98
|
+
let property = registry.get(name)
|
|
99
|
+
if (property === undefined) {
|
|
100
|
+
do {
|
|
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__')
|
|
105
|
+
registry.set(name, property)
|
|
106
|
+
}
|
|
107
|
+
return property
|
|
108
|
+
}
|
|
109
|
+
const memberFor = (property: string): t.MemberExpression =>
|
|
110
|
+
t.memberExpression(t.identifier(objectName), t.stringLiteral(property), true)
|
|
111
|
+
|
|
112
|
+
const isRoutable = (path: NodePath<t.Identifier>): boolean => {
|
|
113
|
+
const name = path.node.name
|
|
114
|
+
// The wrapper parameter stays as written. `arguments` and `eval` are never
|
|
115
|
+
// routed: `arguments` binds to its own function's object, and leaving `eval`
|
|
116
|
+
// bare keeps a direct eval reading the body scope rather than the global one.
|
|
117
|
+
if (name === objectName || name === 'arguments' || name === 'eval') {
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
// Inside `with`, a free name may resolve to a property of the with object at
|
|
121
|
+
// runtime, so routing it to the top-level accessor would change behavior.
|
|
122
|
+
if (isInsideWith(path)) {
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
if (!referencesOrWritesVariable(path)) {
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
return !path.scope.getBinding(name)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const routingVisitor: Visitor = {
|
|
132
|
+
UnaryExpression(path) {
|
|
133
|
+
if (path.node.operator !== 'typeof') {
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
const argument = path.get('argument')
|
|
137
|
+
// A bare `typeof name` must not throw for an undeclared global, so it maps
|
|
138
|
+
// to an accessor that applies `typeof` in the original scope.
|
|
139
|
+
if (!argument.isIdentifier() || !isRoutable(argument)) {
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
path.replaceWith(memberFor(propertyFor(typeofProperties, argument.node.name)))
|
|
143
|
+
},
|
|
144
|
+
Identifier(path) {
|
|
145
|
+
// `delete name` must keep hitting the global binding; `delete obj[prop]`
|
|
146
|
+
// would drop the accessor instead. A real global stays reachable bare.
|
|
147
|
+
if (isBareDeleteArgument(path) || !isRoutable(path)) {
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
const name = path.node.name
|
|
151
|
+
if (isWriteReference(path)) {
|
|
152
|
+
settable.add(name)
|
|
153
|
+
}
|
|
154
|
+
const member = memberFor(propertyFor(readProperties, name))
|
|
155
|
+
// A member callee would bind `this` to the object; the comma discards the
|
|
156
|
+
// base and keeps the original receiver.
|
|
157
|
+
path.replaceWith(
|
|
158
|
+
isCalleeOrTagOf(path) ? t.sequenceExpression([t.numericLiteral(0), member]) : member,
|
|
159
|
+
)
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
programPath.traverse(routingVisitor)
|
|
163
|
+
|
|
164
|
+
// The script completion value is its trailing expression; returning it keeps
|
|
165
|
+
// `eval` of the packed output producing the same value.
|
|
166
|
+
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
|
+
const innerDirectives = bodyStrict ? [t.directive(t.directiveLiteral('use strict'))] : []
|
|
173
|
+
const innerCode = generate(t.program(body, innerDirectives, 'script'), {
|
|
174
|
+
minified: true,
|
|
175
|
+
comments: false,
|
|
176
|
+
}).code
|
|
177
|
+
|
|
178
|
+
const objectExpression = buildAccessorObject({
|
|
179
|
+
readProperties,
|
|
180
|
+
typeofProperties,
|
|
181
|
+
settable,
|
|
182
|
+
setterParam,
|
|
183
|
+
strict: bodyStrict,
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const constructorReference = shadowedConstructor
|
|
187
|
+
? t.memberExpression(
|
|
188
|
+
t.functionExpression(null, [], t.blockStatement([])),
|
|
189
|
+
t.identifier('constructor'),
|
|
190
|
+
)
|
|
191
|
+
: t.identifier('Function')
|
|
192
|
+
const wrapper = t.callExpression(constructorReference, [
|
|
193
|
+
t.stringLiteral(objectName),
|
|
194
|
+
t.stringLiteral(innerCode),
|
|
195
|
+
])
|
|
196
|
+
// A strict script keeps top-level `this` bound to the global object. A plainly
|
|
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])
|
|
206
|
+
|
|
207
|
+
programNode.body = [...importDeclarations, t.expressionStatement(invocation)]
|
|
208
|
+
programNode.directives = []
|
|
209
|
+
programNode.sourceType = importDeclarations.length > 0 ? 'module' : 'script'
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Export statements are module-only syntax and cannot exist inside a Function
|
|
214
|
+
// body. Pack throws rather than emit output that fails to parse at runtime.
|
|
215
|
+
function assertNoExports(programPath: NodePath<t.Program>): void {
|
|
216
|
+
const hasExport = programPath.node.body.some(
|
|
217
|
+
(statement) =>
|
|
218
|
+
t.isExportNamedDeclaration(statement) ||
|
|
219
|
+
t.isExportDefaultDeclaration(statement) ||
|
|
220
|
+
t.isExportAllDeclaration(statement),
|
|
221
|
+
)
|
|
222
|
+
if (hasExport) {
|
|
223
|
+
throw new Error('pack cannot wrap a program that uses export statements')
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function collectImportDeclarations(
|
|
228
|
+
programPath: NodePath<t.Program>,
|
|
229
|
+
): NodePath<t.ImportDeclaration>[] {
|
|
230
|
+
return programPath
|
|
231
|
+
.get('body')
|
|
232
|
+
.filter((statement): statement is NodePath<t.ImportDeclaration> =>
|
|
233
|
+
statement.isImportDeclaration(),
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function bindsFunctionConstructor(declaration: t.ImportDeclaration): boolean {
|
|
238
|
+
return declaration.specifiers.some((specifier) => specifier.local.name === 'Function')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function isBareDeleteArgument(path: NodePath<t.Identifier>): boolean {
|
|
242
|
+
const parent = path.parentPath
|
|
243
|
+
return (
|
|
244
|
+
parent != null &&
|
|
245
|
+
parent.isUnaryExpression({ operator: 'delete' }) &&
|
|
246
|
+
parent.node.argument === path.node
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isWriteReference(path: NodePath<t.Identifier>): boolean {
|
|
251
|
+
const parent = path.parentPath
|
|
252
|
+
if (!parent) {
|
|
253
|
+
return false
|
|
254
|
+
}
|
|
255
|
+
const node = path.node
|
|
256
|
+
if (parent.isAssignmentExpression() && parent.node.left === node) {
|
|
257
|
+
return true
|
|
258
|
+
}
|
|
259
|
+
if (parent.isUpdateExpression() && parent.node.argument === node) {
|
|
260
|
+
return true
|
|
261
|
+
}
|
|
262
|
+
if ((parent.isForInStatement() || parent.isForOfStatement()) && parent.node.left === node) {
|
|
263
|
+
return true
|
|
264
|
+
}
|
|
265
|
+
// Destructuring assignment targets bind through patterns Babel flags here.
|
|
266
|
+
return path.isBindingIdentifier()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function collectUsedNames(
|
|
270
|
+
program: t.Program,
|
|
271
|
+
importDeclarations: t.ImportDeclaration[],
|
|
272
|
+
): Set<string> {
|
|
273
|
+
// The wrapper parameter and setter parameter must avoid every name the body
|
|
274
|
+
// uses, so no nested binding can shadow their accesses.
|
|
275
|
+
const names = new Set<string>(['Function'])
|
|
276
|
+
for (const declaration of importDeclarations) {
|
|
277
|
+
for (const specifier of declaration.specifiers) {
|
|
278
|
+
names.add(specifier.local.name)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
t.traverseFast(program, (node) => {
|
|
282
|
+
if (t.isIdentifier(node)) {
|
|
283
|
+
names.add(node.name)
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
return names
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
interface AccessorObjectSpec {
|
|
290
|
+
readProperties: Map<string, string>
|
|
291
|
+
setterParam: string
|
|
292
|
+
settable: Set<string>
|
|
293
|
+
strict: boolean
|
|
294
|
+
typeofProperties: Map<string, string>
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function buildAccessorObject(spec: AccessorObjectSpec): t.ObjectExpression {
|
|
298
|
+
const properties: t.ObjectMethod[] = []
|
|
299
|
+
for (const [name, property] of spec.readProperties) {
|
|
300
|
+
properties.push(
|
|
301
|
+
t.objectMethod(
|
|
302
|
+
'get',
|
|
303
|
+
t.stringLiteral(property),
|
|
304
|
+
[],
|
|
305
|
+
t.blockStatement([t.returnStatement(t.identifier(name))]),
|
|
306
|
+
),
|
|
307
|
+
)
|
|
308
|
+
if (spec.settable.has(name)) {
|
|
309
|
+
const setterBody = t.blockStatement([
|
|
310
|
+
t.expressionStatement(
|
|
311
|
+
t.assignmentExpression('=', t.identifier(name), t.identifier(spec.setterParam)),
|
|
312
|
+
),
|
|
313
|
+
])
|
|
314
|
+
// A strict script becomes a sloppy output script, so the setter stays
|
|
315
|
+
// strict to keep an undeclared-global write throwing.
|
|
316
|
+
if (spec.strict) {
|
|
317
|
+
setterBody.directives.push(t.directive(t.directiveLiteral('use strict')))
|
|
318
|
+
}
|
|
319
|
+
properties.push(
|
|
320
|
+
t.objectMethod(
|
|
321
|
+
'set',
|
|
322
|
+
t.stringLiteral(property),
|
|
323
|
+
[t.identifier(spec.setterParam)],
|
|
324
|
+
setterBody,
|
|
325
|
+
),
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
for (const [name, property] of spec.typeofProperties) {
|
|
330
|
+
properties.push(
|
|
331
|
+
t.objectMethod(
|
|
332
|
+
'get',
|
|
333
|
+
t.stringLiteral(property),
|
|
334
|
+
[],
|
|
335
|
+
t.blockStatement([t.returnStatement(t.unaryExpression('typeof', t.identifier(name)))]),
|
|
336
|
+
),
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
return t.objectExpression(properties)
|
|
340
|
+
}
|
|
@@ -2,8 +2,8 @@ import type { NodePath, Visitor } from '@babel/traverse'
|
|
|
2
2
|
import * as t from '@babel/types'
|
|
3
3
|
import type { File } from '@babel/types'
|
|
4
4
|
|
|
5
|
-
import { traverseForChanges } from '
|
|
6
|
-
import type { ChangeState } from '
|
|
5
|
+
import { traverseForChanges } from '../utils/change-tracking'
|
|
6
|
+
import type { ChangeState } from '../utils/change-tracking'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Removes a function expression's name when the body does not use its binding
|
|
@@ -25,7 +25,7 @@ export function removeAnonymousFunctionNames(ast: File): boolean {
|
|
|
25
25
|
|
|
26
26
|
const visitor: Visitor<ChangeState> = {
|
|
27
27
|
FunctionExpression(path, state) {
|
|
28
|
-
const
|
|
28
|
+
const id = path.node.id
|
|
29
29
|
if (!id) {
|
|
30
30
|
return
|
|
31
31
|
}
|
|
@@ -48,8 +48,8 @@ const visitor: Visitor<ChangeState> = {
|
|
|
48
48
|
|
|
49
49
|
// No inferred name means `.name` would become empty.
|
|
50
50
|
function inferredName(path: NodePath<t.FunctionExpression>): string | null {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
51
|
+
const node = path.node
|
|
52
|
+
const parentPath = path.parentPath
|
|
53
53
|
if (!parentPath) {
|
|
54
54
|
return null
|
|
55
55
|
}
|
|
@@ -2,8 +2,8 @@ import type { NodePath, Visitor } from '@babel/traverse'
|
|
|
2
2
|
import * as t from '@babel/types'
|
|
3
3
|
import type { File } from '@babel/types'
|
|
4
4
|
|
|
5
|
-
import { traverseForChanges } from '
|
|
6
|
-
import type { ChangeState } from '
|
|
5
|
+
import { traverseForChanges } from '../utils/change-tracking'
|
|
6
|
+
import type { ChangeState } from '../utils/change-tracking'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Removes statements after unconditional control flow in the same statement list.
|
|
@@ -72,7 +72,7 @@ function isCompletion(node: t.Node): boolean {
|
|
|
72
72
|
// Hoisted bindings and referenced lexical TDZ remain observable from reachable
|
|
73
73
|
// code. An unreferenced lexical declaration is inert.
|
|
74
74
|
function isSafeToRemoveAfterCompletion(path: NodePath<t.Statement>): boolean {
|
|
75
|
-
const
|
|
75
|
+
const node = path.node
|
|
76
76
|
if (t.isFunctionDeclaration(node) || (t.isVariableDeclaration(node) && node.kind === 'var')) {
|
|
77
77
|
return false
|
|
78
78
|
}
|
|
@@ -3,10 +3,10 @@ import type { NodePath, Scope, Visitor } from '@babel/traverse'
|
|
|
3
3
|
import * as t from '@babel/types'
|
|
4
4
|
import type { File } from '@babel/types'
|
|
5
5
|
|
|
6
|
-
import { isSideEffectFree } from '
|
|
7
|
-
import { isDirectEvalCall } from '
|
|
8
|
-
import type { ChangeState } from '
|
|
9
|
-
import { hasAnnexBFunctionAlias } from '
|
|
6
|
+
import { isSideEffectFree } from '../analysis/purity'
|
|
7
|
+
import { isDirectEvalCall } from '../utils/ast'
|
|
8
|
+
import type { ChangeState } from '../utils/change-tracking'
|
|
9
|
+
import { hasAnnexBFunctionAlias } from '../utils/paths'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Removes unused declarations and expression statements that have no effects.
|
|
@@ -150,12 +150,12 @@ function tryRemoveDeclarator(
|
|
|
150
150
|
return false
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
const
|
|
153
|
+
const id = path.node.id
|
|
154
154
|
if (!t.isIdentifier(id)) {
|
|
155
155
|
return false
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
const
|
|
158
|
+
const init = path.node.init
|
|
159
159
|
if (init && !isSideEffectFree(init, path.scope, path.get('init') as NodePath)) {
|
|
160
160
|
return false
|
|
161
161
|
}
|
|
@@ -196,7 +196,7 @@ function tryRemoveFunctionDecl(
|
|
|
196
196
|
if (hasAnnexBFunctionAlias(path)) {
|
|
197
197
|
return false
|
|
198
198
|
}
|
|
199
|
-
const
|
|
199
|
+
const id = path.node.id
|
|
200
200
|
if (!id) {
|
|
201
201
|
return false
|
|
202
202
|
}
|
|
@@ -228,7 +228,7 @@ function tryRemoveClassDecl(
|
|
|
228
228
|
if (isExported(path)) {
|
|
229
229
|
return false
|
|
230
230
|
}
|
|
231
|
-
const
|
|
231
|
+
const id = path.node.id
|
|
232
232
|
if (!id) {
|
|
233
233
|
return false
|
|
234
234
|
}
|
|
@@ -3,11 +3,11 @@ import type { Binding, Scope } from '@babel/traverse'
|
|
|
3
3
|
import * as t from '@babel/types'
|
|
4
4
|
import type { File } from '@babel/types'
|
|
5
5
|
|
|
6
|
-
import type { TransformContext } from '
|
|
7
|
-
import { isDirectEvalCall, isInsideWith } from '
|
|
8
|
-
import { hasAnnexBFunctionAlias, referencesOrWritesVariable } from '
|
|
9
|
-
import { mulberry32 } from '
|
|
10
|
-
import { StringGenerator } from '
|
|
6
|
+
import type { TransformContext } from '../options'
|
|
7
|
+
import { isDirectEvalCall, isInsideWith } from '../utils/ast'
|
|
8
|
+
import { hasAnnexBFunctionAlias, referencesOrWritesVariable } from '../utils/paths'
|
|
9
|
+
import { mulberry32 } from '../utils/random'
|
|
10
|
+
import { StringGenerator } from '../utils/string-generator'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Renames bindings and labels with a fresh `StringGenerator`. Each scope avoids
|
|
@@ -125,7 +125,7 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
|
|
|
125
125
|
|
|
126
126
|
traverse(ast, {
|
|
127
127
|
Scopable(path) {
|
|
128
|
-
const
|
|
128
|
+
const scope = path.scope
|
|
129
129
|
// Test global reservations during allocation to avoid copying them per scope.
|
|
130
130
|
const offLimits = new Set<string>()
|
|
131
131
|
const live = liveAncestors.get(scope)
|
|
@@ -174,7 +174,7 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
|
|
|
174
174
|
|
|
175
175
|
traverse(ast, {
|
|
176
176
|
Identifier(path) {
|
|
177
|
-
const
|
|
177
|
+
const node = path.node
|
|
178
178
|
const directName = idToName.get(node)
|
|
179
179
|
if (directName !== undefined) {
|
|
180
180
|
node.name = directName
|
|
@@ -200,7 +200,8 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
function isNonBindingPosition(path: { node: t.Identifier; parent: t.Node }): boolean {
|
|
203
|
-
const
|
|
203
|
+
const node = path.node
|
|
204
|
+
const parent = path.parent
|
|
204
205
|
// A PrivateName id is not a variable and must match its class declaration.
|
|
205
206
|
if (t.isPrivateName(parent)) {
|
|
206
207
|
return true
|
|
@@ -3,11 +3,11 @@ 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
|
-
import type { TransformContext } from '
|
|
7
|
-
import { enclosingScopeHasDirectEval, isInsideWith } from '
|
|
8
|
-
import type { ChangeState } from '
|
|
9
|
-
import { mulberry32 } from '
|
|
10
|
-
import { StringGenerator } from '
|
|
6
|
+
import type { TransformContext } from '../options'
|
|
7
|
+
import { enclosingScopeHasDirectEval, isInsideWith } from '../utils/ast'
|
|
8
|
+
import type { ChangeState } from '../utils/change-tracking'
|
|
9
|
+
import { mulberry32 } from '../utils/random'
|
|
10
|
+
import { StringGenerator } from '../utils/string-generator'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Rewrites five constant-like values through strings for later string mangling:
|
|
@@ -59,7 +59,7 @@ const visitor: Visitor<State> = {
|
|
|
59
59
|
state.changed = true
|
|
60
60
|
},
|
|
61
61
|
Identifier(path, state) {
|
|
62
|
-
const
|
|
62
|
+
const name = path.node.name
|
|
63
63
|
if (!GLOBAL_SPECIALS.has(name)) {
|
|
64
64
|
return
|
|
65
65
|
}
|
|
@@ -93,7 +93,7 @@ function buildGlobalReplacement(name: string, str: t.StringLiteral): t.Expressio
|
|
|
93
93
|
// Babel calls update and iterator targets references, but replacements there
|
|
94
94
|
// would not be assignable. Delete also distinguishes references from values.
|
|
95
95
|
function isWriteTarget(path: NodePath<t.Identifier>): boolean {
|
|
96
|
-
const
|
|
96
|
+
const parentPath = path.parentPath
|
|
97
97
|
if (!parentPath) {
|
|
98
98
|
return false
|
|
99
99
|
}
|
|
@@ -2,11 +2,11 @@ import type { NodePath, Scope, Visitor } from '@babel/traverse'
|
|
|
2
2
|
import * as t from '@babel/types'
|
|
3
3
|
import type { File } from '@babel/types'
|
|
4
4
|
|
|
5
|
-
import { isPure } from '
|
|
6
|
-
import { traverseForChanges } from '
|
|
7
|
-
import type { ChangeState } from '
|
|
8
|
-
import { isPrimitiveLiteral } from '
|
|
9
|
-
import { generateLoopLabel } from '
|
|
5
|
+
import { isPure } from '../analysis/purity'
|
|
6
|
+
import { traverseForChanges } from '../utils/change-tracking'
|
|
7
|
+
import type { ChangeState } from '../utils/change-tracking'
|
|
8
|
+
import { isPrimitiveLiteral } from '../utils/literal'
|
|
9
|
+
import { generateLoopLabel } from '../utils/loop-lowering'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Lowers `switch` to an `if` chain or, when cases fall through, a labeled block
|
|
@@ -46,7 +46,7 @@ const visitor: Visitor<ChangeState> = {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function transformSwitch(path: NodePath<t.SwitchStatement>): boolean {
|
|
49
|
-
const
|
|
49
|
+
const node = path.node
|
|
50
50
|
// Separate `if` blocks cannot preserve cross-case lexical scope or Annex B
|
|
51
51
|
// function hoisting.
|
|
52
52
|
if (hasBlockScopedDeclaration(node)) {
|
|
@@ -114,7 +114,7 @@ function canLowerToIfElseChain(node: t.SwitchStatement): boolean {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
function caseEligibleForIfElseChain(caseClause: t.SwitchCase, isLast: boolean): boolean {
|
|
117
|
-
const
|
|
117
|
+
const consequent = caseClause.consequent
|
|
118
118
|
if (consequent.length === 0) {
|
|
119
119
|
return isLast
|
|
120
120
|
}
|
|
@@ -189,8 +189,8 @@ function buildBody(consequent: t.Statement[]): t.BlockStatement {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
function lowerToIfElseChain(path: NodePath<t.SwitchStatement>): void {
|
|
192
|
-
const
|
|
193
|
-
const
|
|
192
|
+
const node = path.node
|
|
193
|
+
const scope = path.scope
|
|
194
194
|
|
|
195
195
|
let discriminantExpr: t.Expression
|
|
196
196
|
const preStmts: t.Statement[] = []
|
|
@@ -244,8 +244,8 @@ function lowerToIfElseChain(path: NodePath<t.SwitchStatement>): void {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
function lowerToLabeledBlock(path: NodePath<t.SwitchStatement>): void {
|
|
247
|
-
const
|
|
248
|
-
const
|
|
247
|
+
const node = path.node
|
|
248
|
+
const scope = path.scope
|
|
249
249
|
|
|
250
250
|
// `scope.generateUid` alone can collide with enclosing or nested labels.
|
|
251
251
|
const labelName = generateLoopLabel(path, 'switch')
|
|
@@ -314,7 +314,7 @@ function lowerToLabeledBlock(path: NodePath<t.SwitchStatement>): void {
|
|
|
314
314
|
|
|
315
315
|
const labeledBlock = t.labeledStatement(t.identifier(labelName), t.blockStatement(statements))
|
|
316
316
|
|
|
317
|
-
const
|
|
317
|
+
const labeledPath = path.replaceWith(labeledBlock)[0]
|
|
318
318
|
rewriteUnlabeledBreaks(labeledPath, labelName)
|
|
319
319
|
}
|
|
320
320
|
|
|
@@ -2,11 +2,11 @@ import type { NodePath, Visitor } from '@babel/traverse'
|
|
|
2
2
|
import * as t from '@babel/types'
|
|
3
3
|
import type { File } from '@babel/types'
|
|
4
4
|
|
|
5
|
-
import { isInsideWith } from '
|
|
6
|
-
import { traverseForChanges } from '
|
|
7
|
-
import type { ChangeState } from '
|
|
8
|
-
import { isConditionalGate, reorderableToStatement } from '
|
|
9
|
-
import { isStandaloneDeclaration } from '
|
|
5
|
+
import { isInsideWith } from '../utils/ast'
|
|
6
|
+
import { traverseForChanges } from '../utils/change-tracking'
|
|
7
|
+
import type { ChangeState } from '../utils/change-tracking'
|
|
8
|
+
import { isConditionalGate, reorderableToStatement } from '../utils/evaluation-order'
|
|
9
|
+
import { isStandaloneDeclaration } from '../utils/paths'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Lowers conditional expressions to `if` statements. Expression values use a
|
|
@@ -67,7 +67,9 @@ function lowerConditional(path: NodePath<t.ConditionalExpression>): boolean {
|
|
|
67
67
|
return false
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
const
|
|
70
|
+
const test = path.node.test
|
|
71
|
+
const consequent = path.node.consequent
|
|
72
|
+
const alternate = path.node.alternate
|
|
71
73
|
|
|
72
74
|
if (parent.isExpressionStatement() && parent.node.expression === path.node) {
|
|
73
75
|
parent.replaceWith(
|
|
@@ -163,7 +165,7 @@ function lowerConditional(path: NodePath<t.ConditionalExpression>): boolean {
|
|
|
163
165
|
return false
|
|
164
166
|
}
|
|
165
167
|
|
|
166
|
-
const
|
|
168
|
+
const scope = path.scope
|
|
167
169
|
const tmpId = scope.generateUidIdentifier('ternaryResult')
|
|
168
170
|
scope.push({ id: t.cloneNode(tmpId) })
|
|
169
171
|
|
|
@@ -293,8 +295,9 @@ function lowerEnclosingLogical(path: NodePath, stmtPath: NodePath): boolean {
|
|
|
293
295
|
if (!logical || logical.removed || !logical.container) {
|
|
294
296
|
return false
|
|
295
297
|
}
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
+
const node = logical.node
|
|
299
|
+
const left = node.left
|
|
300
|
+
const right = node.right
|
|
298
301
|
let test: t.Expression
|
|
299
302
|
let valueRef: t.Expression
|
|
300
303
|
if (t.isIdentifier(left) && logical.scope.hasBinding(left.name)) {
|
|
@@ -2,9 +2,9 @@ import type { Binding, NodePath, Visitor } from '@babel/traverse'
|
|
|
2
2
|
import * as t from '@babel/types'
|
|
3
3
|
import type { File } from '@babel/types'
|
|
4
4
|
|
|
5
|
-
import { isDirectEvalCall, isInsideWith } from '
|
|
6
|
-
import { traverseForChanges } from '
|
|
7
|
-
import type { ChangeState } from '
|
|
5
|
+
import { isDirectEvalCall, isInsideWith } from '../utils/ast'
|
|
6
|
+
import { traverseForChanges } from '../utils/change-tracking'
|
|
7
|
+
import type { ChangeState } from '../utils/change-tracking'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Rewrites numeric updates as compound assignments for later expansion. Prefix
|
|
@@ -35,7 +35,9 @@ export function updateToAssignment(ast: File): boolean {
|
|
|
35
35
|
|
|
36
36
|
const visitor: Visitor<ChangeState> = {
|
|
37
37
|
UpdateExpression(path, state) {
|
|
38
|
-
const
|
|
38
|
+
const argument = path.node.argument
|
|
39
|
+
const prefix = path.node.prefix
|
|
40
|
+
const operator = path.node.operator
|
|
39
41
|
if (!t.isIdentifier(argument)) {
|
|
40
42
|
return
|
|
41
43
|
}
|
|
@@ -61,7 +63,7 @@ const visitor: Visitor<ChangeState> = {
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
function valueIsDiscarded(path: NodePath<t.UpdateExpression>): boolean {
|
|
64
|
-
const
|
|
66
|
+
const parentPath = path.parentPath
|
|
65
67
|
if (!parentPath) {
|
|
66
68
|
return false
|
|
67
69
|
}
|