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,1074 @@
1
+ import traverse from '@babel/traverse'
2
+ import type { NodePath, Scope } from '@babel/traverse'
3
+ import * as t from '@babel/types'
4
+ import type { File } from '@babel/types'
5
+
6
+ import { analyzeInlineability } from 'src/analysis/inlineability'
7
+ import type { InlineCandidate } from 'src/analysis/inlineability'
8
+ import { isPure } from 'src/analysis/purity'
9
+ import { enclosingScopeHasDirectEval, isInsideWith, walkOwnFunctionScope } from 'src/utils/ast'
10
+ import { reorderableToStatement } from 'src/utils/evaluation-order'
11
+ import { generateLoopLabel } from 'src/utils/loop-lowering'
12
+ import { hasAnnexBFunctionAlias, isInStrictContext } from 'src/utils/paths'
13
+
14
+ /**
15
+ * Replaces eligible calls with function bodies, working from inner calls out.
16
+ * Declarations are removed after their last reference disappears.
17
+ *
18
+ * @example
19
+ * // ◀️ before
20
+ * function id(x) { return x; }
21
+ * var y = id(compute());
22
+ *
23
+ * // ▶️ after
24
+ * var _x = compute();
25
+ * var y = _x;
26
+ */
27
+ export function inlineFunctions(ast: File): boolean {
28
+ const candidates = analyzeInlineability(ast)
29
+ if (candidates.size === 0) {
30
+ return false
31
+ }
32
+
33
+ let anyInlined = false
34
+
35
+ traverse(ast, {
36
+ CallExpression: {
37
+ exit(path) {
38
+ // Direct statement-array edits bypass Babel's binding updates. Crawl the
39
+ // owner so later sibling calls see new initialized temps in this pass.
40
+ const tempOwner = path.getFunctionParent() ?? path.scope.getProgramParent().path
41
+ if (tryInlineAtCallExpr(path, candidates)) {
42
+ anyInlined = true
43
+ tempOwner.scope.crawl()
44
+ rebindCandidates(candidates)
45
+ }
46
+ },
47
+ },
48
+ })
49
+
50
+ if (!anyInlined) {
51
+ return false
52
+ }
53
+
54
+ dropZeroRefCandidates(candidates)
55
+ return true
56
+ }
57
+
58
+ function tryInlineAtCallExpr(
59
+ path: NodePath<t.CallExpression>,
60
+ candidates: Map<string, InlineCandidate>,
61
+ ): boolean {
62
+ const { callee } = path.node
63
+ if (!t.isIdentifier(callee)) {
64
+ return false
65
+ }
66
+ const candidate = candidates.get(callee.name)
67
+ if (!candidate) {
68
+ return false
69
+ }
70
+ const binding = path.scope.getBinding(callee.name)
71
+ if (!binding || binding !== candidate.binding) {
72
+ return false
73
+ }
74
+ // Earlier inlining can add free references to this candidate's live body.
75
+ const freeVars = computeLiveFreeVars(candidate)
76
+ if (!isSafeAtCallSite(candidate, freeVars, path, candidates)) {
77
+ return false
78
+ }
79
+ return inlineCallSite(path, candidate, freeVars)
80
+ }
81
+
82
+ // A crawl replaces Binding objects. Re-resolve candidates from their declarations.
83
+ function rebindCandidates(candidates: Map<string, InlineCandidate>): void {
84
+ for (const [name, candidate] of candidates) {
85
+ const declId = declarationIdentifier(candidate.declarationPath)
86
+ const binding = candidate.declarationPath.removed
87
+ ? null
88
+ : candidate.declarationPath.scope.getBinding(name)
89
+ if (!binding || !declId || binding.identifier !== declId) {
90
+ candidates.delete(name)
91
+ continue
92
+ }
93
+ candidate.binding = binding
94
+ }
95
+ }
96
+
97
+ function dropZeroRefCandidates(candidates: Map<string, InlineCandidate>): void {
98
+ const first = candidates.values().next().value
99
+ if (!first) {
100
+ return
101
+ }
102
+ first.declarationPath.scope.getProgramParent().crawl()
103
+ for (const candidate of candidates.values()) {
104
+ const declPath = candidate.declarationPath
105
+ if (declPath.removed) {
106
+ continue
107
+ }
108
+ const declId = declarationIdentifier(declPath)
109
+ if (!declId) {
110
+ continue
111
+ }
112
+ // An outer same-named binding must not decide whether this declaration drops.
113
+ const binding = declPath.scope.getBinding(candidate.name)
114
+ if (!binding || binding.identifier !== declId || binding.references > 0) {
115
+ continue
116
+ }
117
+ declPath.remove()
118
+ }
119
+ }
120
+
121
+ function declarationIdentifier(declPath: NodePath): t.Identifier | null {
122
+ if (declPath.isFunctionDeclaration()) {
123
+ return declPath.node.id ?? null
124
+ }
125
+ if (declPath.isVariableDeclaration() && declPath.node.declarations.length === 1) {
126
+ const { id } = declPath.node.declarations[0]!
127
+ return t.isIdentifier(id) ? id : null
128
+ }
129
+ return null
130
+ }
131
+
132
+ function isSafeAtCallSite(
133
+ candidate: InlineCandidate,
134
+ freeVars: Set<string>,
135
+ callPath: NodePath<t.CallExpression>,
136
+ candidates: Map<string, InlineCandidate>,
137
+ ): boolean {
138
+ // `with`, direct eval, and Annex B aliases can redirect free names outside
139
+ // Babel's scope table.
140
+ if (isInsideWith(callPath) || enclosingScopeHasDirectEval(callPath)) {
141
+ return false
142
+ }
143
+ if (annexBHoistShadowsFreeVar(callPath, freeVars)) {
144
+ return false
145
+ }
146
+ // Spliced code inherits call-site strictness, so it must match the callee.
147
+ if (isInStrictContext(candidate.fnPath) !== isInStrictContext(callPath)) {
148
+ return false
149
+ }
150
+ // In a loop, spliced uninitialized `var` bindings would retain prior values.
151
+ if (
152
+ callIsInsideLoop(callPath) &&
153
+ bodyHasUninitializedVar(candidate.fnNode.body as t.BlockStatement)
154
+ ) {
155
+ return false
156
+ }
157
+
158
+ for (const freeVar of freeVars) {
159
+ const declSiteBinding = candidate.fnPath.scope.parent?.getBinding(freeVar)
160
+ const callSiteBinding = callPath.scope.getBinding(freeVar)
161
+ if (declSiteBinding !== callSiteBinding) {
162
+ return false
163
+ }
164
+ }
165
+
166
+ if (isInUnsafeExpressionPosition(callPath)) {
167
+ return false
168
+ }
169
+
170
+ const stmtParent = callPath.getStatementParent()
171
+ if (!stmtParent) {
172
+ return false
173
+ }
174
+ // The call must run exactly once at statement position. Repeating loop slots,
175
+ // short-circuiting chains, case tests, and earlier effects are refused.
176
+ if (stmtParent.isLoop() && callInReevaluatedLoopPosition(callPath, stmtParent)) {
177
+ return false
178
+ }
179
+ if (withinOptionalChain(callPath, stmtParent)) {
180
+ return false
181
+ }
182
+ if (!reorderableToStatement(callPath, stmtParent)) {
183
+ return false
184
+ }
185
+
186
+ if (argsContainContextExpression(callPath)) {
187
+ return false
188
+ }
189
+ if (argsContainCandidateCalls(callPath, candidates)) {
190
+ return false
191
+ }
192
+
193
+ return true
194
+ }
195
+
196
+ // ECMA-262 B.3.2.1 creates a sloppy block-function alias that Babel misses in
197
+ // the enclosing scope.
198
+ function annexBHoistShadowsFreeVar(
199
+ callPath: NodePath<t.CallExpression>,
200
+ freeVars: Set<string>,
201
+ ): boolean {
202
+ if (freeVars.size === 0 || isInStrictContext(callPath)) {
203
+ return false
204
+ }
205
+ const owner = callPath.getFunctionParent() ?? callPath.scope.getProgramParent().path
206
+ let found = false
207
+ owner.traverse({
208
+ Function(path) {
209
+ path.skip()
210
+ },
211
+ FunctionDeclaration(path) {
212
+ const { id } = path.node
213
+ if (id && freeVars.has(id.name) && hasAnnexBFunctionAlias(path)) {
214
+ found = true
215
+ path.stop()
216
+ }
217
+ },
218
+ })
219
+ return found
220
+ }
221
+
222
+ // Only loops in the same function repeat the spliced body.
223
+ function callIsInsideLoop(callPath: NodePath): boolean {
224
+ for (let current: NodePath | null = callPath.parentPath; current; current = current.parentPath) {
225
+ if (current.isLoop()) {
226
+ return true
227
+ }
228
+ if (current.isFunction() || current.isProgram()) {
229
+ return false
230
+ }
231
+ }
232
+ return false
233
+ }
234
+
235
+ function bodyHasUninitializedVar(body: t.BlockStatement): boolean {
236
+ let found = false
237
+ walkOwnFunctionScope(body, (node) => {
238
+ if (t.isVariableDeclarator(node) && t.isIdentifier(node.id) && node.init == null) {
239
+ found = true
240
+ }
241
+ })
242
+ return found
243
+ }
244
+
245
+ function bodyHasTryFinally(body: t.BlockStatement): boolean {
246
+ let found = false
247
+ walkOwnFunctionScope(body, (node) => {
248
+ if (t.isTryStatement(node) && node.finalizer != null) {
249
+ found = true
250
+ }
251
+ })
252
+ return found
253
+ }
254
+
255
+ // Loop tests and updates repeat. Initializers and iterator sources run once.
256
+ function callInReevaluatedLoopPosition(callPath: NodePath, loopPath: NodePath): boolean {
257
+ let cur: NodePath = callPath
258
+ while (cur.parentPath && cur.parentPath !== loopPath) {
259
+ cur = cur.parentPath
260
+ }
261
+ if (loopPath.isForStatement()) {
262
+ return cur.key !== 'init'
263
+ }
264
+ if (loopPath.isForInStatement() || loopPath.isForOfStatement()) {
265
+ return cur.key !== 'right'
266
+ }
267
+ return true
268
+ }
269
+
270
+ // Any optional ancestor may short-circuit the call.
271
+ function withinOptionalChain(callPath: NodePath, stmtParent: NodePath): boolean {
272
+ let cur: NodePath = callPath
273
+ for (;;) {
274
+ const parent = cur.parentPath
275
+ if (!parent || parent === stmtParent) {
276
+ return false
277
+ }
278
+ if (parent.isOptionalMemberExpression() || parent.isOptionalCallExpression()) {
279
+ return true
280
+ }
281
+ cur = parent
282
+ }
283
+ }
284
+
285
+ function argsContainContextExpression(callPath: NodePath<t.CallExpression>): boolean {
286
+ const stmtParent = callPath.getStatementParent()
287
+ if (!stmtParent) {
288
+ return false
289
+ }
290
+
291
+ if (
292
+ callPath.parentPath === stmtParent ||
293
+ (callPath.parentPath?.isExpressionStatement() && callPath.parentPath === stmtParent)
294
+ ) {
295
+ return false
296
+ }
297
+
298
+ for (const arg of callPath.node.arguments) {
299
+ if (containsThisOrArguments(arg)) {
300
+ return true
301
+ }
302
+ }
303
+ return false
304
+ }
305
+
306
+ function containsThisOrArguments(node: t.Node): boolean {
307
+ let found = false
308
+ t.traverseFast(node, (visited) => {
309
+ // Non-arrow functions bind their own `this` and `arguments`.
310
+ if (t.isFunction(visited) && !t.isArrowFunctionExpression(visited)) {
311
+ return t.traverseFast.skip
312
+ }
313
+ const isContextRef =
314
+ t.isThisExpression(visited) || (t.isIdentifier(visited) && visited.name === 'arguments')
315
+ if (isContextRef) {
316
+ found = true
317
+ }
318
+ return isContextRef ? t.traverseFast.stop : undefined
319
+ })
320
+ return found
321
+ }
322
+
323
+ function argsContainCandidateCalls(
324
+ callPath: NodePath<t.CallExpression>,
325
+ candidates: Map<string, InlineCandidate>,
326
+ ): boolean {
327
+ for (const arg of callPath.node.arguments) {
328
+ if (containsCandidateCall(arg, candidates)) {
329
+ return true
330
+ }
331
+ }
332
+ return false
333
+ }
334
+
335
+ function containsCandidateCall(node: t.Node, candidates: Map<string, InlineCandidate>): boolean {
336
+ let found = false
337
+ t.traverseFast(node, (visited) => {
338
+ const isCandidateCall =
339
+ t.isCallExpression(visited) &&
340
+ t.isIdentifier(visited.callee) &&
341
+ candidates.has(visited.callee.name)
342
+ if (isCandidateCall) {
343
+ found = true
344
+ }
345
+ return isCandidateCall ? t.traverseFast.stop : undefined
346
+ })
347
+ return found
348
+ }
349
+
350
+ function isInUnsafeExpressionPosition(path: NodePath): boolean {
351
+ let current: NodePath = path
352
+ while (current.parentPath) {
353
+ const parent = current.parentPath
354
+ if (parent.isStatement()) {
355
+ return false
356
+ }
357
+
358
+ if (
359
+ parent.isConditionalExpression() &&
360
+ (current.key === 'consequent' || current.key === 'alternate')
361
+ ) {
362
+ return true
363
+ }
364
+ if (parent.isLogicalExpression() && current.key === 'right') {
365
+ return true
366
+ }
367
+ if (
368
+ parent.isSequenceExpression() &&
369
+ parent.node.expressions.indexOf(current.node as t.Expression) > 0
370
+ ) {
371
+ return true
372
+ }
373
+ // Default values run per call in the function's parameter scope.
374
+ if (parent.isAssignmentPattern() && current.key === 'right') {
375
+ return true
376
+ }
377
+ // Logical-assignment right sides are conditional.
378
+ if (
379
+ parent.isAssignmentExpression() &&
380
+ current.key === 'right' &&
381
+ (parent.node.operator === '||=' ||
382
+ parent.node.operator === '&&=' ||
383
+ parent.node.operator === '??=')
384
+ ) {
385
+ return true
386
+ }
387
+
388
+ current = parent
389
+ }
390
+ return false
391
+ }
392
+
393
+ function getReturnPattern(stmts: t.Statement[]): 'none' | 'trailing' | 'complex' {
394
+ if (stmts.length === 0) {
395
+ return 'none'
396
+ }
397
+ for (let i = 0; i < stmts.length - 1; i++) {
398
+ if (containsReturn(stmts[i]!)) {
399
+ return 'complex'
400
+ }
401
+ }
402
+ const last = stmts.at(-1)!
403
+ if (t.isReturnStatement(last)) {
404
+ return 'trailing'
405
+ }
406
+ if (containsReturn(last)) {
407
+ return 'complex'
408
+ }
409
+ return 'none'
410
+ }
411
+
412
+ function containsReturn(node: t.Node): boolean {
413
+ let found = false
414
+ t.traverseFast(node, (visited) => {
415
+ if (t.isFunction(visited) && visited !== node) {
416
+ return t.traverseFast.skip
417
+ }
418
+ const isReturn = t.isReturnStatement(visited)
419
+ if (isReturn) {
420
+ found = true
421
+ }
422
+ return isReturn ? t.traverseFast.stop : undefined
423
+ })
424
+ return found
425
+ }
426
+
427
+ function inlineCallSite(
428
+ callPath: NodePath<t.CallExpression>,
429
+ candidate: InlineCandidate,
430
+ freeVars: Set<string>,
431
+ ): boolean {
432
+ if (callPath.removed) {
433
+ return false
434
+ }
435
+
436
+ const args = callPath.node.arguments as t.Expression[]
437
+ const argPaths = callPath.get('arguments') as NodePath<t.Expression>[]
438
+ const body = candidate.fnNode.body as t.BlockStatement
439
+ const returnPattern = getReturnPattern(body.body)
440
+
441
+ const stmtParent = callPath.getStatementParent()
442
+ if (!stmtParent) {
443
+ return false
444
+ }
445
+
446
+ const containerPath = stmtParent.parentPath
447
+ if (!containerPath) {
448
+ return false
449
+ }
450
+
451
+ let containerArray: t.Statement[] | null = null
452
+ if (containerPath.isBlock()) {
453
+ containerArray = (containerPath.node as t.Block).body
454
+ } else if (containerPath.isSwitchCase()) {
455
+ containerArray = containerPath.node.consequent
456
+ }
457
+ if (!containerArray) {
458
+ return false
459
+ }
460
+
461
+ const stmtIndex = containerArray.indexOf(stmtParent.node)
462
+ if (stmtIndex === -1) {
463
+ return false
464
+ }
465
+
466
+ const { scope } = stmtParent
467
+ const labelName = generateLoopLabel(stmtParent, 'inlined')
468
+
469
+ // Returns can write directly into a bare assignment or `var` initializer.
470
+ // Hoisted `var` already defaults to `undefined`. A plain assignment is safe
471
+ // only with one trailing return and no fall-through path.
472
+ const trailingReturn =
473
+ returnPattern === 'trailing' ? (body.body.at(-1) as t.ReturnStatement) : null
474
+ const trailingReturnHasArgument = trailingReturn != null && trailingReturn.argument != null
475
+ const rawTarget =
476
+ returnPattern === 'none' ? null : identifyDirectAssignmentTarget(callPath, freeVars, args)
477
+ // A finalizer runs before the call publishes its return value. Direct writes
478
+ // inside its `try` would expose the value too early.
479
+ const returnObservableByFinally = returnPattern === 'complex' && bodyHasTryFinally(body)
480
+ const directTarget =
481
+ rawTarget == null ||
482
+ returnObservableByFinally ||
483
+ (rawTarget.kind === 'assign' && !trailingReturnHasArgument)
484
+ ? null
485
+ : rawTarget
486
+ const retVarName = directTarget ? directTarget.name : scope.generateUid('returnValue')
487
+
488
+ const nameMap = new Map<string, string>()
489
+ const elidedParams = new Set<string>()
490
+ for (let i = 0; i < candidate.params.length; i++) {
491
+ const param = candidate.params[i]!
492
+ const argPath = i < args.length ? argPaths[i]! : null
493
+ if (
494
+ argPath &&
495
+ argEligibleForElision(argPath, callPath.scope) &&
496
+ !freeVars.has((argPath.node as t.Identifier).name) &&
497
+ isParamReadOnly(candidate, param) &&
498
+ // An elided parameter must not alias the target that receives return writes.
499
+ (argPath.node as t.Identifier).name !== retVarName
500
+ ) {
501
+ nameMap.set(param, (argPath.node as t.Identifier).name)
502
+ elidedParams.add(param)
503
+ } else {
504
+ nameMap.set(param, scope.generateUid(param))
505
+ }
506
+ }
507
+ for (const name of collectFunctionScopedNames(body)) {
508
+ if (!nameMap.has(name)) {
509
+ nameMap.set(name, scope.generateUid(name))
510
+ }
511
+ }
512
+
513
+ const labelMap = new Map<string, string>()
514
+ collectLabels(body, labelMap, scope)
515
+
516
+ // Each call site needs an independently renamed deep clone.
517
+ const clonedBody = t.cloneNode(body, true).body
518
+
519
+ for (const stmt of clonedBody) {
520
+ renameInBody(stmt, nameMap)
521
+ }
522
+ for (const stmt of clonedBody) {
523
+ renameLabelsInNode(stmt, labelMap)
524
+ }
525
+
526
+ let emittedAnyBreak = false
527
+ if (returnPattern === 'complex') {
528
+ walkStatementsMut(
529
+ clonedBody,
530
+ {
531
+ onReturn(node, ctx) {
532
+ // A bare return must overwrite any earlier return value with `undefined`.
533
+ const argument = node.argument ?? t.unaryExpression('void', t.numericLiteral(0))
534
+ const stmts: t.Statement[] = [
535
+ t.expressionStatement(t.assignmentExpression('=', t.identifier(retVarName), argument)),
536
+ ]
537
+ if (!ctx.tailPosition) {
538
+ stmts.push(t.breakStatement(t.identifier(labelName)))
539
+ emittedAnyBreak = true
540
+ }
541
+ return stmts
542
+ },
543
+ },
544
+ { insideNestedLoop: false, insideNestedSwitch: false, tailPosition: true },
545
+ )
546
+ } else if (returnPattern === 'trailing') {
547
+ const lastIdx = clonedBody.length - 1
548
+ const last = clonedBody[lastIdx]!
549
+ if (t.isReturnStatement(last)) {
550
+ if (last.argument) {
551
+ clonedBody[lastIdx] = t.expressionStatement(
552
+ t.assignmentExpression('=', t.identifier(retVarName), last.argument),
553
+ )
554
+ } else {
555
+ clonedBody.splice(lastIdx, 1)
556
+ }
557
+ }
558
+ }
559
+
560
+ const paramAssignments: t.Statement[] = []
561
+ for (let i = 0; i < candidate.params.length; i++) {
562
+ if (elidedParams.has(candidate.params[i]!)) {
563
+ continue
564
+ }
565
+ const newName = nameMap.get(candidate.params[i]!)!
566
+ const argNode =
567
+ i < args.length ? t.cloneNode(args[i]!) : t.unaryExpression('void', t.numericLiteral(0))
568
+ paramAssignments.push(
569
+ t.variableDeclaration('var', [t.variableDeclarator(t.identifier(newName), argNode)]),
570
+ )
571
+ }
572
+
573
+ for (let i = candidate.params.length; i < args.length; i++) {
574
+ paramAssignments.push(t.expressionStatement(t.cloneNode(args[i]!)))
575
+ }
576
+
577
+ // Only rewritten early returns need the labeled wrapper.
578
+ const needsLabel = returnPattern === 'complex' && emittedAnyBreak
579
+
580
+ if (directTarget) {
581
+ const replacement: t.Statement[] = []
582
+ if (directTarget.kind === 'declarator') {
583
+ replacement.push(
584
+ t.variableDeclaration('var', [t.variableDeclarator(t.identifier(directTarget.name))]),
585
+ )
586
+ }
587
+ if (needsLabel) {
588
+ replacement.push(
589
+ t.labeledStatement(
590
+ t.identifier(labelName),
591
+ t.blockStatement([...paramAssignments, ...clonedBody]),
592
+ ),
593
+ )
594
+ } else {
595
+ replacement.push(...paramAssignments, ...clonedBody)
596
+ }
597
+ stmtParent.replaceWithMultiple(replacement)
598
+ return true
599
+ }
600
+
601
+ if (needsLabel) {
602
+ const retVarDecl = t.variableDeclaration('var', [
603
+ t.variableDeclarator(t.identifier(retVarName)),
604
+ ])
605
+ const labeledBlock = t.labeledStatement(
606
+ t.identifier(labelName),
607
+ t.blockStatement([...paramAssignments, ...clonedBody]),
608
+ )
609
+ containerArray.splice(stmtIndex, 0, retVarDecl, labeledBlock)
610
+ callPath.replaceWith(t.identifier(retVarName))
611
+ } else if (returnPattern === 'none') {
612
+ containerArray.splice(stmtIndex, 0, ...paramAssignments, ...clonedBody)
613
+ callPath.replaceWith(t.unaryExpression('void', t.numericLiteral(0)))
614
+ } else {
615
+ const retVarDecl = t.variableDeclaration('var', [
616
+ t.variableDeclarator(t.identifier(retVarName)),
617
+ ])
618
+ containerArray.splice(stmtIndex, 0, retVarDecl, ...paramAssignments, ...clonedBody)
619
+ callPath.replaceWith(t.identifier(retVarName))
620
+ }
621
+ return true
622
+ }
623
+
624
+ type DirectTarget = { kind: 'declarator'; name: string } | { kind: 'assign'; name: string }
625
+
626
+ function identifyDirectAssignmentTarget(
627
+ callPath: NodePath<t.CallExpression>,
628
+ freeVars: Set<string>,
629
+ args: t.Expression[],
630
+ ): DirectTarget | null {
631
+ const parent = callPath.parentPath
632
+ if (!parent) {
633
+ return null
634
+ }
635
+
636
+ if (
637
+ parent.isVariableDeclarator() &&
638
+ t.isIdentifier(parent.node.id) &&
639
+ parent.node.init === callPath.node
640
+ ) {
641
+ const declPath = parent.parentPath
642
+ if (!declPath || !declPath.isVariableDeclaration() || declPath.node.declarations.length !== 1) {
643
+ return null
644
+ }
645
+ // Only `var` can become an uninitialized direct target without changing TDZ
646
+ // or block scope.
647
+ if (declPath.node.kind !== 'var') {
648
+ return null
649
+ }
650
+ const owner = declPath.parentPath
651
+ if (!owner) {
652
+ return null
653
+ }
654
+ if (owner.isForStatement() && owner.node.init === declPath.node) {
655
+ return null
656
+ }
657
+ if (owner.isForInStatement() && owner.node.left === declPath.node) {
658
+ return null
659
+ }
660
+ if (owner.isForOfStatement() && owner.node.left === declPath.node) {
661
+ return null
662
+ }
663
+ const { name } = parent.node.id
664
+ if (freeVars.has(name)) {
665
+ return null
666
+ }
667
+ if (argsContainIdentName(args, name)) {
668
+ return null
669
+ }
670
+ return { kind: 'declarator', name }
671
+ }
672
+
673
+ if (
674
+ parent.isAssignmentExpression() &&
675
+ parent.node.operator === '=' &&
676
+ t.isIdentifier(parent.node.left) &&
677
+ parent.node.right === callPath.node
678
+ ) {
679
+ const stmt = parent.parentPath
680
+ if (!stmt || !stmt.isExpressionStatement() || stmt.node.expression !== parent.node) {
681
+ return null
682
+ }
683
+ const { name } = parent.node.left
684
+ if (freeVars.has(name)) {
685
+ return null
686
+ }
687
+ if (argsContainIdentName(args, name)) {
688
+ return null
689
+ }
690
+ return { kind: 'assign', name }
691
+ }
692
+
693
+ return null
694
+ }
695
+
696
+ function argsContainIdentName(args: t.Expression[], name: string): boolean {
697
+ for (const arg of args) {
698
+ if (t.isIdentifier(arg) && arg.name === name) {
699
+ return true
700
+ }
701
+ }
702
+ return false
703
+ }
704
+
705
+ function isParamReadOnly(candidate: InlineCandidate, paramName: string): boolean {
706
+ const binding = candidate.fnPath.scope.bindings[paramName]
707
+ return !binding || binding.constantViolations.length === 0
708
+ }
709
+
710
+ // Eliding a parameter repeats the argument read at each use. The read must be
711
+ // pure and resolve to an invariant binding.
712
+ function argEligibleForElision(argPath: NodePath, scope: Scope): boolean {
713
+ if (!argPath.isIdentifier()) {
714
+ return false
715
+ }
716
+ const binding = scope.getBinding(argPath.node.name)
717
+ if (!binding || binding.constantViolations.length > 0) {
718
+ return false
719
+ }
720
+ return isPure(argPath.node, scope, argPath)
721
+ }
722
+
723
+ // Catch parameters stay scoped to their copied handlers. Counting them here
724
+ // would rename same-named free references outside the handler.
725
+ function collectFunctionScopedNames(body: t.Node): Set<string> {
726
+ const names = new Set<string>()
727
+ walkOwnFunctionScope(body, (node) => {
728
+ if (t.isVariableDeclarator(node) && t.isIdentifier(node.id)) {
729
+ names.add(node.id.name)
730
+ }
731
+ })
732
+ return names
733
+ }
734
+
735
+ // Optional catch bindings contribute no names.
736
+ function catchParamNames(clause: t.CatchClause): Set<string> {
737
+ if (!clause.param) {
738
+ return new Set()
739
+ }
740
+ return new Set(Object.keys(t.getBindingIdentifiers(clause.param)))
741
+ }
742
+
743
+ // Nested functions do not contribute names to the current function scope.
744
+ function collectOwnScopeBindings(fn: t.Function): Set<string> {
745
+ const declared = new Set<string>()
746
+ for (const param of fn.params) {
747
+ for (const name of Object.keys(t.getBindingIdentifiers(param))) {
748
+ declared.add(name)
749
+ }
750
+ }
751
+ if (t.isBlockStatement(fn.body)) {
752
+ for (const name of collectFunctionScopedNames(fn.body)) {
753
+ declared.add(name)
754
+ }
755
+ }
756
+ return declared
757
+ }
758
+
759
+ // Recompute free names from the live body after earlier inlining. Conservative
760
+ // extras only add call-site identity checks.
761
+ function computeLiveFreeVars(candidate: InlineCandidate): Set<string> {
762
+ const body = candidate.fnNode.body as t.BlockStatement
763
+ const ownBound = collectOwnScopeBindings(candidate.fnNode)
764
+
765
+ const free = new Set<string>()
766
+ const shadows = new ShadowStack()
767
+
768
+ t.traverse(body, {
769
+ enter(node, ancestors) {
770
+ if (t.isFunction(node)) {
771
+ shadows.push(node, collectOwnScopeBindings(node))
772
+ return
773
+ }
774
+ if (t.isCatchClause(node)) {
775
+ shadows.push(node, catchParamNames(node))
776
+ return
777
+ }
778
+ if (!t.isIdentifier(node)) {
779
+ return
780
+ }
781
+ const slot = ancestors.at(-1)
782
+ if (slot && isNonReferenceSlot(slot.node, slot.key)) {
783
+ return
784
+ }
785
+ const { name } = node
786
+ if (ownBound.has(name) || shadows.covers(name)) {
787
+ return
788
+ }
789
+ free.add(name)
790
+ },
791
+ exit(node) {
792
+ if (t.isFunction(node) || t.isCatchClause(node)) {
793
+ shadows.pop(node)
794
+ }
795
+ },
796
+ })
797
+ return free
798
+ }
799
+
800
+ // Live shadow frames prevent renaming references owned by an inner binding.
801
+ class ShadowStack {
802
+ private readonly counts = new Map<string, number>()
803
+ private readonly framesByNode = new WeakMap<t.Node, Set<string>>()
804
+
805
+ push(node: t.Node, names: Set<string>): void {
806
+ if (names.size === 0) {
807
+ return
808
+ }
809
+ this.framesByNode.set(node, names)
810
+ for (const name of names) {
811
+ this.counts.set(name, (this.counts.get(name) ?? 0) + 1)
812
+ }
813
+ }
814
+
815
+ pop(node: t.Node): void {
816
+ const names = this.framesByNode.get(node)
817
+ if (!names) {
818
+ return
819
+ }
820
+ this.framesByNode.delete(node)
821
+ for (const name of names) {
822
+ const count = this.counts.get(name)!
823
+ if (count <= 1) {
824
+ this.counts.delete(name)
825
+ } else {
826
+ this.counts.set(name, count - 1)
827
+ }
828
+ }
829
+ }
830
+
831
+ covers(name: string): boolean {
832
+ return (this.counts.get(name) ?? 0) > 0
833
+ }
834
+ }
835
+
836
+ function collectLabels(body: t.BlockStatement, labelMap: Map<string, string>, scope: Scope): void {
837
+ walkOwnFunctionScope(body, (node) => {
838
+ if (t.isLabeledStatement(node) && !labelMap.has(node.label.name)) {
839
+ labelMap.set(node.label.name, scope.generateUid(node.label.name))
840
+ }
841
+ })
842
+ }
843
+
844
+ function renameLabelsInNode(node: t.Node, labelMap: Map<string, string>): void {
845
+ if (t.isFunction(node)) {
846
+ return
847
+ }
848
+ walkOwnFunctionScope(node, (visited) => {
849
+ if (t.isLabeledStatement(visited) && labelMap.has(visited.label.name)) {
850
+ visited.label = t.identifier(labelMap.get(visited.label.name)!)
851
+ }
852
+ if (t.isBreakStatement(visited) && visited.label && labelMap.has(visited.label.name)) {
853
+ visited.label = t.identifier(labelMap.get(visited.label.name)!)
854
+ }
855
+ if (t.isContinueStatement(visited) && visited.label && labelMap.has(visited.label.name)) {
856
+ visited.label = t.identifier(labelMap.get(visited.label.name)!)
857
+ }
858
+ })
859
+ }
860
+
861
+ // Rename the detached clone before splicing. Babel's scope renamer needs a live
862
+ // path, while splicing first would publish unrenamed hoisted variables.
863
+ function renameInBody(node: t.Node, nameMap: Map<string, string>): void {
864
+ const shadows = new ShadowStack()
865
+
866
+ t.traverse(node, {
867
+ enter(visited, ancestors) {
868
+ if (t.isFunction(visited)) {
869
+ shadows.push(visited, mappedShadow(collectOwnScopeBindings(visited), nameMap))
870
+ return
871
+ }
872
+ if (t.isCatchClause(visited)) {
873
+ shadows.push(visited, mappedShadow(catchParamNames(visited), nameMap))
874
+ return
875
+ }
876
+ if (!t.isIdentifier(visited)) {
877
+ return
878
+ }
879
+ const mapped = nameMap.get(visited.name)
880
+ if (mapped === undefined) {
881
+ return
882
+ }
883
+ if (shadows.covers(visited.name)) {
884
+ return
885
+ }
886
+ const slot = ancestors.at(-1)
887
+ if (slot && isNonReferenceSlot(slot.node, slot.key)) {
888
+ return
889
+ }
890
+ visited.name = mapped
891
+ },
892
+ exit(visited) {
893
+ if (t.isFunction(visited) || t.isCatchClause(visited)) {
894
+ shadows.pop(visited)
895
+ }
896
+ },
897
+ })
898
+ }
899
+
900
+ // Only remapped names need shadow tracking.
901
+ function mappedShadow(names: Set<string>, nameMap: Map<string, string>): Set<string> {
902
+ const introduced = new Set<string>()
903
+ for (const name of names) {
904
+ if (nameMap.has(name)) {
905
+ introduced.add(name)
906
+ }
907
+ }
908
+ return introduced
909
+ }
910
+
911
+ function isNonReferenceSlot(parent: t.Node, key: string): boolean {
912
+ if (key === 'property' && t.isMemberExpression(parent) && !parent.computed) {
913
+ return true
914
+ }
915
+ if (
916
+ key === 'key' &&
917
+ (t.isObjectProperty(parent) || t.isObjectMethod(parent) || t.isClassMethod(parent)) &&
918
+ !(parent as t.ObjectProperty).computed
919
+ ) {
920
+ return true
921
+ }
922
+ if (
923
+ key === 'label' &&
924
+ (t.isLabeledStatement(parent) || t.isBreakStatement(parent) || t.isContinueStatement(parent))
925
+ ) {
926
+ return true
927
+ }
928
+ return false
929
+ }
930
+
931
+ interface StatementWalkContext {
932
+ insideNestedLoop: boolean
933
+ insideNestedSwitch: boolean
934
+ // Tail position propagates through final blocks, branches, and labels. Try,
935
+ // switch, loops, and `with` interrupt it because more code may run afterward.
936
+ tailPosition: boolean
937
+ }
938
+
939
+ interface StatementWalkHooks {
940
+ onContinue?: (node: t.ContinueStatement, ctx: StatementWalkContext) => t.Statement | null
941
+ onBreak?: (node: t.BreakStatement, ctx: StatementWalkContext) => void
942
+ onReturn?: (node: t.ReturnStatement, ctx: StatementWalkContext) => t.Statement[] | null
943
+ }
944
+
945
+ // Nested-function control flow belongs to that function.
946
+ function walkStatementsMut(
947
+ stmts: t.Statement[],
948
+ hooks: StatementWalkHooks,
949
+ ctx?: StatementWalkContext,
950
+ ): void {
951
+ const context = ctx ?? {
952
+ insideNestedLoop: false,
953
+ insideNestedSwitch: false,
954
+ tailPosition: false,
955
+ }
956
+
957
+ for (let index = 0; index < stmts.length; index++) {
958
+ const stmt = stmts[index]!
959
+ const isLast = index === stmts.length - 1
960
+ const stmtCtx: StatementWalkContext = {
961
+ ...context,
962
+ tailPosition: context.tailPosition && isLast,
963
+ }
964
+
965
+ if (t.isContinueStatement(stmt) && hooks.onContinue) {
966
+ const replacement = hooks.onContinue(stmt, stmtCtx)
967
+ if (replacement) {
968
+ stmts[index] = replacement
969
+ }
970
+ continue
971
+ }
972
+
973
+ if (t.isBreakStatement(stmt) && hooks.onBreak) {
974
+ hooks.onBreak(stmt, stmtCtx)
975
+ continue
976
+ }
977
+
978
+ if (t.isReturnStatement(stmt) && hooks.onReturn) {
979
+ const replacement = hooks.onReturn(stmt, stmtCtx)
980
+ if (replacement) {
981
+ stmts.splice(index, 1, ...replacement)
982
+ index += replacement.length - 1
983
+ }
984
+ continue
985
+ }
986
+
987
+ walkIntoCompound(stmt, hooks, stmtCtx)
988
+ }
989
+ }
990
+
991
+ function walkIntoCompound(
992
+ node: t.Statement,
993
+ hooks: StatementWalkHooks,
994
+ ctx: StatementWalkContext,
995
+ ): void {
996
+ if (t.isBlockStatement(node)) {
997
+ walkStatementsMut(node.body, hooks, ctx)
998
+ return
999
+ }
1000
+
1001
+ if (t.isIfStatement(node)) {
1002
+ node.consequent = walkSingleChild(node.consequent, hooks, ctx)
1003
+ if (node.alternate) {
1004
+ node.alternate = walkSingleChild(node.alternate, hooks, ctx)
1005
+ }
1006
+ return
1007
+ }
1008
+
1009
+ if (t.isLabeledStatement(node)) {
1010
+ node.body = walkSingleChild(node.body, hooks, ctx)
1011
+ return
1012
+ }
1013
+
1014
+ if (t.isTryStatement(node)) {
1015
+ const inner: StatementWalkContext = { ...ctx, tailPosition: false }
1016
+ walkStatementsMut(node.block.body, hooks, inner)
1017
+ if (node.handler) {
1018
+ walkStatementsMut(node.handler.body.body, hooks, inner)
1019
+ }
1020
+ if (node.finalizer) {
1021
+ walkStatementsMut(node.finalizer.body, hooks, inner)
1022
+ }
1023
+ return
1024
+ }
1025
+
1026
+ if (t.isSwitchStatement(node)) {
1027
+ const inner: StatementWalkContext = {
1028
+ ...ctx,
1029
+ insideNestedSwitch: true,
1030
+ tailPosition: false,
1031
+ }
1032
+ for (const switchCase of node.cases) {
1033
+ walkStatementsMut(switchCase.consequent, hooks, inner)
1034
+ }
1035
+ return
1036
+ }
1037
+
1038
+ if (
1039
+ t.isWhileStatement(node) ||
1040
+ t.isDoWhileStatement(node) ||
1041
+ t.isForStatement(node) ||
1042
+ t.isForInStatement(node) ||
1043
+ t.isForOfStatement(node)
1044
+ ) {
1045
+ const inner: StatementWalkContext = {
1046
+ ...ctx,
1047
+ insideNestedLoop: true,
1048
+ tailPosition: false,
1049
+ }
1050
+ node.body = walkSingleChild(node.body, hooks, inner)
1051
+ return
1052
+ }
1053
+
1054
+ if (t.isWithStatement(node)) {
1055
+ node.body = walkSingleChild(node.body, hooks, { ...ctx, tailPosition: false })
1056
+ }
1057
+ }
1058
+
1059
+ function walkSingleChild(
1060
+ child: t.Statement,
1061
+ hooks: StatementWalkHooks,
1062
+ ctx: StatementWalkContext,
1063
+ ): t.Statement {
1064
+ if (t.isBlockStatement(child)) {
1065
+ walkStatementsMut(child.body, hooks, ctx)
1066
+ return child
1067
+ }
1068
+ const wrapped = [child]
1069
+ walkStatementsMut(wrapped, hooks, ctx)
1070
+ if (wrapped.length === 1) {
1071
+ return wrapped[0]!
1072
+ }
1073
+ return t.blockStatement(wrapped)
1074
+ }