webfuscator 1.2.1 → 1.3.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 (209) hide show
  1. package/README.md +3 -3
  2. package/dist/analysis/constant.d.ts.map +1 -1
  3. package/dist/analysis/constant.js +10 -27
  4. package/dist/analysis/constant.js.map +1 -1
  5. package/dist/analysis/inlineability.d.ts.map +1 -1
  6. package/dist/analysis/inlineability.js +5 -13
  7. package/dist/analysis/inlineability.js.map +1 -1
  8. package/dist/analysis/purity.d.ts +3 -3
  9. package/dist/analysis/purity.d.ts.map +1 -1
  10. package/dist/analysis/purity.js +28 -75
  11. package/dist/analysis/purity.js.map +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/obfuscator.d.ts +2 -1
  15. package/dist/obfuscator.d.ts.map +1 -1
  16. package/dist/obfuscator.js +47 -12
  17. package/dist/obfuscator.js.map +1 -1
  18. package/dist/options.d.ts +17 -4
  19. package/dist/options.d.ts.map +1 -1
  20. package/dist/preparation/expand-destructuring.d.ts +2 -3
  21. package/dist/preparation/expand-destructuring.d.ts.map +1 -1
  22. package/dist/preparation/expand-destructuring.js +2 -8
  23. package/dist/preparation/expand-destructuring.js.map +1 -1
  24. package/dist/transforms/arguments-to-parameters.d.ts +5 -4
  25. package/dist/transforms/arguments-to-parameters.d.ts.map +1 -1
  26. package/dist/transforms/arguments-to-parameters.js +5 -4
  27. package/dist/transforms/arguments-to-parameters.js.map +1 -1
  28. package/dist/transforms/arrow-to-function.d.ts +4 -3
  29. package/dist/transforms/arrow-to-function.d.ts.map +1 -1
  30. package/dist/transforms/arrow-to-function.js +6 -10
  31. package/dist/transforms/arrow-to-function.js.map +1 -1
  32. package/dist/transforms/collapse-single-use-temps.d.ts +2 -1
  33. package/dist/transforms/collapse-single-use-temps.d.ts.map +1 -1
  34. package/dist/transforms/collapse-single-use-temps.js +12 -20
  35. package/dist/transforms/collapse-single-use-temps.js.map +1 -1
  36. package/dist/transforms/conceal-strings.d.ts +56 -0
  37. package/dist/transforms/conceal-strings.d.ts.map +1 -0
  38. package/dist/transforms/conceal-strings.js +356 -0
  39. package/dist/transforms/conceal-strings.js.map +1 -0
  40. package/dist/transforms/const-let-to-var.d.ts +4 -3
  41. package/dist/transforms/const-let-to-var.d.ts.map +1 -1
  42. package/dist/transforms/const-let-to-var.js +8 -67
  43. package/dist/transforms/const-let-to-var.js.map +1 -1
  44. package/dist/transforms/const-to-let.d.ts +1 -3
  45. package/dist/transforms/const-to-let.d.ts.map +1 -1
  46. package/dist/transforms/const-to-let.js +1 -8
  47. package/dist/transforms/const-to-let.js.map +1 -1
  48. package/dist/transforms/drop-console.d.ts.map +1 -1
  49. package/dist/transforms/drop-console.js +3 -5
  50. package/dist/transforms/drop-console.js.map +1 -1
  51. package/dist/transforms/expand-binary-assignment.d.ts.map +1 -1
  52. package/dist/transforms/expand-binary-assignment.js +0 -5
  53. package/dist/transforms/expand-binary-assignment.js.map +1 -1
  54. package/dist/transforms/expand-logical-assignment.d.ts.map +1 -1
  55. package/dist/transforms/expand-logical-assignment.js +0 -5
  56. package/dist/transforms/expand-logical-assignment.js.map +1 -1
  57. package/dist/transforms/extract-object-properties.js +5 -12
  58. package/dist/transforms/extract-object-properties.js.map +1 -1
  59. package/dist/transforms/fold-builtin-methods.d.ts.map +1 -1
  60. package/dist/transforms/fold-builtin-methods.js +44 -14
  61. package/dist/transforms/fold-builtin-methods.js.map +1 -1
  62. package/dist/transforms/fold-constants.js +5 -2
  63. package/dist/transforms/fold-constants.js.map +1 -1
  64. package/dist/transforms/function-declaration-to-expression.d.ts +5 -6
  65. package/dist/transforms/function-declaration-to-expression.d.ts.map +1 -1
  66. package/dist/transforms/function-declaration-to-expression.js +5 -6
  67. package/dist/transforms/function-declaration-to-expression.js.map +1 -1
  68. package/dist/transforms/hoist-variables.d.ts +29 -0
  69. package/dist/transforms/hoist-variables.d.ts.map +1 -0
  70. package/dist/transforms/hoist-variables.js +215 -0
  71. package/dist/transforms/hoist-variables.js.map +1 -0
  72. package/dist/transforms/inline-functions.js +11 -40
  73. package/dist/transforms/inline-functions.js.map +1 -1
  74. package/dist/transforms/logical-to-ternary.d.ts +1 -1
  75. package/dist/transforms/logical-to-ternary.js +5 -9
  76. package/dist/transforms/logical-to-ternary.js.map +1 -1
  77. package/dist/transforms/mangle-properties.d.ts +2 -5
  78. package/dist/transforms/mangle-properties.d.ts.map +1 -1
  79. package/dist/transforms/mangle-properties.js +9 -9
  80. package/dist/transforms/mangle-properties.js.map +1 -1
  81. package/dist/transforms/merge-variable-declarations.d.ts +17 -0
  82. package/dist/transforms/merge-variable-declarations.d.ts.map +1 -0
  83. package/dist/transforms/merge-variable-declarations.js +68 -0
  84. package/dist/transforms/merge-variable-declarations.js.map +1 -0
  85. package/dist/transforms/nullish-coalescing-to-ternary.d.ts +4 -5
  86. package/dist/transforms/nullish-coalescing-to-ternary.d.ts.map +1 -1
  87. package/dist/transforms/nullish-coalescing-to-ternary.js +7 -12
  88. package/dist/transforms/nullish-coalescing-to-ternary.js.map +1 -1
  89. package/dist/transforms/numbers-to-strings.d.ts.map +1 -1
  90. package/dist/transforms/numbers-to-strings.js +1 -8
  91. package/dist/transforms/numbers-to-strings.js.map +1 -1
  92. package/dist/transforms/object-method-to-property.d.ts +1 -1
  93. package/dist/transforms/object-method-to-property.js +1 -1
  94. package/dist/transforms/optional-chaining-to-ternary.d.ts +4 -3
  95. package/dist/transforms/optional-chaining-to-ternary.d.ts.map +1 -1
  96. package/dist/transforms/optional-chaining-to-ternary.js +9 -9
  97. package/dist/transforms/optional-chaining-to-ternary.js.map +1 -1
  98. package/dist/transforms/pack-declarations-into-parameters.d.ts.map +1 -1
  99. package/dist/transforms/pack-declarations-into-parameters.js +5 -21
  100. package/dist/transforms/pack-declarations-into-parameters.js.map +1 -1
  101. package/dist/transforms/pack.d.ts +4 -13
  102. package/dist/transforms/pack.d.ts.map +1 -1
  103. package/dist/transforms/pack.js +9 -29
  104. package/dist/transforms/pack.js.map +1 -1
  105. package/dist/transforms/remove-anonymous-function-names.d.ts +2 -4
  106. package/dist/transforms/remove-anonymous-function-names.d.ts.map +1 -1
  107. package/dist/transforms/remove-anonymous-function-names.js +2 -40
  108. package/dist/transforms/remove-anonymous-function-names.js.map +1 -1
  109. package/dist/transforms/remove-unreachable-code.js +2 -21
  110. package/dist/transforms/remove-unreachable-code.js.map +1 -1
  111. package/dist/transforms/remove-unused-code.d.ts.map +1 -1
  112. package/dist/transforms/remove-unused-code.js +12 -49
  113. package/dist/transforms/remove-unused-code.js.map +1 -1
  114. package/dist/transforms/rename-identifiers.d.ts +3 -3
  115. package/dist/transforms/rename-identifiers.d.ts.map +1 -1
  116. package/dist/transforms/rename-identifiers.js +31 -35
  117. package/dist/transforms/rename-identifiers.js.map +1 -1
  118. package/dist/transforms/reorder-function-declarations.d.ts +24 -0
  119. package/dist/transforms/reorder-function-declarations.d.ts.map +1 -0
  120. package/dist/transforms/reorder-function-declarations.js +62 -0
  121. package/dist/transforms/reorder-function-declarations.js.map +1 -0
  122. package/dist/transforms/specials-to-strings.d.ts.map +1 -1
  123. package/dist/transforms/specials-to-strings.js +2 -3
  124. package/dist/transforms/specials-to-strings.js.map +1 -1
  125. package/dist/transforms/switch-to-if.js +5 -15
  126. package/dist/transforms/switch-to-if.js.map +1 -1
  127. package/dist/transforms/ternary-to-if.d.ts.map +1 -1
  128. package/dist/transforms/ternary-to-if.js +5 -9
  129. package/dist/transforms/ternary-to-if.js.map +1 -1
  130. package/dist/transforms/update-to-assignment.d.ts.map +1 -1
  131. package/dist/transforms/update-to-assignment.js +6 -23
  132. package/dist/transforms/update-to-assignment.js.map +1 -1
  133. package/dist/transforms/yodify-conditions.d.ts +3 -4
  134. package/dist/transforms/yodify-conditions.d.ts.map +1 -1
  135. package/dist/transforms/yodify-conditions.js +3 -4
  136. package/dist/transforms/yodify-conditions.js.map +1 -1
  137. package/dist/utils/ast.d.ts +1 -3
  138. package/dist/utils/ast.d.ts.map +1 -1
  139. package/dist/utils/ast.js +4 -60
  140. package/dist/utils/ast.js.map +1 -1
  141. package/dist/utils/evaluation-order.js +1 -1
  142. package/dist/utils/evaluation-order.js.map +1 -1
  143. package/dist/utils/literal.d.ts.map +1 -1
  144. package/dist/utils/literal.js +74 -2
  145. package/dist/utils/literal.js.map +1 -1
  146. package/dist/utils/logical-lowering.d.ts.map +1 -1
  147. package/dist/utils/logical-lowering.js +7 -9
  148. package/dist/utils/logical-lowering.js.map +1 -1
  149. package/dist/utils/nullish.d.ts +4 -0
  150. package/dist/utils/nullish.d.ts.map +1 -0
  151. package/dist/utils/nullish.js +8 -0
  152. package/dist/utils/nullish.js.map +1 -0
  153. package/dist/utils/paths.d.ts +0 -2
  154. package/dist/utils/paths.d.ts.map +1 -1
  155. package/dist/utils/paths.js +0 -28
  156. package/dist/utils/paths.js.map +1 -1
  157. package/dist/utils/statement-lists.d.ts +9 -0
  158. package/dist/utils/statement-lists.d.ts.map +1 -0
  159. package/dist/utils/statement-lists.js +27 -0
  160. package/dist/utils/statement-lists.js.map +1 -0
  161. package/package.json +6 -2
  162. package/src/analysis/constant.ts +12 -33
  163. package/src/analysis/inlineability.ts +5 -13
  164. package/src/analysis/purity.ts +29 -92
  165. package/src/index.ts +1 -0
  166. package/src/obfuscator.ts +51 -14
  167. package/src/options.ts +21 -3
  168. package/src/preparation/expand-destructuring.ts +2 -10
  169. package/src/transforms/arguments-to-parameters.ts +5 -4
  170. package/src/transforms/arrow-to-function.ts +6 -10
  171. package/src/transforms/collapse-single-use-temps.ts +12 -23
  172. package/src/transforms/conceal-strings.ts +525 -0
  173. package/src/transforms/const-let-to-var.ts +9 -72
  174. package/src/transforms/const-to-let.ts +1 -8
  175. package/src/transforms/drop-console.ts +3 -5
  176. package/src/transforms/expand-binary-assignment.ts +0 -5
  177. package/src/transforms/expand-logical-assignment.ts +0 -5
  178. package/src/transforms/extract-object-properties.ts +5 -14
  179. package/src/transforms/fold-builtin-methods.ts +63 -17
  180. package/src/transforms/fold-constants.ts +5 -2
  181. package/src/transforms/function-declaration-to-expression.ts +5 -6
  182. package/src/transforms/hoist-variables.ts +262 -0
  183. package/src/transforms/inline-functions.ts +16 -47
  184. package/src/transforms/logical-to-ternary.ts +5 -10
  185. package/src/transforms/mangle-properties.ts +9 -12
  186. package/src/transforms/merge-variable-declarations.ts +91 -0
  187. package/src/transforms/nullish-coalescing-to-ternary.ts +7 -13
  188. package/src/transforms/numbers-to-strings.ts +2 -12
  189. package/src/transforms/object-method-to-property.ts +1 -1
  190. package/src/transforms/optional-chaining-to-ternary.ts +9 -9
  191. package/src/transforms/pack-declarations-into-parameters.ts +11 -21
  192. package/src/transforms/pack.ts +9 -30
  193. package/src/transforms/remove-anonymous-function-names.ts +3 -46
  194. package/src/transforms/remove-unreachable-code.ts +2 -22
  195. package/src/transforms/remove-unused-code.ts +14 -74
  196. package/src/transforms/rename-identifiers.ts +44 -35
  197. package/src/transforms/reorder-function-declarations.ts +78 -0
  198. package/src/transforms/specials-to-strings.ts +2 -3
  199. package/src/transforms/switch-to-if.ts +5 -22
  200. package/src/transforms/ternary-to-if.ts +5 -9
  201. package/src/transforms/update-to-assignment.ts +6 -24
  202. package/src/transforms/yodify-conditions.ts +3 -4
  203. package/src/utils/ast.ts +4 -69
  204. package/src/utils/evaluation-order.ts +1 -1
  205. package/src/utils/literal.ts +91 -2
  206. package/src/utils/logical-lowering.ts +7 -11
  207. package/src/utils/nullish.ts +23 -0
  208. package/src/utils/paths.ts +0 -32
  209. package/src/utils/statement-lists.ts +45 -0
@@ -1,14 +1,11 @@
1
- import type { NodePath, Visitor } from '@babel/traverse'
2
- import * as t from '@babel/types'
1
+ import type { Visitor } from '@babel/traverse'
3
2
  import type { File } from '@babel/types'
4
3
 
5
4
  import { traverseForChanges } from '../utils/change-tracking'
6
5
  import type { ChangeState } from '../utils/change-tracking'
7
6
 
8
7
  /**
9
- * Removes a function expression's name when the body does not use its binding
10
- * and NamedEvaluation restores the same `.name`. Other positions could infer no
11
- * name or a different one.
8
+ * Removes a function expression's name when its body does not reference it.
12
9
  *
13
10
  * @example
14
11
  * // ◀️ before
@@ -17,7 +14,7 @@ import type { ChangeState } from '../utils/change-tracking'
17
14
  *
18
15
  * // ▶️ after
19
16
  * var greet = function () {};
20
- * var a = function bar() {};
17
+ * var a = function () {};
21
18
  */
22
19
  export function removeAnonymousFunctionNames(ast: File): boolean {
23
20
  return traverseForChanges(ast, visitor)
@@ -38,47 +35,7 @@ const visitor: Visitor<ChangeState> = {
38
35
  ) {
39
36
  return
40
37
  }
41
- if (inferredName(path) !== id.name) {
42
- return
43
- }
44
38
  path.node.id = null
45
39
  state.changed = true
46
40
  },
47
41
  }
48
-
49
- // No inferred name means `.name` would become empty.
50
- function inferredName(path: NodePath<t.FunctionExpression>): string | null {
51
- const node = path.node
52
- const parentPath = path.parentPath
53
- if (!parentPath) {
54
- return null
55
- }
56
- const parent = parentPath.node
57
- if (t.isVariableDeclarator(parent) && parent.init === node && t.isIdentifier(parent.id)) {
58
- return parent.id.name
59
- }
60
- if (
61
- t.isAssignmentExpression(parent) &&
62
- parent.operator === '=' &&
63
- parent.right === node &&
64
- t.isIdentifier(parent.left)
65
- ) {
66
- return parent.left.name
67
- }
68
- if (t.isAssignmentPattern(parent) && parent.right === node && t.isIdentifier(parent.left)) {
69
- return parent.left.name
70
- }
71
- if (
72
- (t.isObjectProperty(parent) || t.isClassProperty(parent)) &&
73
- parent.value === node &&
74
- !parent.computed
75
- ) {
76
- if (t.isIdentifier(parent.key)) {
77
- return parent.key.name
78
- }
79
- if (t.isStringLiteral(parent.key)) {
80
- return parent.key.value
81
- }
82
- }
83
- return null
84
- }
@@ -69,41 +69,21 @@ function isCompletion(node: t.Node): boolean {
69
69
  )
70
70
  }
71
71
 
72
- // Hoisted bindings and referenced lexical TDZ remain observable from reachable
73
- // code. An unreferenced lexical declaration is inert.
74
72
  function isSafeToRemoveAfterCompletion(path: NodePath<t.Statement>): boolean {
75
73
  const node = path.node
76
74
  if (t.isFunctionDeclaration(node) || (t.isVariableDeclaration(node) && node.kind === 'var')) {
77
75
  return false
78
76
  }
79
- if (t.isClassDeclaration(node) || (t.isVariableDeclaration(node) && node.kind !== 'var')) {
80
- return !declaresReferencedBinding(path)
81
- }
82
- return !nestsHoistedDeclaration(node)
83
- }
84
-
85
- // A write before `let x` also observes its TDZ.
86
- function declaresReferencedBinding(path: NodePath<t.Statement>): boolean {
87
- for (const name of Object.keys(path.getBindingIdentifiers())) {
88
- const binding = path.scope.getBinding(name)
89
- if (binding && (binding.references > 0 || binding.constantViolations.length > 0)) {
90
- return true
91
- }
92
- }
93
- return false
77
+ return !nestsHoistedVar(node)
94
78
  }
95
79
 
96
- function nestsHoistedDeclaration(node: t.Node): boolean {
80
+ function nestsHoistedVar(node: t.Node): boolean {
97
81
  let found = false
98
82
  t.traverseFast(node, (child) => {
99
83
  if (found) {
100
84
  return t.traverseFast.skip
101
85
  }
102
86
  if (child !== node && t.isFunction(child)) {
103
- // Function declarations may add Annex B aliases. Function bodies do not.
104
- if (t.isFunctionDeclaration(child)) {
105
- found = true
106
- }
107
87
  return t.traverseFast.skip
108
88
  }
109
89
  if (t.isVariableDeclaration(child) && child.kind === 'var') {
@@ -1,12 +1,10 @@
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
- import { hasAnnexBFunctionAlias } from '../utils/paths'
10
8
 
11
9
  /**
12
10
  * Removes unused declarations and expression statements that have no effects.
@@ -28,10 +26,7 @@ export function removeUnusedCode(ast: File): boolean {
28
26
  // then maintain counts during removal.
29
27
  crawlProgramScope(ast)
30
28
  let anyChanged = false
31
- const state: SweepState = {
32
- changed: true,
33
- evalReachableScopes: collectEvalReachableScopes(ast),
34
- }
29
+ const state: ChangeState = { changed: true }
35
30
  while (state.changed) {
36
31
  state.changed = false
37
32
  traverse(ast, sweepVisitor, undefined, state)
@@ -51,49 +46,19 @@ function crawlProgramScope(ast: File): void {
51
46
  })
52
47
  }
53
48
 
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> = {
49
+ const sweepVisitor: Visitor<ChangeState> = {
85
50
  VariableDeclarator(path, state) {
86
- if (tryRemoveDeclarator(path, state.evalReachableScopes)) {
51
+ if (tryRemoveDeclarator(path)) {
87
52
  state.changed = true
88
53
  }
89
54
  },
90
55
  FunctionDeclaration(path, state) {
91
- if (tryRemoveFunctionDecl(path, state.evalReachableScopes)) {
56
+ if (tryRemoveFunctionDecl(path)) {
92
57
  state.changed = true
93
58
  }
94
59
  },
95
60
  ClassDeclaration(path, state) {
96
- if (tryRemoveClassDecl(path, state.evalReachableScopes)) {
61
+ if (tryRemoveClassDecl(path)) {
97
62
  state.changed = true
98
63
  }
99
64
  },
@@ -126,10 +91,7 @@ function dereferenceSubtree(rootPath: NodePath): void {
126
91
  rootPath.traverse(dereferenceVisitor)
127
92
  }
128
93
 
129
- function tryRemoveDeclarator(
130
- path: NodePath<t.VariableDeclarator>,
131
- evalReachableScopes: Set<t.Node>,
132
- ): boolean {
94
+ function tryRemoveDeclarator(path: NodePath<t.VariableDeclarator>): boolean {
133
95
  const declaration = path.parentPath
134
96
  if (!declaration || !declaration.isVariableDeclaration()) {
135
97
  return false
@@ -156,7 +118,7 @@ function tryRemoveDeclarator(
156
118
  }
157
119
 
158
120
  const init = path.node.init
159
- if (init && !isSideEffectFree(init, path.scope, path.get('init') as NodePath)) {
121
+ if (init && !isSideEffectFree(init, path.scope)) {
160
122
  return false
161
123
  }
162
124
 
@@ -167,13 +129,9 @@ function tryRemoveDeclarator(
167
129
  if (binding.references > 0) {
168
130
  return false
169
131
  }
170
- if (binding.constantViolations.length > 0) {
171
- return false
172
- }
173
- if (bindingReachableByEval(binding, evalReachableScopes)) {
132
+ if (declaration.node.kind !== 'const' && binding.constantViolations.length > 0) {
174
133
  return false
175
134
  }
176
-
177
135
  if (path.node.init) {
178
136
  dereferenceSubtree(path.get('init') as NodePath)
179
137
  }
@@ -185,17 +143,10 @@ function tryRemoveDeclarator(
185
143
  return true
186
144
  }
187
145
 
188
- function tryRemoveFunctionDecl(
189
- path: NodePath<t.FunctionDeclaration>,
190
- evalReachableScopes: Set<t.Node>,
191
- ): boolean {
146
+ function tryRemoveFunctionDecl(path: NodePath<t.FunctionDeclaration>): boolean {
192
147
  if (isExported(path)) {
193
148
  return false
194
149
  }
195
- // Babel misses the sloppy Annex B alias of a block-level function.
196
- if (hasAnnexBFunctionAlias(path)) {
197
- return false
198
- }
199
150
  const id = path.node.id
200
151
  if (!id) {
201
152
  return false
@@ -211,20 +162,13 @@ function tryRemoveFunctionDecl(
211
162
  if (binding.constantViolations.length > 0) {
212
163
  return false
213
164
  }
214
- if (bindingReachableByEval(binding, evalReachableScopes)) {
215
- return false
216
- }
217
-
218
165
  dereferenceSubtree(path.get('body') as NodePath)
219
166
  path.scope.removeBinding(id.name)
220
167
  path.remove()
221
168
  return true
222
169
  }
223
170
 
224
- function tryRemoveClassDecl(
225
- path: NodePath<t.ClassDeclaration>,
226
- evalReachableScopes: Set<t.Node>,
227
- ): boolean {
171
+ function tryRemoveClassDecl(path: NodePath<t.ClassDeclaration>): boolean {
228
172
  if (isExported(path)) {
229
173
  return false
230
174
  }
@@ -234,7 +178,7 @@ function tryRemoveClassDecl(
234
178
  }
235
179
 
236
180
  // `extends`, computed keys, static fields, and static blocks run on evaluation.
237
- if (path.node.superClass && !isSideEffectFree(path.node.superClass, path.scope, path)) {
181
+ if (path.node.superClass && !isSideEffectFree(path.node.superClass, path.scope)) {
238
182
  return false
239
183
  }
240
184
  for (const member of path.node.body.body) {
@@ -249,7 +193,7 @@ function tryRemoveClassDecl(
249
193
  'computed' in member &&
250
194
  member.computed &&
251
195
  'key' in member &&
252
- !isSideEffectFree(member.key, path.scope, path)
196
+ !isSideEffectFree(member.key, path.scope)
253
197
  ) {
254
198
  return false
255
199
  }
@@ -265,10 +209,6 @@ function tryRemoveClassDecl(
265
209
  if (binding.constantViolations.length > 0) {
266
210
  return false
267
211
  }
268
- if (bindingReachableByEval(binding, evalReachableScopes)) {
269
- return false
270
- }
271
-
272
212
  dereferenceSubtree(path.get('body') as NodePath)
273
213
  if (path.node.superClass) {
274
214
  dereferenceSubtree(path.get('superClass') as NodePath)
@@ -279,7 +219,7 @@ function tryRemoveClassDecl(
279
219
  }
280
220
 
281
221
  function tryRemoveExpressionStatement(path: NodePath<t.ExpressionStatement>): boolean {
282
- if (!isSideEffectFree(path.node.expression, path.scope, path.get('expression'))) {
222
+ if (!isSideEffectFree(path.node.expression, path.scope)) {
283
223
  return false
284
224
  }
285
225
  dereferenceSubtree(path.get('expression') as NodePath)
@@ -4,15 +4,14 @@ 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'
8
- import { hasAnnexBFunctionAlias, referencesOrWritesVariable } from '../utils/paths'
7
+ import { referencesOrWritesVariable } from '../utils/paths'
9
8
  import { mulberry32 } from '../utils/random'
10
9
  import { StringGenerator } from '../utils/string-generator'
11
10
 
12
11
  /**
13
- * Renames bindings and labels with a fresh `StringGenerator`. Each scope avoids
14
- * live ancestor names but otherwise restarts the cursor, allowing safe reuse in
15
- * sibling scopes.
12
+ * Renames bindings and labels with the configured string generator. Generated
13
+ * names do not capture unresolved names or bindings still used from an outer
14
+ * scope. Separate sibling scopes may reuse the same names.
16
15
  *
17
16
  * @example
18
17
  * // ◀️ before
@@ -31,6 +30,7 @@ import { StringGenerator } from '../utils/string-generator'
31
30
  */
32
31
  export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
33
32
  const generator = new StringGenerator(ctx.stringGeneratorMode, mulberry32(ctx.seed))
33
+ const separatedDirectExports = separateDirectExports(ast.program)
34
34
 
35
35
  let programScope: Scope | undefined
36
36
  traverse(ast, {
@@ -40,14 +40,13 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
40
40
  },
41
41
  })
42
42
  if (!programScope) {
43
- return false
43
+ return separatedDirectExports
44
44
  }
45
45
  programScope.crawl()
46
46
 
47
47
  const liveAncestors = new Map<Scope, Set<Binding>>()
48
48
  // Reserve unbound names so generated bindings cannot capture them.
49
49
  const freeNames = new Set<string>()
50
- // `with` and direct eval pin bindings to their source names.
51
50
  const unrenamable = new Set<Binding>()
52
51
  // Reserve pinned source names globally, including before their binding is seen.
53
52
  const pinnedNames = new Set<string>()
@@ -68,9 +67,6 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
68
67
  }
69
68
  return
70
69
  }
71
- if (isInsideWith(path)) {
72
- pin(binding, path.node.name)
73
- }
74
70
  let scope: Scope | undefined = path.scope
75
71
  while (scope && scope !== binding.scope) {
76
72
  let live = liveAncestors.get(scope)
@@ -82,30 +78,6 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
82
78
  scope = scope.parent
83
79
  }
84
80
  },
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
- FunctionDeclaration(path) {
99
- // ECMA-262 B.3.3 gives sloppy block functions an alias Babel does not model.
100
- if (!hasAnnexBFunctionAlias(path)) {
101
- return
102
- }
103
- const name = path.node.id?.name
104
- const binding = name === undefined ? undefined : path.scope.getBinding(name)
105
- if (binding && name !== undefined) {
106
- pin(binding, name)
107
- }
108
- },
109
81
  CatchClause(path) {
110
82
  // Babel splits a catch parameter and same-named `var` that share one runtime
111
83
  // variable. They must keep the same name.
@@ -169,7 +141,7 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
169
141
  })
170
142
 
171
143
  if (bindingToName.size === 0) {
172
- return renameLabels(ast, generator)
144
+ return renameLabels(ast, generator) || separatedDirectExports
173
145
  }
174
146
 
175
147
  traverse(ast, {
@@ -199,6 +171,43 @@ export function renameIdentifiers(ast: File, ctx: TransformContext): boolean {
199
171
  return true
200
172
  }
201
173
 
174
+ function separateDirectExports(program: t.Program): boolean {
175
+ const body: t.Program['body'] = []
176
+ let changed = false
177
+
178
+ for (const statement of program.body) {
179
+ const declaration = t.isExportNamedDeclaration(statement) ? statement.declaration : null
180
+ if (
181
+ !t.isVariableDeclaration(declaration) &&
182
+ !t.isFunctionDeclaration(declaration) &&
183
+ !t.isClassDeclaration(declaration)
184
+ ) {
185
+ body.push(statement)
186
+ continue
187
+ }
188
+
189
+ const names = Object.keys(t.getBindingIdentifiers(declaration))
190
+ if (names.length === 0) {
191
+ body.push(statement)
192
+ continue
193
+ }
194
+
195
+ body.push(
196
+ declaration,
197
+ t.exportNamedDeclaration(
198
+ null,
199
+ names.map((name) => t.exportSpecifier(t.identifier(name), t.identifier(name))),
200
+ ),
201
+ )
202
+ changed = true
203
+ }
204
+
205
+ if (changed) {
206
+ program.body = body
207
+ }
208
+ return changed
209
+ }
210
+
202
211
  function isNonBindingPosition(path: { node: t.Identifier; parent: t.Node }): boolean {
203
212
  const node = path.node
204
213
  const parent = path.parent
@@ -0,0 +1,78 @@
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 { rewriteStatementLists } from '../utils/statement-lists'
6
+ import type { StatementListContainer } from '../utils/statement-lists'
7
+
8
+ /**
9
+ * Moves hoisted function declarations to the end of each program, block, switch
10
+ * case, or static block. It keeps declaration order, including duplicate names.
11
+ *
12
+ * If moving every declaration would turn a string expression into a directive,
13
+ * one declaration stays before the string. Direct exports stay in place.
14
+ * Indirect exports move with their local declaration.
15
+ *
16
+ * @example
17
+ * // ◀️ before
18
+ * function area(r) {
19
+ * return r * r;
20
+ * }
21
+ * log(area(2));
22
+ *
23
+ * // ▶️ after
24
+ * log(area(2));
25
+ * function area(r) {
26
+ * return r * r;
27
+ * }
28
+ */
29
+ export function reorderFunctionDeclarations(ast: File): boolean {
30
+ return rewriteStatementLists(ast, (statements, container) =>
31
+ reorderedStatements(statements, containerAllowsDirective(container)),
32
+ )
33
+ }
34
+
35
+ // Only programs and function bodies recognize directives, so only they need the guard.
36
+ function containerAllowsDirective(container: NodePath<StatementListContainer>): boolean {
37
+ if (container.isProgram()) {
38
+ return true
39
+ }
40
+ return container.isBlockStatement() && container.parentPath.isFunction()
41
+ }
42
+
43
+ function reorderedStatements(
44
+ children: NodePath<t.Statement>[],
45
+ canCreateDirective: boolean,
46
+ ): t.Statement[] | null {
47
+ const rest: t.Statement[] = []
48
+ const moved: t.Statement[] = []
49
+ for (const child of children) {
50
+ if (isMovable(child)) {
51
+ moved.push(child.node)
52
+ } else {
53
+ rest.push(child.node)
54
+ }
55
+ }
56
+
57
+ if (moved.length === 0) {
58
+ return null
59
+ }
60
+ if (canCreateDirective && createsDirective(rest)) {
61
+ rest.unshift(moved.shift()!)
62
+ }
63
+ const reordered = [...rest, ...moved]
64
+ return reordered.every((node, index) => node === children[index]?.node) ? null : reordered
65
+ }
66
+
67
+ function createsDirective(statements: readonly t.Statement[]): boolean {
68
+ const first = statements[0]
69
+ return (
70
+ first !== undefined &&
71
+ first.type === 'ExpressionStatement' &&
72
+ first.expression.type === 'StringLiteral'
73
+ )
74
+ }
75
+
76
+ function isMovable(child: NodePath<t.Statement>): boolean {
77
+ return child.isFunctionDeclaration() && child.node.id !== null
78
+ }
@@ -4,7 +4,6 @@ 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'
8
7
  import type { ChangeState } from '../utils/change-tracking'
9
8
  import { mulberry32 } from '../utils/random'
10
9
  import { StringGenerator } from '../utils/string-generator'
@@ -69,8 +68,8 @@ const visitor: Visitor<State> = {
69
68
  if (path.scope.getBinding(name)) {
70
69
  return
71
70
  }
72
- // Replacements are not write targets. `with` and eval can also shadow globals.
73
- if (isWriteTarget(path) || isInsideWith(path) || enclosingScopeHasDirectEval(path)) {
71
+ // A replacement cannot occupy a write target.
72
+ if (isWriteTarget(path)) {
74
73
  return
75
74
  }
76
75
  const str = t.stringLiteral(state.generator.next())
@@ -47,8 +47,7 @@ const visitor: Visitor<ChangeState> = {
47
47
 
48
48
  function transformSwitch(path: NodePath<t.SwitchStatement>): boolean {
49
49
  const node = path.node
50
- // Separate `if` blocks cannot preserve cross-case lexical scope or Annex B
51
- // function hoisting.
50
+ // Separate `if` blocks cannot preserve cross-case lexical scope.
52
51
  if (hasBlockScopedDeclaration(node)) {
53
52
  return false
54
53
  }
@@ -56,7 +55,7 @@ function transformSwitch(path: NodePath<t.SwitchStatement>): boolean {
56
55
  lowerToIfElseChain(path)
57
56
  return true
58
57
  }
59
- if (!labeledLoweringIsSafe(node, path.scope, path)) {
58
+ if (!labeledLoweringIsSafe(node, path.scope)) {
60
59
  return false
61
60
  }
62
61
  lowerToLabeledBlock(path)
@@ -69,8 +68,7 @@ function hasBlockScopedDeclaration(node: t.SwitchStatement): boolean {
69
68
  if (
70
69
  (t.isVariableDeclaration(stmt) && stmt.kind !== 'var') ||
71
70
  t.isClassDeclaration(stmt) ||
72
- t.isFunctionDeclaration(stmt) ||
73
- isLabeledFunctionDeclaration(stmt)
71
+ t.isFunctionDeclaration(stmt)
74
72
  ) {
75
73
  return true
76
74
  }
@@ -79,29 +77,14 @@ function hasBlockScopedDeclaration(node: t.SwitchStatement): boolean {
79
77
  return false
80
78
  }
81
79
 
82
- // Babel hides an Annex B.3.4 labeled function under LabeledStatement wrappers.
83
- function isLabeledFunctionDeclaration(stmt: t.Statement): boolean {
84
- let current: t.Statement = stmt
85
- while (t.isLabeledStatement(current)) {
86
- current = current.body
87
- }
88
- return t.isFunctionDeclaration(current)
89
- }
90
-
91
80
  // A non-final default needs match results from later cases. Precomputing that
92
81
  // result rereads tests, so every test must be pure in this shape.
93
- function labeledLoweringIsSafe(
94
- node: t.SwitchStatement,
95
- scope: Scope,
96
- referencePath: NodePath,
97
- ): boolean {
82
+ function labeledLoweringIsSafe(node: t.SwitchStatement, scope: Scope): boolean {
98
83
  const defaultIndex = node.cases.findIndex((caseClause) => caseClause.test === null)
99
84
  if (defaultIndex === -1 || defaultIndex === node.cases.length - 1) {
100
85
  return true
101
86
  }
102
- return node.cases.every(
103
- (caseClause) => caseClause.test == null || isPure(caseClause.test, scope, referencePath),
104
- )
87
+ return node.cases.every((caseClause) => caseClause.test == null || isPure(caseClause.test, scope))
105
88
  }
106
89
 
107
90
  function canLowerToIfElseChain(node: t.SwitchStatement): boolean {
@@ -2,7 +2,6 @@ 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 '../utils/ast'
6
5
  import { traverseForChanges } from '../utils/change-tracking'
7
6
  import type { ChangeState } from '../utils/change-tracking'
8
7
  import { isConditionalGate, reorderableToStatement } from '../utils/evaluation-order'
@@ -35,7 +34,7 @@ export function ternaryToIf(ast: File): boolean {
35
34
 
36
35
  const preparationVisitor: Visitor<ChangeState> = {
37
36
  ConditionalExpression(path, state) {
38
- if (!isPathLive(path) || isInsideWith(path)) {
37
+ if (!isPathLive(path)) {
39
38
  return
40
39
  }
41
40
  const stmtPath = path.getStatementParent()
@@ -50,9 +49,8 @@ const preparationVisitor: Visitor<ChangeState> = {
50
49
 
51
50
  const visitor: Visitor<ChangeState> = {
52
51
  ConditionalExpression(path, state) {
53
- // Statement replacement detaches queued siblings. `with` can also shadow the
54
- // synthesized result temporary.
55
- if (!isPathLive(path) || isInsideWith(path)) {
52
+ // Statement replacement detaches queued siblings.
53
+ if (!isPathLive(path)) {
56
54
  return
57
55
  }
58
56
  if (lowerConditional(path)) {
@@ -129,16 +127,14 @@ function lowerConditional(path: NodePath<t.ConditionalExpression>): boolean {
129
127
  t.isIdentifier(parent.node.id) &&
130
128
  parent.node.init === path.node &&
131
129
  parent.parentPath?.isVariableDeclaration() &&
132
- // Splitting lexical declarations would end TDZ early, and bare `const` is
133
- // invalid. Keep their initializer behind a result temp.
134
- parent.parentPath.node.kind === 'var' &&
135
130
  parent.parentPath.node.declarations.length === 1 &&
136
131
  isStandaloneDeclaration(parent.parentPath)
137
132
  ) {
138
133
  const declPath = parent.parentPath
139
134
  const target = parent.node.id
135
+ const declarationKind = declPath.node.kind === 'const' ? 'let' : declPath.node.kind
140
136
  declPath.replaceWithMultiple([
141
- t.variableDeclaration(declPath.node.kind, [t.variableDeclarator(t.cloneNode(target))]),
137
+ t.variableDeclaration(declarationKind, [t.variableDeclarator(t.cloneNode(target))]),
142
138
  t.ifStatement(
143
139
  test,
144
140
  t.blockStatement(seqWithAssignment(consequent, target)),