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
@@ -2,7 +2,6 @@ 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'
6
5
  import { traverseForChanges } from '../utils/change-tracking'
7
6
  import type { ChangeState } from '../utils/change-tracking'
8
7
 
@@ -44,18 +43,10 @@ const visitor: Visitor<ChangeState> = {
44
43
  if (!prefix && !valueIsDiscarded(path)) {
45
44
  return
46
45
  }
47
- // `with` can retarget the name and defeat the numeric proof.
48
- if (isInsideWith(path)) {
49
- return
50
- }
51
46
  const binding = path.scope.getBinding(argument.name)
52
47
  if (!isNumericBinding(binding)) {
53
48
  return
54
49
  }
55
- // Direct eval anywhere in the binding's scope can change its type.
56
- if (bindingScopeReachesDirectEval(binding!)) {
57
- return
58
- }
59
50
  const compound = operator === '++' ? '+=' : '-='
60
51
  path.replaceWith(t.assignmentExpression(compound, argument, t.numericLiteral(1)))
61
52
  state.changed = true
@@ -79,34 +70,25 @@ function valueIsDiscarded(path: NodePath<t.UpdateExpression>): boolean {
79
70
  return false
80
71
  }
81
72
 
82
- // The initializer and every mutation must keep the binding numeric.
73
+ // Mutable bindings must stay numeric. A const write cannot change its value.
83
74
  function isNumericBinding(binding: Binding | undefined): boolean {
84
- if (!binding || (binding.kind !== 'var' && binding.kind !== 'let')) {
75
+ if (!binding || (binding.kind !== 'var' && binding.kind !== 'let' && binding.kind !== 'const')) {
85
76
  return false
86
77
  }
87
78
  const declarator = binding.path.node
88
79
  if (!t.isVariableDeclarator(declarator) || !t.isNumericLiteral(declarator.init)) {
89
80
  return false
90
81
  }
91
- // Babel misses the string writes from an Annex B `for-in` initializer.
82
+ // Babel does not record each value assigned by a `for-in` or `for-of` head.
92
83
  if (isForInOfLoopVariable(binding)) {
93
84
  return false
94
85
  }
86
+ if (binding.kind === 'const') {
87
+ return true
88
+ }
95
89
  return binding.constantViolations.every((violation) => keepsNumeric(violation.node))
96
90
  }
97
91
 
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
92
  function isForInOfLoopVariable(binding: Binding): boolean {
111
93
  const declaration = binding.path.parentPath
112
94
  if (!declaration || !declaration.isVariableDeclaration()) {
@@ -7,10 +7,9 @@ import type { ChangeState } from '../utils/change-tracking'
7
7
  import { isLiteralShaped } from '../utils/literal'
8
8
 
9
9
  /**
10
- * Moves a safe literal-shaped right operand to the left of a comparison.
11
- * Relational operators are flipped after swapping.
12
- *
13
- * Unary operations over literals also count when their coercion cannot act.
10
+ * Moves a right operand made from literals and unary operators to the left of a
11
+ * comparison when swapping does not change coercion order. Relational operators
12
+ * are flipped after swapping.
14
13
  *
15
14
  * @example
16
15
  * // ◀️ before
package/src/utils/ast.ts CHANGED
@@ -62,75 +62,10 @@ export function isProtoKey(key: t.Node): boolean {
62
62
  )
63
63
  }
64
64
 
65
- // `with` can shadow names outside Babel's scope table.
66
- export function isInsideWith(path: NodePath): boolean {
67
- if (!programContains(path.scope.getProgramParent().block, programWith, t.isWithStatement)) {
68
- return false
69
- }
70
- return path.findParent((ancestor) => ancestor.isWithStatement()) !== null
71
- }
72
-
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>()
92
- const programWith = new WeakMap<t.Node, boolean>()
93
-
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)
100
- if (cached !== undefined) {
101
- return cached
102
- }
103
- let found = false
104
- t.traverseFast(programNode, (node) => {
105
- if (!matches(node)) {
106
- return
107
- }
108
- found = true
109
- return t.traverseFast.stop
110
- })
111
- cache.set(programNode, found)
112
- return found
113
- }
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.
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
131
68
  return (
132
- (t.isCallExpression(node) || t.isOptionalCallExpression(node)) &&
133
- t.isIdentifier(node.callee) &&
134
- node.callee.name === 'eval'
69
+ 'key' in parent && parent.key === path.node && 'computed' in parent && parent.computed === false
135
70
  )
136
71
  }
@@ -133,7 +133,7 @@ function precedingSiblings<T extends NodePath>(
133
133
  }
134
134
 
135
135
  function isPurePath(path: NodePath): boolean {
136
- return isPure(path.node, path.scope, path)
136
+ return isPure(path.node, path.scope)
137
137
  }
138
138
 
139
139
  function impure(paths: (NodePath | null | undefined)[]): boolean {
@@ -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
+ }
@@ -9,7 +9,7 @@ import { isStandaloneDeclaration } from './paths'
9
9
  // x = L;
10
10
  // if (<fallbackTest(x)>) x = R;
11
11
  //
12
- // `fallbackTest` selects `!x`, `x`, or `x == null`. The parent must accept a
12
+ // `fallbackTest` selects the short-circuit condition. The parent must accept a
13
13
  // multi-statement replacement.
14
14
  export function lowerLogicalWriteStatement(
15
15
  path: NodePath<t.LogicalExpression>,
@@ -29,21 +29,20 @@ export function lowerLogicalWriteStatement(
29
29
  const declStmt = parent.parentPath
30
30
  if (
31
31
  !declStmt?.isVariableDeclaration() ||
32
- // The split would emit illegal `const x;` and then write it.
33
- declStmt.node.kind === 'const' ||
34
32
  declStmt.node.declarations.length !== 1 ||
35
33
  !isStandaloneDeclaration(declStmt)
36
34
  ) {
37
35
  return false
38
36
  }
39
37
  const target = parent.node.id
40
- // The split writes `x` before evaluating the right side. Refuse any right
41
- // side that could observe the new value or the ended TDZ.
42
- if (rightCanObserveTarget(node.right, target.name)) {
38
+ // A `var` initializer can observe its previous value while evaluating the
39
+ // right side. Reads before lexical initialization are intentionally ignored.
40
+ if (declStmt.node.kind === 'var' && rightCanObserveTarget(node.right, target.name)) {
43
41
  return false
44
42
  }
43
+ const declarationKind = declStmt.node.kind === 'const' ? 'let' : declStmt.node.kind
45
44
  declStmt.replaceWithMultiple([
46
- t.variableDeclaration(declStmt.node.kind, [
45
+ t.variableDeclaration(declarationKind, [
47
46
  t.variableDeclarator(t.cloneNode(target), node.left),
48
47
  ]),
49
48
  guardedReassignment(target, fallbackTest, node.right),
@@ -68,10 +67,7 @@ export function lowerLogicalWriteStatement(
68
67
  return false
69
68
  }
70
69
  // Writing first would change `x = 0 || x` before the right side reads `x`.
71
- if (
72
- !isSideEffectFree(node.right, path.scope, path) ||
73
- referencesName(node.right, target.name)
74
- ) {
70
+ if (!isSideEffectFree(node.right, path.scope) || referencesName(node.right, target.name)) {
75
71
  return false
76
72
  }
77
73
  exprStmt.replaceWithMultiple([
@@ -0,0 +1,23 @@
1
+ import * as t from '@babel/types'
2
+
3
+ export function buildNullishCheck(
4
+ firstRead: t.Expression,
5
+ secondRead: t.Expression,
6
+ ): t.ConditionalExpression {
7
+ return t.conditionalExpression(
8
+ t.binaryExpression('===', firstRead, t.nullLiteral()),
9
+ t.booleanLiteral(true),
10
+ t.binaryExpression('===', secondRead, t.unaryExpression('void', t.numericLiteral(0))),
11
+ )
12
+ }
13
+
14
+ export function buildNonNullishCheck(
15
+ firstRead: t.Expression,
16
+ secondRead: t.Expression,
17
+ ): t.ConditionalExpression {
18
+ return t.conditionalExpression(
19
+ t.binaryExpression('!==', firstRead, t.nullLiteral()),
20
+ t.binaryExpression('!==', secondRead, t.unaryExpression('void', t.numericLiteral(0))),
21
+ t.booleanLiteral(false),
22
+ )
23
+ }
@@ -69,35 +69,3 @@ export function isInStrictContext(path: NodePath): boolean {
69
69
  function hasUseStrictDirective(directives: readonly t.Directive[]): boolean {
70
70
  return directives.some((directive) => directive.value.value === 'use strict')
71
71
  }
72
-
73
- // ECMA-262 B.3.3 gives sloppy block-level functions a `var` alias that Babel
74
- // omits from the enclosing scope. Renaming, moving, or removing one can expose it.
75
- export function hasAnnexBFunctionAlias(path: NodePath): boolean {
76
- if (!path.isFunctionDeclaration()) {
77
- return false
78
- }
79
- const parent = path.parentPath
80
- if (!parent || parent.isProgram()) {
81
- return false
82
- }
83
- if (
84
- parent.isBlockStatement() &&
85
- parent.key === 'body' &&
86
- parent.parentPath != null &&
87
- parent.parentPath.isFunction()
88
- ) {
89
- return false
90
- }
91
- return !isInStrictContext(parent)
92
- }
93
-
94
- // A later write to `const` must keep throwing after transformation.
95
- export function hasConstantViolation(path: NodePath<t.VariableDeclaration>): boolean {
96
- for (const name of Object.keys(path.getBindingIdentifiers())) {
97
- const binding = path.scope.getBinding(name)
98
- if (binding && binding.constantViolations.length > 0) {
99
- return true
100
- }
101
- }
102
- return false
103
- }
@@ -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
+ }