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,387 @@
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 { isInsideWith } from 'src/utils/ast'
6
+ import { traverseForChanges } from 'src/utils/change-tracking'
7
+ import type { ChangeState } from 'src/utils/change-tracking'
8
+ import { isConditionalGate, reorderableToStatement } from 'src/utils/evaluation-order'
9
+ import { isStandaloneDeclaration } from 'src/utils/paths'
10
+
11
+ /**
12
+ * Lowers conditional expressions to `if` statements. Expression values use a
13
+ * temporary. Parameter defaults and repeating loop tests stay unchanged because
14
+ * hoisting would change when they run.
15
+ *
16
+ * @example
17
+ * // ◀️ before
18
+ * var y = a ? b : c;
19
+ * log(d ? e : f);
20
+ *
21
+ * // ▶️ after
22
+ * var y;
23
+ * if (a) { y = b; } else { y = c; }
24
+ * var _ternaryResult;
25
+ * if (d) { _ternaryResult = e; } else { _ternaryResult = f; }
26
+ * log(_ternaryResult);
27
+ */
28
+ export function ternaryToIf(ast: File): boolean {
29
+ // Flatten sequences and logical gates before lifting ternaries. Replacements
30
+ // detach queued child paths, so every handler checks liveness.
31
+ const preparationChanged = traverseForChanges(ast, preparationVisitor)
32
+ const loweringChanged = traverseForChanges(ast, visitor)
33
+ return preparationChanged || loweringChanged
34
+ }
35
+
36
+ const preparationVisitor: Visitor<ChangeState> = {
37
+ ConditionalExpression(path, state) {
38
+ if (!isPathLive(path) || isInsideWith(path)) {
39
+ return
40
+ }
41
+ const stmtPath = path.getStatementParent()
42
+ if (!stmtPath) {
43
+ return
44
+ }
45
+ if (hoistEnclosingSequence(path, stmtPath) || lowerEnclosingLogical(path, stmtPath)) {
46
+ state.changed = true
47
+ }
48
+ },
49
+ }
50
+
51
+ const visitor: Visitor<ChangeState> = {
52
+ ConditionalExpression(path, state) {
53
+ // Statement replacement detaches queued siblings. `with` can also shadow the
54
+ // synthesized result temporary.
55
+ if (!isPathLive(path) || isInsideWith(path)) {
56
+ return
57
+ }
58
+ if (lowerConditional(path)) {
59
+ state.changed = true
60
+ }
61
+ },
62
+ }
63
+
64
+ function lowerConditional(path: NodePath<t.ConditionalExpression>): boolean {
65
+ const parent = path.parentPath
66
+ if (!parent) {
67
+ return false
68
+ }
69
+
70
+ const { test, consequent, alternate } = path.node
71
+
72
+ if (parent.isExpressionStatement() && parent.node.expression === path.node) {
73
+ parent.replaceWith(
74
+ t.ifStatement(
75
+ test,
76
+ t.blockStatement(seqToExpressionStatements(consequent)),
77
+ t.blockStatement(seqToExpressionStatements(alternate)),
78
+ ),
79
+ )
80
+ // Skip the orphaned path after replacing its parent.
81
+ path.skip()
82
+ return true
83
+ }
84
+ if (parent.isReturnStatement() && parent.node.argument === path.node) {
85
+ parent.replaceWith(
86
+ t.ifStatement(
87
+ test,
88
+ t.blockStatement(seqWithFinal(consequent, t.returnStatement)),
89
+ t.blockStatement(seqWithFinal(alternate, t.returnStatement)),
90
+ ),
91
+ )
92
+ path.skip()
93
+ return true
94
+ }
95
+ if (parent.isThrowStatement() && parent.node.argument === path.node) {
96
+ parent.replaceWith(
97
+ t.ifStatement(
98
+ test,
99
+ t.blockStatement(seqWithFinal(consequent, t.throwStatement)),
100
+ t.blockStatement(seqWithFinal(alternate, t.throwStatement)),
101
+ ),
102
+ )
103
+ path.skip()
104
+ return true
105
+ }
106
+ if (
107
+ parent.isAssignmentExpression() &&
108
+ parent.node.operator === '=' &&
109
+ t.isIdentifier(parent.node.left) &&
110
+ parent.node.right === path.node &&
111
+ parent.parentPath?.isExpressionStatement() &&
112
+ parent.parentPath.node.expression === parent.node
113
+ ) {
114
+ const target = parent.node.left
115
+ parent.parentPath.replaceWith(
116
+ t.ifStatement(
117
+ test,
118
+ t.blockStatement(seqWithAssignment(consequent, target)),
119
+ t.blockStatement(seqWithAssignment(alternate, target)),
120
+ ),
121
+ )
122
+ path.skip()
123
+ return true
124
+ }
125
+ if (
126
+ parent.isVariableDeclarator() &&
127
+ t.isIdentifier(parent.node.id) &&
128
+ parent.node.init === path.node &&
129
+ parent.parentPath?.isVariableDeclaration() &&
130
+ // Splitting lexical declarations would end TDZ early, and bare `const` is
131
+ // invalid. Keep their initializer behind a result temp.
132
+ parent.parentPath.node.kind === 'var' &&
133
+ parent.parentPath.node.declarations.length === 1 &&
134
+ isStandaloneDeclaration(parent.parentPath)
135
+ ) {
136
+ const declPath = parent.parentPath
137
+ const target = parent.node.id
138
+ declPath.replaceWithMultiple([
139
+ t.variableDeclaration(declPath.node.kind, [t.variableDeclarator(t.cloneNode(target))]),
140
+ t.ifStatement(
141
+ test,
142
+ t.blockStatement(seqWithAssignment(consequent, target)),
143
+ t.blockStatement(seqWithAssignment(alternate, target)),
144
+ ),
145
+ ])
146
+ path.skip()
147
+ return true
148
+ }
149
+
150
+ const stmtPath = path.getStatementParent()
151
+ if (!stmtPath) {
152
+ return false
153
+ }
154
+
155
+ if (isInsideAssignmentPattern(path, stmtPath)) {
156
+ return false
157
+ }
158
+ if (isInsideLoopHeader(path, stmtPath)) {
159
+ return false
160
+ }
161
+ // Lifting requires no short-circuit gate or earlier effect in the statement.
162
+ if (!reorderableToStatement(path, stmtPath)) {
163
+ return false
164
+ }
165
+
166
+ const { scope } = path
167
+ const tmpId = scope.generateUidIdentifier('ternaryResult')
168
+ scope.push({ id: t.cloneNode(tmpId) })
169
+
170
+ const ifStmt = t.ifStatement(
171
+ test,
172
+ t.blockStatement([
173
+ t.expressionStatement(t.assignmentExpression('=', t.cloneNode(tmpId), consequent)),
174
+ ]),
175
+ t.blockStatement([
176
+ t.expressionStatement(t.assignmentExpression('=', t.cloneNode(tmpId), alternate)),
177
+ ]),
178
+ )
179
+
180
+ // Detach the ternary before reusing its children in the new `if`.
181
+ path.replaceWith(t.cloneNode(tmpId))
182
+ stmtPath.insertBefore(ifStmt)
183
+ return true
184
+ }
185
+
186
+ function isInsideAssignmentPattern(path: NodePath, stmtPath: NodePath): boolean {
187
+ let ancestor: NodePath | null = path.parentPath
188
+ while (ancestor && ancestor !== stmtPath) {
189
+ if (ancestor.isAssignmentPattern()) {
190
+ return true
191
+ }
192
+ ancestor = ancestor.parentPath
193
+ }
194
+ return false
195
+ }
196
+
197
+ // Split the innermost sequence so its ternary reaches a safe statement position.
198
+ function hoistEnclosingSequence(path: NodePath, stmtPath: NodePath): boolean {
199
+ const seqPath = findEnclosingSequence(path, stmtPath)
200
+ if (!seqPath) {
201
+ return false
202
+ }
203
+ const elements = seqPath.node.expressions
204
+ if (elements.length < 2) {
205
+ return false
206
+ }
207
+ const seqStmt = seqPath.getStatementParent()
208
+ if (!seqStmt) {
209
+ return false
210
+ }
211
+ // The sequence must run once at statement position without crossing a gate,
212
+ // case test, parameter default, loop header, or earlier effect.
213
+ if (
214
+ isInsideLoopHeader(seqPath, seqStmt) ||
215
+ isInsideAssignmentPattern(seqPath, seqStmt) ||
216
+ isInsideSwitchCaseTest(seqPath, seqStmt) ||
217
+ !reorderableToStatement(seqPath, seqStmt)
218
+ ) {
219
+ return false
220
+ }
221
+ const preceding = elements.slice(0, -1).map((e) => t.expressionStatement(t.cloneNode(e)))
222
+ const last = t.cloneNode(elements.at(-1)!)
223
+ seqPath.replaceWith(last)
224
+ seqStmt.insertBefore(preceding)
225
+ path.skip()
226
+ return true
227
+ }
228
+
229
+ // Hoisting from a case test would run its work before the switch reaches that case.
230
+ function isInsideSwitchCaseTest(path: NodePath, stmtPath: NodePath): boolean {
231
+ let ancestor: NodePath | null = path.parentPath
232
+ while (ancestor && ancestor !== stmtPath) {
233
+ if (ancestor.isSwitchCase()) {
234
+ return true
235
+ }
236
+ ancestor = ancestor.parentPath
237
+ }
238
+ return false
239
+ }
240
+
241
+ // A live path must still occupy every recorded parent key up to the Program.
242
+ function isPathLive(path: NodePath): boolean {
243
+ let cur: NodePath | null = path
244
+ while (cur) {
245
+ if (!cur.node) {
246
+ return false
247
+ }
248
+ const parent: NodePath | null = cur.parentPath
249
+ if (!parent) {
250
+ return cur.isProgram() || cur.isFile()
251
+ }
252
+ if (!parent.node) {
253
+ return false
254
+ }
255
+ const parentNode = parent.node as unknown as Record<string, unknown>
256
+ let held: unknown = parentNode[cur.key as string]
257
+ if (cur.listKey != null) {
258
+ const arr = parentNode[cur.listKey]
259
+ held = Array.isArray(arr) ? arr[cur.key as number] : arr
260
+ }
261
+ if (held !== cur.node) {
262
+ return false
263
+ }
264
+ cur = parent
265
+ }
266
+ return false
267
+ }
268
+
269
+ function findEnclosingSequence(
270
+ path: NodePath,
271
+ stmtPath: NodePath,
272
+ ): NodePath<t.SequenceExpression> | null {
273
+ let cur: NodePath = path
274
+ let parent = cur.parentPath
275
+ let found: NodePath<t.SequenceExpression> | null = null
276
+ while (parent && parent !== stmtPath) {
277
+ if (isConditionalGate(parent, cur)) {
278
+ return null
279
+ }
280
+ if (!found && parent.isSequenceExpression()) {
281
+ found = parent as NodePath<t.SequenceExpression>
282
+ }
283
+ cur = parent
284
+ parent = cur.parentPath
285
+ }
286
+ return found
287
+ }
288
+
289
+ // Convert an enclosing logical gate to a conditional so this pass remains
290
+ // self-contained and can lower it on the next visit.
291
+ function lowerEnclosingLogical(path: NodePath, stmtPath: NodePath): boolean {
292
+ const logical = findEnclosingShortCircuitLogical(path, stmtPath)
293
+ if (!logical || logical.removed || !logical.container) {
294
+ return false
295
+ }
296
+ const { node } = logical
297
+ const { left, right } = node
298
+ let test: t.Expression
299
+ let valueRef: t.Expression
300
+ if (t.isIdentifier(left) && logical.scope.hasBinding(left.name)) {
301
+ test = left
302
+ valueRef = t.cloneNode(left)
303
+ } else {
304
+ const tmp = logical.scope.generateUidIdentifierBasedOnNode(left)
305
+ logical.scope.push({ id: t.cloneNode(tmp) })
306
+ test = t.assignmentExpression('=', t.cloneNode(tmp), left)
307
+ valueRef = t.cloneNode(tmp)
308
+ }
309
+ const testExpr = node.operator === '??' ? t.binaryExpression('!=', test, t.nullLiteral()) : test
310
+ logical.replaceWith(
311
+ node.operator === '&&'
312
+ ? t.conditionalExpression(testExpr, right, valueRef)
313
+ : t.conditionalExpression(testExpr, valueRef, right),
314
+ )
315
+ path.skip()
316
+ return true
317
+ }
318
+
319
+ function findEnclosingShortCircuitLogical(
320
+ path: NodePath,
321
+ stmtPath: NodePath,
322
+ ): NodePath<t.LogicalExpression> | null {
323
+ let cur: NodePath = path
324
+ let parent = cur.parentPath
325
+ let found: NodePath<t.LogicalExpression> | null = null
326
+ while (parent && parent !== stmtPath) {
327
+ if (!found) {
328
+ if (
329
+ parent.isConditionalExpression() &&
330
+ (parent.node.consequent === cur.node || parent.node.alternate === cur.node)
331
+ ) {
332
+ return null
333
+ }
334
+ if (parent.isLogicalExpression() && parent.node.right === cur.node) {
335
+ found = parent as NodePath<t.LogicalExpression>
336
+ }
337
+ } else if (isConditionalGate(parent, cur)) {
338
+ return null
339
+ }
340
+ cur = parent
341
+ parent = cur.parentPath
342
+ }
343
+ return found
344
+ }
345
+
346
+ // Loop tests and updates repeat, while a `for` initializer runs once.
347
+ function isInsideLoopHeader(path: NodePath, stmtPath: NodePath): boolean {
348
+ let ancestor: NodePath | null = path
349
+ while (ancestor && ancestor.parentPath !== stmtPath) {
350
+ ancestor = ancestor.parentPath
351
+ }
352
+ if (!ancestor) {
353
+ return false
354
+ }
355
+ if (stmtPath.isWhileStatement() || stmtPath.isDoWhileStatement()) {
356
+ return ancestor.parentKey === 'test'
357
+ }
358
+ if (stmtPath.isForStatement()) {
359
+ return ancestor.parentKey === 'test' || ancestor.parentKey === 'update'
360
+ }
361
+ return false
362
+ }
363
+
364
+ // Split sequences so later lifts target the right statement.
365
+ function seqToExpressionStatements(expr: t.Expression): t.Statement[] {
366
+ if (t.isSequenceExpression(expr)) {
367
+ return expr.expressions.map((e) => t.expressionStatement(e))
368
+ }
369
+ return [t.expressionStatement(expr)]
370
+ }
371
+
372
+ // In return and throw, only the final sequence element supplies the value.
373
+ function seqWithFinal(expr: t.Expression, build: (e: t.Expression) => t.Statement): t.Statement[] {
374
+ if (t.isSequenceExpression(expr)) {
375
+ const last = expr.expressions.at(-1)!
376
+ const init = expr.expressions.slice(0, -1).map((e) => t.expressionStatement(e))
377
+ return [...init, build(last)]
378
+ }
379
+ return [build(expr)]
380
+ }
381
+
382
+ // In a bare assignment, only the final sequence element supplies the value.
383
+ function seqWithAssignment(expr: t.Expression, target: t.Identifier): t.Statement[] {
384
+ const build = (value: t.Expression): t.Statement =>
385
+ t.expressionStatement(t.assignmentExpression('=', t.cloneNode(target), value))
386
+ return seqWithFinal(expr, build)
387
+ }
@@ -0,0 +1,132 @@
1
+ import type { Binding, NodePath, Visitor } from '@babel/traverse'
2
+ import * as t from '@babel/types'
3
+ import type { File } from '@babel/types'
4
+
5
+ import { isDirectEvalCall, isInsideWith } from 'src/utils/ast'
6
+ import { traverseForChanges } from 'src/utils/change-tracking'
7
+ import type { ChangeState } from 'src/utils/change-tracking'
8
+
9
+ /**
10
+ * Rewrites numeric updates as compound assignments for later expansion. Prefix
11
+ * results already match. Postfix forms require a position that discards the old
12
+ * value.
13
+ *
14
+ * BigInt differs because adding the number `1` throws. Only bindings proven to
15
+ * stay numeric are rewritten. Members and parameters stay unchanged.
16
+ *
17
+ * @example
18
+ * // ◀️ before
19
+ * let i = 0;
20
+ * i++;
21
+ * arr[++i] = 1;
22
+ * let n = 5n;
23
+ * n++;
24
+ *
25
+ * // ▶️ after
26
+ * let i = 0;
27
+ * i += 1;
28
+ * arr[i += 1] = 1;
29
+ * let n = 5n;
30
+ * n++;
31
+ */
32
+ export function updateToAssignment(ast: File): boolean {
33
+ return traverseForChanges(ast, visitor)
34
+ }
35
+
36
+ const visitor: Visitor<ChangeState> = {
37
+ UpdateExpression(path, state) {
38
+ const { argument, prefix, operator } = path.node
39
+ if (!t.isIdentifier(argument)) {
40
+ return
41
+ }
42
+ if (!prefix && !valueIsDiscarded(path)) {
43
+ return
44
+ }
45
+ // `with` can retarget the name and defeat the numeric proof.
46
+ if (isInsideWith(path)) {
47
+ return
48
+ }
49
+ const binding = path.scope.getBinding(argument.name)
50
+ if (!isNumericBinding(binding)) {
51
+ return
52
+ }
53
+ // Direct eval anywhere in the binding's scope can change its type.
54
+ if (bindingScopeReachesDirectEval(binding!)) {
55
+ return
56
+ }
57
+ const compound = operator === '++' ? '+=' : '-='
58
+ path.replaceWith(t.assignmentExpression(compound, argument, t.numericLiteral(1)))
59
+ state.changed = true
60
+ },
61
+ }
62
+
63
+ function valueIsDiscarded(path: NodePath<t.UpdateExpression>): boolean {
64
+ const { parentPath } = path
65
+ if (!parentPath) {
66
+ return false
67
+ }
68
+ if (parentPath.isExpressionStatement()) {
69
+ return true
70
+ }
71
+ if (parentPath.isForStatement() && parentPath.node.update === path.node) {
72
+ return true
73
+ }
74
+ if (parentPath.isSequenceExpression()) {
75
+ return parentPath.node.expressions.at(-1) !== path.node
76
+ }
77
+ return false
78
+ }
79
+
80
+ // The initializer and every mutation must keep the binding numeric.
81
+ function isNumericBinding(binding: Binding | undefined): boolean {
82
+ if (!binding || (binding.kind !== 'var' && binding.kind !== 'let')) {
83
+ return false
84
+ }
85
+ const declarator = binding.path.node
86
+ if (!t.isVariableDeclarator(declarator) || !t.isNumericLiteral(declarator.init)) {
87
+ return false
88
+ }
89
+ // Babel misses the string writes from an Annex B `for-in` initializer.
90
+ if (isForInOfLoopVariable(binding)) {
91
+ return false
92
+ }
93
+ return binding.constantViolations.every((violation) => keepsNumeric(violation.node))
94
+ }
95
+
96
+ // A sibling or nested direct eval can still close over and reassign the binding.
97
+ function bindingScopeReachesDirectEval(binding: Binding): boolean {
98
+ let found = false
99
+ t.traverseFast(binding.scope.block, (node) => {
100
+ if (isDirectEvalCall(node)) {
101
+ found = true
102
+ }
103
+ return found ? t.traverseFast.stop : undefined
104
+ })
105
+ return found
106
+ }
107
+
108
+ function isForInOfLoopVariable(binding: Binding): boolean {
109
+ const declaration = binding.path.parentPath
110
+ if (!declaration || !declaration.isVariableDeclaration()) {
111
+ return false
112
+ }
113
+ const loop = declaration.parentPath
114
+ return (
115
+ loop != null &&
116
+ (loop.isForInStatement() || loop.isForOfStatement()) &&
117
+ loop.node.left === declaration.node
118
+ )
119
+ }
120
+
121
+ function keepsNumeric(node: t.Node): boolean {
122
+ if (t.isUpdateExpression(node)) {
123
+ return true
124
+ }
125
+ if (t.isVariableDeclarator(node)) {
126
+ return t.isNumericLiteral(node.init)
127
+ }
128
+ if (t.isAssignmentExpression(node)) {
129
+ return t.isNumericLiteral(node.right)
130
+ }
131
+ return false
132
+ }
@@ -0,0 +1,103 @@
1
+ import type { 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 { isLiteralShaped } from 'src/utils/literal'
8
+
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.
14
+ *
15
+ * @example
16
+ * // ◀️ before
17
+ * if (x === 1) {}
18
+ * if (name !== "foo") {}
19
+ * if (count > 5) {}
20
+ * if (idx <= 10) {}
21
+ *
22
+ * // ▶️ after
23
+ * if (1 === x) {}
24
+ * if ("foo" !== name) {}
25
+ * if (5 < count) {}
26
+ * if (10 >= idx) {}
27
+ */
28
+ export function yodifyConditions(ast: File): boolean {
29
+ return traverseForChanges(ast, visitor)
30
+ }
31
+
32
+ const visitor: Visitor<ChangeState> = {
33
+ BinaryExpression(path, state) {
34
+ const { node } = path
35
+ const op = node.operator
36
+ if (!isComparisonOp(op)) {
37
+ return
38
+ }
39
+ if (!t.isExpression(node.left)) {
40
+ return
41
+ }
42
+ if (isLiteralShaped(node.left)) {
43
+ return
44
+ }
45
+ // The moved right side must run no user code, including object coercion.
46
+ if (!swappingRightIsEffectFree(node.right)) {
47
+ return
48
+ }
49
+ // Relational comparison coerces left first, so a regexp operand cannot swap.
50
+ if (RELATIONAL_FLIP[op] !== undefined && containsRegExpLiteral(node.right)) {
51
+ return
52
+ }
53
+
54
+ const { left } = node
55
+ const { right } = node
56
+ node.left = right
57
+ node.right = left
58
+ const flipped = RELATIONAL_FLIP[op]
59
+ if (flipped !== undefined) {
60
+ node.operator = flipped
61
+ }
62
+ state.changed = true
63
+ },
64
+ }
65
+
66
+ const COMPARISON_OPS = new Set<string>(['==', '!=', '===', '!==', '<', '<=', '>', '>='])
67
+
68
+ function isComparisonOp(op: string): boolean {
69
+ return COMPARISON_OPS.has(op)
70
+ }
71
+
72
+ const RELATIONAL_FLIP: Record<string, t.BinaryExpression['operator']> = {
73
+ '<': '>',
74
+ '>': '<',
75
+ '<=': '>=',
76
+ '>=': '<=',
77
+ }
78
+
79
+ // Numeric unary over regexp invokes ToPrimitive and cannot move earlier.
80
+ function swappingRightIsEffectFree(node: t.Expression): boolean {
81
+ return isLiteralShaped(node) && !numericUnaryCoercesRegExp(node)
82
+ }
83
+
84
+ // `!` only propagates risk because ToBoolean never invokes object coercion.
85
+ function numericUnaryCoercesRegExp(node: t.Node): boolean {
86
+ if (!t.isUnaryExpression(node) || !t.isExpression(node.argument)) {
87
+ return false
88
+ }
89
+ if (node.operator === '!') {
90
+ return numericUnaryCoercesRegExp(node.argument)
91
+ }
92
+ return t.isRegExpLiteral(node.argument) || numericUnaryCoercesRegExp(node.argument)
93
+ }
94
+
95
+ function containsRegExpLiteral(node: t.Expression): boolean {
96
+ if (t.isRegExpLiteral(node)) {
97
+ return true
98
+ }
99
+ if (t.isUnaryExpression(node) && t.isExpression(node.argument)) {
100
+ return containsRegExpLiteral(node.argument)
101
+ }
102
+ return false
103
+ }