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,957 @@
1
+ import traverse from '@babel/traverse'
2
+ import type { Binding, NodePath, Scope } from '@babel/traverse'
3
+ import * as t from '@babel/types'
4
+ import type { File } from '@babel/types'
5
+
6
+ import { evaluateConstant } from 'src/analysis/constant'
7
+ import {
8
+ declarationReaches,
9
+ initializerReaches,
10
+ readCrossesFunctionBoundary,
11
+ } from 'src/analysis/document-order'
12
+ import { isPure } from 'src/analysis/purity'
13
+ import { isInsideWith } from 'src/utils/ast'
14
+ import { reorderableToStatement } from 'src/utils/evaluation-order'
15
+ import { valueToLiteral } from 'src/utils/literal'
16
+ import { isCalleeOrTagOf } from 'src/utils/paths'
17
+
18
+ /**
19
+ * Drops single-use temporaries through substitution, direct branch writes, or
20
+ * constant replacement. Sweeps continue until no shape exposes another.
21
+ *
22
+ * @example
23
+ * // ◀️ before
24
+ * var _t;
25
+ * if (cond) { _t = 1; } else { _t = 2; }
26
+ * return _t;
27
+ *
28
+ * // ▶️ after
29
+ * if (cond) { return 1; } else { return 2; }
30
+ */
31
+ export function collapseSingleUseTemps(ast: File): boolean {
32
+ let anyChanged = false
33
+ for (let pass = 0; pass < 20; pass++) {
34
+ const programScope = findProgramScope(ast)
35
+ if (!programScope) {
36
+ return anyChanged
37
+ }
38
+ programScope.crawl()
39
+
40
+ const candidates: NodePath<t.VariableDeclarator>[] = []
41
+ traverse(ast, {
42
+ VariableDeclarator(path) {
43
+ if (resolveSingleUseBinding(path)) {
44
+ candidates.push(path)
45
+ }
46
+ },
47
+ })
48
+
49
+ let changedThisPass = false
50
+ for (const candidate of candidates) {
51
+ if (candidate.removed) {
52
+ continue
53
+ }
54
+ // Earlier rewrites can invalidate bindings collected by this sweep.
55
+ const binding = resolveSingleUseBinding(candidate)
56
+ if (!binding) {
57
+ continue
58
+ }
59
+
60
+ if (
61
+ binding.references === 1 &&
62
+ (trySubstituteSingleUse(binding, candidate) ||
63
+ tryRenameWritesToReader(binding, candidate) ||
64
+ tryScatterIntoTerminator(binding, candidate))
65
+ ) {
66
+ changedThisPass = true
67
+ continue
68
+ }
69
+ if (tryPropagateConstant(binding, candidate)) {
70
+ changedThisPass = true
71
+ }
72
+ }
73
+
74
+ if (!changedThisPass) {
75
+ break
76
+ }
77
+ anyChanged = true
78
+ }
79
+ return anyChanged
80
+ }
81
+
82
+ // Holes, patterns, dead bindings, and rebound names have no usable binding.
83
+ function resolveSingleUseBinding(path: NodePath<t.VariableDeclarator>): Binding | null {
84
+ if (!t.isIdentifier(path.node.id)) {
85
+ return null
86
+ }
87
+ const binding = path.scope.getBinding(path.node.id.name)
88
+ if (!binding || binding.path.node !== path.node) {
89
+ return null
90
+ }
91
+ if (binding.kind !== 'var' && binding.kind !== 'let' && binding.kind !== 'const') {
92
+ return null
93
+ }
94
+ if (binding.references === 0) {
95
+ return null
96
+ }
97
+ // `with` can redirect the temporary or its consumer outside Babel's scope table.
98
+ if (bindingTouchesWith(binding)) {
99
+ return null
100
+ }
101
+ return binding
102
+ }
103
+
104
+ function bindingTouchesWith(binding: Binding): boolean {
105
+ return (
106
+ isInsideWith(binding.path) ||
107
+ binding.referencePaths.some((ref) => isInsideWith(ref)) ||
108
+ binding.constantViolations.some((violation) => isInsideWith(violation))
109
+ )
110
+ }
111
+
112
+ function findProgramScope(ast: File): Scope | undefined {
113
+ let result: Scope | undefined
114
+ traverse(ast, {
115
+ Program(path) {
116
+ result = path.scope
117
+ path.stop()
118
+ },
119
+ })
120
+ return result
121
+ }
122
+
123
+ // Move a single initializer into its later read when the span is safe.
124
+ function trySubstituteSingleUse(
125
+ binding: Binding,
126
+ declaratorPath: NodePath<t.VariableDeclarator>,
127
+ ): boolean {
128
+ const [readPath] = binding.referencePaths
129
+ if (!readPath || !isPathLive(readPath)) {
130
+ return false
131
+ }
132
+
133
+ const declStmt = declaratorPath.parentPath
134
+ if (!declStmt || !declStmt.isVariableDeclaration()) {
135
+ return false
136
+ }
137
+ if (declStmt.node.declarations.length !== 1) {
138
+ return false
139
+ }
140
+
141
+ const { init } = declaratorPath.node
142
+ let writeStmt: NodePath
143
+ let writeValueOwner: NodePath
144
+ let writeRhs: t.Expression
145
+ let removeWriteStmt: boolean
146
+
147
+ if (init) {
148
+ if (binding.constantViolations.length > 0) {
149
+ return false
150
+ }
151
+ writeStmt = declStmt
152
+ writeValueOwner = declaratorPath.get('init') as NodePath
153
+ writeRhs = init
154
+ removeWriteStmt = false
155
+ } else {
156
+ if (binding.constantViolations.length !== 1) {
157
+ return false
158
+ }
159
+ const violation = binding.constantViolations[0]!
160
+ if (!violation.isAssignmentExpression() || violation.node.operator !== '=') {
161
+ return false
162
+ }
163
+ if (
164
+ !t.isIdentifier(violation.node.left) ||
165
+ violation.node.left.name !== binding.identifier.name
166
+ ) {
167
+ return false
168
+ }
169
+ if (!violation.parentPath?.isExpressionStatement()) {
170
+ return false
171
+ }
172
+ if (violation.parentPath.node.expression !== violation.node) {
173
+ return false
174
+ }
175
+ writeStmt = violation.parentPath
176
+ writeValueOwner = violation.get('right') as NodePath
177
+ writeRhs = violation.node.right
178
+ removeWriteStmt = true
179
+ }
180
+
181
+ const readStmt = safeGetStatementParent(readPath)
182
+ if (!readStmt) {
183
+ return false
184
+ }
185
+
186
+ // The read must be unconditional and have no earlier observable work.
187
+ if (!reorderableToStatement(readPath, readStmt)) {
188
+ return false
189
+ }
190
+ // Moving into a loop header would repeat evaluation.
191
+ if (readIsInLoopHeader(readPath, readStmt)) {
192
+ return false
193
+ }
194
+ // Substituting a member into callee position would bind its receiver as `this`.
195
+ if (
196
+ isCalleeOrTagOf(readPath) &&
197
+ (t.isMemberExpression(writeRhs) || t.isOptionalMemberExpression(writeRhs))
198
+ ) {
199
+ return false
200
+ }
201
+
202
+ const container = writeStmt.parentPath
203
+ if (!container) {
204
+ return false
205
+ }
206
+ if (readStmt.parentPath !== container) {
207
+ return false
208
+ }
209
+
210
+ const stmtPaths = getContainerStatementPaths(container)
211
+ if (!stmtPaths) {
212
+ return false
213
+ }
214
+ const writeIdx = stmtPaths.findIndex((stmt) => stmt.node === writeStmt.node)
215
+ const readIdx = stmtPaths.findIndex((stmt) => stmt.node === readStmt.node)
216
+ if (writeIdx === -1 || readIdx === -1) {
217
+ return false
218
+ }
219
+ if (writeIdx >= readIdx) {
220
+ return false
221
+ }
222
+
223
+ if (writeIdx + 1 < readIdx) {
224
+ // Moving across a statement requires pure code and no intervening declaration.
225
+ if (!writeValueOwner.isPure()) {
226
+ return false
227
+ }
228
+ for (let i = writeIdx + 1; i < readIdx; i++) {
229
+ const stmtPath = stmtPaths[i]
230
+ if (!stmtPath || !isStatementMovementSafe(stmtPath)) {
231
+ return false
232
+ }
233
+ }
234
+ }
235
+
236
+ readPath.replaceWith(t.cloneNode(writeRhs))
237
+
238
+ if (removeWriteStmt) {
239
+ writeStmt.remove()
240
+ }
241
+ declStmt.remove()
242
+
243
+ return true
244
+ }
245
+
246
+ // Redirect scattered temporary writes into their final consumer.
247
+ function tryRenameWritesToReader(
248
+ binding: Binding,
249
+ declaratorPath: NodePath<t.VariableDeclarator>,
250
+ ): boolean {
251
+ if (declaratorPath.node.init !== null) {
252
+ return false
253
+ }
254
+ if (binding.constantViolations.length === 0) {
255
+ return false
256
+ }
257
+
258
+ const [readPath] = binding.referencePaths
259
+ if (!readPath || !isPathLive(readPath)) {
260
+ return false
261
+ }
262
+
263
+ const readParent = readPath.parentPath
264
+ if (
265
+ !readParent?.isAssignmentExpression() ||
266
+ readParent.node.operator !== '=' ||
267
+ readParent.node.right !== readPath.node ||
268
+ !t.isIdentifier(readParent.node.left)
269
+ ) {
270
+ return false
271
+ }
272
+
273
+ const readStmt = readParent.parentPath
274
+ if (!readStmt?.isExpressionStatement() || readStmt.node.expression !== readParent.node) {
275
+ return false
276
+ }
277
+
278
+ const yIdent = readParent.node.left
279
+ const yBinding = readParent.scope.getBinding(yIdent.name)
280
+ if (!yBinding) {
281
+ return false
282
+ }
283
+
284
+ const declStmt = declaratorPath.parentPath
285
+ if (!declStmt?.isVariableDeclaration()) {
286
+ return false
287
+ }
288
+ if (declStmt.node.declarations.length !== 1) {
289
+ return false
290
+ }
291
+
292
+ const container = declStmt.parentPath
293
+ if (!container || readStmt.parentPath !== container) {
294
+ return false
295
+ }
296
+
297
+ const stmtPaths = getContainerStatementPaths(container)
298
+ if (!stmtPaths) {
299
+ return false
300
+ }
301
+ const declIdx = stmtPaths.findIndex((stmt) => stmt.node === declStmt.node)
302
+ const readIdx = stmtPaths.findIndex((stmt) => stmt.node === readStmt.node)
303
+ if (declIdx === -1 || readIdx === -1) {
304
+ return false
305
+ }
306
+ if (declIdx >= readIdx) {
307
+ return false
308
+ }
309
+
310
+ for (const violation of binding.constantViolations) {
311
+ if (!violation.isAssignmentExpression() || violation.node.operator !== '=') {
312
+ return false
313
+ }
314
+ if (
315
+ !t.isIdentifier(violation.node.left) ||
316
+ violation.node.left.name !== binding.identifier.name
317
+ ) {
318
+ return false
319
+ }
320
+ if (!violation.parentPath?.isExpressionStatement()) {
321
+ return false
322
+ }
323
+
324
+ const stmtAncestorIdx = ancestorIndexInContainer(violation, container, stmtPaths)
325
+ if (stmtAncestorIdx === -1) {
326
+ return false
327
+ }
328
+ if (stmtAncestorIdx <= declIdx || stmtAncestorIdx >= readIdx) {
329
+ return false
330
+ }
331
+
332
+ if (violation.scope.getBinding(yIdent.name) !== yBinding) {
333
+ return false
334
+ }
335
+
336
+ // Redirecting into a later lexical binding could write it in TDZ.
337
+ if (!declarationReaches(yBinding.path, violation)) {
338
+ return false
339
+ }
340
+ }
341
+
342
+ const spanPaths: NodePath[] = []
343
+ for (let i = declIdx + 1; i < readIdx; i++) {
344
+ const stmtPath = stmtPaths[i]
345
+ if (!stmtPath || !stmtPath.node) {
346
+ return false
347
+ }
348
+ spanPaths.push(stmtPath)
349
+ }
350
+
351
+ // Every path reaching the consumer must assign the temporary first.
352
+ if (!spanDefinitelyAssigns(binding, spanPaths)) {
353
+ return false
354
+ }
355
+
356
+ for (const stmtPath of spanPaths) {
357
+ // Redirected writes make the consumer value visible earlier.
358
+ if (subtreeReadsBinding(stmtPath, yBinding)) {
359
+ return false
360
+ }
361
+ // Other work in the span could observe the earlier consumer writes.
362
+ if (!spanStatementIsInert(stmtPath, binding)) {
363
+ return false
364
+ }
365
+ }
366
+
367
+ for (const violation of binding.constantViolations) {
368
+ if (!t.isAssignmentExpression(violation.node)) {
369
+ continue
370
+ }
371
+ ;(violation.node.left as t.Identifier).name = yIdent.name
372
+ }
373
+
374
+ declStmt.remove()
375
+ readStmt.remove()
376
+
377
+ return true
378
+ }
379
+
380
+ // Replace branch-tail writes followed by a terminator with direct terminators.
381
+ function tryScatterIntoTerminator(
382
+ binding: Binding,
383
+ declaratorPath: NodePath<t.VariableDeclarator>,
384
+ ): boolean {
385
+ if (declaratorPath.node.init !== null) {
386
+ return false
387
+ }
388
+ if (binding.constantViolations.length === 0) {
389
+ return false
390
+ }
391
+
392
+ const [readPath] = binding.referencePaths
393
+ if (!readPath || !isPathLive(readPath)) {
394
+ return false
395
+ }
396
+
397
+ const readParent = readPath.parentPath
398
+ if (!readParent) {
399
+ return false
400
+ }
401
+
402
+ const isReturn = readParent.isReturnStatement() && readParent.node.argument === readPath.node
403
+ const isThrow = readParent.isThrowStatement() && readParent.node.argument === readPath.node
404
+ if (!isReturn && !isThrow) {
405
+ return false
406
+ }
407
+
408
+ const declStmt = declaratorPath.parentPath
409
+ if (!declStmt?.isVariableDeclaration()) {
410
+ return false
411
+ }
412
+ if (declStmt.node.declarations.length !== 1) {
413
+ return false
414
+ }
415
+
416
+ const container = declStmt.parentPath
417
+ if (!container || readParent.parentPath !== container) {
418
+ return false
419
+ }
420
+
421
+ const stmtPaths = getContainerStatementPaths(container)
422
+ if (!stmtPaths) {
423
+ return false
424
+ }
425
+ const declIdx = stmtPaths.findIndex((stmt) => stmt.node === declStmt.node)
426
+ const readIdx = stmtPaths.findIndex((stmt) => stmt.node === readParent.node)
427
+ if (declIdx === -1 || readIdx === -1) {
428
+ return false
429
+ }
430
+ if (declIdx >= readIdx) {
431
+ return false
432
+ }
433
+ // Intervening statements would be skipped by direct branch terminators.
434
+ if (declIdx + 1 >= readIdx) {
435
+ return false
436
+ }
437
+
438
+ const scatterIdx = readIdx - 1
439
+ const scatterStmt = stmtPaths[scatterIdx]!.node as t.Statement
440
+ const writeOwners = collectScatterWriteOwners(binding, scatterStmt)
441
+ if (!writeOwners) {
442
+ return false
443
+ }
444
+
445
+ // Uncollected writes would survive after the binding is removed.
446
+ if (writeOwners.length !== binding.constantViolations.length) {
447
+ return false
448
+ }
449
+ const violationNodes = new Set(binding.constantViolations.map((violation) => violation.node))
450
+ for (const owner of writeOwners) {
451
+ if (!violationNodes.has(owner.exprStatement.expression)) {
452
+ return false
453
+ }
454
+ }
455
+
456
+ // Earlier statements stay in place and must not observe the temporary.
457
+ for (let i = declIdx + 1; i < scatterIdx; i++) {
458
+ const stmtPath = stmtPaths[i]
459
+ if (!stmtPath) {
460
+ return false
461
+ }
462
+ if (subtreeReadsBinding(stmtPath, binding)) {
463
+ return false
464
+ }
465
+ }
466
+
467
+ const build = isReturn
468
+ ? (value: t.Expression): t.Statement => t.returnStatement(value)
469
+ : (value: t.Expression): t.Statement => t.throwStatement(value)
470
+
471
+ for (const owner of writeOwners) {
472
+ const value = (owner.exprStatement.expression as t.AssignmentExpression).right
473
+ owner.replaceInBlock(build(value))
474
+ }
475
+
476
+ declStmt.remove()
477
+ readParent.remove()
478
+ return true
479
+ }
480
+
481
+ interface ScatterWriteOwner {
482
+ exprStatement: t.ExpressionStatement
483
+ replaceInBlock: (stmt: t.Statement) => void
484
+ }
485
+
486
+ // Every branch must end with a matching temporary write.
487
+ function collectScatterWriteOwners(
488
+ binding: Binding,
489
+ stmt: t.Statement,
490
+ ): ScatterWriteOwner[] | null {
491
+ const owners: ScatterWriteOwner[] = []
492
+ if (!collectFromBranch(binding, stmt, owners)) {
493
+ return null
494
+ }
495
+ return owners.length > 0 ? owners : null
496
+ }
497
+
498
+ function collectFromBranch(
499
+ binding: Binding,
500
+ stmt: t.Statement,
501
+ owners: ScatterWriteOwner[],
502
+ ): boolean {
503
+ if (t.isIfStatement(stmt)) {
504
+ if (!stmt.alternate) {
505
+ return false
506
+ }
507
+ return (
508
+ collectFromBranch(binding, stmt.consequent, owners) &&
509
+ collectFromBranch(binding, stmt.alternate, owners)
510
+ )
511
+ }
512
+ if (t.isBlockStatement(stmt)) {
513
+ if (stmt.body.length === 0) {
514
+ return false
515
+ }
516
+ const last = stmt.body.at(-1)!
517
+ if (isWriteToBinding(binding, last)) {
518
+ owners.push({
519
+ exprStatement: last as t.ExpressionStatement,
520
+ replaceInBlock(replacement) {
521
+ stmt.body[stmt.body.length - 1] = replacement
522
+ },
523
+ })
524
+ return true
525
+ }
526
+ return collectFromBranch(binding, last, owners)
527
+ }
528
+ if (isWriteToBinding(binding, stmt)) {
529
+ // A bare branch write needs a parent-aware replacement this helper lacks.
530
+ return false
531
+ }
532
+ return false
533
+ }
534
+
535
+ // `fall` tracks assignment on normal completion. `null` means control cannot
536
+ // fall through. Each abrupt exit records whether assignment happened first.
537
+ interface AssignFlow {
538
+ fall: boolean | null
539
+ escapes: {
540
+ label: string | null
541
+ kind: 'break' | 'continue' | 'return' | 'throw'
542
+ assigned: boolean
543
+ }[]
544
+ }
545
+
546
+ // Every path to the trailing read must assign first. Any abrupt exit can expose a
547
+ // redirected write elsewhere and is refused.
548
+ function spanDefinitelyAssigns(binding: Binding, spanPaths: NodePath[]): boolean {
549
+ const flow = analyzeStatementList(
550
+ binding,
551
+ spanPaths.map((path) => path.node as t.Statement),
552
+ false,
553
+ )
554
+ return flow.escapes.length === 0 && flow.fall === true
555
+ }
556
+
557
+ function analyzeStatementList(
558
+ binding: Binding,
559
+ statements: t.Statement[],
560
+ entry: boolean,
561
+ ): AssignFlow {
562
+ let cur: boolean | null = entry
563
+ const escapes: AssignFlow['escapes'] = []
564
+ for (const statement of statements) {
565
+ if (cur === null) {
566
+ // An unconditional exit makes the remaining statements unreachable.
567
+ break
568
+ }
569
+ const flow = analyzeStatement(binding, statement, cur)
570
+ escapes.push(...flow.escapes)
571
+ cur = flow.fall
572
+ }
573
+ return { fall: cur, escapes }
574
+ }
575
+
576
+ function analyzeStatement(binding: Binding, stmt: t.Statement, entry: boolean): AssignFlow {
577
+ if (isWriteToBinding(binding, stmt)) {
578
+ return { fall: true, escapes: [] }
579
+ }
580
+ if (
581
+ t.isEmptyStatement(stmt) ||
582
+ t.isExpressionStatement(stmt) ||
583
+ t.isVariableDeclaration(stmt) ||
584
+ t.isDebuggerStatement(stmt)
585
+ ) {
586
+ return { fall: entry, escapes: [] }
587
+ }
588
+ if (t.isBreakStatement(stmt)) {
589
+ return {
590
+ fall: null,
591
+ escapes: [{ label: stmt.label?.name ?? null, kind: 'break', assigned: entry }],
592
+ }
593
+ }
594
+ if (t.isContinueStatement(stmt)) {
595
+ return {
596
+ fall: null,
597
+ escapes: [{ label: stmt.label?.name ?? null, kind: 'continue', assigned: entry }],
598
+ }
599
+ }
600
+ if (t.isReturnStatement(stmt) || t.isThrowStatement(stmt)) {
601
+ const kind = t.isReturnStatement(stmt) ? 'return' : 'throw'
602
+ return { fall: null, escapes: [{ label: null, kind, assigned: entry }] }
603
+ }
604
+ if (t.isBlockStatement(stmt)) {
605
+ return analyzeStatementList(binding, stmt.body, entry)
606
+ }
607
+ if (t.isIfStatement(stmt)) {
608
+ const consequent = analyzeStatement(binding, stmt.consequent, entry)
609
+ const alternate = stmt.alternate
610
+ ? analyzeStatement(binding, stmt.alternate, entry)
611
+ : { fall: entry, escapes: [] as AssignFlow['escapes'] }
612
+ return {
613
+ fall: combineFall(consequent.fall, alternate.fall),
614
+ escapes: [...consequent.escapes, ...alternate.escapes],
615
+ }
616
+ }
617
+ if (t.isLabeledStatement(stmt)) {
618
+ const inner = analyzeStatement(binding, stmt.body, entry)
619
+ const label = stmt.label.name
620
+ const remaining: AssignFlow['escapes'] = []
621
+ const capturedFall: (boolean | null)[] = [inner.fall]
622
+ for (const escape of inner.escapes) {
623
+ if (escape.kind === 'break' && escape.label === label) {
624
+ // A `break <thisLabel>` completes the labeled statement normally.
625
+ capturedFall.push(escape.assigned)
626
+ } else {
627
+ remaining.push(escape)
628
+ }
629
+ }
630
+ const fall = capturedFall.reduce<boolean | null>((acc, state) => combineFall(acc, state), null)
631
+ return { fall, escapes: remaining }
632
+ }
633
+ // These containers are not proven to assign on every path.
634
+ return { fall: false, escapes: [] }
635
+ }
636
+
637
+ // A non-falling branch defers to the other. Two falling branches must both assign.
638
+ function combineFall(a: boolean | null, b: boolean | null): boolean | null {
639
+ if (a === null) {
640
+ return b
641
+ }
642
+ if (b === null) {
643
+ return a
644
+ }
645
+ return a && b
646
+ }
647
+
648
+ // Apart from redirected writes, the span must run no user code or observations.
649
+ // Redirected right sides must also be pure.
650
+ function spanStatementIsInert(path: NodePath, tempBinding: Binding): boolean {
651
+ const { node } = path
652
+ if (t.isEmptyStatement(node) || t.isBreakStatement(node) || t.isContinueStatement(node)) {
653
+ return true
654
+ }
655
+ if (t.isBlockStatement(node)) {
656
+ return (path.get('body') as NodePath[]).every((inner) =>
657
+ spanStatementIsInert(inner, tempBinding),
658
+ )
659
+ }
660
+ if (t.isLabeledStatement(node)) {
661
+ return spanStatementIsInert(path.get('body') as NodePath, tempBinding)
662
+ }
663
+ if (t.isIfStatement(node)) {
664
+ const test = path.get('test') as NodePath
665
+ if (!isPure(test.node, test.scope, test)) {
666
+ return false
667
+ }
668
+ if (!spanStatementIsInert(path.get('consequent') as NodePath, tempBinding)) {
669
+ return false
670
+ }
671
+ const alternate = path.get('alternate') as NodePath
672
+ return !alternate.node || spanStatementIsInert(alternate, tempBinding)
673
+ }
674
+ if (t.isExpressionStatement(node)) {
675
+ const expr = path.get('expression') as NodePath
676
+ if (isTempWrite(expr.node, tempBinding)) {
677
+ const rhs = expr.get('right') as NodePath
678
+ return isPure(rhs.node, rhs.scope, rhs)
679
+ }
680
+ return isPure(expr.node, expr.scope, expr)
681
+ }
682
+ return false
683
+ }
684
+
685
+ function isTempWrite(node: t.Node, tempBinding: Binding): boolean {
686
+ return (
687
+ t.isAssignmentExpression(node) &&
688
+ node.operator === '=' &&
689
+ t.isIdentifier(node.left) &&
690
+ node.left.name === tempBinding.identifier.name
691
+ )
692
+ }
693
+
694
+ function isWriteToBinding(binding: Binding, stmt: t.Statement): boolean {
695
+ if (!t.isExpressionStatement(stmt)) {
696
+ return false
697
+ }
698
+ const expr = stmt.expression
699
+ if (!t.isAssignmentExpression(expr) || expr.operator !== '=') {
700
+ return false
701
+ }
702
+ if (!t.isIdentifier(expr.left) || expr.left.name !== binding.identifier.name) {
703
+ return false
704
+ }
705
+ return true
706
+ }
707
+
708
+ // A known single-write value can replace reads only after an unconditional write.
709
+ function tryPropagateConstant(
710
+ binding: Binding,
711
+ declaratorPath: NodePath<t.VariableDeclarator>,
712
+ ): boolean {
713
+ const declStmt = declaratorPath.parentPath
714
+ if (!declStmt || !declStmt.isVariableDeclaration()) {
715
+ return false
716
+ }
717
+
718
+ const { init } = declaratorPath.node
719
+ let writeRhs: t.Expression
720
+ let writeRhsPath: NodePath
721
+ let writeStmtToRemove: NodePath | null
722
+ let evalScope: Scope
723
+ let writeSourcePath: NodePath
724
+
725
+ if (init) {
726
+ if (binding.constantViolations.length > 0) {
727
+ return false
728
+ }
729
+ if (!isUnconditionallyExecuted(declStmt, declaratorPath.scope)) {
730
+ return false
731
+ }
732
+ writeRhs = init
733
+ writeRhsPath = declaratorPath.get('init') as NodePath
734
+ writeStmtToRemove = null
735
+ evalScope = declaratorPath.scope
736
+ writeSourcePath = declaratorPath
737
+ } else {
738
+ if (binding.constantViolations.length !== 1) {
739
+ return false
740
+ }
741
+ const violation = binding.constantViolations[0]!
742
+ if (!violation.isAssignmentExpression() || violation.node.operator !== '=') {
743
+ return false
744
+ }
745
+ if (
746
+ !t.isIdentifier(violation.node.left) ||
747
+ violation.node.left.name !== binding.identifier.name
748
+ ) {
749
+ return false
750
+ }
751
+ if (!violation.parentPath?.isExpressionStatement()) {
752
+ return false
753
+ }
754
+ if (violation.parentPath.node.expression !== violation.node) {
755
+ return false
756
+ }
757
+ if (
758
+ violation.scope.getFunctionParent() !== declaratorPath.scope.getFunctionParent() ||
759
+ violation.scope.getProgramParent() !== declaratorPath.scope.getProgramParent()
760
+ ) {
761
+ return false
762
+ }
763
+ if (!isUnconditionallyExecuted(violation.parentPath, declaratorPath.scope)) {
764
+ return false
765
+ }
766
+ writeRhs = violation.node.right
767
+ writeRhsPath = violation.get('right') as NodePath
768
+ writeStmtToRemove = violation.parentPath
769
+ evalScope = violation.scope
770
+ writeSourcePath = violation
771
+ }
772
+
773
+ const evaluation = evaluateConstant(writeRhs, evalScope, writeRhsPath)
774
+ if (!evaluation.known) {
775
+ return false
776
+ }
777
+
778
+ const literal = valueToLiteral(evaluation.value)
779
+ if (!literal) {
780
+ return false
781
+ }
782
+
783
+ for (const ref of binding.referencePaths) {
784
+ // A read before the write still sees `undefined`.
785
+ if (!isPathLive(ref) || !initializerReaches(writeSourcePath, ref)) {
786
+ return false
787
+ }
788
+ // A nested function can run before the declarator despite document order.
789
+ if (readCrossesFunctionBoundary(binding, ref)) {
790
+ return false
791
+ }
792
+ }
793
+ for (const ref of binding.referencePaths) {
794
+ ref.replaceWith(t.cloneNode(literal))
795
+ }
796
+
797
+ if (writeStmtToRemove) {
798
+ writeStmtToRemove.remove()
799
+ }
800
+ if (declStmt.node.declarations.length === 1) {
801
+ declStmt.remove()
802
+ } else {
803
+ declaratorPath.remove()
804
+ }
805
+
806
+ return true
807
+ }
808
+
809
+ // The write must sit only inside unconditional blocks before reaching its
810
+ // function or program. Any other container may leave the binding `undefined`.
811
+ function isUnconditionallyExecuted(stmtPath: NodePath, declScope: Scope): boolean {
812
+ const declFnScope = declScope.getFunctionParent()
813
+ let current: NodePath = stmtPath
814
+ while (current.parentPath) {
815
+ const parent: NodePath = current.parentPath
816
+ if (parent.isFunction()) {
817
+ return parent.scope === declFnScope
818
+ }
819
+ if (parent.isProgram()) {
820
+ return declFnScope === null || declFnScope === undefined
821
+ }
822
+ if (!parent.isBlockStatement()) {
823
+ return false
824
+ }
825
+ current = parent
826
+ }
827
+ return false
828
+ }
829
+
830
+ function ancestorIndexInContainer(
831
+ path: NodePath,
832
+ container: NodePath,
833
+ stmtPaths: NodePath[],
834
+ ): number {
835
+ let current: NodePath = path
836
+ while (current.parentPath && current.parentPath !== container) {
837
+ current = current.parentPath
838
+ }
839
+ if (!current.parentPath || current.parentPath !== container) {
840
+ return -1
841
+ }
842
+ return stmtPaths.findIndex((stmt) => stmt.node === current.node)
843
+ }
844
+
845
+ // Stale paths may have no statement ancestor, where Babel's helper throws.
846
+ function safeGetStatementParent(path: NodePath): NodePath | null {
847
+ let current: NodePath | null = path
848
+ while (current) {
849
+ if (current.isStatement()) {
850
+ return current
851
+ }
852
+ if (!current.parentPath) {
853
+ return null
854
+ }
855
+ current = current.parentPath
856
+ }
857
+ return null
858
+ }
859
+
860
+ function subtreeReadsBinding(stmtPath: NodePath, target: Binding): boolean {
861
+ let found = false
862
+ const visitor = {
863
+ Identifier(path: NodePath<t.Identifier>) {
864
+ if (path.node.name !== target.identifier.name) {
865
+ return
866
+ }
867
+ if (!path.isReferencedIdentifier()) {
868
+ return
869
+ }
870
+ if (path.scope.getBinding(path.node.name) !== target) {
871
+ return
872
+ }
873
+ found = true
874
+ path.stop()
875
+ },
876
+ }
877
+ if (
878
+ stmtPath.isReferencedIdentifier() &&
879
+ (stmtPath.node as t.Identifier).name === target.identifier.name &&
880
+ stmtPath.scope.getBinding((stmtPath.node as t.Identifier).name) === target
881
+ ) {
882
+ return true
883
+ }
884
+ stmtPath.traverse(visitor)
885
+ return found
886
+ }
887
+
888
+ function isStatementMovementSafe(path: NodePath): boolean {
889
+ const { node } = path
890
+ if (t.isEmptyStatement(node)) {
891
+ return true
892
+ }
893
+ // Crossing a declaration can change name resolution or TDZ.
894
+ if (t.isExpressionStatement(node)) {
895
+ const expr = path.get('expression') as NodePath
896
+ return isPure(expr.node, expr.scope, expr)
897
+ }
898
+ return false
899
+ }
900
+
901
+ // Callers locate and index statements within this same path array.
902
+ function getContainerStatementPaths(container: NodePath): NodePath[] | null {
903
+ if (container.isBlock()) {
904
+ return container.get('body') as NodePath[]
905
+ }
906
+ if (container.isSwitchCase()) {
907
+ return container.get('consequent') as NodePath[]
908
+ }
909
+ return null
910
+ }
911
+
912
+ // Loop tests, updates, and iterator targets repeat each iteration.
913
+ function readIsInLoopHeader(readPath: NodePath, readStmt: NodePath): boolean {
914
+ if (
915
+ !readStmt.isForStatement() &&
916
+ !readStmt.isWhileStatement() &&
917
+ !readStmt.isDoWhileStatement() &&
918
+ !readStmt.isForInStatement() &&
919
+ !readStmt.isForOfStatement()
920
+ ) {
921
+ return false
922
+ }
923
+ let ancestor: NodePath | null = readPath
924
+ while (ancestor && ancestor.parentPath !== readStmt) {
925
+ ancestor = ancestor.parentPath
926
+ }
927
+ if (!ancestor) {
928
+ return false
929
+ }
930
+ if (readStmt.isForStatement()) {
931
+ return ancestor.parentKey === 'test' || ancestor.parentKey === 'update'
932
+ }
933
+ if (readStmt.isForInStatement() || readStmt.isForOfStatement()) {
934
+ // The iterable runs once. Only the target repeats.
935
+ return ancestor.parentKey === 'left'
936
+ }
937
+ return ancestor.parentKey === 'test'
938
+ }
939
+
940
+ // Babel marks only the directly removed path. A live descendant must still reach
941
+ // the Program through parents that were not removed.
942
+ function isPathLive(path: NodePath): boolean {
943
+ if (path.removed || path.node == null) {
944
+ return false
945
+ }
946
+ let current: NodePath | null = path
947
+ while (current) {
948
+ if (current.removed) {
949
+ return false
950
+ }
951
+ if (current.isProgram()) {
952
+ return true
953
+ }
954
+ current = current.parentPath
955
+ }
956
+ return false
957
+ }