js-confuser 2.0.0-alpha.5 → 2.0.1

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.
Files changed (113) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +43 -43
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -20
  3. package/.github/workflows/node.js.yml +28 -28
  4. package/.prettierrc +4 -4
  5. package/CHANGELOG.md +1015 -989
  6. package/CODE_OF_CONDUCT.md +131 -131
  7. package/CONTRIBUTING.md +52 -52
  8. package/LICENSE +21 -21
  9. package/Migration.md +72 -71
  10. package/README.md +86 -78
  11. package/dist/constants.js +43 -43
  12. package/dist/index.js +14 -23
  13. package/dist/obfuscator.js +31 -25
  14. package/dist/order.js +4 -4
  15. package/dist/presets.js +31 -31
  16. package/dist/templates/integrityTemplate.js +4 -4
  17. package/dist/templates/template.js +1 -2
  18. package/dist/transforms/astScrambler.js +1 -2
  19. package/dist/transforms/calculator.js +1 -2
  20. package/dist/transforms/controlFlowFlattening.js +93 -63
  21. package/dist/transforms/deadCode.js +1 -2
  22. package/dist/transforms/dispatcher.js +4 -5
  23. package/dist/transforms/extraction/duplicateLiteralsRemoval.js +1 -2
  24. package/dist/transforms/extraction/objectExtraction.js +1 -2
  25. package/dist/transforms/finalizer.js +1 -2
  26. package/dist/transforms/flatten.js +1 -2
  27. package/dist/transforms/identifier/globalConcealing.js +15 -2
  28. package/dist/transforms/identifier/movedDeclarations.js +8 -7
  29. package/dist/transforms/identifier/renameVariables.js +7 -7
  30. package/dist/transforms/lock/integrity.js +11 -10
  31. package/dist/transforms/lock/lock.js +2 -3
  32. package/dist/transforms/minify.js +11 -29
  33. package/dist/transforms/opaquePredicates.js +1 -2
  34. package/dist/transforms/pack.js +5 -2
  35. package/dist/transforms/plugin.js +18 -19
  36. package/dist/transforms/preparation.js +16 -16
  37. package/dist/transforms/renameLabels.js +1 -2
  38. package/dist/transforms/rgf.js +8 -9
  39. package/dist/transforms/shuffle.js +1 -2
  40. package/dist/transforms/string/encoding.js +1 -2
  41. package/dist/transforms/string/stringCompression.js +3 -4
  42. package/dist/transforms/string/stringConcealing.js +8 -3
  43. package/dist/transforms/string/stringEncoding.js +1 -2
  44. package/dist/transforms/variableMasking.js +1 -2
  45. package/dist/utils/NameGen.js +2 -2
  46. package/dist/utils/PredicateGen.js +1 -2
  47. package/dist/utils/ast-utils.js +87 -88
  48. package/dist/utils/function-utils.js +8 -8
  49. package/dist/utils/node.js +5 -6
  50. package/dist/utils/object-utils.js +4 -4
  51. package/dist/utils/random-utils.js +20 -20
  52. package/dist/utils/static-utils.js +1 -2
  53. package/dist/validateOptions.js +4 -7
  54. package/index.d.ts +17 -17
  55. package/package.json +61 -59
  56. package/src/constants.ts +168 -168
  57. package/src/index.ts +118 -118
  58. package/src/obfuscationResult.ts +49 -49
  59. package/src/obfuscator.ts +501 -497
  60. package/src/options.ts +407 -407
  61. package/src/order.ts +54 -54
  62. package/src/presets.ts +125 -125
  63. package/src/templates/bufferToStringTemplate.ts +57 -57
  64. package/src/templates/deadCodeTemplates.ts +1185 -1185
  65. package/src/templates/getGlobalTemplate.ts +76 -76
  66. package/src/templates/integrityTemplate.ts +64 -64
  67. package/src/templates/setFunctionLengthTemplate.ts +11 -11
  68. package/src/templates/stringCompressionTemplate.ts +20 -20
  69. package/src/templates/tamperProtectionTemplates.ts +120 -120
  70. package/src/templates/template.ts +224 -224
  71. package/src/transforms/astScrambler.ts +99 -99
  72. package/src/transforms/calculator.ts +99 -99
  73. package/src/transforms/controlFlowFlattening.ts +1716 -1664
  74. package/src/transforms/deadCode.ts +82 -82
  75. package/src/transforms/dispatcher.ts +450 -450
  76. package/src/transforms/extraction/duplicateLiteralsRemoval.ts +156 -158
  77. package/src/transforms/extraction/objectExtraction.ts +186 -186
  78. package/src/transforms/finalizer.ts +74 -74
  79. package/src/transforms/flatten.ts +421 -420
  80. package/src/transforms/identifier/globalConcealing.ts +315 -295
  81. package/src/transforms/identifier/movedDeclarations.ts +252 -251
  82. package/src/transforms/identifier/renameVariables.ts +328 -321
  83. package/src/transforms/lock/integrity.ts +117 -114
  84. package/src/transforms/lock/lock.ts +418 -425
  85. package/src/transforms/minify.ts +615 -629
  86. package/src/transforms/opaquePredicates.ts +100 -100
  87. package/src/transforms/pack.ts +239 -231
  88. package/src/transforms/plugin.ts +173 -173
  89. package/src/transforms/preparation.ts +349 -347
  90. package/src/transforms/renameLabels.ts +175 -175
  91. package/src/transforms/rgf.ts +322 -322
  92. package/src/transforms/shuffle.ts +82 -82
  93. package/src/transforms/string/encoding.ts +144 -144
  94. package/src/transforms/string/stringCompression.ts +128 -128
  95. package/src/transforms/string/stringConcealing.ts +312 -298
  96. package/src/transforms/string/stringEncoding.ts +80 -80
  97. package/src/transforms/string/stringSplitting.ts +77 -77
  98. package/src/transforms/variableMasking.ts +257 -257
  99. package/src/utils/IntGen.ts +33 -33
  100. package/src/utils/NameGen.ts +116 -116
  101. package/src/utils/PredicateGen.ts +61 -61
  102. package/src/utils/ast-utils.ts +663 -663
  103. package/src/utils/function-utils.ts +50 -50
  104. package/src/utils/gen-utils.ts +48 -48
  105. package/src/utils/node.ts +78 -78
  106. package/src/utils/object-utils.ts +21 -21
  107. package/src/utils/random-utils.ts +93 -93
  108. package/src/utils/static-utils.ts +66 -66
  109. package/src/validateOptions.ts +256 -259
  110. package/tsconfig.json +13 -14
  111. package/dist/probability.js +0 -1
  112. package/dist/transforms/functionOutlining.js +0 -230
  113. package/dist/utils/ControlObject.js +0 -125
@@ -1,629 +1,615 @@
1
- import { NodePath } from "@babel/traverse";
2
- import { PluginArg, PluginObject } from "./plugin";
3
- import * as t from "@babel/types";
4
- import { Order } from "../order";
5
- import {
6
- ensureComputedExpression,
7
- getParentFunctionOrProgram,
8
- isUndefined,
9
- } from "../utils/ast-utils";
10
- import { Binding, Scope } from "@babel/traverse";
11
- import {
12
- NO_REMOVE,
13
- NodeSymbol,
14
- placeholderVariablePrefix,
15
- UNSAFE,
16
- } from "../constants";
17
-
18
- const identifierMap = new Map<string, () => t.Expression>();
19
- identifierMap.set("undefined", () =>
20
- t.unaryExpression("void", t.numericLiteral(0))
21
- );
22
- identifierMap.set("Infinity", () =>
23
- t.binaryExpression("/", t.numericLiteral(1), t.numericLiteral(0))
24
- );
25
-
26
- function trySimpleDestructuring(id, init) {
27
- // Simple array/object destructuring
28
- if (id.isArrayPattern() && init.isArrayExpression()) {
29
- const elements = id.get("elements");
30
- const initElements = init.get("elements");
31
-
32
- if (elements.length === 1 && initElements.length === 1) {
33
- id.replaceWith(elements[0]);
34
- init.replaceWith(initElements[0]);
35
- }
36
- }
37
-
38
- if (id.isObjectPattern() && init.isObjectExpression()) {
39
- const properties = id.get("properties");
40
- const initProperties = init.get("properties");
41
-
42
- if (properties.length === 1 && initProperties.length === 1) {
43
- const firstProperty = properties[0];
44
- const firstInitProperty = initProperties[0];
45
-
46
- if (
47
- firstProperty.isObjectProperty() &&
48
- firstInitProperty.isObjectProperty()
49
- ) {
50
- const firstKey = firstProperty.get("key");
51
- const firstInitKey = firstInitProperty.get("key");
52
- if (
53
- firstKey.isIdentifier() &&
54
- firstInitKey.isIdentifier() &&
55
- firstKey.node.name === firstInitKey.node.name
56
- ) {
57
- id.replaceWith(firstProperty.node.value);
58
- init.replaceWith(firstInitProperty.node.value);
59
- }
60
- }
61
- }
62
- }
63
- }
64
-
65
- /**
66
- * Minify removes unnecessary code and shortens the length for file size.
67
- *
68
- * - Dead code elimination
69
- * - Variable grouping
70
- * - Constant folding
71
- * - Shorten literals: True to !0, False to !1, Infinity to 1/0, Undefined to void 0
72
- * - Remove unused variables, functions
73
- */
74
- export default ({ Plugin }: PluginArg): PluginObject => {
75
- const me = Plugin(Order.Minify);
76
- return {
77
- visitor: {
78
- Program(path) {
79
- path.scope.crawl();
80
- },
81
- // var a; var b; -> var a,b;
82
- VariableDeclaration: {
83
- exit(path) {
84
- if (typeof path.key !== "number") return;
85
- const kind = path.node.kind;
86
-
87
- // get declaration after this
88
- const nextDeclaration = path.getSibling(path.key + 1);
89
- if (
90
- nextDeclaration.isVariableDeclaration({
91
- kind: kind,
92
- })
93
- ) {
94
- const declarations = path.get("declarations");
95
-
96
- // Preserve bindings!
97
- // This is important for dead code elimination
98
- const bindings: { [name: string]: Binding } = Object.create(null);
99
- for (var declaration of declarations) {
100
- for (var idPath of Object.values(
101
- declaration.getBindingIdentifierPaths()
102
- )) {
103
- bindings[idPath.node.name] = idPath.scope.getBinding(
104
- idPath.node.name
105
- );
106
- }
107
- }
108
-
109
- nextDeclaration.node.declarations.unshift(
110
- ...declarations.map((x) => x.node)
111
- );
112
-
113
- const newBindingIdentifierPaths =
114
- nextDeclaration.getBindingIdentifierPaths();
115
-
116
- // path.remove() unfortunately removes the bindings
117
- // We must perverse the entire binding object (referencePaths, constantViolations, etc)
118
- // and re-add them to the new scope
119
- path.remove();
120
-
121
- // Add bindings back
122
- function addBindingsToScope(scope: Scope) {
123
- for (var name in bindings) {
124
- const binding = bindings[name];
125
- if (binding) {
126
- binding.path = newBindingIdentifierPaths[name];
127
- scope.bindings[name] = binding;
128
- }
129
- }
130
- }
131
-
132
- if (kind === "var") {
133
- addBindingsToScope(getParentFunctionOrProgram(path).scope);
134
- }
135
- addBindingsToScope(path.scope);
136
- }
137
- },
138
- },
139
- // true -> !0, false -> !1
140
- BooleanLiteral: {
141
- exit(path) {
142
- if (path.node.value) {
143
- path.replaceWith(t.unaryExpression("!", t.numericLiteral(0)));
144
- } else {
145
- path.replaceWith(t.unaryExpression("!", t.numericLiteral(1)));
146
- }
147
- },
148
- },
149
- // !"" -> !1
150
- UnaryExpression: {
151
- exit(path) {
152
- if (path.node.operator === "!") {
153
- var argument = path.get("argument");
154
- if (argument.isNumericLiteral()) return;
155
- const value = argument.evaluateTruthy();
156
- const parent = getParentFunctionOrProgram(path);
157
- if (parent && (parent.node as NodeSymbol)[UNSAFE]) return;
158
-
159
- if (value === undefined) return;
160
-
161
- path.replaceWith(
162
- t.unaryExpression("!", t.numericLiteral(value ? 1 : 0))
163
- );
164
- }
165
- },
166
- },
167
- // "a" + "b" -> "ab"
168
- BinaryExpression: {
169
- exit(path) {
170
- if (path.node.operator !== "+") return;
171
-
172
- const left = path.get("left");
173
- const right = path.get("right");
174
-
175
- if (!left.isStringLiteral() || !right.isStringLiteral()) return;
176
-
177
- path.replaceWith(t.stringLiteral(left.node.value + right.node.value));
178
- },
179
- },
180
- // a["key"] -> a.key
181
- MemberExpression: {
182
- exit(path) {
183
- if (!path.node.computed) return;
184
-
185
- const property = path.get("property");
186
- if (!property.isStringLiteral()) return;
187
-
188
- const key = property.node.value;
189
- if (!t.isValidIdentifier(key)) return;
190
-
191
- path.node.computed = false;
192
- path.node.property = t.identifier(key);
193
- },
194
- },
195
- // {["key"]: 1} -> {key: 1}
196
- // {"key": 1} -> {key: 1}
197
- ObjectProperty: {
198
- exit(path) {
199
- var key = path.get("key");
200
- if (path.node.computed && key.isStringLiteral()) {
201
- path.node.computed = false;
202
- }
203
-
204
- if (
205
- !path.node.computed &&
206
- key.isStringLiteral() &&
207
- t.isValidIdentifier(key.node.value)
208
- ) {
209
- if (identifierMap.has(key.node.value)) {
210
- path.node.computed = true;
211
- key.replaceWith(identifierMap.get(key.node.value)!());
212
- } else {
213
- key.replaceWith(t.identifier(key.node.value));
214
- }
215
- }
216
- },
217
- },
218
- // (a); -> a;
219
- SequenceExpression: {
220
- exit(path) {
221
- if (path.node.expressions.length === 1) {
222
- path.replaceWith(path.node.expressions[0]);
223
- }
224
- },
225
- },
226
- // ; -> ()
227
- EmptyStatement: {
228
- exit(path) {
229
- path.remove();
230
- },
231
- },
232
- // console; -> ();
233
- ExpressionStatement: {
234
- exit(path) {
235
- if (path.get("expression").isIdentifier()) {
236
- // Preserve last expression of program for RGF
237
- if (
238
- path.parentPath?.isProgram() &&
239
- path.parentPath?.get("body").at(-1) === path
240
- )
241
- return;
242
- path.remove();
243
- }
244
- },
245
- },
246
- // undefined -> void 0
247
- // Infinity -> 1/0
248
- Identifier: {
249
- exit(path) {
250
- if (path.isReferencedIdentifier()) {
251
- if (identifierMap.has(path.node.name)) {
252
- ensureComputedExpression(path);
253
- path.replaceWith(identifierMap.get(path.node.name)!());
254
- }
255
- }
256
- },
257
- },
258
- // true ? a : b -> a
259
- ConditionalExpression: {
260
- exit(path) {
261
- const testValue = path.get("test").evaluateTruthy();
262
- if (testValue === undefined) return;
263
-
264
- path.replaceWith(
265
- testValue ? path.node.consequent : path.node.alternate
266
- );
267
- },
268
- },
269
- // Remove unused functions
270
- FunctionDeclaration: {
271
- exit(path) {
272
- const id = path.get("id");
273
- if (
274
- id.isIdentifier() &&
275
- !id.node.name.startsWith(placeholderVariablePrefix) &&
276
- !(path.node as NodeSymbol)[NO_REMOVE]
277
- ) {
278
- const binding = path.scope.getBinding(id.node.name);
279
- if (
280
- binding &&
281
- binding.constantViolations.length === 0 &&
282
- binding.referencePaths.length === 0 &&
283
- !binding.referenced
284
- ) {
285
- path.remove();
286
- }
287
- }
288
- },
289
- },
290
- // var x=undefined -> var x
291
- // Remove unused variables
292
- // Simple destructuring
293
- VariableDeclarator: {
294
- exit(path) {
295
- if (isUndefined(path.get("init"))) {
296
- path.node.init = null;
297
- }
298
-
299
- const id = path.get("id");
300
- const init = path.get("init");
301
-
302
- trySimpleDestructuring(id, init);
303
-
304
- // Remove unused variables
305
- // Can only remove if it's pure
306
- if (id.isIdentifier()) {
307
- // Do not remove variables in unsafe functions
308
- const fn = getParentFunctionOrProgram(path);
309
- if ((fn.node as NodeSymbol)[UNSAFE]) return;
310
-
311
- // Node explicitly marked as not to be removed
312
- if ((id as NodeSymbol)[NO_REMOVE]) return;
313
-
314
- const binding = path.scope.getBinding(id.node.name);
315
-
316
- if (
317
- binding &&
318
- binding.constantViolations.length === 0 &&
319
- binding.referencePaths.length === 0
320
- ) {
321
- if (!init.node || init.isPure()) {
322
- path.remove();
323
- } else if (
324
- path.parentPath.isVariableDeclaration() &&
325
- path.parentPath.node.declarations.length === 1
326
- ) {
327
- path.parentPath.replaceWith(t.expressionStatement(init.node));
328
- }
329
- }
330
- }
331
- },
332
- },
333
- // Simple destructuring
334
- // Simple arithmetic operations
335
- AssignmentExpression: {
336
- exit(path) {
337
- if (path.node.operator === "=") {
338
- trySimpleDestructuring(path.get("left"), path.get("right"));
339
- }
340
- if (path.node.operator === "+=") {
341
- const left = path.get("left");
342
- const right = path.get("right");
343
-
344
- // a += 1 -> a++
345
- if (right.isNumericLiteral({ value: 1 })) {
346
- if (left.isIdentifier() || left.isMemberExpression()) {
347
- path.replaceWith(t.updateExpression("++", left.node));
348
- }
349
- }
350
- }
351
- },
352
- },
353
-
354
- // return undefined->return
355
- ReturnStatement: {
356
- exit(path) {
357
- if (isUndefined(path.get("argument"))) {
358
- path.node.argument = null;
359
- }
360
- },
361
- },
362
- // while(true) {a();} -> while(true) a();
363
- // for(;;) {a();} -> for(;;) a();
364
- // with(a) {a();} -> with(a) a();
365
- "While|For|WithStatement": {
366
- exit(_path) {
367
- var path = _path as NodePath<t.While | t.For | t.WithStatement>;
368
- var body = path.get("body");
369
-
370
- if (body.isBlock() && body.node.body.length === 1) {
371
- body.replaceWith(body.node.body[0]);
372
- }
373
- },
374
- },
375
- // if(a) a(); -> a && a();
376
- // if(a) { return b; } -> if(a) return b;
377
- // if(a) { a(); } else { b(); } -> a ? a() : b();
378
- // if(a) { return b; } else { return c; } -> return a ? b : c;
379
- IfStatement: {
380
- exit(path) {
381
- // BlockStatement to single statement
382
- const consequent = path.get("consequent");
383
- const alternate = path.get("alternate");
384
-
385
- const isMoveable = (node: t.Statement) => {
386
- if (t.isDeclaration(node)) return false;
387
-
388
- return true;
389
- };
390
-
391
- let testValue = path.get("test").evaluateTruthy();
392
-
393
- const parent = getParentFunctionOrProgram(path);
394
- if (parent && (parent.node as NodeSymbol)[UNSAFE]) {
395
- testValue = undefined;
396
- }
397
-
398
- if (typeof testValue !== "undefined") {
399
- if (
400
- !alternate.node &&
401
- consequent.isBlock() &&
402
- consequent.node.body.length === 1 &&
403
- isMoveable(consequent.node.body[0])
404
- ) {
405
- consequent.replaceWith(consequent.node.body[0]);
406
- }
407
-
408
- if (
409
- alternate.node &&
410
- alternate.isBlock() &&
411
- alternate.node.body.length === 1 &&
412
- isMoveable(alternate.node.body[0])
413
- ) {
414
- alternate.replaceWith(alternate.node.body[0]);
415
- }
416
- }
417
-
418
- if (testValue === false) {
419
- // if(false){} -> ()
420
- if (!alternate.node) {
421
- path.remove();
422
- return;
423
-
424
- // if(false){a()}else{b()} -> b()
425
- } else {
426
- path.replaceWith(alternate.node);
427
- return;
428
- }
429
-
430
- // if(true){a()} -> {a()}
431
- } else if (testValue === true) {
432
- path.replaceWith(consequent.node);
433
- return;
434
- }
435
-
436
- function getResult(path: NodePath): {
437
- returnPath: NodePath<t.ReturnStatement> | null;
438
- expressions: t.Expression[];
439
- } {
440
- if (!path.node) return null;
441
-
442
- if (path.isReturnStatement()) {
443
- return { returnPath: path, expressions: [] };
444
- }
445
- if (path.isExpressionStatement()) {
446
- return {
447
- returnPath: null,
448
- expressions: [path.get("expression").node],
449
- };
450
- }
451
-
452
- if (path.isBlockStatement()) {
453
- var expressions = [];
454
- for (var statement of path.get("body")) {
455
- if (statement.isReturnStatement()) {
456
- return { returnPath: statement, expressions: expressions };
457
- } else if (statement.isExpressionStatement()) {
458
- expressions.push(statement.get("expression").node);
459
- } else {
460
- return null;
461
- }
462
- }
463
-
464
- return { returnPath: null, expressions: expressions };
465
- }
466
-
467
- return null;
468
- }
469
-
470
- var consequentReturn = getResult(consequent);
471
- var alternateReturn = getResult(alternate);
472
-
473
- if (consequentReturn && alternateReturn) {
474
- if (consequentReturn.returnPath && alternateReturn.returnPath) {
475
- function createReturnArgument(
476
- resultInfo: ReturnType<typeof getResult>
477
- ) {
478
- return t.sequenceExpression([
479
- ...resultInfo.expressions,
480
- resultInfo.returnPath.node.argument ||
481
- t.identifier("undefined"),
482
- ]);
483
- }
484
-
485
- path.replaceWith(
486
- t.returnStatement(
487
- t.conditionalExpression(
488
- path.node.test,
489
- createReturnArgument(consequentReturn),
490
- createReturnArgument(alternateReturn)
491
- )
492
- )
493
- );
494
- } else if (
495
- !consequentReturn.returnPath &&
496
- !alternateReturn.returnPath
497
- ) {
498
- function joinExpressions(expressions: t.Expression[]) {
499
- // condition?():() is invalid syntax
500
- // Just use 0 as a placeholder
501
- if (expressions.length === 0) return t.numericLiteral(0);
502
-
503
- // No need for sequence expression if there's only one expression
504
- if (expressions.length === 1) return expressions[0];
505
-
506
- return t.sequenceExpression(expressions);
507
- }
508
-
509
- path.replaceWith(
510
- t.conditionalExpression(
511
- path.node.test,
512
- joinExpressions(consequentReturn.expressions),
513
- joinExpressions(alternateReturn.expressions)
514
- )
515
- );
516
- }
517
- }
518
- },
519
- },
520
- // Remove unreachable code
521
- // Code after a return/throw/break/continue is unreachable
522
- // Remove implied returns
523
- // Remove code after if all branches are unreachable
524
- "Block|SwitchCase": {
525
- enter(path) {
526
- if (path.isProgram()) {
527
- path.scope.crawl();
528
- }
529
- },
530
- exit(path) {
531
- var statementList = path.isBlock()
532
- ? (path.get("body") as NodePath<t.Statement>[])
533
- : (path.get("consequent") as NodePath<t.Statement>[]);
534
-
535
- var impliedReturn: NodePath<t.ReturnStatement>;
536
-
537
- function isUnreachable(
538
- statementList: NodePath<t.Statement>[],
539
- topLevel = false
540
- ) {
541
- var unreachableState = false;
542
-
543
- for (var statement of statementList) {
544
- if (unreachableState) {
545
- statement.remove();
546
- continue;
547
- }
548
-
549
- if (statement.isIfStatement()) {
550
- const consequent = statement.get("consequent");
551
- const alternate = statement.get("alternate");
552
-
553
- if (
554
- [consequent, alternate].every(
555
- (x) =>
556
- x.node &&
557
- x.isBlockStatement() &&
558
- isUnreachable(x.get("body"))
559
- )
560
- ) {
561
- unreachableState = true;
562
- if (!topLevel) {
563
- return true;
564
- } else {
565
- continue;
566
- }
567
- }
568
- }
569
-
570
- if (statement.isSwitchStatement()) {
571
- // Can only remove switch statements if all cases are unreachable
572
- // And all paths are exhausted
573
- const cases = statement.get("cases");
574
- const hasDefaultCase = cases.some((x) => !x.node.test);
575
- if (
576
- hasDefaultCase &&
577
- cases.every((x) => isUnreachable(x.get("consequent")))
578
- ) {
579
- unreachableState = true;
580
- if (!topLevel) {
581
- return true;
582
- } else {
583
- continue;
584
- }
585
- }
586
- }
587
-
588
- if (
589
- statement.isReturnStatement() ||
590
- statement.isThrowStatement() ||
591
- statement.isBreakStatement() ||
592
- statement.isContinueStatement()
593
- ) {
594
- unreachableState = true;
595
- if (!topLevel) {
596
- return true;
597
- }
598
- }
599
-
600
- if (topLevel) {
601
- if (
602
- statement == statementList.at(-1) &&
603
- statement.isReturnStatement() &&
604
- !statement.node.argument
605
- ) {
606
- impliedReturn = statement;
607
- }
608
- }
609
- }
610
- return false;
611
- }
612
-
613
- isUnreachable(statementList, true);
614
-
615
- if (impliedReturn) {
616
- var functionParent = path.getFunctionParent();
617
- if (
618
- functionParent &&
619
- t.isBlockStatement(functionParent.node.body) &&
620
- functionParent.node.body === path.node
621
- ) {
622
- impliedReturn.remove();
623
- }
624
- }
625
- },
626
- },
627
- },
628
- };
629
- };
1
+ import { NodePath } from "@babel/traverse";
2
+ import { PluginArg, PluginObject } from "./plugin";
3
+ import * as t from "@babel/types";
4
+ import { Order } from "../order";
5
+ import {
6
+ ensureComputedExpression,
7
+ getParentFunctionOrProgram,
8
+ isUndefined,
9
+ } from "../utils/ast-utils";
10
+ import { Binding, Scope } from "@babel/traverse";
11
+ import {
12
+ NO_REMOVE,
13
+ NodeSymbol,
14
+ placeholderVariablePrefix,
15
+ UNSAFE,
16
+ } from "../constants";
17
+
18
+ const identifierMap = new Map<string, () => t.Expression>();
19
+ identifierMap.set("undefined", () =>
20
+ t.unaryExpression("void", t.numericLiteral(0)),
21
+ );
22
+ identifierMap.set("Infinity", () =>
23
+ t.binaryExpression("/", t.numericLiteral(1), t.numericLiteral(0)),
24
+ );
25
+
26
+ function trySimpleDestructuring(id, init) {
27
+ // Simple array/object destructuring
28
+ if (id.isArrayPattern() && init.isArrayExpression()) {
29
+ const elements = id.get("elements");
30
+ const initElements = init.get("elements");
31
+
32
+ if (elements.length === 1 && initElements.length === 1) {
33
+ id.replaceWith(elements[0]);
34
+ init.replaceWith(initElements[0]);
35
+ }
36
+ }
37
+
38
+ if (id.isObjectPattern() && init.isObjectExpression()) {
39
+ const properties = id.get("properties");
40
+ const initProperties = init.get("properties");
41
+
42
+ if (properties.length === 1 && initProperties.length === 1) {
43
+ const firstProperty = properties[0];
44
+ const firstInitProperty = initProperties[0];
45
+
46
+ if (
47
+ firstProperty.isObjectProperty() &&
48
+ firstInitProperty.isObjectProperty()
49
+ ) {
50
+ const firstKey = firstProperty.get("key");
51
+ const firstInitKey = firstInitProperty.get("key");
52
+ if (
53
+ firstKey.isIdentifier() &&
54
+ firstInitKey.isIdentifier() &&
55
+ firstKey.node.name === firstInitKey.node.name
56
+ ) {
57
+ id.replaceWith(firstProperty.node.value);
58
+ init.replaceWith(firstInitProperty.node.value);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Minify removes unnecessary code and shortens the length for file size.
67
+ *
68
+ * - Dead code elimination
69
+ * - Variable grouping
70
+ * - Constant folding
71
+ * - Shorten literals: True to !0, False to !1, Infinity to 1/0, Undefined to void 0
72
+ * - Remove unused variables, functions
73
+ */
74
+ export default ({ Plugin }: PluginArg): PluginObject => {
75
+ const me = Plugin(Order.Minify);
76
+ return {
77
+ visitor: {
78
+ Program(path) {
79
+ path.scope.crawl();
80
+ },
81
+ // var a; var b; -> var a,b;
82
+ VariableDeclaration: {
83
+ exit(path) {
84
+ if (typeof path.key !== "number") return;
85
+ const kind = path.node.kind;
86
+
87
+ // get declaration after this
88
+ const nextDeclaration = path.getSibling(path.key + 1);
89
+ if (
90
+ nextDeclaration.isVariableDeclaration({
91
+ kind: kind,
92
+ })
93
+ ) {
94
+ const declarations = path.get("declarations");
95
+
96
+ // Preserve bindings!
97
+ // This is important for dead code elimination
98
+ const bindings: { [name: string]: Binding } = Object.create(null);
99
+ for (var declaration of declarations) {
100
+ for (var idPath of Object.values(
101
+ declaration.getBindingIdentifierPaths(),
102
+ )) {
103
+ bindings[idPath.node.name] = idPath.scope.getBinding(
104
+ idPath.node.name,
105
+ );
106
+ }
107
+ }
108
+
109
+ nextDeclaration.node.declarations.unshift(
110
+ ...declarations.map((x) => x.node),
111
+ );
112
+
113
+ const newBindingIdentifierPaths =
114
+ nextDeclaration.getBindingIdentifierPaths();
115
+
116
+ // path.remove() unfortunately removes the bindings
117
+ // We must perverse the entire binding object (referencePaths, constantViolations, etc)
118
+ // and re-add them to the new scope
119
+ path.remove();
120
+
121
+ // Add bindings back
122
+ function addBindingsToScope(scope: Scope) {
123
+ for (var name in bindings) {
124
+ const binding = bindings[name];
125
+ if (binding) {
126
+ binding.path = newBindingIdentifierPaths[name];
127
+ scope.bindings[name] = binding;
128
+ }
129
+ }
130
+ }
131
+
132
+ if (kind === "var") {
133
+ addBindingsToScope(getParentFunctionOrProgram(path).scope);
134
+ }
135
+ addBindingsToScope(path.scope);
136
+ }
137
+ },
138
+ },
139
+ // true -> !0, false -> !1
140
+ BooleanLiteral: {
141
+ exit(path) {
142
+ if (path.node.value) {
143
+ path.replaceWith(t.unaryExpression("!", t.numericLiteral(0)));
144
+ } else {
145
+ path.replaceWith(t.unaryExpression("!", t.numericLiteral(1)));
146
+ }
147
+ },
148
+ },
149
+ // !"" -> !1
150
+ UnaryExpression: {
151
+ exit(path) {
152
+ if (path.node.operator === "!") {
153
+ var argument = path.get("argument");
154
+ if (argument.isNumericLiteral()) return;
155
+ const value = argument.evaluateTruthy();
156
+ const parent = getParentFunctionOrProgram(path);
157
+ if (parent && (parent.node as NodeSymbol)[UNSAFE]) return;
158
+
159
+ if (value === undefined) return;
160
+
161
+ path.replaceWith(
162
+ t.unaryExpression("!", t.numericLiteral(value ? 1 : 0)),
163
+ );
164
+ }
165
+ },
166
+ },
167
+ // "a" + "b" -> "ab"
168
+ BinaryExpression: {
169
+ exit(path) {
170
+ if (path.node.operator !== "+") return;
171
+
172
+ const left = path.get("left");
173
+ const right = path.get("right");
174
+
175
+ if (!left.isStringLiteral() || !right.isStringLiteral()) return;
176
+
177
+ path.replaceWith(t.stringLiteral(left.node.value + right.node.value));
178
+ },
179
+ },
180
+ // a["key"] -> a.key
181
+ MemberExpression: {
182
+ exit(path) {
183
+ if (!path.node.computed) return;
184
+
185
+ const property = path.get("property");
186
+ if (!property.isStringLiteral()) return;
187
+
188
+ const key = property.node.value;
189
+ if (!t.isValidIdentifier(key)) return;
190
+
191
+ path.node.computed = false;
192
+ path.node.property = t.identifier(key);
193
+ },
194
+ },
195
+ // {["key"]: 1} -> {key: 1}
196
+ // {"key": 1} -> {key: 1}
197
+ ObjectProperty: {
198
+ exit(path) {
199
+ var key = path.get("key");
200
+ if (path.node.computed && key.isStringLiteral()) {
201
+ path.node.computed = false;
202
+ }
203
+
204
+ if (
205
+ !path.node.computed &&
206
+ key.isStringLiteral() &&
207
+ t.isValidIdentifier(key.node.value)
208
+ ) {
209
+ if (identifierMap.has(key.node.value)) {
210
+ path.node.computed = true;
211
+ key.replaceWith(identifierMap.get(key.node.value)!());
212
+ } else {
213
+ key.replaceWith(t.identifier(key.node.value));
214
+ }
215
+ }
216
+ },
217
+ },
218
+ // (a); -> a;
219
+ SequenceExpression: {
220
+ exit(path) {
221
+ if (path.node.expressions.length === 1) {
222
+ path.replaceWith(path.node.expressions[0]);
223
+ }
224
+ },
225
+ },
226
+ // ; -> ()
227
+ EmptyStatement: {
228
+ exit(path) {
229
+ path.remove();
230
+ },
231
+ },
232
+ // console; -> ();
233
+ ExpressionStatement: {
234
+ exit(path) {
235
+ if (path.get("expression").isIdentifier()) {
236
+ // Preserve last expression of program for RGF
237
+ if (
238
+ path.parentPath?.isProgram() &&
239
+ path.parentPath?.get("body").at(-1) === path
240
+ )
241
+ return;
242
+ path.remove();
243
+ }
244
+ },
245
+ },
246
+ // undefined -> void 0
247
+ // Infinity -> 1/0
248
+ Identifier: {
249
+ exit(path) {
250
+ if (path.isReferencedIdentifier()) {
251
+ if (identifierMap.has(path.node.name)) {
252
+ ensureComputedExpression(path);
253
+ path.replaceWith(identifierMap.get(path.node.name)!());
254
+ }
255
+ }
256
+ },
257
+ },
258
+ // true ? a : b -> a
259
+ ConditionalExpression: {
260
+ exit(path) {
261
+ const testValue = path.get("test").evaluateTruthy();
262
+ if (testValue === undefined) return;
263
+
264
+ path.replaceWith(
265
+ testValue ? path.node.consequent : path.node.alternate,
266
+ );
267
+ },
268
+ },
269
+ // Remove unused functions
270
+ FunctionDeclaration: {
271
+ exit(path) {
272
+ const id = path.get("id");
273
+ if (
274
+ id.isIdentifier() &&
275
+ !id.node.name.startsWith(placeholderVariablePrefix) &&
276
+ !(path.node as NodeSymbol)[NO_REMOVE]
277
+ ) {
278
+ const binding = path.scope.getBinding(id.node.name);
279
+ if (
280
+ binding &&
281
+ binding.constantViolations.length === 0 &&
282
+ binding.referencePaths.length === 0 &&
283
+ !binding.referenced
284
+ ) {
285
+ path.remove();
286
+ }
287
+ }
288
+ },
289
+ },
290
+ // var x=undefined -> var x
291
+ // Remove unused variables
292
+ // Simple destructuring
293
+ VariableDeclarator: {
294
+ exit(path) {
295
+ if (
296
+ path.parentPath?.isVariableDeclaration() &&
297
+ path.parentPath.node.kind !== "const" &&
298
+ isUndefined(path.get("init"))
299
+ ) {
300
+ path.node.init = null;
301
+ }
302
+
303
+ const id = path.get("id");
304
+ const init = path.get("init");
305
+
306
+ trySimpleDestructuring(id, init);
307
+
308
+ // Remove unused variables
309
+ // Can only remove if it's pure
310
+ if (id.isIdentifier()) {
311
+ // Do not remove variables in unsafe functions
312
+ const fn = getParentFunctionOrProgram(path);
313
+ if ((fn.node as NodeSymbol)[UNSAFE]) return;
314
+
315
+ // Node explicitly marked as not to be removed
316
+ if ((id as NodeSymbol)[NO_REMOVE]) return;
317
+
318
+ const binding = path.scope.getBinding(id.node.name);
319
+
320
+ if (
321
+ binding &&
322
+ binding.constantViolations.length === 0 &&
323
+ binding.referencePaths.length === 0
324
+ ) {
325
+ if (!init.node || init.isPure()) {
326
+ path.remove();
327
+ } else if (
328
+ path.parentPath.isVariableDeclaration() &&
329
+ path.parentPath.node.declarations.length === 1
330
+ ) {
331
+ path.parentPath.replaceWith(t.expressionStatement(init.node));
332
+ }
333
+ }
334
+ }
335
+ },
336
+ },
337
+ // Simple destructuring
338
+ // Simple arithmetic operations
339
+ AssignmentExpression: {
340
+ exit(path) {
341
+ if (path.node.operator === "=") {
342
+ trySimpleDestructuring(path.get("left"), path.get("right"));
343
+ }
344
+ if (path.node.operator === "+=") {
345
+ const left = path.get("left");
346
+ const right = path.get("right");
347
+
348
+ // a += 1 -> a++
349
+ if (right.isNumericLiteral({ value: 1 })) {
350
+ if (left.isIdentifier() || left.isMemberExpression()) {
351
+ path.replaceWith(t.updateExpression("++", left.node));
352
+ }
353
+ }
354
+ }
355
+ },
356
+ },
357
+
358
+ // return undefined->return
359
+ ReturnStatement: {
360
+ exit(path) {
361
+ if (isUndefined(path.get("argument"))) {
362
+ path.node.argument = null;
363
+ }
364
+ },
365
+ },
366
+ // while(true) {a();} -> while(true) a();
367
+ // for(;;) {a();} -> for(;;) a();
368
+ // with(a) {a();} -> with(a) a();
369
+ "While|For|WithStatement": {
370
+ exit(_path) {
371
+ var path = _path as NodePath<t.While | t.For | t.WithStatement>;
372
+ var body = path.get("body");
373
+
374
+ if (body.isBlock() && body.node.body.length === 1) {
375
+ body.replaceWith(body.node.body[0]);
376
+ }
377
+ },
378
+ },
379
+ // if(a) a(); -> a && a();
380
+ // if(a) { return b; } -> if(a) return b;
381
+ // if(a) { a(); } else { b(); } -> a ? a() : b();
382
+ // if(a) { return b; } else { return c; } -> return a ? b : c;
383
+ IfStatement: {
384
+ exit(path) {
385
+ // BlockStatement to single statement
386
+ const consequent = path.get("consequent");
387
+ const alternate = path.get("alternate");
388
+
389
+ const isMoveable = (node: t.Statement) => {
390
+ if (t.isDeclaration(node)) return false;
391
+
392
+ return true;
393
+ };
394
+
395
+ let testValue = path.get("test").evaluateTruthy();
396
+
397
+ const parent = getParentFunctionOrProgram(path);
398
+ if (parent && (parent.node as NodeSymbol)[UNSAFE]) {
399
+ testValue = undefined;
400
+ }
401
+
402
+ if (typeof testValue !== "undefined") {
403
+ if (
404
+ !alternate.node &&
405
+ consequent.isBlock() &&
406
+ consequent.node.body.length === 1 &&
407
+ isMoveable(consequent.node.body[0])
408
+ ) {
409
+ consequent.replaceWith(consequent.node.body[0]);
410
+ }
411
+
412
+ if (
413
+ alternate.node &&
414
+ alternate.isBlock() &&
415
+ alternate.node.body.length === 1 &&
416
+ isMoveable(alternate.node.body[0])
417
+ ) {
418
+ alternate.replaceWith(alternate.node.body[0]);
419
+ }
420
+ }
421
+
422
+ if (testValue === false) {
423
+ // if(false){} -> ()
424
+ if (!alternate.node) {
425
+ path.remove();
426
+ return;
427
+
428
+ // if(false){a()}else{b()} -> b()
429
+ } else {
430
+ path.replaceWith(alternate.node);
431
+ return;
432
+ }
433
+
434
+ // if(true){a()} -> {a()}
435
+ } else if (testValue === true) {
436
+ path.replaceWith(consequent.node);
437
+ return;
438
+ }
439
+
440
+ function getResult(path: NodePath): {
441
+ returnPath: NodePath<t.ReturnStatement> | null;
442
+ expressions: t.Expression[];
443
+ } {
444
+ if (!path.node) return null;
445
+
446
+ if (path.isReturnStatement()) {
447
+ return { returnPath: path, expressions: [] };
448
+ }
449
+ if (path.isExpressionStatement()) {
450
+ return {
451
+ returnPath: null,
452
+ expressions: [path.get("expression").node],
453
+ };
454
+ }
455
+
456
+ if (path.isBlockStatement()) {
457
+ var expressions = [];
458
+ for (var statement of path.get("body")) {
459
+ if (statement.isReturnStatement()) {
460
+ return { returnPath: statement, expressions: expressions };
461
+ } else if (statement.isExpressionStatement()) {
462
+ expressions.push(statement.get("expression").node);
463
+ } else {
464
+ return null;
465
+ }
466
+ }
467
+
468
+ return { returnPath: null, expressions: expressions };
469
+ }
470
+
471
+ return null;
472
+ }
473
+
474
+ var consequentReturn = getResult(consequent);
475
+ var alternateReturn = getResult(alternate);
476
+
477
+ if (consequentReturn && alternateReturn) {
478
+ if (consequentReturn.returnPath && alternateReturn.returnPath) {
479
+ function createReturnArgument(
480
+ resultInfo: ReturnType<typeof getResult>,
481
+ ) {
482
+ return t.sequenceExpression([
483
+ ...resultInfo.expressions,
484
+ resultInfo.returnPath.node.argument ||
485
+ t.identifier("undefined"),
486
+ ]);
487
+ }
488
+
489
+ path.replaceWith(
490
+ t.returnStatement(
491
+ t.conditionalExpression(
492
+ path.node.test,
493
+ createReturnArgument(consequentReturn),
494
+ createReturnArgument(alternateReturn),
495
+ ),
496
+ ),
497
+ );
498
+ } else if (
499
+ !consequentReturn.returnPath &&
500
+ !alternateReturn.returnPath
501
+ ) {
502
+ function joinExpressions(expressions: t.Expression[]) {
503
+ // condition?():() is invalid syntax
504
+ // Just use 0 as a placeholder
505
+ if (expressions.length === 0) return t.numericLiteral(0);
506
+
507
+ // No need for sequence expression if there's only one expression
508
+ if (expressions.length === 1) return expressions[0];
509
+
510
+ return t.sequenceExpression(expressions);
511
+ }
512
+
513
+ path.replaceWith(
514
+ t.conditionalExpression(
515
+ path.node.test,
516
+ joinExpressions(consequentReturn.expressions),
517
+ joinExpressions(alternateReturn.expressions),
518
+ ),
519
+ );
520
+ }
521
+ }
522
+ },
523
+ },
524
+ // Remove unreachable code
525
+ // Code after a return/throw/break/continue is unreachable
526
+ // Remove implied returns
527
+ // Remove code after if all branches are unreachable
528
+ "Block|SwitchCase": {
529
+ enter(path) {
530
+ if (path.isProgram()) {
531
+ path.scope.crawl();
532
+ }
533
+ },
534
+ exit(path) {
535
+ var statementList = path.isBlock()
536
+ ? (path.get("body") as NodePath<t.Statement>[])
537
+ : (path.get("consequent") as NodePath<t.Statement>[]);
538
+
539
+ var impliedReturn: NodePath<t.ReturnStatement>;
540
+
541
+ function isUnreachable(
542
+ statementList: NodePath<t.Statement>[],
543
+ topLevel = false,
544
+ ) {
545
+ var unreachableState = false;
546
+
547
+ for (var statement of statementList) {
548
+ if (unreachableState) {
549
+ statement.remove();
550
+ continue;
551
+ }
552
+
553
+ if (statement.isIfStatement()) {
554
+ const consequent = statement.get("consequent");
555
+ const alternate = statement.get("alternate");
556
+
557
+ if (
558
+ [consequent, alternate].every(
559
+ (x) =>
560
+ x.node &&
561
+ x.isBlockStatement() &&
562
+ isUnreachable(x.get("body")),
563
+ )
564
+ ) {
565
+ unreachableState = true;
566
+ if (!topLevel) {
567
+ return true;
568
+ } else {
569
+ continue;
570
+ }
571
+ }
572
+ }
573
+
574
+ if (
575
+ statement.isReturnStatement() ||
576
+ statement.isThrowStatement() ||
577
+ statement.isBreakStatement() ||
578
+ statement.isContinueStatement()
579
+ ) {
580
+ unreachableState = true;
581
+ if (!topLevel) {
582
+ return true;
583
+ }
584
+ }
585
+
586
+ if (topLevel) {
587
+ if (
588
+ statement == statementList.at(-1) &&
589
+ statement.isReturnStatement() &&
590
+ !statement.node.argument
591
+ ) {
592
+ impliedReturn = statement;
593
+ }
594
+ }
595
+ }
596
+ return false;
597
+ }
598
+
599
+ isUnreachable(statementList, true);
600
+
601
+ if (impliedReturn) {
602
+ var functionParent = path.getFunctionParent();
603
+ if (
604
+ functionParent &&
605
+ t.isBlockStatement(functionParent.node.body) &&
606
+ functionParent.node.body === path.node
607
+ ) {
608
+ impliedReturn.remove();
609
+ }
610
+ }
611
+ },
612
+ },
613
+ },
614
+ };
615
+ };