webfuscator 1.2.0 → 1.3.0

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 (111) hide show
  1. package/dist/analysis/constant.js +3 -4
  2. package/dist/analysis/constant.js.map +1 -1
  3. package/dist/analysis/inlineability.d.ts.map +1 -1
  4. package/dist/analysis/inlineability.js +37 -25
  5. package/dist/analysis/inlineability.js.map +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/obfuscator.d.ts +2 -1
  9. package/dist/obfuscator.d.ts.map +1 -1
  10. package/dist/obfuscator.js +47 -12
  11. package/dist/obfuscator.js.map +1 -1
  12. package/dist/options.d.ts +24 -4
  13. package/dist/options.d.ts.map +1 -1
  14. package/dist/preparation/expand-destructuring.d.ts +3 -3
  15. package/dist/preparation/expand-destructuring.js +6 -6
  16. package/dist/preparation/expand-destructuring.js.map +1 -1
  17. package/dist/transforms/arrow-to-function.js +3 -5
  18. package/dist/transforms/arrow-to-function.js.map +1 -1
  19. package/dist/transforms/conceal-strings.d.ts +57 -0
  20. package/dist/transforms/conceal-strings.d.ts.map +1 -0
  21. package/dist/transforms/conceal-strings.js +361 -0
  22. package/dist/transforms/conceal-strings.js.map +1 -0
  23. package/dist/transforms/const-let-to-var.d.ts +3 -3
  24. package/dist/transforms/const-let-to-var.d.ts.map +1 -1
  25. package/dist/transforms/const-let-to-var.js +36 -9
  26. package/dist/transforms/const-let-to-var.js.map +1 -1
  27. package/dist/transforms/const-to-let.d.ts +2 -3
  28. package/dist/transforms/const-to-let.d.ts.map +1 -1
  29. package/dist/transforms/const-to-let.js +3 -5
  30. package/dist/transforms/const-to-let.js.map +1 -1
  31. package/dist/transforms/fold-builtin-methods.d.ts.map +1 -1
  32. package/dist/transforms/fold-builtin-methods.js +48 -10
  33. package/dist/transforms/fold-builtin-methods.js.map +1 -1
  34. package/dist/transforms/fold-constants.js +3 -0
  35. package/dist/transforms/fold-constants.js.map +1 -1
  36. package/dist/transforms/hoist-variables.d.ts +30 -0
  37. package/dist/transforms/hoist-variables.d.ts.map +1 -0
  38. package/dist/transforms/hoist-variables.js +224 -0
  39. package/dist/transforms/hoist-variables.js.map +1 -0
  40. package/dist/transforms/inline-functions.js +3 -4
  41. package/dist/transforms/inline-functions.js.map +1 -1
  42. package/dist/transforms/mangle-properties.js +7 -4
  43. package/dist/transforms/mangle-properties.js.map +1 -1
  44. package/dist/transforms/merge-variable-declarations.d.ts +17 -0
  45. package/dist/transforms/merge-variable-declarations.d.ts.map +1 -0
  46. package/dist/transforms/merge-variable-declarations.js +68 -0
  47. package/dist/transforms/merge-variable-declarations.js.map +1 -0
  48. package/dist/transforms/numbers-to-strings.d.ts.map +1 -1
  49. package/dist/transforms/numbers-to-strings.js +1 -8
  50. package/dist/transforms/numbers-to-strings.js.map +1 -1
  51. package/dist/transforms/pack-declarations-into-parameters.js +1 -5
  52. package/dist/transforms/pack-declarations-into-parameters.js.map +1 -1
  53. package/dist/transforms/pack.d.ts +5 -7
  54. package/dist/transforms/pack.d.ts.map +1 -1
  55. package/dist/transforms/pack.js +194 -32
  56. package/dist/transforms/pack.js.map +1 -1
  57. package/dist/transforms/remove-unused-code.d.ts.map +1 -1
  58. package/dist/transforms/remove-unused-code.js +7 -39
  59. package/dist/transforms/remove-unused-code.js.map +1 -1
  60. package/dist/transforms/rename-identifiers.d.ts.map +1 -1
  61. package/dist/transforms/rename-identifiers.js +29 -17
  62. package/dist/transforms/rename-identifiers.js.map +1 -1
  63. package/dist/transforms/reorder-function-declarations.d.ts +25 -0
  64. package/dist/transforms/reorder-function-declarations.d.ts.map +1 -0
  65. package/dist/transforms/reorder-function-declarations.js +64 -0
  66. package/dist/transforms/reorder-function-declarations.js.map +1 -0
  67. package/dist/transforms/specials-to-strings.js +4 -3
  68. package/dist/transforms/specials-to-strings.js.map +1 -1
  69. package/dist/transforms/update-to-assignment.js +1 -16
  70. package/dist/transforms/update-to-assignment.js.map +1 -1
  71. package/dist/utils/ast.d.ts +1 -2
  72. package/dist/utils/ast.d.ts.map +1 -1
  73. package/dist/utils/ast.js +12 -42
  74. package/dist/utils/ast.js.map +1 -1
  75. package/dist/utils/literal.d.ts.map +1 -1
  76. package/dist/utils/literal.js +74 -2
  77. package/dist/utils/literal.js.map +1 -1
  78. package/dist/utils/statement-lists.d.ts +9 -0
  79. package/dist/utils/statement-lists.d.ts.map +1 -0
  80. package/dist/utils/statement-lists.js +27 -0
  81. package/dist/utils/statement-lists.js.map +1 -0
  82. package/package.json +12 -13
  83. package/src/analysis/constant.ts +3 -6
  84. package/src/analysis/inlineability.ts +40 -32
  85. package/src/index.ts +1 -0
  86. package/src/obfuscator.ts +51 -14
  87. package/src/options.ts +28 -3
  88. package/src/preparation/expand-destructuring.ts +6 -6
  89. package/src/transforms/arrow-to-function.ts +3 -5
  90. package/src/transforms/conceal-strings.ts +530 -0
  91. package/src/transforms/const-let-to-var.ts +37 -9
  92. package/src/transforms/const-to-let.ts +3 -5
  93. package/src/transforms/fold-builtin-methods.ts +67 -12
  94. package/src/transforms/fold-constants.ts +3 -0
  95. package/src/transforms/hoist-variables.ts +272 -0
  96. package/src/transforms/inline-functions.ts +3 -4
  97. package/src/transforms/mangle-properties.ts +7 -7
  98. package/src/transforms/merge-variable-declarations.ts +91 -0
  99. package/src/transforms/numbers-to-strings.ts +2 -12
  100. package/src/transforms/pack-declarations-into-parameters.ts +1 -5
  101. package/src/transforms/pack.ts +202 -35
  102. package/src/transforms/remove-unused-code.ts +9 -64
  103. package/src/transforms/rename-identifiers.ts +42 -17
  104. package/src/transforms/reorder-function-declarations.ts +80 -0
  105. package/src/transforms/specials-to-strings.ts +4 -3
  106. package/src/transforms/update-to-assignment.ts +1 -17
  107. package/src/utils/ast.ts +15 -51
  108. package/src/utils/literal.ts +91 -2
  109. package/src/utils/statement-lists.ts +45 -0
  110. package/DISCLOSURE +0 -9
  111. package/README.md +0 -62
@@ -4,7 +4,8 @@ import * as t from '@babel/types'
4
4
  import type { File } from '@babel/types'
5
5
 
6
6
  import { evaluateConstant, isOpaque } from '../analysis/constant'
7
- import { enclosingScopeHasDirectEval, isInDirectivePrologue, isInsideWith } from '../utils/ast'
7
+ import { isSideEffectFree } from '../analysis/purity'
8
+ import { isInDirectivePrologue, isInsideWith } from '../utils/ast'
8
9
  import type { ChangeState } from '../utils/change-tracking'
9
10
  import { valueToLiteral } from '../utils/literal'
10
11
 
@@ -104,18 +105,13 @@ function collectPatchedIntrinsics(ast: File): Set<string> {
104
105
  markMutationCall(path, poison)
105
106
  },
106
107
  Identifier(path) {
107
- // Global eval and Function can patch any intrinsic outside this scan.
108
108
  const name = path.node.name
109
- if (name !== 'eval' && name !== 'Function') {
109
+ if (name !== 'Function') {
110
110
  return
111
111
  }
112
112
  if (!path.isReferencedIdentifier() || path.scope.getBinding(name)) {
113
113
  return
114
114
  }
115
- if (name === 'eval') {
116
- poison(INTRINSIC_ROOTS)
117
- return
118
- }
119
115
  const parent = path.parentPath
120
116
  if (
121
117
  parent &&
@@ -496,7 +492,7 @@ function isReferenceRequiredPosition(path: NodePath<t.MemberExpression>): boolea
496
492
  }
497
493
 
498
494
  // Folding a leading call to a string can create a directive and change strictness.
499
- function foldsIntoDirective(path: NodePath<t.CallExpression>, replacement: t.Expression): boolean {
495
+ function foldsIntoDirective(path: NodePath, replacement: t.Expression): boolean {
500
496
  if (!t.isStringLiteral(replacement)) {
501
497
  return false
502
498
  }
@@ -506,9 +502,61 @@ function foldsIntoDirective(path: NodePath<t.CallExpression>, replacement: t.Exp
506
502
  )
507
503
  }
508
504
 
505
+ function replaceWithPreservedExpression(
506
+ path: NodePath<t.CallExpression | t.MemberExpression>,
507
+ expression: t.Expression,
508
+ result: t.Expression,
509
+ ): void {
510
+ if (!isSideEffectFree(expression, path.scope, path)) {
511
+ path.replaceWith(t.sequenceExpression([expression, result]))
512
+ return
513
+ }
514
+ if (foldsIntoDirective(path, result)) {
515
+ path.replaceWith(t.sequenceExpression([t.numericLiteral(0), result]))
516
+ return
517
+ }
518
+ path.replaceWith(result)
519
+ }
520
+
521
+ function foldArrayLiteralLength(
522
+ path: NodePath<t.MemberExpression>,
523
+ array: t.ArrayExpression,
524
+ ): boolean {
525
+ if (array.elements.some((element) => t.isSpreadElement(element))) {
526
+ return false
527
+ }
528
+ const effects = array.elements.filter(
529
+ (element): element is t.Expression =>
530
+ element !== null && t.isExpression(element) && !isSideEffectFree(element, path.scope, path),
531
+ )
532
+ const length = t.numericLiteral(array.elements.length)
533
+ path.replaceWith(effects.length === 0 ? length : t.sequenceExpression([...effects, length]))
534
+ return true
535
+ }
536
+
537
+ function foldStringIndex(
538
+ path: NodePath<t.MemberExpression>,
539
+ receiverNode: t.Expression,
540
+ propertyName: string,
541
+ ): boolean {
542
+ const index = Number(propertyName)
543
+ if (!Number.isSafeInteger(index) || index < 0 || String(index) !== propertyName) {
544
+ return false
545
+ }
546
+ const receiver = evaluateConstant(receiverNode, path.scope, path)
547
+ if (!receiver.known || typeof receiver.value !== 'string') {
548
+ return false
549
+ }
550
+ if (index >= receiver.value.length) {
551
+ return false
552
+ }
553
+ replaceWithPreservedExpression(path, receiverNode, t.stringLiteral(receiver.value[index]!))
554
+ return true
555
+ }
556
+
509
557
  function handleCall(path: NodePath<t.CallExpression>, patchedRoots: Set<string>): void {
510
- // `with` and direct eval can shadow names outside Babel's scope table.
511
- if (isInsideWith(path) || enclosingScopeHasDirectEval(path)) {
558
+ // `with` can replace an intrinsic that Babel treats as an unbound global.
559
+ if (isInsideWith(path)) {
512
560
  return
513
561
  }
514
562
  const callee = path.node.callee
@@ -559,7 +607,7 @@ function handleMember(path: NodePath<t.MemberExpression>, patchedRoots: Set<stri
559
607
  return
560
608
  }
561
609
  // Delay the ancestor walk until cheap shape checks pass.
562
- if (isInsideWith(path) || enclosingScopeHasDirectEval(path)) {
610
+ if (isInsideWith(path)) {
563
611
  return
564
612
  }
565
613
 
@@ -589,10 +637,18 @@ function handleMember(path: NodePath<t.MemberExpression>, patchedRoots: Set<stri
589
637
  }
590
638
 
591
639
  if (propName === 'length') {
640
+ if (t.isArrayExpression(obj) && foldArrayLiteralLength(path, obj)) {
641
+ return
642
+ }
592
643
  const recv = evaluateConstant(obj, path.scope, path)
593
644
  if (recv.known && typeof recv.value === 'string') {
594
645
  path.replaceWith(t.numericLiteral(recv.value.length))
595
646
  }
647
+ return
648
+ }
649
+
650
+ if (t.isExpression(obj)) {
651
+ foldStringIndex(path, obj, propName)
596
652
  }
597
653
  }
598
654
 
@@ -625,7 +681,6 @@ function foldGlobalCall(
625
681
  foldNumberCtor(path)
626
682
  return
627
683
  }
628
-
629
684
  if (GLOBAL_FUNCTIONS.has(name)) {
630
685
  if (path.scope.getBinding(name)) {
631
686
  return
@@ -131,6 +131,9 @@ function tryFoldToLiteral(
131
131
  if (!result.known) {
132
132
  return false
133
133
  }
134
+ if (typeof result.value === 'number' && !Number.isFinite(result.value)) {
135
+ return false
136
+ }
134
137
  const lit = valueToLiteral(result.value)
135
138
  if (!lit) {
136
139
  return false
@@ -0,0 +1,272 @@
1
+ import traverse from '@babel/traverse'
2
+ import type { NodePath } from '@babel/traverse'
3
+ import * as t from '@babel/types'
4
+ import type { File } from '@babel/types'
5
+
6
+ import type { ChangeState } from '../utils/change-tracking'
7
+
8
+ /**
9
+ * Moves `var` declarations to the start of their var scope without moving
10
+ * initializer evaluation.
11
+ *
12
+ * Direct exports and declarations inside `with` stay in place. Static blocks
13
+ * keep separate declarations.
14
+ *
15
+ * @example
16
+ * // ◀️ before
17
+ * function f() {
18
+ * var x = 1;
19
+ * if (c) {
20
+ * var y = g();
21
+ * }
22
+ * return x + y;
23
+ * }
24
+ *
25
+ * // ▶️ after
26
+ * function f() {
27
+ * var x, y;
28
+ * x = 1;
29
+ * if (c) {
30
+ * y = g();
31
+ * }
32
+ * return x + y;
33
+ * }
34
+ */
35
+ export function hoistVariables(ast: File): boolean {
36
+ const state: HoistState = {
37
+ changed: false,
38
+ scopes: new Map(),
39
+ }
40
+ traverse(ast, {
41
+ VariableDeclaration(path) {
42
+ if (path.node.kind !== 'var' || path.parentPath.isExportNamedDeclaration()) {
43
+ return
44
+ }
45
+ const owner = ownerPath(path)
46
+ if (owner === null || isInsideOwnerWith(path, owner)) {
47
+ return
48
+ }
49
+
50
+ const scope = hoistedScope(state.scopes, owner)
51
+ addNames(scope.names, path.getBindingIdentifiers())
52
+ if (scope.declaration?.node === path.node) {
53
+ return
54
+ }
55
+
56
+ demoteDeclaration(path)
57
+ scope.changed = true
58
+ state.changed = true
59
+ },
60
+ })
61
+
62
+ for (const scope of state.scopes.values()) {
63
+ if (scope.changed) {
64
+ finishDeclaration(scope)
65
+ }
66
+ }
67
+
68
+ return state.changed
69
+ }
70
+
71
+ type OwnerPath = NodePath<t.Function | t.Program | t.StaticBlock>
72
+
73
+ interface HoistState extends ChangeState {
74
+ scopes: Map<t.Node, HoistedScope>
75
+ }
76
+
77
+ interface HoistedScope {
78
+ changed: boolean
79
+ declaration: NodePath<t.VariableDeclaration> | null
80
+ names: Map<string, t.Identifier>
81
+ owner: OwnerPath
82
+ }
83
+
84
+ function ownerPath(path: NodePath<t.VariableDeclaration>): OwnerPath | null {
85
+ let current: NodePath | null = path.parentPath
86
+ while (current) {
87
+ if (current.isStaticBlock()) {
88
+ return current as OwnerPath
89
+ }
90
+ if (current.isProgram()) {
91
+ return current as OwnerPath
92
+ }
93
+ if (t.isFunction(current.node) && t.isBlockStatement(current.node.body)) {
94
+ return current as OwnerPath
95
+ }
96
+ current = current.parentPath
97
+ }
98
+ return null
99
+ }
100
+
101
+ function isInsideOwnerWith(path: NodePath, owner: OwnerPath): boolean {
102
+ for (let current = path.parentPath; current && current !== owner; current = current.parentPath) {
103
+ if (current.isWithStatement()) {
104
+ return true
105
+ }
106
+ }
107
+ return false
108
+ }
109
+
110
+ function hoistedScope(scopes: Map<t.Node, HoistedScope>, owner: OwnerPath): HoistedScope {
111
+ const existing = scopes.get(owner.node)
112
+ if (existing) {
113
+ return existing
114
+ }
115
+
116
+ const declaration = leadingBareDeclaration(owner)
117
+ const scope: HoistedScope = {
118
+ changed: false,
119
+ declaration,
120
+ names: new Map(),
121
+ owner,
122
+ }
123
+ if (declaration) {
124
+ addNames(scope.names, declaration.getBindingIdentifiers())
125
+ }
126
+ scopes.set(owner.node, scope)
127
+ return scope
128
+ }
129
+
130
+ function leadingBareDeclaration(owner: OwnerPath): NodePath<t.VariableDeclaration> | null {
131
+ const first = ownerBody(owner)[0]
132
+ if (!first?.isVariableDeclaration({ kind: 'var' })) {
133
+ return null
134
+ }
135
+ return first.node.declarations.every((declarator) => declarator.init == null) ? first : null
136
+ }
137
+
138
+ function ownerBody(owner: OwnerPath): NodePath<t.Statement>[] {
139
+ if (owner.isFunction()) {
140
+ const body = owner.get('body')
141
+ return body.isBlockStatement() ? (body.get('body') as NodePath<t.Statement>[]) : []
142
+ }
143
+ return owner.get('body') as NodePath<t.Statement>[]
144
+ }
145
+
146
+ function demoteDeclaration(path: NodePath<t.VariableDeclaration>): void {
147
+ const parent = path.parentPath
148
+ if (parent.isForStatement() && path.key === 'init') {
149
+ demoteForInit(path)
150
+ return
151
+ }
152
+ if ((parent.isForInStatement() || parent.isForOfStatement()) && path.key === 'left') {
153
+ demoteForX(path, parent)
154
+ return
155
+ }
156
+
157
+ const assignments = initializerAssignments(path.node).map((expression) =>
158
+ t.expressionStatement(expression),
159
+ )
160
+
161
+ if (typeof path.listKey === 'string') {
162
+ if (assignments.length === 0) {
163
+ path.remove()
164
+ } else {
165
+ path.replaceWithMultiple(assignments)
166
+ }
167
+ return
168
+ }
169
+ if (assignments.length === 0) {
170
+ path.replaceWith(t.emptyStatement())
171
+ } else if (assignments.length === 1) {
172
+ path.replaceWith(assignments[0]!)
173
+ } else {
174
+ path.replaceWith(t.blockStatement(assignments))
175
+ }
176
+ }
177
+
178
+ function demoteForInit(path: NodePath<t.VariableDeclaration>): void {
179
+ const assignments = initializerAssignments(path.node)
180
+ if (assignments.length === 0) {
181
+ path.remove()
182
+ return
183
+ }
184
+ const replacement = assignments.length === 1 ? assignments[0]! : t.sequenceExpression(assignments)
185
+ path.replaceWith(replacement)
186
+ }
187
+
188
+ function demoteForX(
189
+ path: NodePath<t.VariableDeclaration>,
190
+ parent: NodePath<t.ForInStatement | t.ForOfStatement>,
191
+ ): void {
192
+ const declarator = path.node.declarations[0]!
193
+ const target = declarator.id as t.ArrayPattern | t.Identifier | t.ObjectPattern
194
+ if (declarator.init) {
195
+ const assignment = initializerAssignment(declarator, target)
196
+ const right = parent.get('right') as NodePath<t.Expression>
197
+ right.replaceWith(t.sequenceExpression([assignment, right.node]))
198
+ }
199
+ path.replaceWith(t.cloneNode(target))
200
+ }
201
+
202
+ function initializerAssignments(node: t.VariableDeclaration): t.AssignmentExpression[] {
203
+ const assignments: t.AssignmentExpression[] = []
204
+ for (const declarator of node.declarations) {
205
+ if (declarator.init != null) {
206
+ const target = declarator.id as t.ArrayPattern | t.Identifier | t.ObjectPattern
207
+ assignments.push(initializerAssignment(declarator, target))
208
+ }
209
+ }
210
+ return assignments
211
+ }
212
+
213
+ function initializerAssignment(
214
+ declarator: t.VariableDeclarator,
215
+ target: t.ArrayPattern | t.Identifier | t.ObjectPattern,
216
+ ): t.AssignmentExpression {
217
+ return t.assignmentExpression('=', t.cloneNode(target), declarator.init!)
218
+ }
219
+
220
+ function addNames(
221
+ names: Map<string, t.Identifier>,
222
+ identifiers: Record<string, t.Identifier>,
223
+ ): void {
224
+ for (const [name, identifier] of Object.entries(identifiers)) {
225
+ if (!names.has(name)) {
226
+ names.set(name, identifier)
227
+ }
228
+ }
229
+ }
230
+
231
+ function finishDeclaration(scope: HoistedScope): void {
232
+ const existingNames = new Set<string>()
233
+ if (scope.declaration) {
234
+ for (const name of Object.keys(scope.declaration.getBindingIdentifiers())) {
235
+ existingNames.add(name)
236
+ }
237
+ }
238
+ const declarators = [...scope.names]
239
+ .filter(([name]) => !existingNames.has(name))
240
+ .map(([, identifier]) => t.variableDeclarator(t.cloneNode(identifier)))
241
+
242
+ if (scope.declaration) {
243
+ if (declarators.length > 0) {
244
+ scope.declaration.pushContainer('declarations', declarators)
245
+ }
246
+ return
247
+ }
248
+
249
+ if (declarators.length === 0) {
250
+ return
251
+ }
252
+
253
+ const declaration = t.variableDeclaration('var', declarators)
254
+ insertDeclaration(scope.owner, declaration)
255
+ }
256
+
257
+ function insertDeclaration(owner: OwnerPath, declaration: t.VariableDeclaration): void {
258
+ if (owner.isFunction()) {
259
+ const body = owner.get('body')
260
+ if (body.isBlockStatement()) {
261
+ body.unshiftContainer('body', declaration)
262
+ }
263
+ return
264
+ }
265
+ if (owner.isProgram()) {
266
+ owner.unshiftContainer('body', declaration)
267
+ return
268
+ }
269
+ if (owner.isStaticBlock()) {
270
+ owner.unshiftContainer('body', declaration)
271
+ }
272
+ }
@@ -6,7 +6,7 @@ import type { File } from '@babel/types'
6
6
  import { analyzeInlineability } from '../analysis/inlineability'
7
7
  import type { InlineCandidate } from '../analysis/inlineability'
8
8
  import { isPure } from '../analysis/purity'
9
- import { enclosingScopeHasDirectEval, isInsideWith, walkOwnFunctionScope } from '../utils/ast'
9
+ import { isInsideWith, walkOwnFunctionScope } from '../utils/ast'
10
10
  import { reorderableToStatement } from '../utils/evaluation-order'
11
11
  import { generateLoopLabel } from '../utils/loop-lowering'
12
12
  import { hasAnnexBFunctionAlias, isInStrictContext } from '../utils/paths'
@@ -135,9 +135,8 @@ function isSafeAtCallSite(
135
135
  callPath: NodePath<t.CallExpression>,
136
136
  candidates: Map<string, InlineCandidate>,
137
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)) {
138
+ // `with` and Annex B aliases can resolve a free name outside Babel's scope table.
139
+ if (isInsideWith(callPath)) {
141
140
  return false
142
141
  }
143
142
  if (annexBHoistShadowsFreeVar(callPath, freeVars)) {
@@ -507,7 +507,7 @@ function rewriteKeyedProperty(path: NodePath<KeyedProperty>, state: PropertyMang
507
507
  if (mangled === name) {
508
508
  return
509
509
  }
510
- node.key = t.inheritsComments(t.stringLiteral(mangled), node.key)
510
+ node.key = t.stringLiteral(mangled)
511
511
  node.computed = false
512
512
  if (t.isObjectProperty(node)) {
513
513
  node.shorthand = false
@@ -532,7 +532,7 @@ function rewriteMember(path: NodePath<Member>, state: PropertyManglerState): voi
532
532
  }
533
533
  // Babel narrows members by `computed`, so both fields need widening.
534
534
  const computed = node as Member & { computed: boolean; property: t.Expression }
535
- computed.property = t.inheritsComments(t.stringLiteral(mangled), node.property)
535
+ computed.property = t.stringLiteral(mangled)
536
536
  computed.computed = true
537
537
  state.changed = true
538
538
  }
@@ -586,7 +586,7 @@ function rewriteTerminalNames(
586
586
  node.value = mangled
587
587
  return node
588
588
  }
589
- return t.inheritsComments(t.stringLiteral(mangled), node)
589
+ return t.stringLiteral(mangled)
590
590
  }
591
591
  if (t.isSequenceExpression(node)) {
592
592
  const last = node.expressions.length - 1
@@ -684,10 +684,10 @@ function isTransparentAnnotationParent(path: NodePath, child: t.Node): boolean {
684
684
 
685
685
  function hasAnnotation(node: t.Node, annotation: string): boolean {
686
686
  return (
687
- node.leadingComments?.some(
688
- (comment) =>
689
- comment.value.includes(`@${annotation}`) || comment.value.includes(`#${annotation}`),
690
- ) ?? false
687
+ node.leadingComments?.some((comment) => {
688
+ const value = comment.value.trim()
689
+ return value === `@${annotation}` || value === `#${annotation}`
690
+ }) ?? false
691
691
  )
692
692
  }
693
693
 
@@ -0,0 +1,91 @@
1
+ import * as t from '@babel/types'
2
+ import type { File } from '@babel/types'
3
+
4
+ import { rewriteStatementLists } from '../utils/statement-lists'
5
+
6
+ /**
7
+ * Merges adjacent variable declarations when their kinds and export wrappers
8
+ * match. It runs in programs, blocks, switch cases, and static blocks, and
9
+ * removes unlabeled empty statements between compatible declarations.
10
+ *
11
+ * @example
12
+ * // ◀️ before
13
+ * let a = 1;
14
+ * let b = 2;
15
+ *
16
+ * // ▶️ after
17
+ * let a = 1,
18
+ * b = 2;
19
+ */
20
+ export function mergeVariableDeclarations(ast: File): boolean {
21
+ return rewriteStatementLists(ast, (statements) =>
22
+ mergeAdjacent(statements.map((path) => path.node)),
23
+ )
24
+ }
25
+
26
+ type StatementLike = t.Program['body'][number]
27
+
28
+ interface MergeableDeclaration {
29
+ declaration: t.VariableDeclaration
30
+ exported: boolean
31
+ }
32
+
33
+ function mergeAdjacent(list: readonly StatementLike[]): StatementLike[] | null {
34
+ const out: StatementLike[] = []
35
+ let pendingEmpty: t.EmptyStatement[] = []
36
+ let merged = false
37
+
38
+ for (const statement of list) {
39
+ if (t.isEmptyStatement(statement) && mergeableDeclaration(out.at(-1)) !== null) {
40
+ pendingEmpty.push(statement)
41
+ continue
42
+ }
43
+
44
+ const previous = mergeableDeclaration(out.at(-1))
45
+ const current = mergeableDeclaration(statement)
46
+ if (previous && current && canMerge(previous, current)) {
47
+ out[out.length - 1] = mergePair(previous, current)
48
+ pendingEmpty = []
49
+ merged = true
50
+ continue
51
+ }
52
+
53
+ out.push(...pendingEmpty, statement)
54
+ pendingEmpty = []
55
+ }
56
+
57
+ out.push(...pendingEmpty)
58
+ return merged ? out : null
59
+ }
60
+
61
+ function mergeableDeclaration(statement: StatementLike | undefined): MergeableDeclaration | null {
62
+ if (statement === undefined) {
63
+ return null
64
+ }
65
+ if (t.isVariableDeclaration(statement)) {
66
+ return { declaration: statement, exported: false }
67
+ }
68
+ if (t.isExportNamedDeclaration(statement) && t.isVariableDeclaration(statement.declaration)) {
69
+ return { declaration: statement.declaration, exported: true }
70
+ }
71
+ return null
72
+ }
73
+
74
+ function canMerge(previous: MergeableDeclaration, current: MergeableDeclaration): boolean {
75
+ return (
76
+ previous.exported === current.exported && previous.declaration.kind === current.declaration.kind
77
+ )
78
+ }
79
+
80
+ function mergePair(previous: MergeableDeclaration, current: MergeableDeclaration): StatementLike {
81
+ const declaration = t.variableDeclaration(previous.declaration.kind, [
82
+ ...previous.declaration.declarations,
83
+ ...current.declaration.declarations,
84
+ ])
85
+
86
+ if (!previous.exported) {
87
+ return declaration
88
+ }
89
+
90
+ return t.exportNamedDeclaration(declaration)
91
+ }
@@ -1,7 +1,8 @@
1
- import type { NodePath, Visitor } from '@babel/traverse'
1
+ import type { Visitor } from '@babel/traverse'
2
2
  import * as t from '@babel/types'
3
3
  import type { File } from '@babel/types'
4
4
 
5
+ import { isNonComputedKey } from '../utils/ast'
5
6
  import { traverseForChanges } from '../utils/change-tracking'
6
7
  import type { ChangeState } from '../utils/change-tracking'
7
8
 
@@ -48,14 +49,3 @@ const visitor: Visitor<ChangeState> = {
48
49
  state.changed = true
49
50
  },
50
51
  }
51
-
52
- function isNonComputedKey(path: NodePath<t.NumericLiteral>): boolean {
53
- const parentPath = path.parentPath
54
- if (!parentPath) {
55
- return false
56
- }
57
- const parent = parentPath.node
58
- return (
59
- 'key' in parent && parent.key === path.node && 'computed' in parent && parent.computed === false
60
- )
61
- }
@@ -2,7 +2,7 @@ import type { Binding, NodePath, Visitor } from '@babel/traverse'
2
2
  import * as t from '@babel/types'
3
3
  import type { File } from '@babel/types'
4
4
 
5
- import { enclosingScopeHasDirectEval, hasDuplicateParamNames } from '../utils/ast'
5
+ import { hasDuplicateParamNames } from '../utils/ast'
6
6
  import { traverseForChanges } from '../utils/change-tracking'
7
7
  import type { ChangeState } from '../utils/change-tracking'
8
8
  import { isLiteralShaped } from '../utils/literal'
@@ -75,10 +75,6 @@ function packLeadingIntoParams(path: NodePath<t.Function>): boolean {
75
75
  if (referencesArguments(path)) {
76
76
  return false
77
77
  }
78
- // Direct eval can inspect the newly unmapped `arguments` by name.
79
- if (enclosingScopeHasDirectEval(path)) {
80
- return false
81
- }
82
78
  // A caller with extra arguments could fill the new parameter slot.
83
79
  if (!allCallSitesRespectArity(path, params.length)) {
84
80
  return false