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
@@ -4,7 +4,6 @@ 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 { isInsideWith } from '../utils/ast'
8
7
  import { traverseForChanges } from '../utils/change-tracking'
9
8
  import type { ChangeState } from '../utils/change-tracking'
10
9
 
@@ -90,8 +89,7 @@ const visitor: Visitor<ChangeState> = {
90
89
  if (!t.isIdentifier(object) || object.name !== 'console') {
91
90
  return
92
91
  }
93
- // `with` can supply a different `console` outside Babel's scope table.
94
- if (path.scope.getBinding('console') || isInsideWith(path)) {
92
+ if (path.scope.getBinding('console')) {
95
93
  return
96
94
  }
97
95
  // Removing the call also removes argument and computed-key evaluation.
@@ -108,11 +106,11 @@ function callArgumentsAreInert(
108
106
  callee: t.MemberExpression | t.OptionalMemberExpression,
109
107
  path: NodePath<t.ExpressionStatement>,
110
108
  ): boolean {
111
- if (callee.computed && !isSideEffectFree(callee.property, path.scope, path)) {
109
+ if (callee.computed && !isSideEffectFree(callee.property, path.scope)) {
112
110
  return false
113
111
  }
114
112
  for (const arg of expr.arguments) {
115
- if (!t.isExpression(arg) || !isSideEffectFree(arg, path.scope, path)) {
113
+ if (!t.isExpression(arg) || !isSideEffectFree(arg, path.scope)) {
116
114
  return false
117
115
  }
118
116
  }
@@ -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
 
@@ -58,10 +57,6 @@ function lowerBinaryAssignment(path: NodePath<t.AssignmentExpression>): boolean
58
57
  const read = t.cloneNode(left, true)
59
58
 
60
59
  if (t.isMemberExpression(left) && t.isMemberExpression(read)) {
61
- // `with` can turn a bare object name into a getter or shadow a memo temp.
62
- if (isInsideWith(path)) {
63
- return false
64
- }
65
60
  const object = left.object
66
61
  if (t.isExpression(object)) {
67
62
  const memo = path.scope.maybeGenerateMemoised(object)
@@ -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
 
@@ -52,10 +51,6 @@ function lowerLogicalAssignment(path: NodePath<t.AssignmentExpression>): boolean
52
51
  const lhs = t.cloneNode(left, true)
53
52
 
54
53
  if (t.isMemberExpression(left) && t.isMemberExpression(lhs)) {
55
- // `with` can turn a bare object name into a getter or shadow a memo temp.
56
- if (isInsideWith(path)) {
57
- return false
58
- }
59
54
  const object = left.object
60
55
  if (t.isExpression(object)) {
61
56
  const memo = path.scope.maybeGenerateMemoised(object)
@@ -3,7 +3,7 @@ import * as t from '@babel/types'
3
3
  import type { File } from '@babel/types'
4
4
 
5
5
  import { initializerReaches } from '../analysis/document-order'
6
- import { isInsideWith, isProtoKey } from '../utils/ast'
6
+ import { isProtoKey } from '../utils/ast'
7
7
  import { traverseForChanges } from '../utils/change-tracking'
8
8
  import type { ChangeState } from '../utils/change-tracking'
9
9
  import { isCalleeOrTagOf } from '../utils/paths'
@@ -172,12 +172,7 @@ function performExtraction(
172
172
  if (!binding) {
173
173
  return false
174
174
  }
175
- if (!binding.constant) {
176
- return false
177
- }
178
-
179
- // `with` can shadow a synthesized entry binding.
180
- if (isInsideWith(path)) {
175
+ if (path.node.kind !== 'const' && !binding.constant) {
181
176
  return false
182
177
  }
183
178
 
@@ -196,15 +191,11 @@ function performExtraction(
196
191
  const seenMembers = new Set<t.MemberExpression>()
197
192
 
198
193
  for (const ref of binding.referencePaths) {
199
- // Before initialization, a wrapper read throws while a bare binding may not.
200
- if (!initializerReaches(path, ref)) {
194
+ // A pre-initializer `var` wrapper read sees `undefined`, while an extracted
195
+ // binding read would not perform the member access.
196
+ if (path.node.kind === 'var' && !initializerReaches(path, ref)) {
201
197
  return false
202
198
  }
203
- // `with` can redirect the rewritten reference.
204
- if (isInsideWith(ref)) {
205
- return false
206
- }
207
-
208
199
  const parent = ref.parentPath
209
200
  if (!parent || !parent.isMemberExpression()) {
210
201
  return false
@@ -4,7 +4,8 @@ import * as t from '@babel/types'
4
4
  import type { File } from '@babel/types'
5
5
 
6
6
  import { evaluateConstant, isOpaque } from '../analysis/constant'
7
- import { enclosingScopeHasDirectEval, isInDirectivePrologue, isInsideWith } from '../utils/ast'
7
+ import { isSideEffectFree } from '../analysis/purity'
8
+ import { isInDirectivePrologue } from '../utils/ast'
8
9
  import type { ChangeState } from '../utils/change-tracking'
9
10
  import { valueToLiteral } from '../utils/literal'
10
11
 
@@ -104,18 +105,13 @@ function collectPatchedIntrinsics(ast: File): Set<string> {
104
105
  markMutationCall(path, poison)
105
106
  },
106
107
  Identifier(path) {
107
- // Global eval and Function can patch any intrinsic outside this scan.
108
108
  const name = path.node.name
109
- if (name !== 'eval' && name !== 'Function') {
109
+ if (name !== 'Function') {
110
110
  return
111
111
  }
112
112
  if (!path.isReferencedIdentifier() || path.scope.getBinding(name)) {
113
113
  return
114
114
  }
115
- if (name === 'eval') {
116
- poison(INTRINSIC_ROOTS)
117
- return
118
- }
119
115
  const parent = path.parentPath
120
116
  if (
121
117
  parent &&
@@ -496,7 +492,7 @@ function isReferenceRequiredPosition(path: NodePath<t.MemberExpression>): boolea
496
492
  }
497
493
 
498
494
  // Folding a leading call to a string can create a directive and change strictness.
499
- function foldsIntoDirective(path: NodePath<t.CallExpression>, replacement: t.Expression): boolean {
495
+ function foldsIntoDirective(path: NodePath, replacement: t.Expression): boolean {
500
496
  if (!t.isStringLiteral(replacement)) {
501
497
  return false
502
498
  }
@@ -506,11 +502,59 @@ function foldsIntoDirective(path: NodePath<t.CallExpression>, replacement: t.Exp
506
502
  )
507
503
  }
508
504
 
509
- function handleCall(path: NodePath<t.CallExpression>, patchedRoots: Set<string>): void {
510
- // `with` and direct eval can shadow names outside Babel's scope table.
511
- if (isInsideWith(path) || enclosingScopeHasDirectEval(path)) {
505
+ function replaceWithPreservedExpression(
506
+ path: NodePath<t.CallExpression | t.MemberExpression>,
507
+ expression: t.Expression,
508
+ result: t.Expression,
509
+ ): void {
510
+ if (!isSideEffectFree(expression, path.scope)) {
511
+ path.replaceWith(t.sequenceExpression([expression, result]))
512
512
  return
513
513
  }
514
+ if (foldsIntoDirective(path, result)) {
515
+ path.replaceWith(t.sequenceExpression([t.numericLiteral(0), result]))
516
+ return
517
+ }
518
+ path.replaceWith(result)
519
+ }
520
+
521
+ function foldArrayLiteralLength(
522
+ path: NodePath<t.MemberExpression>,
523
+ array: t.ArrayExpression,
524
+ ): boolean {
525
+ if (array.elements.some((element) => t.isSpreadElement(element))) {
526
+ return false
527
+ }
528
+ const effects = array.elements.filter(
529
+ (element): element is t.Expression =>
530
+ element !== null && t.isExpression(element) && !isSideEffectFree(element, path.scope),
531
+ )
532
+ const length = t.numericLiteral(array.elements.length)
533
+ path.replaceWith(effects.length === 0 ? length : t.sequenceExpression([...effects, length]))
534
+ return true
535
+ }
536
+
537
+ function foldStringIndex(
538
+ path: NodePath<t.MemberExpression>,
539
+ receiverNode: t.Expression,
540
+ propertyName: string,
541
+ ): boolean {
542
+ const index = Number(propertyName)
543
+ if (!Number.isSafeInteger(index) || index < 0 || String(index) !== propertyName) {
544
+ return false
545
+ }
546
+ const receiver = evaluateConstant(receiverNode, path.scope, path)
547
+ if (!receiver.known || typeof receiver.value !== 'string') {
548
+ return false
549
+ }
550
+ if (index >= receiver.value.length) {
551
+ return false
552
+ }
553
+ replaceWithPreservedExpression(path, receiverNode, t.stringLiteral(receiver.value[index]!))
554
+ return true
555
+ }
556
+
557
+ function handleCall(path: NodePath<t.CallExpression>, patchedRoots: Set<string>): void {
514
558
  const callee = path.node.callee
515
559
 
516
560
  if (t.isIdentifier(callee)) {
@@ -558,11 +602,6 @@ function handleMember(path: NodePath<t.MemberExpression>, patchedRoots: Set<stri
558
602
  if (isReferenceRequiredPosition(path)) {
559
603
  return
560
604
  }
561
- // Delay the ancestor walk until cheap shape checks pass.
562
- if (isInsideWith(path) || enclosingScopeHasDirectEval(path)) {
563
- return
564
- }
565
-
566
605
  const obj = path.node.object
567
606
 
568
607
  if (t.isIdentifier(obj) && !path.scope.getBinding(obj.name) && !patchedRoots.has(obj.name)) {
@@ -589,10 +628,18 @@ function handleMember(path: NodePath<t.MemberExpression>, patchedRoots: Set<stri
589
628
  }
590
629
 
591
630
  if (propName === 'length') {
631
+ if (t.isArrayExpression(obj) && foldArrayLiteralLength(path, obj)) {
632
+ return
633
+ }
592
634
  const recv = evaluateConstant(obj, path.scope, path)
593
635
  if (recv.known && typeof recv.value === 'string') {
594
636
  path.replaceWith(t.numericLiteral(recv.value.length))
595
637
  }
638
+ return
639
+ }
640
+
641
+ if (t.isExpression(obj)) {
642
+ foldStringIndex(path, obj, propName)
596
643
  }
597
644
  }
598
645
 
@@ -625,7 +672,6 @@ function foldGlobalCall(
625
672
  foldNumberCtor(path)
626
673
  return
627
674
  }
628
-
629
675
  if (GLOBAL_FUNCTIONS.has(name)) {
630
676
  if (path.scope.getBinding(name)) {
631
677
  return
@@ -131,6 +131,9 @@ function tryFoldToLiteral(
131
131
  if (!result.known) {
132
132
  return false
133
133
  }
134
+ if (typeof result.value === 'number' && !Number.isFinite(result.value)) {
135
+ return false
136
+ }
134
137
  const lit = valueToLiteral(result.value)
135
138
  if (!lit) {
136
139
  return false
@@ -187,7 +190,7 @@ function tryReassociateCommutative(path: NodePath<t.BinaryExpression>, op: Commu
187
190
  }
188
191
  // Grouping constants can move one operand's coercion past another operand's
189
192
  // evaluation. Multiple non-constant operands must therefore be pure.
190
- if (others.length > 1 && !others.every((leaf) => isPure(leaf, path.scope, path))) {
193
+ if (others.length > 1 && !others.every((leaf) => isPure(leaf, path.scope))) {
191
194
  return false
192
195
  }
193
196
 
@@ -293,7 +296,7 @@ function tryReassociateSubtract(path: NodePath<t.BinaryExpression>): boolean {
293
296
  }
294
297
  // Reassociation can move `base` coercion past a subtractee's evaluation.
295
298
  // Non-constant subtractees must be pure.
296
- if (!otherSubs.every((leaf) => isPure(leaf, path.scope, path))) {
299
+ if (!otherSubs.every((leaf) => isPure(leaf, path.scope))) {
297
300
  return false
298
301
  }
299
302
  const sum = combineConstants(constSubs, '+')
@@ -6,14 +6,13 @@ import { traverseForChanges } from '../utils/change-tracking'
6
6
  import type { ChangeState } from '../utils/change-tracking'
7
7
 
8
8
  /**
9
- * Rewrites safe function declarations as `var` initializers. The function value
10
- * is no longer hoisted, so nothing may observe the binding before assignment.
9
+ * Rewrites function declarations at script top level or directly inside a
10
+ * function body as `var` initializers. It leaves a declaration unchanged when
11
+ * an earlier statement could execute user code before the assignment.
11
12
  *
12
- * Preceding statements must be inert. Name inference and recursive references
13
- * still use the `var` binding.
13
+ * Name inference and recursive references still use the `var` binding.
14
14
  *
15
- * Block-level declarations and module top-level declarations stay unchanged due
16
- * to Annex B aliases and live exports.
15
+ * Block-level and module top-level declarations stay unchanged.
17
16
  *
18
17
  * @example
19
18
  * // ◀️ before
@@ -0,0 +1,262 @@
1
+ import traverse from '@babel/traverse'
2
+ import type { NodePath } from '@babel/traverse'
3
+ import * as t from '@babel/types'
4
+ import type { File } from '@babel/types'
5
+
6
+ import type { ChangeState } from '../utils/change-tracking'
7
+
8
+ /**
9
+ * Moves `var` declarations to the start of their var scope without moving
10
+ * initializer evaluation.
11
+ *
12
+ * Direct exports stay in place. Static blocks keep separate declarations.
13
+ *
14
+ * @example
15
+ * // ◀️ before
16
+ * function f() {
17
+ * var x = 1;
18
+ * if (c) {
19
+ * var y = g();
20
+ * }
21
+ * return x + y;
22
+ * }
23
+ *
24
+ * // ▶️ after
25
+ * function f() {
26
+ * var x, y;
27
+ * x = 1;
28
+ * if (c) {
29
+ * y = g();
30
+ * }
31
+ * return x + y;
32
+ * }
33
+ */
34
+ export function hoistVariables(ast: File): boolean {
35
+ const state: HoistState = {
36
+ changed: false,
37
+ scopes: new Map(),
38
+ }
39
+ traverse(ast, {
40
+ VariableDeclaration(path) {
41
+ if (path.node.kind !== 'var' || path.parentPath.isExportNamedDeclaration()) {
42
+ return
43
+ }
44
+ const owner = ownerPath(path)
45
+ if (owner === null) {
46
+ return
47
+ }
48
+
49
+ const scope = hoistedScope(state.scopes, owner)
50
+ addNames(scope.names, path.getBindingIdentifiers())
51
+ if (scope.declaration?.node === path.node) {
52
+ return
53
+ }
54
+
55
+ demoteDeclaration(path)
56
+ scope.changed = true
57
+ state.changed = true
58
+ },
59
+ })
60
+
61
+ for (const scope of state.scopes.values()) {
62
+ if (scope.changed) {
63
+ finishDeclaration(scope)
64
+ }
65
+ }
66
+
67
+ return state.changed
68
+ }
69
+
70
+ type OwnerPath = NodePath<t.Function | t.Program | t.StaticBlock>
71
+
72
+ interface HoistState extends ChangeState {
73
+ scopes: Map<t.Node, HoistedScope>
74
+ }
75
+
76
+ interface HoistedScope {
77
+ changed: boolean
78
+ declaration: NodePath<t.VariableDeclaration> | null
79
+ names: Map<string, t.Identifier>
80
+ owner: OwnerPath
81
+ }
82
+
83
+ function ownerPath(path: NodePath<t.VariableDeclaration>): OwnerPath | null {
84
+ let current: NodePath | null = path.parentPath
85
+ while (current) {
86
+ if (current.isStaticBlock()) {
87
+ return current as OwnerPath
88
+ }
89
+ if (current.isProgram()) {
90
+ return current as OwnerPath
91
+ }
92
+ if (t.isFunction(current.node) && t.isBlockStatement(current.node.body)) {
93
+ return current as OwnerPath
94
+ }
95
+ current = current.parentPath
96
+ }
97
+ return null
98
+ }
99
+
100
+ function hoistedScope(scopes: Map<t.Node, HoistedScope>, owner: OwnerPath): HoistedScope {
101
+ const existing = scopes.get(owner.node)
102
+ if (existing) {
103
+ return existing
104
+ }
105
+
106
+ const declaration = leadingBareDeclaration(owner)
107
+ const scope: HoistedScope = {
108
+ changed: false,
109
+ declaration,
110
+ names: new Map(),
111
+ owner,
112
+ }
113
+ if (declaration) {
114
+ addNames(scope.names, declaration.getBindingIdentifiers())
115
+ }
116
+ scopes.set(owner.node, scope)
117
+ return scope
118
+ }
119
+
120
+ function leadingBareDeclaration(owner: OwnerPath): NodePath<t.VariableDeclaration> | null {
121
+ const first = ownerBody(owner)[0]
122
+ if (!first?.isVariableDeclaration({ kind: 'var' })) {
123
+ return null
124
+ }
125
+ return first.node.declarations.every((declarator) => declarator.init == null) ? first : null
126
+ }
127
+
128
+ function ownerBody(owner: OwnerPath): NodePath<t.Statement>[] {
129
+ if (owner.isFunction()) {
130
+ const body = owner.get('body')
131
+ return body.isBlockStatement() ? (body.get('body') as NodePath<t.Statement>[]) : []
132
+ }
133
+ return owner.get('body') as NodePath<t.Statement>[]
134
+ }
135
+
136
+ function demoteDeclaration(path: NodePath<t.VariableDeclaration>): void {
137
+ const parent = path.parentPath
138
+ if (parent.isForStatement() && path.key === 'init') {
139
+ demoteForInit(path)
140
+ return
141
+ }
142
+ if ((parent.isForInStatement() || parent.isForOfStatement()) && path.key === 'left') {
143
+ demoteForX(path, parent)
144
+ return
145
+ }
146
+
147
+ const assignments = initializerAssignments(path.node).map((expression) =>
148
+ t.expressionStatement(expression),
149
+ )
150
+
151
+ if (typeof path.listKey === 'string') {
152
+ if (assignments.length === 0) {
153
+ path.remove()
154
+ } else {
155
+ path.replaceWithMultiple(assignments)
156
+ }
157
+ return
158
+ }
159
+ if (assignments.length === 0) {
160
+ path.replaceWith(t.emptyStatement())
161
+ } else if (assignments.length === 1) {
162
+ path.replaceWith(assignments[0]!)
163
+ } else {
164
+ path.replaceWith(t.blockStatement(assignments))
165
+ }
166
+ }
167
+
168
+ function demoteForInit(path: NodePath<t.VariableDeclaration>): void {
169
+ const assignments = initializerAssignments(path.node)
170
+ if (assignments.length === 0) {
171
+ path.remove()
172
+ return
173
+ }
174
+ const replacement = assignments.length === 1 ? assignments[0]! : t.sequenceExpression(assignments)
175
+ path.replaceWith(replacement)
176
+ }
177
+
178
+ function demoteForX(
179
+ path: NodePath<t.VariableDeclaration>,
180
+ parent: NodePath<t.ForInStatement | t.ForOfStatement>,
181
+ ): void {
182
+ const declarator = path.node.declarations[0]!
183
+ const target = declarator.id as t.ArrayPattern | t.Identifier | t.ObjectPattern
184
+ if (declarator.init) {
185
+ const assignment = initializerAssignment(declarator, target)
186
+ const right = parent.get('right') as NodePath<t.Expression>
187
+ right.replaceWith(t.sequenceExpression([assignment, right.node]))
188
+ }
189
+ path.replaceWith(t.cloneNode(target))
190
+ }
191
+
192
+ function initializerAssignments(node: t.VariableDeclaration): t.AssignmentExpression[] {
193
+ const assignments: t.AssignmentExpression[] = []
194
+ for (const declarator of node.declarations) {
195
+ if (declarator.init != null) {
196
+ const target = declarator.id as t.ArrayPattern | t.Identifier | t.ObjectPattern
197
+ assignments.push(initializerAssignment(declarator, target))
198
+ }
199
+ }
200
+ return assignments
201
+ }
202
+
203
+ function initializerAssignment(
204
+ declarator: t.VariableDeclarator,
205
+ target: t.ArrayPattern | t.Identifier | t.ObjectPattern,
206
+ ): t.AssignmentExpression {
207
+ return t.assignmentExpression('=', t.cloneNode(target), declarator.init!)
208
+ }
209
+
210
+ function addNames(
211
+ names: Map<string, t.Identifier>,
212
+ identifiers: Record<string, t.Identifier>,
213
+ ): void {
214
+ for (const [name, identifier] of Object.entries(identifiers)) {
215
+ if (!names.has(name)) {
216
+ names.set(name, identifier)
217
+ }
218
+ }
219
+ }
220
+
221
+ function finishDeclaration(scope: HoistedScope): void {
222
+ const existingNames = new Set<string>()
223
+ if (scope.declaration) {
224
+ for (const name of Object.keys(scope.declaration.getBindingIdentifiers())) {
225
+ existingNames.add(name)
226
+ }
227
+ }
228
+ const declarators = [...scope.names]
229
+ .filter(([name]) => !existingNames.has(name))
230
+ .map(([, identifier]) => t.variableDeclarator(t.cloneNode(identifier)))
231
+
232
+ if (scope.declaration) {
233
+ if (declarators.length > 0) {
234
+ scope.declaration.pushContainer('declarations', declarators)
235
+ }
236
+ return
237
+ }
238
+
239
+ if (declarators.length === 0) {
240
+ return
241
+ }
242
+
243
+ const declaration = t.variableDeclaration('var', declarators)
244
+ insertDeclaration(scope.owner, declaration)
245
+ }
246
+
247
+ function insertDeclaration(owner: OwnerPath, declaration: t.VariableDeclaration): void {
248
+ if (owner.isFunction()) {
249
+ const body = owner.get('body')
250
+ if (body.isBlockStatement()) {
251
+ body.unshiftContainer('body', declaration)
252
+ }
253
+ return
254
+ }
255
+ if (owner.isProgram()) {
256
+ owner.unshiftContainer('body', declaration)
257
+ return
258
+ }
259
+ if (owner.isStaticBlock()) {
260
+ owner.unshiftContainer('body', declaration)
261
+ }
262
+ }