webfuscator 1.2.1 → 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.
Files changed (110) hide show
  1. package/dist/analysis/constant.js +3 -4
  2. package/dist/analysis/constant.js.map +1 -1
  3. package/dist/analysis/inlineability.js +1 -1
  4. package/dist/analysis/inlineability.js.map +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/obfuscator.d.ts +2 -1
  8. package/dist/obfuscator.d.ts.map +1 -1
  9. package/dist/obfuscator.js +47 -12
  10. package/dist/obfuscator.js.map +1 -1
  11. package/dist/options.d.ts +17 -4
  12. package/dist/options.d.ts.map +1 -1
  13. package/dist/preparation/expand-destructuring.d.ts +3 -3
  14. package/dist/preparation/expand-destructuring.js +6 -6
  15. package/dist/preparation/expand-destructuring.js.map +1 -1
  16. package/dist/transforms/arrow-to-function.js +3 -5
  17. package/dist/transforms/arrow-to-function.js.map +1 -1
  18. package/dist/transforms/conceal-strings.d.ts +57 -0
  19. package/dist/transforms/conceal-strings.d.ts.map +1 -0
  20. package/dist/transforms/conceal-strings.js +361 -0
  21. package/dist/transforms/conceal-strings.js.map +1 -0
  22. package/dist/transforms/const-let-to-var.d.ts +3 -3
  23. package/dist/transforms/const-let-to-var.d.ts.map +1 -1
  24. package/dist/transforms/const-let-to-var.js +36 -9
  25. package/dist/transforms/const-let-to-var.js.map +1 -1
  26. package/dist/transforms/const-to-let.d.ts +2 -3
  27. package/dist/transforms/const-to-let.d.ts.map +1 -1
  28. package/dist/transforms/const-to-let.js +3 -5
  29. package/dist/transforms/const-to-let.js.map +1 -1
  30. package/dist/transforms/fold-builtin-methods.d.ts.map +1 -1
  31. package/dist/transforms/fold-builtin-methods.js +48 -10
  32. package/dist/transforms/fold-builtin-methods.js.map +1 -1
  33. package/dist/transforms/fold-constants.js +3 -0
  34. package/dist/transforms/fold-constants.js.map +1 -1
  35. package/dist/transforms/hoist-variables.d.ts +30 -0
  36. package/dist/transforms/hoist-variables.d.ts.map +1 -0
  37. package/dist/transforms/hoist-variables.js +224 -0
  38. package/dist/transforms/hoist-variables.js.map +1 -0
  39. package/dist/transforms/inline-functions.js +3 -4
  40. package/dist/transforms/inline-functions.js.map +1 -1
  41. package/dist/transforms/mangle-properties.js +7 -4
  42. package/dist/transforms/mangle-properties.js.map +1 -1
  43. package/dist/transforms/merge-variable-declarations.d.ts +17 -0
  44. package/dist/transforms/merge-variable-declarations.d.ts.map +1 -0
  45. package/dist/transforms/merge-variable-declarations.js +68 -0
  46. package/dist/transforms/merge-variable-declarations.js.map +1 -0
  47. package/dist/transforms/numbers-to-strings.d.ts.map +1 -1
  48. package/dist/transforms/numbers-to-strings.js +1 -8
  49. package/dist/transforms/numbers-to-strings.js.map +1 -1
  50. package/dist/transforms/pack-declarations-into-parameters.js +1 -5
  51. package/dist/transforms/pack-declarations-into-parameters.js.map +1 -1
  52. package/dist/transforms/pack.d.ts +5 -7
  53. package/dist/transforms/pack.d.ts.map +1 -1
  54. package/dist/transforms/pack.js +10 -17
  55. package/dist/transforms/pack.js.map +1 -1
  56. package/dist/transforms/remove-unused-code.d.ts.map +1 -1
  57. package/dist/transforms/remove-unused-code.js +7 -39
  58. package/dist/transforms/remove-unused-code.js.map +1 -1
  59. package/dist/transforms/rename-identifiers.d.ts.map +1 -1
  60. package/dist/transforms/rename-identifiers.js +29 -17
  61. package/dist/transforms/rename-identifiers.js.map +1 -1
  62. package/dist/transforms/reorder-function-declarations.d.ts +25 -0
  63. package/dist/transforms/reorder-function-declarations.d.ts.map +1 -0
  64. package/dist/transforms/reorder-function-declarations.js +64 -0
  65. package/dist/transforms/reorder-function-declarations.js.map +1 -0
  66. package/dist/transforms/specials-to-strings.js +4 -3
  67. package/dist/transforms/specials-to-strings.js.map +1 -1
  68. package/dist/transforms/update-to-assignment.js +1 -16
  69. package/dist/transforms/update-to-assignment.js.map +1 -1
  70. package/dist/utils/ast.d.ts +1 -2
  71. package/dist/utils/ast.d.ts.map +1 -1
  72. package/dist/utils/ast.js +12 -42
  73. package/dist/utils/ast.js.map +1 -1
  74. package/dist/utils/literal.d.ts.map +1 -1
  75. package/dist/utils/literal.js +74 -2
  76. package/dist/utils/literal.js.map +1 -1
  77. package/dist/utils/statement-lists.d.ts +9 -0
  78. package/dist/utils/statement-lists.d.ts.map +1 -0
  79. package/dist/utils/statement-lists.js +27 -0
  80. package/dist/utils/statement-lists.js.map +1 -0
  81. package/package.json +10 -12
  82. package/src/analysis/constant.ts +3 -6
  83. package/src/analysis/inlineability.ts +1 -1
  84. package/src/index.ts +1 -0
  85. package/src/obfuscator.ts +51 -14
  86. package/src/options.ts +21 -3
  87. package/src/preparation/expand-destructuring.ts +6 -6
  88. package/src/transforms/arrow-to-function.ts +3 -5
  89. package/src/transforms/conceal-strings.ts +530 -0
  90. package/src/transforms/const-let-to-var.ts +37 -9
  91. package/src/transforms/const-to-let.ts +3 -5
  92. package/src/transforms/fold-builtin-methods.ts +67 -12
  93. package/src/transforms/fold-constants.ts +3 -0
  94. package/src/transforms/hoist-variables.ts +272 -0
  95. package/src/transforms/inline-functions.ts +3 -4
  96. package/src/transforms/mangle-properties.ts +7 -7
  97. package/src/transforms/merge-variable-declarations.ts +91 -0
  98. package/src/transforms/numbers-to-strings.ts +2 -12
  99. package/src/transforms/pack-declarations-into-parameters.ts +1 -5
  100. package/src/transforms/pack.ts +10 -18
  101. package/src/transforms/remove-unused-code.ts +9 -64
  102. package/src/transforms/rename-identifiers.ts +42 -17
  103. package/src/transforms/reorder-function-declarations.ts +80 -0
  104. package/src/transforms/specials-to-strings.ts +4 -3
  105. package/src/transforms/update-to-assignment.ts +1 -17
  106. package/src/utils/ast.ts +15 -51
  107. package/src/utils/literal.ts +91 -2
  108. package/src/utils/statement-lists.ts +45 -0
  109. package/DISCLOSURE +0 -9
  110. package/README.md +0 -67
@@ -1,10 +1,9 @@
1
1
  import traverse from '@babel/traverse'
2
- import type { NodePath, Scope, Visitor } from '@babel/traverse'
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: SweepState = {
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
- // Direct eval can read otherwise unreferenced bindings in its scope ancestry.
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, state.evalReachableScopes)) {
52
+ if (tryRemoveDeclarator(path)) {
87
53
  state.changed = true
88
54
  }
89
55
  },
90
56
  FunctionDeclaration(path, state) {
91
- if (tryRemoveFunctionDecl(path, state.evalReachableScopes)) {
57
+ if (tryRemoveFunctionDecl(path)) {
92
58
  state.changed = true
93
59
  }
94
60
  },
95
61
  ClassDeclaration(path, state) {
96
- if (tryRemoveClassDecl(path, state.evalReachableScopes)) {
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 { isDirectEvalCall, isInsideWith } from '../utils/ast'
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 false
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` and direct eval pin bindings to their source names.
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 { enclosingScopeHasDirectEval, isInsideWith } from '../utils/ast'
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
- // Replacements are not write targets. `with` and eval can also shadow globals.
73
- if (isWriteTarget(path) || isInsideWith(path) || enclosingScopeHasDirectEval(path)) {
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())
@@ -2,7 +2,7 @@ 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 '../utils/ast'
5
+ import { isInsideWith } from '../utils/ast'
6
6
  import { traverseForChanges } from '../utils/change-tracking'
7
7
  import type { ChangeState } from '../utils/change-tracking'
8
8
 
@@ -52,10 +52,6 @@ const visitor: Visitor<ChangeState> = {
52
52
  if (!isNumericBinding(binding)) {
53
53
  return
54
54
  }
55
- // Direct eval anywhere in the binding's scope can change its type.
56
- if (bindingScopeReachesDirectEval(binding!)) {
57
- return
58
- }
59
55
  const compound = operator === '++' ? '+=' : '-='
60
56
  path.replaceWith(t.assignmentExpression(compound, argument, t.numericLiteral(1)))
61
57
  state.changed = true
@@ -95,18 +91,6 @@ function isNumericBinding(binding: Binding | undefined): boolean {
95
91
  return binding.constantViolations.every((violation) => keepsNumeric(violation.node))
96
92
  }
97
93
 
98
- // A sibling or nested direct eval can still close over and reassign the binding.
99
- function bindingScopeReachesDirectEval(binding: Binding): boolean {
100
- let found = false
101
- t.traverseFast(binding.scope.block, (node) => {
102
- if (isDirectEvalCall(node)) {
103
- found = true
104
- }
105
- return found ? t.traverseFast.stop : undefined
106
- })
107
- return found
108
- }
109
-
110
94
  function isForInOfLoopVariable(binding: Binding): boolean {
111
95
  const declaration = binding.path.parentPath
112
96
  if (!declaration || !declaration.isVariableDeclaration()) {
package/src/utils/ast.ts CHANGED
@@ -62,75 +62,39 @@ export function isProtoKey(key: t.Node): boolean {
62
62
  )
63
63
  }
64
64
 
65
+ // A non-computed key names a property; it is not a runtime value.
66
+ export function isNonComputedKey(path: NodePath): boolean {
67
+ const parent = path.parent
68
+ return (
69
+ 'key' in parent && parent.key === path.node && 'computed' in parent && parent.computed === false
70
+ )
71
+ }
72
+
65
73
  // `with` can shadow names outside Babel's scope table.
66
74
  export function isInsideWith(path: NodePath): boolean {
67
- if (!programContains(path.scope.getProgramParent().block, programWith, t.isWithStatement)) {
75
+ if (!programHasWith(path.scope.getProgramParent().block)) {
68
76
  return false
69
77
  }
70
78
  return path.findParent((ancestor) => ancestor.isWithStatement()) !== null
71
79
  }
72
80
 
73
- // Direct eval can access local bindings by name. Indirect eval runs globally and
74
- // does not count.
75
- export function enclosingScopeHasDirectEval(path: NodePath): boolean {
76
- if (!programContains(path.scope.getProgramParent().block, programDirectEval, isDirectEvalCall)) {
77
- return false
78
- }
79
- let current: NodePath | null = path
80
- while (current) {
81
- if ((current.isFunction() || current.isProgram()) && ownScopeHasDirectEval(current.node)) {
82
- return true
83
- }
84
- current = current.parentPath
85
- }
86
- return false
87
- }
88
-
89
- // Cache program-wide `eval` and `with` checks to avoid a subtree walk per name.
90
- // No transform creates either construct, so a negative result stays valid.
91
- const programDirectEval = new WeakMap<t.Node, boolean>()
81
+ // Cache the program-wide scan because `isInsideWith` runs for many identifiers.
82
+ // Transforms never add `with`, so a cached miss remains valid.
92
83
  const programWith = new WeakMap<t.Node, boolean>()
93
84
 
94
- function programContains(
95
- programNode: t.Node,
96
- cache: WeakMap<t.Node, boolean>,
97
- matches: (node: t.Node) => boolean,
98
- ): boolean {
99
- const cached = cache.get(programNode)
85
+ function programHasWith(programNode: t.Node): boolean {
86
+ const cached = programWith.get(programNode)
100
87
  if (cached !== undefined) {
101
88
  return cached
102
89
  }
103
90
  let found = false
104
91
  t.traverseFast(programNode, (node) => {
105
- if (!matches(node)) {
92
+ if (!t.isWithStatement(node)) {
106
93
  return
107
94
  }
108
95
  found = true
109
96
  return t.traverseFast.stop
110
97
  })
111
- cache.set(programNode, found)
98
+ programWith.set(programNode, found)
112
99
  return found
113
100
  }
114
-
115
- function ownScopeHasDirectEval(owner: t.Node): boolean {
116
- let found = false
117
- // This walk stops at the first hit, unlike `walkOwnFunctionScope`.
118
- t.traverseFast(owner, (node) => {
119
- if (isDirectEvalCall(node)) {
120
- found = true
121
- return t.traverseFast.stop
122
- }
123
- return node !== owner && t.isFunction(node) ? t.traverseFast.skip : undefined
124
- })
125
- return found
126
- }
127
-
128
- export function isDirectEvalCall(node: t.Node): boolean {
129
- // `eval?.(...)` is an OptionalCallExpression yet still a direct eval, so both
130
- // call node shapes count.
131
- return (
132
- (t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
133
- t.isIdentifier(node.callee) &&
134
- node.callee.name === 'eval'
135
- )
136
- }
@@ -1,5 +1,9 @@
1
+ import generate from '@babel/generator'
1
2
  import * as t from '@babel/types'
2
3
 
4
+ const MAX_GENERATED_LITERAL_LENGTH = 8192
5
+ const MAX_LITERAL_DEPTH = 32
6
+ const MAX_LITERAL_NODES = 512
3
7
  const MAX_STRING_LITERAL_LENGTH = 4096
4
8
 
5
9
  // RegExp literals allocate mutable objects and are not primitive values.
@@ -45,19 +49,56 @@ function literalShapeKind(node: t.Node): 'bigint' | 'other' | null {
45
49
  }
46
50
 
47
51
  export function valueToLiteral(value: unknown): t.Expression | null {
52
+ const literal = buildValueLiteral(value, new Set(), { nodesRemaining: MAX_LITERAL_NODES }, 0)
53
+ if (!literal || generate(literal).code.length > MAX_GENERATED_LITERAL_LENGTH) {
54
+ return null
55
+ }
56
+ return literal
57
+ }
58
+
59
+ interface LiteralBudget {
60
+ nodesRemaining: number
61
+ }
62
+
63
+ function reserveLiteralNodes(budget: LiteralBudget, count: number): boolean {
64
+ if (budget.nodesRemaining < count) {
65
+ return false
66
+ }
67
+ budget.nodesRemaining -= count
68
+ return true
69
+ }
70
+
71
+ function buildValueLiteral(
72
+ value: unknown,
73
+ seen: Set<object>,
74
+ budget: LiteralBudget,
75
+ depth: number,
76
+ ): t.Expression | null {
77
+ if (depth > MAX_LITERAL_DEPTH) {
78
+ return null
79
+ }
48
80
  if (typeof value === 'string') {
49
- if (value.length > MAX_STRING_LITERAL_LENGTH) {
81
+ if (value.length > MAX_STRING_LITERAL_LENGTH || !reserveLiteralNodes(budget, 1)) {
50
82
  return null
51
83
  }
52
84
  return t.stringLiteral(value)
53
85
  }
54
86
  if (typeof value === 'boolean') {
87
+ if (!reserveLiteralNodes(budget, 1)) {
88
+ return null
89
+ }
55
90
  return t.booleanLiteral(value)
56
91
  }
57
92
  if (value === null) {
93
+ if (!reserveLiteralNodes(budget, 1)) {
94
+ return null
95
+ }
58
96
  return t.nullLiteral()
59
97
  }
60
98
  if (value === undefined) {
99
+ if (!reserveLiteralNodes(budget, 2)) {
100
+ return null
101
+ }
61
102
  // `void 0` cannot be shadowed like the identifier `undefined`.
62
103
  return t.unaryExpression('void', t.numericLiteral(0))
63
104
  }
@@ -65,8 +106,14 @@ export function valueToLiteral(value: unknown): t.Expression | null {
65
106
  // A UnaryExpression lets the generator parenthesize negative BigInt where
66
107
  // precedence requires it.
67
108
  if (value < 0n) {
109
+ if (!reserveLiteralNodes(budget, 2)) {
110
+ return null
111
+ }
68
112
  return t.unaryExpression('-', t.bigIntLiteral(-value))
69
113
  }
114
+ if (!reserveLiteralNodes(budget, 1)) {
115
+ return null
116
+ }
70
117
  return t.bigIntLiteral(value)
71
118
  }
72
119
  if (typeof value === 'number') {
@@ -74,17 +121,30 @@ export function valueToLiteral(value: unknown): t.Expression | null {
74
121
  return null
75
122
  }
76
123
  if (Object.is(value, -0)) {
124
+ if (!reserveLiteralNodes(budget, 2)) {
125
+ return null
126
+ }
77
127
  return t.unaryExpression('-', t.numericLiteral(0))
78
128
  }
79
129
  if (value < 0) {
130
+ if (!reserveLiteralNodes(budget, 2)) {
131
+ return null
132
+ }
80
133
  return t.unaryExpression('-', t.numericLiteral(-value))
81
134
  }
135
+ if (!reserveLiteralNodes(budget, 1)) {
136
+ return null
137
+ }
82
138
  return t.numericLiteral(value)
83
139
  }
84
140
  if (Array.isArray(value)) {
141
+ if (seen.has(value) || !arrayHasLiteralShape(value) || !reserveLiteralNodes(budget, 1)) {
142
+ return null
143
+ }
144
+ seen.add(value)
85
145
  const elements: t.Expression[] = []
86
146
  for (const item of value) {
87
- const lit = valueToLiteral(item)
147
+ const lit = buildValueLiteral(item, seen, budget, depth + 1)
88
148
  if (!lit) {
89
149
  return null
90
150
  }
@@ -94,3 +154,32 @@ export function valueToLiteral(value: unknown): t.Expression | null {
94
154
  }
95
155
  return null
96
156
  }
157
+
158
+ function arrayHasLiteralShape(value: unknown[]): boolean {
159
+ if (Object.getPrototypeOf(value) !== Array.prototype || !Object.isExtensible(value)) {
160
+ return false
161
+ }
162
+ const names = Object.getOwnPropertyNames(value)
163
+ if (names.length !== value.length + 1 || names.at(-1) !== 'length') {
164
+ return false
165
+ }
166
+ for (let index = 0; index < value.length; index++) {
167
+ if (
168
+ names[index] !== String(index) ||
169
+ !isOrdinaryDataProperty(Object.getOwnPropertyDescriptor(value, String(index)))
170
+ ) {
171
+ return false
172
+ }
173
+ }
174
+ return Object.getOwnPropertySymbols(value).length === 0
175
+ }
176
+
177
+ function isOrdinaryDataProperty(descriptor: PropertyDescriptor | undefined): boolean {
178
+ return (
179
+ descriptor !== undefined &&
180
+ 'value' in descriptor &&
181
+ descriptor.configurable === true &&
182
+ descriptor.enumerable === true &&
183
+ descriptor.writable === true
184
+ )
185
+ }
@@ -0,0 +1,45 @@
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 { traverseForChanges } from './change-tracking'
6
+ import type { ChangeState } from './change-tracking'
7
+
8
+ export type StatementListContainer = t.Program | t.BlockStatement | t.StaticBlock | t.SwitchCase
9
+
10
+ type Rewrite = (
11
+ statements: NodePath<t.Statement>[],
12
+ container: NodePath<StatementListContainer>,
13
+ ) => t.Statement[] | null
14
+
15
+ /** Applies a rewrite to every statement-list container in the AST. */
16
+ export function rewriteStatementLists(ast: File, rewrite: Rewrite): boolean {
17
+ return traverseForChanges(ast, {
18
+ Program(path, state) {
19
+ apply(path, 'body', rewrite, state)
20
+ },
21
+ BlockStatement(path, state) {
22
+ apply(path, 'body', rewrite, state)
23
+ },
24
+ StaticBlock(path, state) {
25
+ apply(path, 'body', rewrite, state)
26
+ },
27
+ SwitchCase(path, state) {
28
+ apply(path, 'consequent', rewrite, state)
29
+ },
30
+ })
31
+ }
32
+
33
+ function apply(
34
+ container: NodePath<StatementListContainer>,
35
+ key: 'body' | 'consequent',
36
+ rewrite: Rewrite,
37
+ state: ChangeState,
38
+ ): void {
39
+ const statements = container.get(key) as NodePath<t.Statement>[]
40
+ const replacement = rewrite(statements, container)
41
+ if (replacement) {
42
+ container.set(key, replacement)
43
+ state.changed = true
44
+ }
45
+ }
package/DISCLOSURE DELETED
@@ -1,9 +0,0 @@
1
- webfuscator transforms and obfuscates JavaScript source code. Its features
2
- include identifier and property-name mangling, constant folding, dead-code
3
- removal, and control-flow and literal rewrites.
4
-
5
- These capabilities can make JavaScript harder to inspect and may have both
6
- legitimate and harmful uses. The intended use is transforming software that
7
- the user owns or is authorized to modify. This package is not intended to
8
- create, conceal, distribute, or operate malware, or to interfere with systems
9
- without authorization.