webfuscator 0.9.4

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 (289) hide show
  1. package/DISCLOSURE +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +83 -0
  4. package/dist/analysis/constant.d.ts +11 -0
  5. package/dist/analysis/constant.d.ts.map +1 -0
  6. package/dist/analysis/constant.js +261 -0
  7. package/dist/analysis/constant.js.map +1 -0
  8. package/dist/analysis/document-order.d.ts +5 -0
  9. package/dist/analysis/document-order.d.ts.map +1 -0
  10. package/dist/analysis/document-order.js +124 -0
  11. package/dist/analysis/document-order.js.map +1 -0
  12. package/dist/analysis/inlineability.d.ts +13 -0
  13. package/dist/analysis/inlineability.d.ts.map +1 -0
  14. package/dist/analysis/inlineability.js +270 -0
  15. package/dist/analysis/inlineability.js.map +1 -0
  16. package/dist/analysis/purity.d.ts +5 -0
  17. package/dist/analysis/purity.d.ts.map +1 -0
  18. package/dist/analysis/purity.js +281 -0
  19. package/dist/analysis/purity.js.map +1 -0
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +2 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/obfuscator.d.ts +4 -0
  25. package/dist/obfuscator.d.ts.map +1 -0
  26. package/dist/obfuscator.js +222 -0
  27. package/dist/obfuscator.js.map +1 -0
  28. package/dist/options.d.ts +73 -0
  29. package/dist/options.d.ts.map +1 -0
  30. package/dist/options.js +2 -0
  31. package/dist/options.js.map +1 -0
  32. package/dist/preparation/dot-to-bracket.d.ts +16 -0
  33. package/dist/preparation/dot-to-bracket.d.ts.map +1 -0
  34. package/dist/preparation/dot-to-bracket.js +36 -0
  35. package/dist/preparation/dot-to-bracket.js.map +1 -0
  36. package/dist/preparation/expand-destructuring.d.ts +26 -0
  37. package/dist/preparation/expand-destructuring.d.ts.map +1 -0
  38. package/dist/preparation/expand-destructuring.js +154 -0
  39. package/dist/preparation/expand-destructuring.js.map +1 -0
  40. package/dist/preparation/expand-template-literals.d.ts +23 -0
  41. package/dist/preparation/expand-template-literals.d.ts.map +1 -0
  42. package/dist/preparation/expand-template-literals.js +78 -0
  43. package/dist/preparation/expand-template-literals.js.map +1 -0
  44. package/dist/preparation/rewrite-class-fields.d.ts +43 -0
  45. package/dist/preparation/rewrite-class-fields.d.ts.map +1 -0
  46. package/dist/preparation/rewrite-class-fields.js +270 -0
  47. package/dist/preparation/rewrite-class-fields.js.map +1 -0
  48. package/dist/preparation/split-sequence-expressions.d.ts +15 -0
  49. package/dist/preparation/split-sequence-expressions.d.ts.map +1 -0
  50. package/dist/preparation/split-sequence-expressions.js +36 -0
  51. package/dist/preparation/split-sequence-expressions.js.map +1 -0
  52. package/dist/preparation/split-variable-declarations.d.ts +20 -0
  53. package/dist/preparation/split-variable-declarations.d.ts.map +1 -0
  54. package/dist/preparation/split-variable-declarations.js +36 -0
  55. package/dist/preparation/split-variable-declarations.js.map +1 -0
  56. package/dist/preparation/stringify-object-keys.d.ts +21 -0
  57. package/dist/preparation/stringify-object-keys.d.ts.map +1 -0
  58. package/dist/preparation/stringify-object-keys.js +96 -0
  59. package/dist/preparation/stringify-object-keys.js.map +1 -0
  60. package/dist/preparation/wrap-single-statements.d.ts +24 -0
  61. package/dist/preparation/wrap-single-statements.d.ts.map +1 -0
  62. package/dist/preparation/wrap-single-statements.js +53 -0
  63. package/dist/preparation/wrap-single-statements.js.map +1 -0
  64. package/dist/transforms/arguments-to-parameters.d.ts +20 -0
  65. package/dist/transforms/arguments-to-parameters.d.ts.map +1 -0
  66. package/dist/transforms/arguments-to-parameters.js +162 -0
  67. package/dist/transforms/arguments-to-parameters.js.map +1 -0
  68. package/dist/transforms/arrow-to-function.d.ts +46 -0
  69. package/dist/transforms/arrow-to-function.d.ts.map +1 -0
  70. package/dist/transforms/arrow-to-function.js +203 -0
  71. package/dist/transforms/arrow-to-function.js.map +1 -0
  72. package/dist/transforms/collapse-single-use-temps.d.ts +16 -0
  73. package/dist/transforms/collapse-single-use-temps.d.ts.map +1 -0
  74. package/dist/transforms/collapse-single-use-temps.js +805 -0
  75. package/dist/transforms/collapse-single-use-temps.js.map +1 -0
  76. package/dist/transforms/const-let-to-var.d.ts +21 -0
  77. package/dist/transforms/const-let-to-var.d.ts.map +1 -0
  78. package/dist/transforms/const-let-to-var.js +217 -0
  79. package/dist/transforms/const-let-to-var.js.map +1 -0
  80. package/dist/transforms/const-to-let.d.ts +17 -0
  81. package/dist/transforms/const-to-let.d.ts.map +1 -0
  82. package/dist/transforms/const-to-let.js +33 -0
  83. package/dist/transforms/const-to-let.js.map +1 -0
  84. package/dist/transforms/do-while-to-while.d.ts +26 -0
  85. package/dist/transforms/do-while-to-while.d.ts.map +1 -0
  86. package/dist/transforms/do-while-to-while.js +54 -0
  87. package/dist/transforms/do-while-to-while.js.map +1 -0
  88. package/dist/transforms/drop-console.d.ts +27 -0
  89. package/dist/transforms/drop-console.d.ts.map +1 -0
  90. package/dist/transforms/drop-console.js +107 -0
  91. package/dist/transforms/drop-console.js.map +1 -0
  92. package/dist/transforms/drop-debugger.d.ts +18 -0
  93. package/dist/transforms/drop-debugger.d.ts.map +1 -0
  94. package/dist/transforms/drop-debugger.js +34 -0
  95. package/dist/transforms/drop-debugger.js.map +1 -0
  96. package/dist/transforms/expand-binary-assignment.d.ts +26 -0
  97. package/dist/transforms/expand-binary-assignment.d.ts.map +1 -0
  98. package/dist/transforms/expand-binary-assignment.js +78 -0
  99. package/dist/transforms/expand-binary-assignment.js.map +1 -0
  100. package/dist/transforms/expand-logical-assignment.d.ts +22 -0
  101. package/dist/transforms/expand-logical-assignment.d.ts.map +1 -0
  102. package/dist/transforms/expand-logical-assignment.js +73 -0
  103. package/dist/transforms/expand-logical-assignment.js.map +1 -0
  104. package/dist/transforms/extract-object-properties.d.ts +21 -0
  105. package/dist/transforms/extract-object-properties.d.ts.map +1 -0
  106. package/dist/transforms/extract-object-properties.js +340 -0
  107. package/dist/transforms/extract-object-properties.js.map +1 -0
  108. package/dist/transforms/fold-builtin-methods.d.ts +23 -0
  109. package/dist/transforms/fold-builtin-methods.d.ts.map +1 -0
  110. package/dist/transforms/fold-builtin-methods.js +839 -0
  111. package/dist/transforms/fold-builtin-methods.js.map +1 -0
  112. package/dist/transforms/fold-constants.d.ts +16 -0
  113. package/dist/transforms/fold-constants.d.ts.map +1 -0
  114. package/dist/transforms/fold-constants.js +421 -0
  115. package/dist/transforms/fold-constants.js.map +1 -0
  116. package/dist/transforms/for-to-while.d.ts +29 -0
  117. package/dist/transforms/for-to-while.d.ts.map +1 -0
  118. package/dist/transforms/for-to-while.js +91 -0
  119. package/dist/transforms/for-to-while.js.map +1 -0
  120. package/dist/transforms/function-declaration-to-expression.d.ts +28 -0
  121. package/dist/transforms/function-declaration-to-expression.d.ts.map +1 -0
  122. package/dist/transforms/function-declaration-to-expression.js +95 -0
  123. package/dist/transforms/function-declaration-to-expression.js.map +1 -0
  124. package/dist/transforms/inline-functions.d.ts +16 -0
  125. package/dist/transforms/inline-functions.d.ts.map +1 -0
  126. package/dist/transforms/inline-functions.js +896 -0
  127. package/dist/transforms/inline-functions.js.map +1 -0
  128. package/dist/transforms/logical-to-ternary.d.ts +20 -0
  129. package/dist/transforms/logical-to-ternary.d.ts.map +1 -0
  130. package/dist/transforms/logical-to-ternary.js +78 -0
  131. package/dist/transforms/logical-to-ternary.js.map +1 -0
  132. package/dist/transforms/mangle-properties.d.ts +27 -0
  133. package/dist/transforms/mangle-properties.d.ts.map +1 -0
  134. package/dist/transforms/mangle-properties.js +703 -0
  135. package/dist/transforms/mangle-properties.js.map +1 -0
  136. package/dist/transforms/nullish-coalescing-to-ternary.d.ts +18 -0
  137. package/dist/transforms/nullish-coalescing-to-ternary.d.ts.map +1 -0
  138. package/dist/transforms/nullish-coalescing-to-ternary.js +61 -0
  139. package/dist/transforms/nullish-coalescing-to-ternary.js.map +1 -0
  140. package/dist/transforms/numbers-to-strings.d.ts +19 -0
  141. package/dist/transforms/numbers-to-strings.d.ts.map +1 -0
  142. package/dist/transforms/numbers-to-strings.js +51 -0
  143. package/dist/transforms/numbers-to-strings.js.map +1 -0
  144. package/dist/transforms/object-method-to-property.d.ts +34 -0
  145. package/dist/transforms/object-method-to-property.d.ts.map +1 -0
  146. package/dist/transforms/object-method-to-property.js +78 -0
  147. package/dist/transforms/object-method-to-property.js.map +1 -0
  148. package/dist/transforms/optional-chaining-to-ternary.d.ts +19 -0
  149. package/dist/transforms/optional-chaining-to-ternary.d.ts.map +1 -0
  150. package/dist/transforms/optional-chaining-to-ternary.js +206 -0
  151. package/dist/transforms/optional-chaining-to-ternary.js.map +1 -0
  152. package/dist/transforms/pack-declarations-into-parameters.d.ts +22 -0
  153. package/dist/transforms/pack-declarations-into-parameters.d.ts.map +1 -0
  154. package/dist/transforms/pack-declarations-into-parameters.js +208 -0
  155. package/dist/transforms/pack-declarations-into-parameters.js.map +1 -0
  156. package/dist/transforms/remove-anonymous-function-names.d.ts +17 -0
  157. package/dist/transforms/remove-anonymous-function-names.d.ts.map +1 -0
  158. package/dist/transforms/remove-anonymous-function-names.js +72 -0
  159. package/dist/transforms/remove-anonymous-function-names.js.map +1 -0
  160. package/dist/transforms/remove-unreachable-code.d.ts +21 -0
  161. package/dist/transforms/remove-unreachable-code.d.ts.map +1 -0
  162. package/dist/transforms/remove-unreachable-code.js +103 -0
  163. package/dist/transforms/remove-unreachable-code.js.map +1 -0
  164. package/dist/transforms/remove-unused-code.d.ts +18 -0
  165. package/dist/transforms/remove-unused-code.d.ts.map +1 -0
  166. package/dist/transforms/remove-unused-code.js +249 -0
  167. package/dist/transforms/remove-unused-code.js.map +1 -0
  168. package/dist/transforms/rename-identifiers.d.ts +24 -0
  169. package/dist/transforms/rename-identifiers.d.ts.map +1 -0
  170. package/dist/transforms/rename-identifiers.js +264 -0
  171. package/dist/transforms/rename-identifiers.js.map +1 -0
  172. package/dist/transforms/specials-to-strings.d.ts +30 -0
  173. package/dist/transforms/specials-to-strings.d.ts.map +1 -0
  174. package/dist/transforms/specials-to-strings.js +101 -0
  175. package/dist/transforms/specials-to-strings.js.map +1 -0
  176. package/dist/transforms/switch-to-if.d.ts +28 -0
  177. package/dist/transforms/switch-to-if.d.ts.map +1 -0
  178. package/dist/transforms/switch-to-if.js +279 -0
  179. package/dist/transforms/switch-to-if.js.map +1 -0
  180. package/dist/transforms/ternary-to-if.d.ts +20 -0
  181. package/dist/transforms/ternary-to-if.d.ts.map +1 -0
  182. package/dist/transforms/ternary-to-if.js +315 -0
  183. package/dist/transforms/ternary-to-if.js.map +1 -0
  184. package/dist/transforms/update-to-assignment.d.ts +26 -0
  185. package/dist/transforms/update-to-assignment.d.ts.map +1 -0
  186. package/dist/transforms/update-to-assignment.js +120 -0
  187. package/dist/transforms/update-to-assignment.js.map +1 -0
  188. package/dist/transforms/yodify-conditions.d.ts +22 -0
  189. package/dist/transforms/yodify-conditions.d.ts.map +1 -0
  190. package/dist/transforms/yodify-conditions.js +91 -0
  191. package/dist/transforms/yodify-conditions.js.map +1 -0
  192. package/dist/utils/ast.d.ts +10 -0
  193. package/dist/utils/ast.d.ts.map +1 -0
  194. package/dist/utils/ast.js +112 -0
  195. package/dist/utils/ast.js.map +1 -0
  196. package/dist/utils/change-tracking.d.ts +8 -0
  197. package/dist/utils/change-tracking.d.ts.map +1 -0
  198. package/dist/utils/change-tracking.js +8 -0
  199. package/dist/utils/change-tracking.js.map +1 -0
  200. package/dist/utils/dom-properties.d.ts +2 -0
  201. package/dist/utils/dom-properties.d.ts.map +1 -0
  202. package/dist/utils/dom-properties.js +9252 -0
  203. package/dist/utils/dom-properties.js.map +1 -0
  204. package/dist/utils/evaluation-order.d.ts +4 -0
  205. package/dist/utils/evaluation-order.d.ts.map +1 -0
  206. package/dist/utils/evaluation-order.js +152 -0
  207. package/dist/utils/evaluation-order.js.map +1 -0
  208. package/dist/utils/literal.d.ts +5 -0
  209. package/dist/utils/literal.d.ts.map +1 -0
  210. package/dist/utils/literal.js +90 -0
  211. package/dist/utils/literal.js.map +1 -0
  212. package/dist/utils/logical-lowering.d.ts +4 -0
  213. package/dist/utils/logical-lowering.d.ts.map +1 -0
  214. package/dist/utils/logical-lowering.js +104 -0
  215. package/dist/utils/logical-lowering.js.map +1 -0
  216. package/dist/utils/loop-lowering.d.ts +23 -0
  217. package/dist/utils/loop-lowering.d.ts.map +1 -0
  218. package/dist/utils/loop-lowering.js +88 -0
  219. package/dist/utils/loop-lowering.js.map +1 -0
  220. package/dist/utils/paths.d.ts +9 -0
  221. package/dist/utils/paths.d.ts.map +1 -0
  222. package/dist/utils/paths.js +90 -0
  223. package/dist/utils/paths.js.map +1 -0
  224. package/dist/utils/random.d.ts +3 -0
  225. package/dist/utils/random.d.ts.map +1 -0
  226. package/dist/utils/random.js +22 -0
  227. package/dist/utils/random.js.map +1 -0
  228. package/dist/utils/string-generator.d.ts +37 -0
  229. package/dist/utils/string-generator.d.ts.map +1 -0
  230. package/dist/utils/string-generator.js +175 -0
  231. package/dist/utils/string-generator.js.map +1 -0
  232. package/package.json +76 -0
  233. package/src/analysis/constant.ts +310 -0
  234. package/src/analysis/document-order.ts +141 -0
  235. package/src/analysis/inlineability.ts +336 -0
  236. package/src/analysis/purity.ts +315 -0
  237. package/src/index.ts +2 -0
  238. package/src/obfuscator.ts +271 -0
  239. package/src/options.ts +114 -0
  240. package/src/preparation/dot-to-bracket.ts +41 -0
  241. package/src/preparation/expand-destructuring.ts +211 -0
  242. package/src/preparation/expand-template-literals.ts +92 -0
  243. package/src/preparation/rewrite-class-fields.ts +320 -0
  244. package/src/preparation/split-sequence-expressions.ts +40 -0
  245. package/src/preparation/split-variable-declarations.ts +39 -0
  246. package/src/preparation/stringify-object-keys.ts +107 -0
  247. package/src/preparation/wrap-single-statements.ts +63 -0
  248. package/src/transforms/arguments-to-parameters.ts +187 -0
  249. package/src/transforms/arrow-to-function.ts +225 -0
  250. package/src/transforms/collapse-single-use-temps.ts +957 -0
  251. package/src/transforms/const-let-to-var.ts +246 -0
  252. package/src/transforms/const-to-let.ts +38 -0
  253. package/src/transforms/do-while-to-while.ts +69 -0
  254. package/src/transforms/drop-console.ts +119 -0
  255. package/src/transforms/drop-debugger.ts +38 -0
  256. package/src/transforms/expand-binary-assignment.ts +94 -0
  257. package/src/transforms/expand-logical-assignment.ts +84 -0
  258. package/src/transforms/extract-object-properties.ts +404 -0
  259. package/src/transforms/fold-builtin-methods.ts +949 -0
  260. package/src/transforms/fold-constants.ts +462 -0
  261. package/src/transforms/for-to-while.ts +109 -0
  262. package/src/transforms/function-declaration-to-expression.ts +108 -0
  263. package/src/transforms/inline-functions.ts +1074 -0
  264. package/src/transforms/logical-to-ternary.ts +92 -0
  265. package/src/transforms/mangle-properties.ts +819 -0
  266. package/src/transforms/nullish-coalescing-to-ternary.ts +77 -0
  267. package/src/transforms/numbers-to-strings.ts +61 -0
  268. package/src/transforms/object-method-to-property.ts +86 -0
  269. package/src/transforms/optional-chaining-to-ternary.ts +225 -0
  270. package/src/transforms/pack-declarations-into-parameters.ts +233 -0
  271. package/src/transforms/remove-anonymous-function-names.ts +84 -0
  272. package/src/transforms/remove-unreachable-code.ts +115 -0
  273. package/src/transforms/remove-unused-code.ts +296 -0
  274. package/src/transforms/rename-identifiers.ts +289 -0
  275. package/src/transforms/specials-to-strings.ts +116 -0
  276. package/src/transforms/switch-to-if.ts +339 -0
  277. package/src/transforms/ternary-to-if.ts +387 -0
  278. package/src/transforms/update-to-assignment.ts +132 -0
  279. package/src/transforms/yodify-conditions.ts +103 -0
  280. package/src/utils/ast.ts +129 -0
  281. package/src/utils/change-tracking.ts +15 -0
  282. package/src/utils/dom-properties.ts +9252 -0
  283. package/src/utils/evaluation-order.ts +178 -0
  284. package/src/utils/literal.ts +96 -0
  285. package/src/utils/logical-lowering.ts +132 -0
  286. package/src/utils/loop-lowering.ts +109 -0
  287. package/src/utils/paths.ts +103 -0
  288. package/src/utils/random.ts +22 -0
  289. package/src/utils/string-generator.ts +206 -0
@@ -0,0 +1,462 @@
1
+ import type { NodePath, Visitor } from '@babel/traverse'
2
+ import * as t from '@babel/types'
3
+ import type { File } from '@babel/types'
4
+
5
+ import { evaluateConstant } from 'src/analysis/constant'
6
+ import { isPure } from 'src/analysis/purity'
7
+ import { traverseForChanges } from 'src/utils/change-tracking'
8
+ import type { ChangeState } from 'src/utils/change-tracking'
9
+ import { valueToLiteral } from 'src/utils/literal'
10
+ import { isCalleeOrTagOf } from 'src/utils/paths'
11
+
12
+ /**
13
+ * Folds pure constant expressions to literals via `evaluateConstant`.
14
+ * Logical and conditional nodes can fold directly to a chosen nonliteral branch.
15
+ *
16
+ * @example
17
+ * // ◀️ before
18
+ * var v = 1 + 2 * 3;
19
+ * if (true && cond) f();
20
+ *
21
+ * // ▶️ after
22
+ * var v = 7;
23
+ * if (cond) f();
24
+ */
25
+ export function foldConstants(ast: File): boolean {
26
+ return traverseForChanges(ast, visitor)
27
+ }
28
+
29
+ const visitor: Visitor<ChangeState> = {
30
+ UnaryExpression: {
31
+ exit(path, state) {
32
+ if (tryFoldExprNode(path)) {
33
+ state.changed = true
34
+ }
35
+ },
36
+ },
37
+ BinaryExpression: {
38
+ exit(path, state) {
39
+ if (tryFoldExprNode(path)) {
40
+ state.changed = true
41
+ }
42
+ },
43
+ },
44
+ LogicalExpression: {
45
+ exit(path, state) {
46
+ if (tryFoldExprNode(path)) {
47
+ state.changed = true
48
+ }
49
+ },
50
+ },
51
+ ConditionalExpression: {
52
+ exit(path, state) {
53
+ if (tryFoldExprNode(path)) {
54
+ state.changed = true
55
+ }
56
+ },
57
+ },
58
+ }
59
+
60
+ function tryFoldExprNode(path: NodePath): boolean {
61
+ if (path.isUnaryExpression()) {
62
+ return tryFoldToLiteral(path)
63
+ }
64
+ if (path.isBinaryExpression()) {
65
+ if (tryFoldToLiteral(path)) {
66
+ return true
67
+ }
68
+ return tryReassociate(path)
69
+ }
70
+ if (path.isLogicalExpression()) {
71
+ return tryFoldLogical(path)
72
+ }
73
+ if (path.isConditionalExpression()) {
74
+ return tryFoldConditional(path)
75
+ }
76
+ return false
77
+ }
78
+
79
+ function tryFoldLogical(path: NodePath<t.LogicalExpression>): boolean {
80
+ // Folding a callee to a member reference can manufacture a `this` binding.
81
+ if (isCalleeOrTagOf(path) || isReferenceSensitiveUnaryOperand(path)) {
82
+ return false
83
+ }
84
+ const left = evaluateConstant(path.node.left, path.scope, path)
85
+ if (!left.known) {
86
+ return false
87
+ }
88
+ const op = path.node.operator
89
+ let chosen: t.Expression
90
+ if (op === '&&') {
91
+ chosen = left.value ? path.node.right : path.node.left
92
+ } else if (op === '||') {
93
+ chosen = left.value ? path.node.left : path.node.right
94
+ } else if (op === '??') {
95
+ chosen = left.value === null || left.value === undefined ? path.node.right : path.node.left
96
+ } else {
97
+ return false
98
+ }
99
+ path.replaceWith(chosen)
100
+ return true
101
+ }
102
+
103
+ function tryFoldConditional(path: NodePath<t.ConditionalExpression>): boolean {
104
+ if (isCalleeOrTagOf(path) || isReferenceSensitiveUnaryOperand(path)) {
105
+ return false
106
+ }
107
+ const test = evaluateConstant(path.node.test, path.scope, path)
108
+ if (!test.known) {
109
+ return false
110
+ }
111
+ path.replaceWith(test.value ? path.node.consequent : path.node.alternate)
112
+ return true
113
+ }
114
+
115
+ // `delete` and `typeof` distinguish references from values. Folding their operand
116
+ // can change deletion, strict syntax, or an unresolved-name exception.
117
+ function isReferenceSensitiveUnaryOperand(path: NodePath): boolean {
118
+ const parent = path.parentPath
119
+ return (
120
+ parent != null &&
121
+ parent.isUnaryExpression() &&
122
+ (parent.node.operator === 'delete' || parent.node.operator === 'typeof') &&
123
+ parent.node.argument === path.node
124
+ )
125
+ }
126
+
127
+ function tryFoldToLiteral(
128
+ path: NodePath<t.UnaryExpression> | NodePath<t.BinaryExpression>,
129
+ ): boolean {
130
+ const result = evaluateConstant(path.node, path.scope, path)
131
+ if (!result.known) {
132
+ return false
133
+ }
134
+ const lit = valueToLiteral(result.value)
135
+ if (!lit) {
136
+ return false
137
+ }
138
+ // Babel revisits replacements, so an identical replacement would loop.
139
+ if (structurallyEqual(path.node, lit)) {
140
+ return false
141
+ }
142
+ path.replaceWith(lit)
143
+ return true
144
+ }
145
+
146
+ type SafeConstant = { kind: 'number'; value: number } | { kind: 'bigint'; value: bigint }
147
+
148
+ type CommutativeOp = '*' | '&' | '|' | '^'
149
+
150
+ function tryReassociate(path: NodePath<t.BinaryExpression>): boolean {
151
+ const op = path.node.operator
152
+ if (op === '*' || op === '&' || op === '|' || op === '^') {
153
+ return tryReassociateCommutative(path, op)
154
+ }
155
+ if (op === '-') {
156
+ return tryReassociateSubtract(path)
157
+ }
158
+ return false
159
+ }
160
+
161
+ function tryReassociateCommutative(path: NodePath<t.BinaryExpression>, op: CommutativeOp): boolean {
162
+ const parent = path.parentPath
163
+ if (parent && parent.isBinaryExpression() && parent.node.operator === op) {
164
+ return false
165
+ }
166
+
167
+ const leaves: t.Expression[] = []
168
+ collectCommutativeChain(path.node, op, leaves)
169
+
170
+ const constants: SafeConstant[] = []
171
+ const others: t.Expression[] = []
172
+ for (const leaf of leaves) {
173
+ const c = asSafeConstant(leaf)
174
+ if (c === null) {
175
+ others.push(leaf)
176
+ } else {
177
+ constants.push(c)
178
+ }
179
+ }
180
+ if (constants.length < 2) {
181
+ return false
182
+ }
183
+ // IEEE-754 multiplication is not associative. BigInt and Int32 bitwise chains
184
+ // are exact, while a wholly constant chain can be evaluated as written.
185
+ if (op === '*' && others.length > 0 && constants.some((c) => c.kind === 'number')) {
186
+ return false
187
+ }
188
+ // Grouping constants can move one operand's coercion past another operand's
189
+ // evaluation. Multiple non-constant operands must therefore be pure.
190
+ if (others.length > 1 && !others.every((leaf) => isPure(leaf, path.scope, path))) {
191
+ return false
192
+ }
193
+
194
+ const combined = combineConstants(constants, op)
195
+ if (combined === null) {
196
+ return false
197
+ }
198
+ const combinedLit = constantToLiteral(combined)
199
+
200
+ let replacement: t.Expression
201
+ if (others.length === 0) {
202
+ replacement = combinedLit
203
+ } else {
204
+ replacement = others[0]!
205
+ for (let i = 1; i < others.length; i++) {
206
+ replacement = t.binaryExpression(op, replacement, others[i]!)
207
+ }
208
+ replacement = t.binaryExpression(op, replacement, combinedLit)
209
+ }
210
+ path.replaceWith(replacement)
211
+ return true
212
+ }
213
+
214
+ function collectCommutativeChain(
215
+ node: t.BinaryExpression,
216
+ op: CommutativeOp,
217
+ out: t.Expression[],
218
+ ): void {
219
+ if (t.isBinaryExpression(node.left) && node.left.operator === op) {
220
+ collectCommutativeChain(node.left, op, out)
221
+ } else if (t.isExpression(node.left)) {
222
+ out.push(node.left)
223
+ }
224
+ if (t.isBinaryExpression(node.right) && node.right.operator === op) {
225
+ collectCommutativeChain(node.right, op, out)
226
+ } else {
227
+ out.push(node.right)
228
+ }
229
+ }
230
+
231
+ function tryReassociateSubtract(path: NodePath<t.BinaryExpression>): boolean {
232
+ const parent = path.parentPath
233
+ if (
234
+ parent &&
235
+ parent.isBinaryExpression() &&
236
+ parent.node.operator === '-' &&
237
+ parent.node.left === path.node
238
+ ) {
239
+ return false
240
+ }
241
+
242
+ const subtractees: t.Expression[] = []
243
+ let node: t.BinaryExpression = path.node
244
+ while (true) {
245
+ subtractees.unshift(node.right)
246
+ const { left } = node
247
+ if (t.isBinaryExpression(left) && left.operator === '-') {
248
+ node = left
249
+ continue
250
+ }
251
+ break
252
+ }
253
+ if (!t.isExpression(node.left)) {
254
+ return false
255
+ }
256
+ const base = node.left
257
+
258
+ const constSubs: SafeConstant[] = []
259
+ const otherSubs: t.Expression[] = []
260
+ for (const subtractee of subtractees) {
261
+ const c = asSafeConstant(subtractee)
262
+ if (c === null) {
263
+ otherSubs.push(subtractee)
264
+ } else {
265
+ constSubs.push(c)
266
+ }
267
+ }
268
+
269
+ const baseConst = asSafeConstant(base)
270
+ // IEEE-754 subtraction is not associative. BigInt subtraction is exact.
271
+ if (constSubs.some((c) => c.kind === 'number') || baseConst?.kind === 'number') {
272
+ return false
273
+ }
274
+ if (baseConst !== null) {
275
+ if (constSubs.length === 0) {
276
+ return false
277
+ }
278
+ const combined = combineConstants([baseConst, ...constSubs], '-')
279
+ if (combined === null) {
280
+ return false
281
+ }
282
+ const combinedLit = constantToLiteral(combined)
283
+ let replacement: t.Expression = combinedLit
284
+ for (const other of otherSubs) {
285
+ replacement = t.binaryExpression('-', replacement, other)
286
+ }
287
+ path.replaceWith(replacement)
288
+ return true
289
+ }
290
+
291
+ if (constSubs.length < 2) {
292
+ return false
293
+ }
294
+ // Reassociation can move `base` coercion past a subtractee's evaluation.
295
+ // Non-constant subtractees must be pure.
296
+ if (!otherSubs.every((leaf) => isPure(leaf, path.scope, path))) {
297
+ return false
298
+ }
299
+ const sum = combineConstants(constSubs, '+')
300
+ if (sum === null) {
301
+ return false
302
+ }
303
+ const sumLit = constantToLiteral(sum)
304
+ let replacement: t.Expression = base
305
+ for (const other of otherSubs) {
306
+ replacement = t.binaryExpression('-', replacement, other)
307
+ }
308
+ replacement = t.binaryExpression('-', replacement, sumLit)
309
+ path.replaceWith(replacement)
310
+ return true
311
+ }
312
+
313
+ function asSafeConstant(node: t.Node): SafeConstant | null {
314
+ if (t.isNumericLiteral(node)) {
315
+ return Number.isInteger(node.value) && Math.abs(node.value) <= Number.MAX_SAFE_INTEGER
316
+ ? { kind: 'number', value: node.value }
317
+ : null
318
+ }
319
+ if (t.isUnaryExpression(node) && node.operator === '-' && t.isNumericLiteral(node.argument)) {
320
+ const negated = -node.argument.value
321
+ return Number.isInteger(negated) && Math.abs(negated) <= Number.MAX_SAFE_INTEGER
322
+ ? { kind: 'number', value: negated }
323
+ : null
324
+ }
325
+ if (t.isBigIntLiteral(node)) {
326
+ try {
327
+ return { kind: 'bigint', value: BigInt(node.value) }
328
+ } catch {
329
+ return null
330
+ }
331
+ }
332
+ return null
333
+ }
334
+
335
+ function constantToLiteral(c: SafeConstant): t.Expression {
336
+ if (c.kind === 'bigint') {
337
+ return c.value < 0n
338
+ ? t.unaryExpression('-', t.bigIntLiteral(-c.value))
339
+ : t.bigIntLiteral(c.value)
340
+ }
341
+ // `t.numericLiteral(-0)` is rejected by Babel's validator; emit `-0`.
342
+ if (Object.is(c.value, -0)) {
343
+ return t.unaryExpression('-', t.numericLiteral(0))
344
+ }
345
+ if (c.value < 0) {
346
+ return t.unaryExpression('-', t.numericLiteral(-c.value))
347
+ }
348
+ return t.numericLiteral(c.value)
349
+ }
350
+
351
+ function isInt32(value: number): boolean {
352
+ return (value | 0) === value
353
+ }
354
+
355
+ function combineConstants(
356
+ consts: SafeConstant[],
357
+ op: CommutativeOp | '-' | '+',
358
+ ): SafeConstant | null {
359
+ if (consts.length === 0) {
360
+ return null
361
+ }
362
+ const { kind } = consts[0]!
363
+ for (const c of consts) {
364
+ if (c.kind !== kind) {
365
+ return null
366
+ }
367
+ }
368
+
369
+ if (kind === 'bigint') {
370
+ let acc = consts[0]!.value as bigint
371
+ for (let i = 1; i < consts.length; i++) {
372
+ const c = consts[i]!.value as bigint
373
+ acc = applyBigint(acc, c, op)
374
+ }
375
+ return { kind: 'bigint', value: acc }
376
+ }
377
+
378
+ if (op === '&' || op === '|' || op === '^') {
379
+ for (const c of consts) {
380
+ if (!isInt32(c.value as number)) {
381
+ return null
382
+ }
383
+ }
384
+ }
385
+ let acc = consts[0]!.value as number
386
+ for (let i = 1; i < consts.length; i++) {
387
+ const c = consts[i]!.value as number
388
+ acc = applyNumber(acc, c, op)
389
+ }
390
+ if (!Number.isFinite(acc)) {
391
+ return null
392
+ }
393
+ if (
394
+ (op === '*' || op === '+' || op === '-') &&
395
+ (!Number.isInteger(acc) || Math.abs(acc) > Number.MAX_SAFE_INTEGER)
396
+ ) {
397
+ return null
398
+ }
399
+ return { kind: 'number', value: acc }
400
+ }
401
+
402
+ function applyBigint(a: bigint, b: bigint, op: CommutativeOp | '-' | '+'): bigint {
403
+ switch (op) {
404
+ case '*':
405
+ return a * b
406
+ case '&':
407
+ return a & b
408
+ case '|':
409
+ return a | b
410
+ case '^':
411
+ return a ^ b
412
+ case '-':
413
+ return a - b
414
+ case '+':
415
+ return a + b
416
+ }
417
+ }
418
+
419
+ function applyNumber(a: number, b: number, op: CommutativeOp | '-' | '+'): number {
420
+ switch (op) {
421
+ case '*':
422
+ return a * b
423
+ case '&':
424
+ return a & b
425
+ case '|':
426
+ return a | b
427
+ case '^':
428
+ return a ^ b
429
+ case '-':
430
+ return a - b
431
+ case '+':
432
+ return a + b
433
+ }
434
+ }
435
+
436
+ function structurallyEqual(a: t.Node, b: t.Node): boolean {
437
+ if (a.type !== b.type) {
438
+ return false
439
+ }
440
+ if (t.isNumericLiteral(a) && t.isNumericLiteral(b)) {
441
+ return a.value === b.value
442
+ }
443
+ if (t.isStringLiteral(a) && t.isStringLiteral(b)) {
444
+ return a.value === b.value
445
+ }
446
+ if (t.isBooleanLiteral(a) && t.isBooleanLiteral(b)) {
447
+ return a.value === b.value
448
+ }
449
+ if (t.isNullLiteral(a)) {
450
+ return true
451
+ }
452
+ if (t.isBigIntLiteral(a) && t.isBigIntLiteral(b)) {
453
+ return a.value === b.value
454
+ }
455
+ if (t.isIdentifier(a) && t.isIdentifier(b)) {
456
+ return a.name === b.name
457
+ }
458
+ if (t.isUnaryExpression(a) && t.isUnaryExpression(b)) {
459
+ return a.operator === b.operator && structurallyEqual(a.argument, b.argument)
460
+ }
461
+ return false
462
+ }
@@ -0,0 +1,109 @@
1
+ import type { NodePath, Visitor } from '@babel/traverse'
2
+ import * as t from '@babel/types'
3
+ import type { File } from '@babel/types'
4
+
5
+ import { traverseForChanges } from 'src/utils/change-tracking'
6
+ import type { ChangeState } from 'src/utils/change-tracking'
7
+ import { collectLoopLabels, generateLoopLabel, redirectContinues } from 'src/utils/loop-lowering'
8
+
9
+ /**
10
+ * Rewrites classic `for` loops as an initializer followed by `while`. Continues
11
+ * become breaks from an inner block so the update still runs. Iterator loops
12
+ * stay unchanged.
13
+ *
14
+ * @example
15
+ * // ◀️ before
16
+ * for (var i = 0; i < 3; i++) {
17
+ * if (skip(i)) {
18
+ * continue;
19
+ * }
20
+ * log(i);
21
+ * }
22
+ *
23
+ * // ▶️ after
24
+ * var i = 0;
25
+ * while (i < 3) {
26
+ * _forIter: {
27
+ * if (skip(i)) {
28
+ * break _forIter;
29
+ * }
30
+ * log(i);
31
+ * }
32
+ * i++;
33
+ * }
34
+ */
35
+ export function forToWhile(ast: File): boolean {
36
+ return traverseForChanges(ast, visitor)
37
+ }
38
+
39
+ const visitor: Visitor<ChangeState> = {
40
+ ForStatement(path, state) {
41
+ if (transformFor(path)) {
42
+ state.changed = true
43
+ }
44
+ },
45
+ }
46
+
47
+ function transformFor(path: NodePath<t.ForStatement>): boolean {
48
+ const { node } = path
49
+
50
+ // One hoisted binding cannot preserve lexical per-iteration captures.
51
+ if (t.isVariableDeclaration(node.init) && node.init.kind !== 'var') {
52
+ return false
53
+ }
54
+
55
+ const { outermost, ourLabels } = collectLoopLabels(path)
56
+ const innerLabel = generateLoopLabel(path, 'forIteration')
57
+
58
+ const initStmt = buildInitStatement(node.init)
59
+
60
+ const blockBody: t.BlockStatement = t.isBlockStatement(node.body)
61
+ ? node.body
62
+ : t.blockStatement([node.body])
63
+
64
+ const labeledBlock = t.labeledStatement(t.identifier(innerLabel), blockBody)
65
+
66
+ const whileBody: t.Statement[] = [labeledBlock]
67
+ if (node.update) {
68
+ whileBody.push(t.expressionStatement(node.update))
69
+ }
70
+
71
+ const test = node.test ?? t.booleanLiteral(true)
72
+ const whileStmt = t.whileStatement(test, t.blockStatement(whileBody))
73
+
74
+ const [newPath] = path.replaceWith(whileStmt)
75
+ const rewriteCount = redirectContinues(newPath, innerLabel, ourLabels)
76
+
77
+ // Keep the block so body bindings cannot shadow names in the update.
78
+ if (rewriteCount === 0) {
79
+ const whileBlock = newPath.node.body as t.BlockStatement
80
+ whileBlock.body[0] = blockBody
81
+ }
82
+
83
+ hoistInit(outermost, initStmt)
84
+ return true
85
+ }
86
+
87
+ // Babel repoints a non-list path after `insertBefore`, which can drop the init
88
+ // during the later loop replacement. Replace that slot with one block instead.
89
+ function hoistInit(outermost: NodePath<t.Statement>, initStmt: t.Statement | null): void {
90
+ if (!initStmt) {
91
+ return
92
+ }
93
+ if (outermost.inList) {
94
+ outermost.insertBefore(initStmt)
95
+ return
96
+ }
97
+ const loopStatement = outermost.node
98
+ outermost.replaceWith(t.blockStatement([initStmt, loopStatement]))
99
+ }
100
+
101
+ function buildInitStatement(init: t.ForStatement['init']): t.Statement | null {
102
+ if (!init) {
103
+ return null
104
+ }
105
+ if (t.isVariableDeclaration(init)) {
106
+ return init
107
+ }
108
+ return t.expressionStatement(init)
109
+ }
@@ -0,0 +1,108 @@
1
+ import type { NodePath, Visitor } from '@babel/traverse'
2
+ import * as t from '@babel/types'
3
+ import type { File } from '@babel/types'
4
+
5
+ import { traverseForChanges } from 'src/utils/change-tracking'
6
+ import type { ChangeState } from 'src/utils/change-tracking'
7
+
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.
11
+ *
12
+ * Preceding statements must be inert. Name inference and recursive references
13
+ * still use the `var` binding.
14
+ *
15
+ * Block-level declarations and module top-level declarations stay unchanged due
16
+ * to Annex B aliases and live exports.
17
+ *
18
+ * @example
19
+ * // ◀️ before
20
+ * const TAX = 0.2;
21
+ * function withTax(n) {
22
+ * return n + n * TAX;
23
+ * }
24
+ * total = withTax(100);
25
+ *
26
+ * // ▶️ after
27
+ * const TAX = 0.2;
28
+ * var withTax = function (n) {
29
+ * return n + n * TAX;
30
+ * };
31
+ * total = withTax(100);
32
+ */
33
+ export function functionDeclarationToExpression(ast: File): boolean {
34
+ return traverseForChanges(ast, visitor)
35
+ }
36
+
37
+ const visitor: Visitor<ChangeState> = {
38
+ FunctionDeclaration(path, state) {
39
+ const { id } = path.node
40
+ if (!id || !isConvertible(path, id)) {
41
+ return
42
+ }
43
+ const { node } = path
44
+ const fn = t.functionExpression(null, node.params, node.body, node.generator, node.async)
45
+ path.replaceWith(t.variableDeclaration('var', [t.variableDeclarator(t.cloneNode(id), fn)]))
46
+ state.changed = true
47
+ },
48
+ }
49
+
50
+ function isConvertible(path: NodePath<t.FunctionDeclaration>, id: t.Identifier): boolean {
51
+ const parent = path.parentPath
52
+ if (!parent) {
53
+ return false
54
+ }
55
+ const atScriptTop = parent.isProgram() && parent.node.sourceType !== 'module'
56
+ const inFunctionBody = parent.isBlockStatement() && parent.parentPath.isFunction()
57
+ if (!atScriptTop && !inFunctionBody) {
58
+ return false
59
+ }
60
+
61
+ const binding = path.scope.getBinding(id.name)
62
+ if (!binding || binding.path.node !== path.node) {
63
+ return false
64
+ }
65
+ if (binding.constantViolations.some(isRedeclaration)) {
66
+ return false
67
+ }
68
+
69
+ const { body } = parent.node as t.Program | t.BlockStatement
70
+ const index = path.key as number
71
+ for (let i = 0; i < index; i++) {
72
+ if (!isInertStatement(body[i]!)) {
73
+ return false
74
+ }
75
+ }
76
+ return true
77
+ }
78
+
79
+ // Redeclarations make the hoisted value order-sensitive.
80
+ function isRedeclaration(path: NodePath): boolean {
81
+ return path.isFunctionDeclaration() || path.isClassDeclaration() || path.isVariableDeclarator()
82
+ }
83
+
84
+ // An inert prologue cannot run user code or read a converted function binding.
85
+ function isInertStatement(stmt: t.Statement): boolean {
86
+ if (t.isFunctionDeclaration(stmt) || t.isImportDeclaration(stmt) || t.isEmptyStatement(stmt)) {
87
+ return true
88
+ }
89
+ if (t.isVariableDeclaration(stmt)) {
90
+ return stmt.declarations.every((declarator) => isInertInit(declarator.init))
91
+ }
92
+ return false
93
+ }
94
+
95
+ function isInertInit(init: t.VariableDeclarator['init']): boolean {
96
+ return (
97
+ init == null ||
98
+ t.isNumericLiteral(init) ||
99
+ t.isStringLiteral(init) ||
100
+ t.isBooleanLiteral(init) ||
101
+ t.isNullLiteral(init) ||
102
+ t.isBigIntLiteral(init) ||
103
+ t.isRegExpLiteral(init) ||
104
+ t.isThisExpression(init) ||
105
+ t.isFunctionExpression(init) ||
106
+ t.isArrowFunctionExpression(init)
107
+ )
108
+ }