@openrewrite/rewrite 8.91.1 → 8.91.3

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 (160) hide show
  1. package/dist/execution.d.ts +8 -0
  2. package/dist/execution.d.ts.map +1 -1
  3. package/dist/execution.js +31 -0
  4. package/dist/execution.js.map +1 -1
  5. package/dist/javascript/add-import.d.ts +127 -38
  6. package/dist/javascript/add-import.d.ts.map +1 -1
  7. package/dist/javascript/add-import.js +684 -195
  8. package/dist/javascript/add-import.js.map +1 -1
  9. package/dist/javascript/amd.d.ts +148 -0
  10. package/dist/javascript/amd.d.ts.map +1 -0
  11. package/dist/javascript/amd.js +802 -0
  12. package/dist/javascript/amd.js.map +1 -0
  13. package/dist/javascript/binding.d.ts +102 -0
  14. package/dist/javascript/binding.d.ts.map +1 -0
  15. package/dist/javascript/binding.js +308 -0
  16. package/dist/javascript/binding.js.map +1 -0
  17. package/dist/javascript/comparator.d.ts +2 -0
  18. package/dist/javascript/comparator.d.ts.map +1 -1
  19. package/dist/javascript/comparator.js +5 -1
  20. package/dist/javascript/comparator.js.map +1 -1
  21. package/dist/javascript/format/format.d.ts +5 -2
  22. package/dist/javascript/format/format.d.ts.map +1 -1
  23. package/dist/javascript/format/format.js +51 -11
  24. package/dist/javascript/format/format.js.map +1 -1
  25. package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts +19 -0
  26. package/dist/javascript/format/minimum-viable-spacing-visitor.d.ts.map +1 -1
  27. package/dist/javascript/format/minimum-viable-spacing-visitor.js +180 -7
  28. package/dist/javascript/format/minimum-viable-spacing-visitor.js.map +1 -1
  29. package/dist/javascript/format/prettier-format.d.ts +2 -1
  30. package/dist/javascript/format/prettier-format.d.ts.map +1 -1
  31. package/dist/javascript/format/prettier-format.js +33 -23
  32. package/dist/javascript/format/prettier-format.js.map +1 -1
  33. package/dist/javascript/index.d.ts +8 -1
  34. package/dist/javascript/index.d.ts.map +1 -1
  35. package/dist/javascript/index.js +46 -1
  36. package/dist/javascript/index.js.map +1 -1
  37. package/dist/javascript/parser-utils.d.ts.map +1 -1
  38. package/dist/javascript/parser-utils.js +7 -9
  39. package/dist/javascript/parser-utils.js.map +1 -1
  40. package/dist/javascript/parser.d.ts +7 -1
  41. package/dist/javascript/parser.d.ts.map +1 -1
  42. package/dist/javascript/parser.js +55 -4
  43. package/dist/javascript/parser.js.map +1 -1
  44. package/dist/javascript/print.d.ts +1 -0
  45. package/dist/javascript/print.d.ts.map +1 -1
  46. package/dist/javascript/print.js +9 -0
  47. package/dist/javascript/print.js.map +1 -1
  48. package/dist/javascript/recipes/change-import.d.ts +4 -3
  49. package/dist/javascript/recipes/change-import.d.ts.map +1 -1
  50. package/dist/javascript/recipes/change-import.js +12 -504
  51. package/dist/javascript/recipes/change-import.js.map +1 -1
  52. package/dist/javascript/remove-import.d.ts +2 -28
  53. package/dist/javascript/remove-import.d.ts.map +1 -1
  54. package/dist/javascript/remove-import.js +19 -57
  55. package/dist/javascript/remove-import.js.map +1 -1
  56. package/dist/javascript/rpc.js +2 -0
  57. package/dist/javascript/rpc.js.map +1 -1
  58. package/dist/javascript/scope.d.ts +82 -0
  59. package/dist/javascript/scope.d.ts.map +1 -0
  60. package/dist/javascript/scope.js +476 -0
  61. package/dist/javascript/scope.js.map +1 -0
  62. package/dist/javascript/templating/bindings.d.ts +0 -11
  63. package/dist/javascript/templating/bindings.d.ts.map +1 -1
  64. package/dist/javascript/templating/bindings.js +2 -49
  65. package/dist/javascript/templating/bindings.js.map +1 -1
  66. package/dist/javascript/templating/comparator.d.ts +12 -0
  67. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  68. package/dist/javascript/templating/comparator.js +68 -14
  69. package/dist/javascript/templating/comparator.js.map +1 -1
  70. package/dist/javascript/templating/engine.d.ts +22 -4
  71. package/dist/javascript/templating/engine.d.ts.map +1 -1
  72. package/dist/javascript/templating/engine.js +68 -24
  73. package/dist/javascript/templating/engine.js.map +1 -1
  74. package/dist/javascript/templating/index.d.ts +1 -1
  75. package/dist/javascript/templating/index.d.ts.map +1 -1
  76. package/dist/javascript/templating/index.js.map +1 -1
  77. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  78. package/dist/javascript/templating/pattern.js +2 -2
  79. package/dist/javascript/templating/pattern.js.map +1 -1
  80. package/dist/javascript/templating/placeholder-replacement.d.ts +2 -0
  81. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  82. package/dist/javascript/templating/placeholder-replacement.js +10 -5
  83. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  84. package/dist/javascript/templating/precedence.d.ts +12 -4
  85. package/dist/javascript/templating/precedence.d.ts.map +1 -1
  86. package/dist/javascript/templating/precedence.js +18 -11
  87. package/dist/javascript/templating/precedence.js.map +1 -1
  88. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  89. package/dist/javascript/templating/rewrite.js +7 -1
  90. package/dist/javascript/templating/rewrite.js.map +1 -1
  91. package/dist/javascript/templating/template.d.ts +7 -1
  92. package/dist/javascript/templating/template.d.ts.map +1 -1
  93. package/dist/javascript/templating/template.js +54 -28
  94. package/dist/javascript/templating/template.js.map +1 -1
  95. package/dist/javascript/templating/types.d.ts +15 -18
  96. package/dist/javascript/templating/types.d.ts.map +1 -1
  97. package/dist/javascript/templating/utils.d.ts +1 -1
  98. package/dist/javascript/templating/utils.d.ts.map +1 -1
  99. package/dist/javascript/templating/utils.js +3 -2
  100. package/dist/javascript/templating/utils.js.map +1 -1
  101. package/dist/javascript/tree.d.ts +1 -0
  102. package/dist/javascript/tree.d.ts.map +1 -1
  103. package/dist/javascript/tree.js.map +1 -1
  104. package/dist/javascript/type-mapping.d.ts.map +1 -1
  105. package/dist/javascript/type-mapping.js +14 -0
  106. package/dist/javascript/type-mapping.js.map +1 -1
  107. package/dist/javascript/visitor.d.ts.map +1 -1
  108. package/dist/javascript/visitor.js +1 -0
  109. package/dist/javascript/visitor.js.map +1 -1
  110. package/dist/parser.d.ts +6 -0
  111. package/dist/parser.d.ts.map +1 -1
  112. package/dist/parser.js +23 -1
  113. package/dist/parser.js.map +1 -1
  114. package/dist/print.d.ts.map +1 -1
  115. package/dist/print.js +5 -2
  116. package/dist/print.js.map +1 -1
  117. package/dist/rewrite-javascript-version.txt +1 -1
  118. package/dist/rpc/request/parse-project.d.ts +11 -1
  119. package/dist/rpc/request/parse-project.d.ts.map +1 -1
  120. package/dist/rpc/request/parse-project.js +7 -2
  121. package/dist/rpc/request/parse-project.js.map +1 -1
  122. package/dist/rpc/request/parse.d.ts +13 -1
  123. package/dist/rpc/request/parse.d.ts.map +1 -1
  124. package/dist/rpc/request/parse.js +8 -2
  125. package/dist/rpc/request/parse.js.map +1 -1
  126. package/package.json +2 -2
  127. package/src/execution.ts +23 -0
  128. package/src/javascript/add-import.ts +753 -238
  129. package/src/javascript/amd.ts +954 -0
  130. package/src/javascript/binding.ts +435 -0
  131. package/src/javascript/comparator.ts +6 -1
  132. package/src/javascript/format/format.ts +56 -12
  133. package/src/javascript/format/minimum-viable-spacing-visitor.ts +196 -7
  134. package/src/javascript/format/prettier-format.ts +35 -25
  135. package/src/javascript/index.ts +19 -1
  136. package/src/javascript/parser-utils.ts +7 -11
  137. package/src/javascript/parser.ts +78 -6
  138. package/src/javascript/print.ts +10 -0
  139. package/src/javascript/recipes/change-import.ts +13 -593
  140. package/src/javascript/remove-import.ts +20 -56
  141. package/src/javascript/rpc.ts +2 -0
  142. package/src/javascript/scope.ts +517 -0
  143. package/src/javascript/templating/bindings.ts +2 -55
  144. package/src/javascript/templating/comparator.ts +81 -16
  145. package/src/javascript/templating/engine.ts +107 -38
  146. package/src/javascript/templating/index.ts +0 -1
  147. package/src/javascript/templating/pattern.ts +4 -2
  148. package/src/javascript/templating/placeholder-replacement.ts +16 -6
  149. package/src/javascript/templating/precedence.ts +21 -13
  150. package/src/javascript/templating/rewrite.ts +9 -1
  151. package/src/javascript/templating/template.ts +68 -29
  152. package/src/javascript/templating/types.ts +17 -18
  153. package/src/javascript/templating/utils.ts +4 -2
  154. package/src/javascript/tree.ts +1 -0
  155. package/src/javascript/type-mapping.ts +15 -0
  156. package/src/javascript/visitor.ts +1 -0
  157. package/src/parser.ts +24 -1
  158. package/src/print.ts +5 -2
  159. package/src/rpc/request/parse-project.ts +6 -2
  160. package/src/rpc/request/parse.ts +7 -2
@@ -0,0 +1,954 @@
1
+ /*
2
+ * Copyright 2025 the original author or authors.
3
+ * <p>
4
+ * Licensed under the Moderne Source Available License (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ * <p>
8
+ * https://docs.moderne.io/licensing/moderne-source-available-license
9
+ * <p>
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import {emptyMarkers, findMarker} from "../markers";
17
+ import {randomId, UUID} from "../uuid";
18
+ import {
19
+ emptyContainer,
20
+ emptySpace,
21
+ Expression,
22
+ isIdentifier,
23
+ isLiteral,
24
+ J,
25
+ rightPadded,
26
+ space,
27
+ spaceContainsNewline,
28
+ Statement,
29
+ TrailingComma
30
+ } from "../java";
31
+ import {JS} from "./tree";
32
+ import {cursorOf, deconflict, namesDeclaredWithin, scopeOf} from "./scope";
33
+ import {Cursor} from "../tree";
34
+ import {JavaScriptVisitor} from "./visitor";
35
+ import {ExecutionContext} from "../execution";
36
+
37
+ /** RequireJS and Dojo write `define`; UI5 namespaces it as `sap.ui.define`. */
38
+ export const DEFAULT_AMD_CALLEES: readonly string[] = ["define", "require"];
39
+
40
+ export interface AmdBlock {
41
+ /** -1 where the block is written without one, as `define(factory)`. */
42
+ readonly dependenciesIndex: number;
43
+ readonly dependencies: J.NewArray;
44
+ readonly factoryIndex: number;
45
+ readonly factory: J.MethodDeclaration | J.Lambda;
46
+ }
47
+
48
+ export function amdBlockOf(
49
+ call: J.MethodInvocation,
50
+ callees: readonly string[] = DEFAULT_AMD_CALLEES
51
+ ): AmdBlock | undefined {
52
+ if (!isAmdCallee(call, callees)) {
53
+ return undefined;
54
+ }
55
+ const args = call.arguments.elements;
56
+ const dependenciesIndex = args.findIndex(arg => arg.element.kind === J.Kind.NewArray);
57
+ const factoryIndex = dependenciesIndex >= 0 ?
58
+ dependenciesIndex + 1 :
59
+ args.findIndex(arg => asFactory(arg.element) !== undefined);
60
+ const factory = factoryIndex >= 0 && factoryIndex < args.length ?
61
+ asFactory(args[factoryIndex].element) :
62
+ undefined;
63
+ if (factory === undefined) {
64
+ return undefined;
65
+ }
66
+ const dependencies = dependenciesIndex >= 0 ?
67
+ args[dependenciesIndex].element as J.NewArray :
68
+ noDependencies();
69
+ return {dependenciesIndex, dependencies, factoryIndex, factory};
70
+ }
71
+
72
+ /** Stands in for the array a block written without one would have had. */
73
+ function noDependencies(): J.NewArray {
74
+ return {
75
+ kind: J.Kind.NewArray,
76
+ id: randomId(),
77
+ prefix: emptySpace,
78
+ markers: emptyMarkers,
79
+ dimensions: [],
80
+ initializer: emptyContainer<Expression>()
81
+ };
82
+ }
83
+
84
+ /**
85
+ * A callee written without a dot (`"define"`) matches by simple name on any receiver, so it
86
+ * also matches `foo.define(...)`; a dotted callee (`"sap.ui.define"`) requires the whole path.
87
+ */
88
+ function isAmdCallee(call: J.MethodInvocation, callees: readonly string[]): boolean {
89
+ // The name check first: this runs for every call in a file, and almost none are these.
90
+ const simpleName = call.name.simpleName;
91
+ return callees.some(callee => {
92
+ const last = callee.substring(callee.lastIndexOf(".") + 1);
93
+ return simpleName === last && (callee === last || namespaceOf(call) === callee);
94
+ });
95
+ }
96
+
97
+ /** The dotted path a call is written under, `sap.ui.define` for `sap.ui.define(…)`. */
98
+ function namespaceOf(call: J.MethodInvocation): string | undefined {
99
+ const segments: string[] = [call.name.simpleName];
100
+ let node: J | undefined = call.select?.element;
101
+ while (node !== undefined) {
102
+ if (node.kind === J.Kind.FieldAccess) {
103
+ const access = node as J.FieldAccess;
104
+ segments.unshift(access.name.element.simpleName);
105
+ node = access.target;
106
+ } else if (isIdentifier(node)) {
107
+ segments.unshift(node.simpleName);
108
+ return segments.join(".");
109
+ } else {
110
+ return undefined;
111
+ }
112
+ }
113
+ return segments.join(".");
114
+ }
115
+
116
+ /** A factory is written either as a function expression or as an arrow function. */
117
+ function asFactory(argument: J): J.MethodDeclaration | J.Lambda | undefined {
118
+ if (argument.kind === JS.Kind.StatementExpression) {
119
+ const statement = (argument as JS.StatementExpression).statement;
120
+ return statement.kind === J.Kind.MethodDeclaration ? statement as J.MethodDeclaration : undefined;
121
+ }
122
+ return argument.kind === JS.Kind.ArrowFunction ? (argument as JS.ArrowFunction).lambda : undefined;
123
+ }
124
+
125
+ /**
126
+ * The elements of a `[]` or `()`, which the parser fills with a single `J.Empty` when
127
+ * there are none.
128
+ */
129
+ export function present<T extends J>(elements: readonly J.RightPadded<T>[]): J.RightPadded<T>[] {
130
+ return elements.length === 1 && elements[0].element.kind === J.Kind.Empty ? [] : [...elements];
131
+ }
132
+
133
+ export function elementsOf(block: AmdBlock): J.RightPadded<Expression>[] {
134
+ return present(block.dependencies.initializer?.elements ?? []);
135
+ }
136
+
137
+ export function parametersOf(block: AmdBlock): J.RightPadded<J>[] {
138
+ return present(block.factory.kind === J.Kind.MethodDeclaration ?
139
+ (block.factory as J.MethodDeclaration).parameters.elements :
140
+ normalizeArrowParameters((block.factory as J.Lambda).parameters.parameters));
141
+ }
142
+
143
+ /**
144
+ * An arrow function's parameter list isn't built the way every other comma-separated list in
145
+ * the tree is: a trailing comma is an extra `J.Empty` entry rather than a `TrailingComma`
146
+ * marker, and a parameter's own trailing whitespace sits on the identifier's declaration
147
+ * rather than on the list entry that holds it. Folding both into the shape the rest of this
148
+ * module already handles lets append/remove treat every kind of factory alike.
149
+ */
150
+ function normalizeArrowParameters(raw: readonly J.RightPadded<J>[]): J.RightPadded<J>[] {
151
+ if (raw.length === 1 && raw[0].element.kind === J.Kind.Empty) {
152
+ return [...raw];
153
+ }
154
+ const trailingEmpty = raw.length > 1 && raw[raw.length - 1].element.kind === J.Kind.Empty ?
155
+ raw[raw.length - 1] : undefined;
156
+ const real = (trailingEmpty === undefined ? raw : raw.slice(0, -1)).map(foldDeclarationTrailingSpace);
157
+ if (trailingEmpty === undefined) {
158
+ return real;
159
+ }
160
+ const last = real[real.length - 1];
161
+ const marker: TrailingComma = {kind: J.Markers.TrailingComma, id: randomId(), suffix: trailingEmpty.after};
162
+ return [...real.slice(0, -1), {...last, markers: {...last.markers, markers: [...last.markers.markers, marker]}}];
163
+ }
164
+
165
+ /** Moves a VariableDeclarations parameter's trailing whitespace from the identifier's own declaration up to the list entry, when the entry doesn't already carry any. */
166
+ function foldDeclarationTrailingSpace(entry: J.RightPadded<J>): J.RightPadded<J> {
167
+ if (entry.element.kind !== J.Kind.VariableDeclarations ||
168
+ entry.after.whitespace !== "" || entry.after.comments.length > 0) {
169
+ return entry;
170
+ }
171
+ const declaration = entry.element as J.VariableDeclarations;
172
+ const variable = declaration.variables[0];
173
+ if (variable === undefined) {
174
+ return entry;
175
+ }
176
+ const folded: J.VariableDeclarations = {
177
+ ...declaration,
178
+ variables: [{...variable, after: emptySpace}, ...declaration.variables.slice(1)]
179
+ };
180
+ return {...entry, element: folded, after: variable.after};
181
+ }
182
+
183
+ export function dependencyNames(block: AmdBlock): string[] {
184
+ return elementsOf(block).map(padded => {
185
+ const element = padded.element;
186
+ return isLiteral(element) && typeof element.value === "string" ? element.value : "";
187
+ });
188
+ }
189
+
190
+ export function parameterNames(block: AmdBlock): (string | undefined)[] {
191
+ return parametersOf(block).map(padded => identifierOf(padded.element)?.simpleName);
192
+ }
193
+
194
+ export function identifierOf(parameter: J): J.Identifier | undefined {
195
+ if (parameter.kind === J.Kind.VariableDeclarations) {
196
+ const name = (parameter as J.VariableDeclarations).variables[0]?.element.name;
197
+ return name !== undefined && isIdentifier(name) ? name : undefined;
198
+ }
199
+ return isIdentifier(parameter) ? parameter : undefined;
200
+ }
201
+
202
+ /**
203
+ * Where an entry's leading whitespace sits. A dependency string carries it directly, while
204
+ * a factory parameter carries it on the identifier inside the declaration that wraps it.
205
+ */
206
+ interface Slot<T extends J> {
207
+ prefixOf(element: T): J.Space;
208
+
209
+ withPrefix(element: T, prefix: J.Space): T;
210
+ }
211
+
212
+ const dependencySlot: Slot<Expression> = {
213
+ prefixOf: element => element.prefix,
214
+ withPrefix: (element, prefix) => ({...element, prefix})
215
+ };
216
+
217
+ const parameterSlot: Slot<J> = {
218
+ prefixOf: element => identifierOf(element)?.prefix ?? element.prefix,
219
+ withPrefix: (element, prefix) => {
220
+ if (element.kind !== J.Kind.VariableDeclarations) {
221
+ return {...element, prefix};
222
+ }
223
+ const declaration = element as J.VariableDeclarations;
224
+ const variable = declaration.variables[0];
225
+ if (variable === undefined) {
226
+ return element;
227
+ }
228
+ return {
229
+ ...declaration,
230
+ variables: [
231
+ {...variable, element: {...variable.element, name: {...variable.element.name, prefix}}},
232
+ ...declaration.variables.slice(1)
233
+ ]
234
+ };
235
+ }
236
+ };
237
+
238
+ /**
239
+ * A trailing comma is a marker on the entry it follows, so it has to travel to whichever
240
+ * entry ends up last. Left where it was, it prints as a second comma and the array gains a
241
+ * hole, which shifts every dependency after it out of step with its parameter.
242
+ */
243
+ function moveTrailingComma<T extends J>(
244
+ from: J.RightPadded<T>,
245
+ to: J.RightPadded<T>
246
+ ): {from: J.RightPadded<T>, to: J.RightPadded<T>} {
247
+ const trailing = findMarker<TrailingComma>(from, J.Markers.TrailingComma);
248
+ if (trailing === undefined) {
249
+ return {from, to};
250
+ }
251
+ return {
252
+ from: {...from, markers: {...from.markers, markers: from.markers.markers.filter(m => m !== trailing)}},
253
+ to: {...to, markers: {...to.markers, markers: [...to.markers.markers, trailing]}}
254
+ };
255
+ }
256
+
257
+ /**
258
+ * The whitespace that separates one entry from the next, taken from the entries already
259
+ * there so that a block listing its dependencies one per line keeps doing so.
260
+ */
261
+ function separator<T extends J>(entries: readonly J.RightPadded<T>[], slot: Slot<T>): J.Space {
262
+ if (entries.length >= 2) {
263
+ return slot.prefixOf(entries[1].element);
264
+ }
265
+ const first = slot.prefixOf(entries[0].element);
266
+ return spaceContainsNewline(first) ? first : space(" ");
267
+ }
268
+
269
+ /**
270
+ * Appends `element`. The trailing entry's padding holds the whitespace before the closing
271
+ * bracket, so it has to move along with the position.
272
+ */
273
+ function appendEntry<T extends J>(
274
+ entries: readonly J.RightPadded<T>[],
275
+ element: T,
276
+ slot: Slot<T>
277
+ ): J.RightPadded<T>[] {
278
+ const last = entries[entries.length - 1];
279
+ if (last === undefined) {
280
+ return [rightPadded<T>(element, emptySpace)];
281
+ }
282
+ const positioned = slot.withPrefix(element, separator(entries, slot));
283
+ const trailing = splitTrailingComments(last.after);
284
+ const moved = moveTrailingComma({...last, after: trailing.comments}, rightPadded<T>(positioned, trailing.whitespace));
285
+ return [...entries.slice(0, -1), moved.from, moved.to];
286
+ }
287
+
288
+ /**
289
+ * Splits an entry's trailing padding when it stops being last. Plain whitespace only ever
290
+ * positioned the closing delimiter, so it travels to whichever entry becomes the new last. A
291
+ * comment documents the entry it follows and keeps its own trailing newline, so it stays put
292
+ * whole — a line comment with nothing left after it would otherwise swallow the comma the
293
+ * printer inserts right after this padding.
294
+ */
295
+ function splitTrailingComments(after: J.Space): {comments: J.Space, whitespace: J.Space} {
296
+ return after.comments.length === 0 ?
297
+ {comments: {...after, whitespace: ""}, whitespace: space(after.whitespace)} :
298
+ {comments: after, whitespace: emptySpace};
299
+ }
300
+
301
+ /**
302
+ * The plain-whitespace part of a leading prefix whose entry is about to be removed. A comment in
303
+ * that prefix documents the entry it precedes and is dropped along with it; the whitespace only
304
+ * ever positioned that entry after the opening bracket or a comma, so it's what travels to
305
+ * whichever entry takes its place.
306
+ */
307
+ function leadingWhitespaceOf(prefix: J.Space): string {
308
+ return prefix.comments.length === 0 ? prefix.whitespace : "";
309
+ }
310
+
311
+ /**
312
+ * Removes the entry at `index`. The first entry carries no separating whitespace and the
313
+ * last carries the whitespace before the closing bracket, so removing either hands its
314
+ * padding to whichever entry takes its place.
315
+ */
316
+ function removeEntry<T extends J>(
317
+ entries: readonly J.RightPadded<T>[],
318
+ index: number,
319
+ slot: Slot<T>
320
+ ): J.RightPadded<T>[] {
321
+ if (index < 0 || index >= entries.length) {
322
+ return [...entries];
323
+ }
324
+ const remaining = [...entries];
325
+ const [removed] = remaining.splice(index, 1);
326
+ if (remaining.length === 0) {
327
+ return remaining;
328
+ }
329
+ if (index === 0) {
330
+ // Spread the survivor's own prefix rather than replacing it outright, so a comment it
331
+ // already carries survives; only its plain whitespace is replaced.
332
+ const whitespace = leadingWhitespaceOf(slot.prefixOf(removed.element));
333
+ const survivor = remaining[0];
334
+ remaining[0] = {
335
+ ...survivor,
336
+ element: slot.withPrefix(survivor.element, {...slot.prefixOf(survivor.element), whitespace})
337
+ };
338
+ } else if (index === remaining.length) {
339
+ const last = {...remaining[index - 1], after: removed.after};
340
+ remaining[index - 1] = moveTrailingComma(removed, last).to;
341
+ }
342
+ return remaining;
343
+ }
344
+
345
+ function emptyExpression(): J.Empty {
346
+ return {kind: J.Kind.Empty, id: randomId(), prefix: emptySpace, markers: emptyMarkers};
347
+ }
348
+
349
+ function withElements(dependencies: J.NewArray, elements: J.RightPadded<Expression>[]): J.NewArray {
350
+ const initializer = dependencies.initializer!;
351
+ return {
352
+ ...dependencies,
353
+ initializer: {
354
+ ...initializer,
355
+ elements: elements.length === 0 ? [rightPadded<Expression>(emptyExpression(), emptySpace)] : elements
356
+ }
357
+ };
358
+ }
359
+
360
+ function withParameters(
361
+ factory: J.MethodDeclaration | J.Lambda,
362
+ parameters: J.RightPadded<J>[]
363
+ ): J.MethodDeclaration | J.Lambda {
364
+ const elements = parameters.length === 0 ? [rightPadded<J>(emptyExpression(), emptySpace)] : parameters;
365
+ if (factory.kind === J.Kind.MethodDeclaration) {
366
+ const method = factory as J.MethodDeclaration;
367
+ return {...method, parameters: {...method.parameters, elements: elements as J.RightPadded<Statement>[]}};
368
+ }
369
+ const lambda = factory as J.Lambda;
370
+ // Unparenthesized arrow parameters are only valid JavaScript for exactly one parameter.
371
+ if (lambda.parameters.parenthesized || parameters.length === 1) {
372
+ return {...lambda, parameters: {...lambda.parameters, parameters: elements}};
373
+ }
374
+ return parenthesize(lambda, elements);
375
+ }
376
+
377
+ /**
378
+ * A bare arrow parameter has no closing delimiter of its own — the space before `=>` is the
379
+ * parameter's trailing padding, so parenthesizing moves its whitespace to `arrow` and leaves any
380
+ * comment with the parameter. The padding comes from the original list, since `elements` stands
381
+ * in for an emptied list with a placeholder that carries none.
382
+ */
383
+ function parenthesize(lambda: J.Lambda, elements: J.RightPadded<J>[]): J.Lambda {
384
+ const original = normalizeArrowParameters(lambda.parameters.parameters);
385
+ const trailing = original[original.length - 1]?.after ?? lambda.arrow;
386
+ const last = elements[elements.length - 1];
387
+ return {
388
+ ...lambda,
389
+ parameters: {...lambda.parameters, parenthesized: true, parameters: [...elements.slice(0, -1), {...last, after: emptySpace}]},
390
+ arrow: space(trailing.whitespace)
391
+ };
392
+ }
393
+
394
+ function identifier(name: string): J.Identifier {
395
+ return {
396
+ kind: J.Kind.Identifier,
397
+ id: randomId(),
398
+ prefix: emptySpace,
399
+ markers: emptyMarkers,
400
+ annotations: [],
401
+ simpleName: name,
402
+ type: undefined,
403
+ fieldType: undefined
404
+ };
405
+ }
406
+
407
+ function dependencyLiteral(module: string, quote: string): J.Literal {
408
+ return {
409
+ kind: J.Kind.Literal,
410
+ id: randomId(),
411
+ prefix: emptySpace,
412
+ markers: emptyMarkers,
413
+ value: module,
414
+ valueSource: `${quote}${module}${quote}`
415
+ };
416
+ }
417
+
418
+ /** The quote the block's own dependencies use, so a new one matches them. */
419
+ function quoteOf(block: AmdBlock): string {
420
+ for (const padded of elementsOf(block)) {
421
+ const source = isLiteral(padded.element) ? (padded.element as J.Literal).valueSource : undefined;
422
+ const quote = source?.charAt(0);
423
+ if (quote === '"' || quote === "'") {
424
+ return quote;
425
+ }
426
+ }
427
+ return '"';
428
+ }
429
+
430
+ function parameterDeclaration(name: string): J.VariableDeclarations {
431
+ return {
432
+ kind: J.Kind.VariableDeclarations,
433
+ id: randomId(),
434
+ prefix: emptySpace,
435
+ markers: emptyMarkers,
436
+ leadingAnnotations: [],
437
+ modifiers: [],
438
+ variables: [rightPadded({
439
+ kind: J.Kind.NamedVariable,
440
+ id: randomId(),
441
+ prefix: emptySpace,
442
+ markers: emptyMarkers,
443
+ name: identifier(name),
444
+ dimensionsAfterName: []
445
+ } as J.VariableDeclarations.NamedVariable, emptySpace)]
446
+ };
447
+ }
448
+
449
+ /**
450
+ * Adds a dependency and its binding, keeping the array and the parameter list index-aligned.
451
+ * Refuses (returns `undefined`) unless the two are already the same length: fewer parameters
452
+ * than dependencies would invent a binding for one an author left unbound on purpose, and more
453
+ * would pair the new dependency with an existing surplus parameter instead of a fresh one —
454
+ * either way corrupting the pairing this module exists to protect.
455
+ */
456
+ export function withDependency(
457
+ call: J.MethodInvocation,
458
+ block: AmdBlock,
459
+ module: string,
460
+ binding: string
461
+ ): J.MethodInvocation | undefined {
462
+ if (parametersOf(block).length !== elementsOf(block).length) {
463
+ return undefined;
464
+ }
465
+ const dependencies = withElements(
466
+ block.dependencies,
467
+ appendEntry(elementsOf(block), dependencyLiteral(module, quoteOf(block)), dependencySlot));
468
+ const factory = withParameters(
469
+ block.factory,
470
+ appendEntry(parametersOf(block), parameterDeclaration(binding), parameterSlot));
471
+ return withParts(call, block, dependencies, factory);
472
+ }
473
+
474
+ /**
475
+ * Adds a dependency the factory takes no parameter for, as AMD allows for a module loaded only
476
+ * for its side effects. Refuses where parameters outnumber dependencies, since the appended one
477
+ * would land on a surplus parameter and bind after all — as it would in `define(factory)`, whose
478
+ * parameters come from the loader.
479
+ */
480
+ export function withUnboundDependency(
481
+ call: J.MethodInvocation,
482
+ block: AmdBlock,
483
+ module: string
484
+ ): J.MethodInvocation | undefined {
485
+ if (parametersOf(block).length > elementsOf(block).length) {
486
+ return undefined;
487
+ }
488
+ const dependencies = withElements(
489
+ block.dependencies,
490
+ appendEntry(elementsOf(block), dependencyLiteral(module, quoteOf(block)), dependencySlot));
491
+ return withParts(call, block, dependencies, block.factory);
492
+ }
493
+
494
+ export function withoutDependencyAt(
495
+ call: J.MethodInvocation,
496
+ block: AmdBlock,
497
+ index: number
498
+ ): J.MethodInvocation {
499
+ const dependencies = withElements(block.dependencies, removeEntry(elementsOf(block), index, dependencySlot));
500
+ const parameters = parametersOf(block);
501
+ const factory = index >= parameters.length ?
502
+ block.factory :
503
+ withParameters(block.factory, removeEntry(parameters, index, parameterSlot));
504
+ return withParts(call, block, dependencies, factory);
505
+ }
506
+
507
+ /**
508
+ * Swaps the module the dependency at `index` names, leaving its parameter and every other
509
+ * entry's position untouched — simpler than a removal and a fresh append, which would shift
510
+ * every dependency after it and re-litigate the parameter pairing this module exists to protect.
511
+ */
512
+ export function withDependencyModuleAt(
513
+ call: J.MethodInvocation,
514
+ block: AmdBlock,
515
+ index: number,
516
+ module: string
517
+ ): J.MethodInvocation {
518
+ const quote = quoteOf(block);
519
+ const elements = [...elementsOf(block)];
520
+ const entry = elements[index];
521
+ const literal = entry.element as J.Literal;
522
+ const updated: J.Literal = {...literal, value: module, valueSource: `${quote}${module}${quote}`};
523
+ elements[index] = {...entry, element: updated};
524
+ const dependencies = withElements(block.dependencies, elements);
525
+ return withParts(call, block, dependencies, block.factory);
526
+ }
527
+
528
+ function withParts(
529
+ call: J.MethodInvocation,
530
+ block: AmdBlock,
531
+ dependencies: J.NewArray,
532
+ factory: J.MethodDeclaration | J.Lambda
533
+ ): J.MethodInvocation {
534
+ const args = [...call.arguments.elements];
535
+ let factoryIndex = block.factoryIndex;
536
+ if (block.dependenciesIndex >= 0) {
537
+ args[block.dependenciesIndex] = {...args[block.dependenciesIndex], element: dependencies};
538
+ } else if (present(dependencies.initializer?.elements ?? []).length > 0) {
539
+ // The array takes the position the factory held, so it inherits what led up to it.
540
+ const leading = args[factoryIndex].element;
541
+ args[factoryIndex] = {...args[factoryIndex], element: {...leading, prefix: space(" ")}};
542
+ args.splice(factoryIndex, 0, rightPadded<Expression>({...dependencies, prefix: leading.prefix}, emptySpace));
543
+ factoryIndex += 1;
544
+ }
545
+ args[factoryIndex] = {...args[factoryIndex], element: restoreFactory(args[factoryIndex].element, factory)};
546
+ return {...call, arguments: {...call.arguments, elements: args}};
547
+ }
548
+
549
+ /** Puts the rewritten factory back into whichever wrapper the original was written as. */
550
+ function restoreFactory(original: Expression, factory: J.MethodDeclaration | J.Lambda): Expression {
551
+ if (original.kind === JS.Kind.StatementExpression) {
552
+ const statementExpression: JS.StatementExpression = {
553
+ ...(original as JS.StatementExpression),
554
+ statement: factory as J.MethodDeclaration
555
+ };
556
+ return statementExpression;
557
+ }
558
+ const arrowFunction: JS.ArrowFunction = {...(original as JS.ArrowFunction), lambda: factory as J.Lambda};
559
+ return arrowFunction;
560
+ }
561
+
562
+ /** The subset of `MaybeBindOptions` that selects which callees introduce an AMD block. */
563
+ export interface AmdCalleeOptions {
564
+ /** Callees that introduce an AMD block. */
565
+ amdCallee?: string | readonly string[];
566
+ }
567
+
568
+ export function calleesOf(options?: AmdCalleeOptions): readonly string[] {
569
+ const callee = options?.amdCallee;
570
+ return callee === undefined ? DEFAULT_AMD_CALLEES : typeof callee === "string" ? [callee] : callee;
571
+ }
572
+
573
+ /** The nearest AMD block the cursor sits inside, which is the one a binding belongs to. */
574
+ export function enclosingAmdBlock(
575
+ from: Cursor | JavaScriptVisitor<any>,
576
+ options?: AmdCalleeOptions
577
+ ): {call: J.MethodInvocation, block: AmdBlock} | undefined {
578
+ const callees = calleesOf(options);
579
+ let cursor = from instanceof Cursor ? from : cursorOf(from);
580
+ while (cursor !== undefined) {
581
+ const value = cursor.value as J | undefined;
582
+ if (value?.kind === J.Kind.MethodInvocation) {
583
+ const block = amdBlockOf(value as J.MethodInvocation, callees);
584
+ if (block !== undefined) {
585
+ return {call: value as J.MethodInvocation, block};
586
+ }
587
+ }
588
+ cursor = cursor.parent;
589
+ }
590
+ return undefined;
591
+ }
592
+
593
+ /** The conventional local name for a module: its last path segment. */
594
+ export function lastSegment(module: string): string {
595
+ return module.substring(module.lastIndexOf("/") + 1);
596
+ }
597
+
598
+ // Always-reserved words, plus the strict-mode-only ones (module code is always strict) and
599
+ // `await`/`yield`, contextual elsewhere but a trap to bind regardless of where they're legal.
600
+ const RESERVED_WORDS = new Set([
601
+ "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do",
602
+ "else", "enum", "export", "extends", "false", "finally", "for", "function", "if", "import",
603
+ "in", "instanceof", "new", "null", "return", "super", "switch", "this", "throw", "true", "try",
604
+ "typeof", "var", "void", "while", "with",
605
+ "await", "implements", "interface", "let", "package", "private", "protected", "public",
606
+ "static", "yield"
607
+ ]);
608
+
609
+ /**
610
+ * `lastSegment(module)`, or `undefined` where that string cannot bind a name — a scoped
611
+ * package's `@scope/my-lib`, a subpath's `lodash.merge`, a `-`/`.`-bearing name, or a reserved
612
+ * word are all real module strings that are not legal identifiers. ASCII identifiers only —
613
+ * stricter than JavaScript itself, which allows Unicode, but refusing there is safe.
614
+ */
615
+ export function derivedBindingName(module: string): string | undefined {
616
+ const segment = lastSegment(module);
617
+ return isBindableName(segment) ? segment : undefined;
618
+ }
619
+
620
+ /** Whether `name` is one a binding can take. ASCII identifiers only, so a legal name may be refused. */
621
+ export function isBindableName(name: string): boolean {
622
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_WORDS.has(name);
623
+ }
624
+
625
+ /** Queued reservations already targeting this block: the shared source for both dedup and deconfliction. */
626
+ function queuedFor(visitor: JavaScriptVisitor<any>, blockId: UUID): AddAmdDependency<any>[] {
627
+ return visitor.afterVisit.filter((v): v is AddAmdDependency<any> => v instanceof AddAmdDependency && v.blockId === blockId);
628
+ }
629
+
630
+ /**
631
+ * A local binding for `module` on the AMD lane, creating one where the block does not already
632
+ * have it, or `undefined` where no safe binding exists.
633
+ *
634
+ * The name is decided from the cursor and returned at once; the edit that creates the binding
635
+ * is deferred onto `visitor.afterVisit`, as `bindImport`'s is.
636
+ */
637
+ export function bindAmd(
638
+ visitor: JavaScriptVisitor<any>,
639
+ amd: {call: J.MethodInvocation, block: AmdBlock},
640
+ module: string,
641
+ preferredName: string | undefined,
642
+ callees: readonly string[],
643
+ pinned: boolean = false
644
+ ): string | undefined {
645
+ const modules = dependencyNames(amd.block);
646
+ const bindings = parameterNames(amd.block);
647
+
648
+ const declared = module === "" ? -1 : modules.indexOf(module);
649
+ if (declared >= 0) {
650
+ const bound = bindings[declared];
651
+ if (bound === undefined) {
652
+ // A parameter that is not a plain name gives the module no binding to hand back.
653
+ return undefined;
654
+ }
655
+ // Only code the factory encloses can shadow its parameter, so a cursor elsewhere in the
656
+ // block — on the `define` call, or on its dependency array — has nothing to measure.
657
+ const cursor = cursorOf(visitor);
658
+ const withinFactory = cursor?.firstEnclosing((v): v is J => v === amd.block.factory);
659
+ if (withinFactory === undefined || scopeOf(cursor!).declaringScope(bound) === amd.block.factory) {
660
+ return bound;
661
+ }
662
+ }
663
+
664
+ // Two calls naming the same module at the same block are the same request (ADR 0013),
665
+ // answered from whichever reservation is already queued rather than doubling the dependency.
666
+ const queued = queuedFor(visitor, amd.call.id);
667
+ const reserved = queued.find(v => v.module === module)?.binding;
668
+ if (reserved !== undefined) {
669
+ return reserved;
670
+ }
671
+
672
+ // A parameter can only be appended at the end, so a block whose dependency and parameter
673
+ // counts already disagree would bind the new module against the wrong parameter either way.
674
+ if (bindings.length !== elementsOf(amd.block).length) {
675
+ return undefined;
676
+ }
677
+
678
+ if (preferredName === undefined && derivedBindingName(module) === undefined) {
679
+ return undefined;
680
+ }
681
+ // A parameter is in scope across the whole factory, and the queue hands this name to later
682
+ // requests at cursors inside it, so every name the factory declares is one it has to clear.
683
+ const taken = [...bindings, ...namesDeclaredWithin(amd.block.factory), ...queued.map(v => v.binding)];
684
+ const requested = preferredName ?? derivedBindingName(module)!;
685
+ const binding = deconflict(requested, candidate => taken.includes(candidate));
686
+ if (pinned && binding !== requested) {
687
+ // An alias is bound verbatim or not at all, since the caller may already have emitted
688
+ // code naming it, and a deconflicted spelling would leave that unbound.
689
+ return undefined;
690
+ }
691
+ visitor.afterVisit.push(new AddAmdDependency(amd.call.id, module, binding, callees));
692
+ return binding;
693
+ }
694
+
695
+ /** The factory's body: a `J.Block` for `function(){}` and most arrows, or the bare expression for `() => expr`. */
696
+ export function bodyOf(block: AmdBlock): J | undefined {
697
+ return block.factory.kind === J.Kind.MethodDeclaration ?
698
+ (block.factory as J.MethodDeclaration).body :
699
+ (block.factory as J.Lambda).body;
700
+ }
701
+
702
+ /**
703
+ * Whether the factory body references `name`. This counts any identifier of that name,
704
+ * including a member in a field access, since a stray dependency is fine where a shadowed one
705
+ * is not. `missingBodyAnswer` covers a body-less factory: `false` for an addition (nothing to
706
+ * conflict with), `true` for a removal (nothing to prove the binding unused).
707
+ */
708
+ export async function references(block: AmdBlock, name: string, missingBodyAnswer: boolean): Promise<boolean> {
709
+ const body = bodyOf(block);
710
+ if (body === undefined) {
711
+ return missingBodyAnswer;
712
+ }
713
+ let found = false;
714
+ const finder = new class extends JavaScriptVisitor<ExecutionContext> {
715
+ override async visitIdentifier(id: J.Identifier, c: ExecutionContext) {
716
+ if (id.simpleName === name) {
717
+ found = true;
718
+ }
719
+ return super.visitIdentifier(id, c);
720
+ }
721
+ };
722
+ await finder.visit(body, new ExecutionContext());
723
+ return found;
724
+ }
725
+
726
+ /**
727
+ * Every name the factory body references, one walk for the whole block rather than one per
728
+ * name. `missingBodyAnswer` means what it means on `references`: the answer `.has` gives, for
729
+ * every name, when the factory has no body to walk.
730
+ */
731
+ export async function namesUsed(block: AmdBlock, missingBodyAnswer: boolean): Promise<{has(name: string): boolean}> {
732
+ const body = bodyOf(block);
733
+ if (body === undefined) {
734
+ return {has: () => missingBodyAnswer};
735
+ }
736
+ const names = new Set<string>();
737
+ const collector = new class extends JavaScriptVisitor<ExecutionContext> {
738
+ override async visitIdentifier(id: J.Identifier, c: ExecutionContext) {
739
+ names.add(id.simpleName);
740
+ return super.visitIdentifier(id, c);
741
+ }
742
+ };
743
+ await collector.visit(body, new ExecutionContext());
744
+ return names;
745
+ }
746
+
747
+ /**
748
+ * Applies the dependency a `maybeBind` call settled on. The block is found by id: its caller
749
+ * has already emitted a reference to the binding, so a block that cannot be found is an error
750
+ * rather than a skipped edit.
751
+ */
752
+ export class AddAmdDependency<P> extends JavaScriptVisitor<P> {
753
+ constructor(
754
+ readonly blockId: UUID,
755
+ readonly module: string,
756
+ readonly binding: string,
757
+ readonly callees: readonly string[]
758
+ ) {
759
+ super();
760
+ }
761
+
762
+ private applied = false;
763
+
764
+ override async visitJsCompilationUnit(cu: JS.CompilationUnit, p: P): Promise<J | undefined> {
765
+ const visited = await super.visitJsCompilationUnit(cu, p) as JS.CompilationUnit;
766
+ if (!this.applied) {
767
+ throw new Error(
768
+ `No AMD block ${this.blockId} to declare '${this.module}' on, but '${this.binding}' was reported bound`);
769
+ }
770
+ return visited;
771
+ }
772
+
773
+ override async visitMethodInvocation(m: J.MethodInvocation, p: P): Promise<J | undefined> {
774
+ const visited = await super.visitMethodInvocation(m, p) as J.MethodInvocation;
775
+ if (visited.id !== this.blockId) {
776
+ return visited;
777
+ }
778
+ const block = amdBlockOf(visited, this.callees);
779
+ if (block === undefined) {
780
+ return visited;
781
+ }
782
+ this.applied = true;
783
+ if (!(await references(block, this.binding, false))) {
784
+ // Nothing referenced the binding, so the caller asked and then did not use the answer.
785
+ return visited;
786
+ }
787
+ // Another edit in the same visit can drop or add a factory parameter, reopening a count
788
+ // mismatch bindAmd's own check already cleared — an error here for the same reason a missing
789
+ // block is one.
790
+ const dependency = withDependency(visited, block, this.module, this.binding);
791
+ if (dependency === undefined) {
792
+ throw new Error(
793
+ `AMD block ${this.blockId} no longer has matching dependency and parameter counts, ` +
794
+ `so '${this.module}' could not be declared for '${this.binding}', which was reported bound`);
795
+ }
796
+ return dependency;
797
+ }
798
+ }
799
+
800
+ /**
801
+ * Applies a `maybeRebind` call's decision on the AMD lane: swaps the dependency's module in
802
+ * place, found by the block's tree id and the module it named when the caller asked.
803
+ */
804
+ export class RebindAmdDependency<P> extends JavaScriptVisitor<P> {
805
+ constructor(
806
+ readonly blockId: UUID,
807
+ readonly fromModule: string,
808
+ readonly toModule: string,
809
+ readonly callees: readonly string[]
810
+ ) {
811
+ super();
812
+ }
813
+
814
+ private applied = false;
815
+
816
+ override async visitJsCompilationUnit(cu: JS.CompilationUnit, p: P): Promise<J | undefined> {
817
+ const visited = await super.visitJsCompilationUnit(cu, p) as JS.CompilationUnit;
818
+ if (!this.applied) {
819
+ throw new Error(`No AMD block ${this.blockId} to rebind '${this.fromModule}' to '${this.toModule}' on`);
820
+ }
821
+ return visited;
822
+ }
823
+
824
+ override async visitMethodInvocation(m: J.MethodInvocation, p: P): Promise<J | undefined> {
825
+ const visited = await super.visitMethodInvocation(m, p) as J.MethodInvocation;
826
+ if (visited.id !== this.blockId) {
827
+ return visited;
828
+ }
829
+ const block = amdBlockOf(visited, this.callees);
830
+ if (block === undefined) {
831
+ return visited;
832
+ }
833
+ this.applied = true;
834
+ const index = this.fromModule === "" ? -1 : dependencyNames(block).indexOf(this.fromModule);
835
+ if (index < 0) {
836
+ throw new Error(
837
+ `AMD block ${this.blockId} no longer has dependency '${this.fromModule}' to rebind to '${this.toModule}'`);
838
+ }
839
+ return withDependencyModuleAt(visited, block, index, this.toModule);
840
+ }
841
+ }
842
+
843
+ /**
844
+ * The AMD counterpart to `RemoveImport`: drops a dependency the block's body no longer names,
845
+ * along with the parameter bound to it. A dependency the factory takes no parameter for is
846
+ * loaded for its side effects and stays, matching `bindAmd`'s own read of such a block. A
847
+ * `member`-scoped request is a no-op here — see `maybeUnbind`.
848
+ */
849
+ export class RemoveAmdDependency<P> extends JavaScriptVisitor<P> {
850
+ constructor(
851
+ readonly module: string,
852
+ readonly member?: string,
853
+ readonly callees: readonly string[] = DEFAULT_AMD_CALLEES
854
+ ) {
855
+ super();
856
+ }
857
+
858
+ override async visitMethodInvocation(m: J.MethodInvocation, p: P): Promise<J | undefined> {
859
+ const visited = await super.visitMethodInvocation(m, p) as J.MethodInvocation;
860
+ if (this.member !== undefined) {
861
+ return visited;
862
+ }
863
+ const block = amdBlockOf(visited, this.callees);
864
+ if (block === undefined) {
865
+ return visited;
866
+ }
867
+ const index = dependencyNames(block).indexOf(this.module);
868
+ if (index < 0) {
869
+ return visited;
870
+ }
871
+ const binding = parameterNames(block)[index];
872
+ if (binding === undefined || await references(block, binding, true)) {
873
+ return visited;
874
+ }
875
+ return withoutDependencyAt(visited, block, index);
876
+ }
877
+ }
878
+
879
+ /**
880
+ * Drops AMD bindings a rewrite left unreferenced. One already unused beforehand stays: it is
881
+ * loaded for its side effects, so dropping it changes what the module loads. Needing the tree as
882
+ * it stood before the rewrite is why this takes both and does not defer. Blocks are matched by
883
+ * the call's id, so one a rewrite rebuilds rather than edits is left alone.
884
+ */
885
+ export async function removeNewlyUnusedAmdBindings(
886
+ before: JS.CompilationUnit,
887
+ after: JS.CompilationUnit,
888
+ ctx: ExecutionContext,
889
+ options?: AmdCalleeOptions
890
+ ): Promise<JS.CompilationUnit> {
891
+ const callees = calleesOf(options);
892
+ const usedBeforeByBlock = new Map<UUID, Set<string>>();
893
+ const collector = new class extends JavaScriptVisitor<ExecutionContext> {
894
+ override async visitMethodInvocation(m: J.MethodInvocation, c: ExecutionContext) {
895
+ const block = amdBlockOf(m, callees);
896
+ if (block !== undefined) {
897
+ usedBeforeByBlock.set(m.id, await usedBindings(block));
898
+ }
899
+ return super.visitMethodInvocation(m, c);
900
+ }
901
+ };
902
+ await collector.visit(before, ctx);
903
+ if (usedBeforeByBlock.size === 0) {
904
+ return after;
905
+ }
906
+
907
+ const sweeper = new class extends JavaScriptVisitor<ExecutionContext> {
908
+ override async visitMethodInvocation(m: J.MethodInvocation, c: ExecutionContext): Promise<J | undefined> {
909
+ let call = await super.visitMethodInvocation(m, c) as J.MethodInvocation;
910
+ const usedBefore = usedBeforeByBlock.get(call.id);
911
+ let block = usedBefore === undefined ? undefined : amdBlockOf(call, callees);
912
+ if (usedBefore === undefined || block === undefined) {
913
+ return call;
914
+ }
915
+ // A block written `define(factory)` takes `require`, `exports` and `module` from the
916
+ // loader, so a parameter there is positional against arguments no dependency list names.
917
+ if (block.dependenciesIndex < 0 || parameterNames(block).length !== elementsOf(block).length) {
918
+ return call;
919
+ }
920
+ // withoutDependencyAt only edits the dependency array and parameter list, never the
921
+ // body, so which names it references can't change between removals.
922
+ const usedNow = await namesUsed(block, true);
923
+ for (; ;) {
924
+ const bindings = parameterNames(block);
925
+ const goneIndex = bindings.findIndex(binding =>
926
+ binding !== undefined && usedBefore.has(binding) && !usedNow.has(binding));
927
+ if (goneIndex < 0) {
928
+ return call;
929
+ }
930
+ call = withoutDependencyAt(call, block, goneIndex);
931
+ // A removal shifts the indices of the dependencies after it, so the block is
932
+ // re-derived from `call` rather than reused with stale offsets.
933
+ const next = amdBlockOf(call, callees);
934
+ if (next === undefined) {
935
+ return call;
936
+ }
937
+ block = next;
938
+ }
939
+ }
940
+ };
941
+ return await sweeper.visit(after, ctx) as JS.CompilationUnit;
942
+ }
943
+
944
+ /** The block's parameter names that its body actually references, from a single walk of it. */
945
+ async function usedBindings(block: AmdBlock): Promise<Set<string>> {
946
+ const usedNames = await namesUsed(block, false);
947
+ const used = new Set<string>();
948
+ for (const binding of parameterNames(block)) {
949
+ if (binding !== undefined && usedNames.has(binding)) {
950
+ used.add(binding);
951
+ }
952
+ }
953
+ return used;
954
+ }