@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
@@ -1,10 +1,14 @@
1
1
  import {JavaScriptVisitor} from "./visitor";
2
- import {emptySpace, J, rightPadded, singleSpace, space, Statement, Type} from "../java";
2
+ import {ElementRemovalFormatter, emptySpace, isIdentifier, J, NameTree, rightPadded, singleSpace, space, Statement, Type} from "../java";
3
3
  import {JS, JSX} from "./tree";
4
- import {randomId} from "../uuid";
4
+ import {randomId, UUID} from "../uuid";
5
5
  import {emptyMarkers, markers} from "../markers";
6
- import {getStyle, PrettierStyle, SpacesStyle, StyleKind} from "./style";
7
- import {Cursor} from "../tree";
6
+ import {getStyle, SpacesStyle, StyleKind} from "./style";
7
+ import {bindingNames, compilationUnitOf, cursorOf, declarationsOf, deconflict, isValueReference, namesDeclaredIn, scopeOf, walk} from "./scope";
8
+ import {create as produce, Draft} from "mutative";
9
+ import {TypeVisitor} from "../java/type-visitor";
10
+ import {autoFormat} from "./format";
11
+ import {getPrettierStyle} from "./format/prettier-format";
8
12
 
9
13
  export type QuoteChar = "'" | '"';
10
14
 
@@ -17,8 +21,9 @@ export enum ImportStyle {
17
21
 
18
22
  export interface AddImportOptions {
19
23
  /** The module name (e.g., 'fs', 'react') to import from.
20
- * Pass a `J.Literal` to reuse its source form verbatim, which carries the quoting,
21
- * escapes and unicode form of a specifier being moved from elsewhere in the source. */
24
+ * Pass a `J.Literal` to reuse its source form, which carries the escapes and unicode form of a
25
+ * specifier being moved from elsewhere in the source. A Prettier configuration settles the
26
+ * quoting; see `quoteStyle`. */
22
27
  module: string | J.Literal;
23
28
 
24
29
  /** Optionally, the specific member to import from the module.
@@ -60,54 +65,21 @@ export interface AddImportOptions {
60
65
  style?: ImportStyle;
61
66
 
62
67
  /** Quote character for the module specifier. If not specified, detected from the file.
63
- * A `J.Literal` module carries its own quoting and takes precedence over this. */
68
+ * A `J.Literal` module carries its own quoting and takes precedence over this. A Prettier
69
+ * configuration outranks both, since running Prettier would arrive at its quote anyway. */
64
70
  quoteStyle?: QuoteChar;
65
71
  }
66
72
 
67
73
  /**
68
- * Register an AddImport visitor to add an import statement to a JavaScript/TypeScript file
69
- * @param visitor The visitor to add the import addition to
70
- * @param options Configuration options for the import to add
71
- * @returns The local name the module is bound to: an existing binding's where one answers this
72
- * request, otherwise the name the new import will use, suffixed if the file already binds it. `onlyIfReferenced` defaults to true, so
73
- * the import may never appear, and the name is then what it would have gone by. A side-effect
74
- * import binds no name and returns `undefined`.
75
- *
76
- * @example
77
- * // Add a named import
78
- * maybeAddImport(visitor, { module: 'fs', member: 'readFile' });
79
- *
80
- * @example
81
- * // Add a default import using the 'default' member specifier
82
- * maybeAddImport(visitor, { module: 'react', member: 'default', alias: 'React' });
83
- *
84
- * @example
85
- * // Add a default import (legacy way, without specifying member)
86
- * maybeAddImport(visitor, { module: 'react', alias: 'React' });
87
- *
88
- * @example
89
- * // Add a namespace import
90
- * maybeAddImport(visitor, { module: 'crypto', member: '*', alias: 'crypto' });
91
- *
92
- * @example
93
- * // Add a side-effect import
94
- * maybeAddImport(visitor, { module: 'core-js/stable', sideEffectOnly: true });
74
+ * Ensures an import for `options.module` exists, queuing an `AddImport` edit where none already
75
+ * serves the request. `maybeBind`'s ESM/CommonJS lane is this function; its own JSDoc carries the
76
+ * return-value contract. `refuseCreate` answers `undefined` instead of queuing a new import,
77
+ * without affecting whether an existing binding answers the request first.
95
78
  */
96
- export function maybeAddImport(
79
+ export function bindImport(
97
80
  visitor: JavaScriptVisitor<any>,
98
- options: AddImportOptions & { sideEffectOnly: true }
99
- ): undefined;
100
- export function maybeAddImport(
101
- visitor: JavaScriptVisitor<any>,
102
- options: AddImportOptions & { sideEffectOnly?: false }
103
- ): string;
104
- export function maybeAddImport(
105
- visitor: JavaScriptVisitor<any>,
106
- options: AddImportOptions
107
- ): string | undefined;
108
- export function maybeAddImport(
109
- visitor: JavaScriptVisitor<any>,
110
- options: AddImportOptions
81
+ options: AddImportOptions,
82
+ refuseCreate?: boolean
111
83
  ): string | undefined {
112
84
  validate(options);
113
85
  const module = moduleNameOf(options.module);
@@ -132,39 +104,53 @@ export function maybeAddImport(
132
104
  }
133
105
 
134
106
  if (sideEffectOnly) {
107
+ if (refuseCreate) {
108
+ return undefined;
109
+ }
135
110
  visitor.afterVisit.push(new AddImport(options));
136
111
  return undefined;
137
112
  }
138
113
 
139
114
  // A pinned alias is the whole answer, so the file has no say and nothing below needs to read it.
140
115
  if (options.alias) {
116
+ if (refuseCreate) {
117
+ return undefined;
118
+ }
141
119
  visitor.afterVisit.push(new AddImport(options, options.alias));
142
120
  return options.alias;
143
121
  }
144
122
 
145
123
  const derived = derivedName(options);
146
- const cu = compilationUnitOf(visitor);
124
+ const cursor = cursorOf(visitor);
125
+ const cu = cursor && compilationUnitOf(cursor);
147
126
  if (!cu) {
127
+ if (refuseCreate) {
128
+ return undefined;
129
+ }
148
130
  visitor.afterVisit.push(new AddImport(options, derived));
149
131
  return derived;
150
132
  }
151
133
 
152
- const bindings = bindingsInScope(cu, cursorOf(visitor));
153
-
154
- // An import already serving this request answers it; queuing one would, on the next cycle,
155
- // derive a suffixed name from the binding this call just added. A caller that named a preference
156
- // takes whatever comes back; one that did not assumes the name it derived, so a binding under
157
- // any other name would leave the references it emits unbound.
158
- for (const binding of bindings) {
134
+ // An import already serving this request answers it, under whatever name the file gave it —
135
+ // only a pinned alias, returned above, asks for a binding of its own. Queuing a second import
136
+ // would, on the next cycle, derive a suffixed name from the binding this call just added.
137
+ const scope = scopeOf(cursor);
138
+ for (const binding of moduleScopeBindings(cu)) {
159
139
  if (binding.module === module && binding.member === memberName(options.member) &&
160
- binding.typeOnly === typeOnly &&
161
- (options.preferredName !== undefined || anyNameAnswers(options) ||
162
- binding.name === derived)) {
140
+ binding.typeOnly === typeOnly && scope.declaringScope(binding.name) === cu) {
163
141
  return binding.name;
164
142
  }
165
143
  }
166
144
 
167
- const name = deconflict(derived, takenNames(bindings, visitor));
145
+ if (refuseCreate) {
146
+ return undefined;
147
+ }
148
+
149
+ // Only the module scope answers for a name, but any scope in the file occupies one. The queue
150
+ // gives every later request for this module the name chosen here, so it has to clear the scopes
151
+ // those references will sit in, which are not known yet.
152
+ const taken = takenNames(namesDeclaredIn(cu), visitor);
153
+ const name = deconflict(derived, candidate => taken.has(candidate));
168
154
  visitor.afterVisit.push(new AddImport(options, name));
169
155
  return name;
170
156
  }
@@ -216,12 +202,12 @@ function derivedName(options: AddImportOptions): string {
216
202
  }
217
203
 
218
204
  /** `'default'` and an absent member both name a default import, which binds no member name of its own. */
219
- function memberName(member: string | undefined): string | undefined {
205
+ export function memberName(member: string | undefined): string | undefined {
220
206
  return member === 'default' ? undefined : member;
221
207
  }
222
208
 
223
209
  /** A name introduced into the file's module scope, and where it came from when that is an import. */
224
- interface ModuleScopeBinding {
210
+ export interface ModuleScopeBinding {
225
211
  name: string;
226
212
  module?: string;
227
213
  /** Carries the {@link AddImportOptions.member} spelling, so `undefined` means a default import. */
@@ -229,107 +215,92 @@ interface ModuleScopeBinding {
229
215
  typeOnly?: boolean;
230
216
  }
231
217
 
232
- function cursorOf(visitor: JavaScriptVisitor<any>): Cursor | undefined {
233
- return (visitor as unknown as { cursor?: Cursor }).cursor;
234
- }
235
-
236
- function compilationUnitOf(visitor: JavaScriptVisitor<any>): JS.CompilationUnit | undefined {
237
- // `cursor` is protected on `TreeVisitor`, and the `maybeAddImport`/`maybeRemoveImport`
238
- // API is free functions, so reaching it takes a cast.
239
- return cursorOf(visitor)?.firstEnclosing((v): v is JS.CompilationUnit => v?.kind === JS.Kind.CompilationUnit);
240
- }
241
-
242
- /** Every name in scope at `cursor`: what the file binds, and what each block enclosing it declares. */
243
- function bindingsInScope(cu: JS.CompilationUnit, cursor: Cursor | undefined): ModuleScopeBinding[] {
244
- const bindings = statementBindings(cu.statements);
245
- // A local shadows an import for the code the template lands in, so a name a block declares is
246
- // taken there even though the module never answers for it.
247
- for (let c = cursor; c && c.value !== cu; c = c.parent) {
248
- for (const name of scopeNames(c.value)) {
249
- bindings.push({name});
250
- }
251
- }
252
- return bindings;
253
- }
254
-
255
- /** The names a scope introduces directly: a block's declarations, or a function's parameters. */
256
- function scopeNames(scope: unknown): string[] {
257
- const node = scope as J | undefined;
258
- if (node?.kind === J.Kind.Block) {
259
- return statementBindings((node as J.Block).statements).map(binding => binding.name);
260
- }
261
- if (node?.kind === J.Kind.MethodDeclaration) {
262
- return (node as J.MethodDeclaration).parameters.elements
263
- .flatMap(param => param.element?.kind === J.Kind.VariableDeclarations
264
- ? (param.element as J.VariableDeclarations).variables.flatMap(v => patternNames(v.element?.name))
265
- : patternNames(param.element))
266
- .map(bound => bound.name);
267
- }
268
- if (node?.kind === J.Kind.Lambda) {
269
- return (node as J.Lambda).parameters.parameters
270
- .flatMap(param => param.element?.kind === J.Kind.VariableDeclarations
271
- ? (param.element as J.VariableDeclarations).variables.flatMap(v => patternNames(v.element?.name))
272
- : patternNames(param.element))
273
- .map(bound => bound.name);
274
- }
275
- return [];
276
- }
277
-
278
- /** Imports are top-level statements and so is anything that can shadow one, so a flat scan sees every name. */
279
- function statementBindings(statements: J.RightPadded<Statement>[]): ModuleScopeBinding[] {
218
+ /** What the file's imports and `require`s bind at module scope, and the module each name comes from. */
219
+ export function moduleScopeBindings(cu: JS.CompilationUnit): ModuleScopeBinding[] {
280
220
  const bindings: ModuleScopeBinding[] = [];
281
221
 
282
- const declaredBy = (name: J | undefined): void => {
283
- for (const bound of patternNames(name)) {
284
- bindings.push({name: bound.name});
285
- }
286
- };
287
-
288
222
  const declaredByVariables = (varDecl: J.VariableDeclarations): void => {
289
223
  for (const variable of varDecl.variables) {
290
224
  const required = requiredModule(variable.element?.initializer?.element);
291
225
  if (required !== undefined) {
292
226
  bindings.push(...requireBindings(variable.element?.name, required));
293
- } else {
294
- declaredBy(variable.element?.name);
295
227
  }
296
228
  }
297
229
  };
298
230
 
299
- for (const stmt of statements) {
231
+ for (const stmt of cu.statements) {
300
232
  const statement = stmt.element;
301
- switch (statement?.kind) {
302
- case JS.Kind.Import:
303
- bindings.push(...importBindings(statement as JS.Import));
304
- break;
305
- case J.Kind.VariableDeclarations:
306
- declaredByVariables(statement as J.VariableDeclarations);
307
- break;
308
- case JS.Kind.ScopedVariableDeclarations:
309
- for (const variable of (statement as JS.ScopedVariableDeclarations).variables) {
310
- if (variable.element?.kind === J.Kind.VariableDeclarations) {
311
- declaredByVariables(variable.element as J.VariableDeclarations);
312
- }
313
- }
314
- break;
315
- case J.Kind.MethodDeclaration:
316
- declaredBy((statement as J.MethodDeclaration).name);
317
- break;
318
- case J.Kind.ClassDeclaration:
319
- declaredBy((statement as J.ClassDeclaration).name);
320
- break;
321
- case JS.Kind.NamespaceDeclaration:
322
- declaredBy((statement as JS.NamespaceDeclaration).name.element);
323
- break;
324
- case JS.Kind.TypeDeclaration:
325
- declaredBy((statement as JS.TypeDeclaration).name.element);
326
- break;
233
+ if (statement?.kind === JS.Kind.Import) {
234
+ bindings.push(...importBindings(statement as JS.Import));
235
+ continue;
236
+ }
237
+ for (const declaration of declarationsOf(statement)) {
238
+ declaredByVariables(declaration);
327
239
  }
328
240
  }
329
241
 
330
242
  return bindings;
331
243
  }
332
244
 
245
+ /** Whether a top-level statement already marks the file as a module: an import or any export form. */
246
+ export function hasEsmSyntax(cu: JS.CompilationUnit): boolean {
247
+ return cu.statements.some(stmt => {
248
+ const element = stmt.element;
249
+ // `export {a, b}`, `export * from` and `export default` are their own statement kinds;
250
+ // `export class`/`function`/`const` instead carry `export` as a modifier on the
251
+ // declaration itself, the same way TypeScript's own AST models it.
252
+ const modifiers = (element as {modifiers?: J.Modifier[]} | undefined)?.modifiers;
253
+ return element?.kind === JS.Kind.Import ||
254
+ element?.kind === JS.Kind.ExportDeclaration ||
255
+ element?.kind === JS.Kind.ExportAssignment ||
256
+ (modifiers?.some(m => m.keyword === "export") ?? false);
257
+ }) || hasTopLevelAwait(cu);
258
+ }
259
+
260
+ /**
261
+ * Whether a statement holds an `await` outside any function of its own — legal only at module
262
+ * top level, unlike an `await` inside an `async function`, which says nothing about the file.
263
+ */
264
+ function hasTopLevelAwait(cu: JS.CompilationUnit): boolean {
265
+ let found = false;
266
+ walk(cu.statements, node => {
267
+ if (found) {
268
+ return false;
269
+ }
270
+ if (node.kind === JS.Kind.Await) {
271
+ found = true;
272
+ return false;
273
+ }
274
+ return node.kind !== J.Kind.MethodDeclaration && node.kind !== J.Kind.Lambda;
275
+ });
276
+ return found;
277
+ }
278
+
279
+ /** Whether the file binds its modules with `require`, which decides whether a create is possible. */
280
+ export function isCommonJs(cu: JS.CompilationUnit): boolean {
281
+ if (cu.sourcePath.endsWith(".cjs") || cu.sourcePath.endsWith(".cts")) {
282
+ return true;
283
+ }
284
+ // Node treats these as ES modules regardless of what they contain, the same way
285
+ // `determineImportStyle` reads them as ES6-preferring; `.js`/`.ts`/`.tsx` stay ambiguous and
286
+ // fall through to the statements below.
287
+ if (cu.sourcePath.endsWith(".mjs") || cu.sourcePath.endsWith(".mts")) {
288
+ return false;
289
+ }
290
+ if (hasEsmSyntax(cu)) {
291
+ return false;
292
+ }
293
+ return cu.statements.some(stmt =>
294
+ declarationsOf(stmt.element).some(d => requiredModuleOfDeclaration(d) !== undefined));
295
+ }
296
+
297
+ /** The module a `const X = require('m')` declaration names, for the one variable it declares. */
298
+ export function requiredModuleOfDeclaration(declaration: J.VariableDeclarations): string | undefined {
299
+ return declaration.variables.length === 1
300
+ ? requiredModule(declaration.variables[0].element?.initializer?.element)
301
+ : undefined;
302
+ }
303
+
333
304
  /** The module a `require('...')` initializer names, `undefined` for an initializer that is anything else. */
334
305
  function requiredModule(initializer: J | undefined): string | undefined {
335
306
  return initializer?.kind === J.Kind.MethodInvocation
@@ -347,7 +318,7 @@ function isRequireCall(methodInv: J.MethodInvocation): boolean {
347
318
  * The module a `require(...)` call loads. `obj.require('x')` selects a method rather than loading a
348
319
  * module, and a specifier that is not a string literal names none that can be read here.
349
320
  */
350
- function requiredModuleOf(methodInv: J.MethodInvocation): string | undefined {
321
+ export function requiredModuleOf(methodInv: J.MethodInvocation): string | undefined {
351
322
  if (!isRequireCall(methodInv)) {
352
323
  return undefined;
353
324
  }
@@ -357,55 +328,13 @@ function requiredModuleOf(methodInv: J.MethodInvocation): string | undefined {
357
328
  : undefined;
358
329
  }
359
330
 
360
- /**
361
- * Every name a binding pattern introduces. `member` is the property a name takes its value from,
362
- * and only a name bound directly by the pattern has one — anything deeper reads a property of a
363
- * property, so it occupies its name without binding a member of the module.
364
- */
365
- function patternNames(pattern: J | undefined): { name: string; member?: string }[] {
366
- if (pattern?.kind === J.Kind.Identifier) {
367
- return [{name: (pattern as J.Identifier).simpleName}];
368
- }
369
- // An array pattern binds by position, so its elements name no member of what they destructure.
370
- if (pattern?.kind === JS.Kind.ArrayBindingPattern) {
371
- return (pattern as JS.ArrayBindingPattern).elements.elements
372
- .flatMap(elem => elem.element?.kind === JS.Kind.BindingElement
373
- ? patternNames((elem.element as JS.BindingElement).name)
374
- : patternNames(elem.element))
375
- .map(bound => ({name: bound.name}));
376
- }
377
- if (pattern?.kind !== JS.Kind.ObjectBindingPattern) {
378
- return [];
379
- }
380
- const names: { name: string; member?: string }[] = [];
381
- for (const elem of (pattern as JS.ObjectBindingPattern).bindings.elements) {
382
- if (elem.element?.kind !== JS.Kind.BindingElement) {
383
- continue;
384
- }
385
- const bindingElem = elem.element as JS.BindingElement;
386
- if (bindingElem.name?.kind === J.Kind.Identifier) {
387
- const name = (bindingElem.name as J.Identifier).simpleName;
388
- const propertyName = bindingElem.propertyName?.element;
389
- names.push({
390
- name,
391
- member: propertyName?.kind === J.Kind.Identifier
392
- ? (propertyName as J.Identifier).simpleName
393
- : name
394
- });
395
- } else {
396
- names.push(...patternNames(bindingElem.name).map(bound => ({name: bound.name})));
397
- }
398
- }
399
- return names;
400
- }
401
-
402
331
  /** A `require` binds a module the way an import does, so the pool records it the same way. */
403
332
  function requireBindings(pattern: J | undefined, module: string): ModuleScopeBinding[] {
404
333
  // A whole-module require binds no member, exactly as a default import does.
405
334
  if (pattern?.kind === J.Kind.Identifier) {
406
335
  return [{name: (pattern as J.Identifier).simpleName, module, member: undefined, typeOnly: false}];
407
336
  }
408
- return patternNames(pattern).map(bound => bound.member === undefined
337
+ return bindingNames(pattern).map(bound => bound.member === undefined
409
338
  ? {name: bound.name}
410
339
  : {name: bound.name, module, member: bound.member, typeOnly: false});
411
340
  }
@@ -465,12 +394,12 @@ function importBindings(jsImport: JS.Import): ModuleScopeBinding[] {
465
394
  }
466
395
 
467
396
  /**
468
- * Names the file binds, plus those pending `AddImport`s on the `afterVisit` queue have claimed. A
469
- * queued `RemoveImport` does not free one: it removes only what the file leaves unused, and binding
470
- * a name it keeps is an error, where an unnecessary suffix merely reads oddly.
397
+ * `names`, plus those pending `AddImport`s on the `afterVisit` queue have claimed. A queued
398
+ * `RemoveImport` does not free one: it removes only what the file leaves unused, and binding a name
399
+ * it keeps is an error, where an unnecessary suffix merely reads oddly.
471
400
  */
472
- function takenNames(bindings: ModuleScopeBinding[], visitor: JavaScriptVisitor<any>): Set<string> {
473
- const taken = new Set<string>();
401
+ export function takenNames(names: ReadonlySet<string>, visitor: JavaScriptVisitor<any>): Set<string> {
402
+ const taken = new Set<string>(names);
474
403
 
475
404
  for (const v of visitor.afterVisit || []) {
476
405
  if (v instanceof AddImport && v.bindingName) {
@@ -478,22 +407,13 @@ function takenNames(bindings: ModuleScopeBinding[], visitor: JavaScriptVisitor<a
478
407
  }
479
408
  }
480
409
 
481
- for (const binding of bindings) {
482
- taken.add(binding.name);
483
- }
484
-
485
410
  return taken;
486
411
  }
487
412
 
488
- function deconflict(derived: string, taken: Set<string>): string {
489
- if (!taken.has(derived)) {
490
- return derived;
491
- }
492
- let suffix = 1;
493
- while (taken.has(`${derived}_${suffix}`)) {
494
- suffix++;
495
- }
496
- return `${derived}_${suffix}`;
413
+ /** As {@link takenNames}, for a caller asking about one name rather than probing repeatedly. */
414
+ export function nameTaken(name: string, names: ReadonlySet<string>, visitor: JavaScriptVisitor<any>): boolean {
415
+ return names.has(name) ||
416
+ (visitor.afterVisit || []).some(v => v instanceof AddImport && v.bindingName === name);
497
417
  }
498
418
 
499
419
  /**
@@ -501,7 +421,7 @@ function deconflict(derived: string, taken: Set<string>): string {
501
421
  * sets `value` to the quoted source (`parser.ts` `mapLiteral`), so neither field alone is the
502
422
  * module name. Reuniting them and stripping the quotes yields it for either shape of literal.
503
423
  */
504
- function moduleNameOf(module: string | J.Literal): string {
424
+ export function moduleNameOf(module: string | J.Literal): string {
505
425
  if (typeof module === 'string') {
506
426
  return module;
507
427
  }
@@ -601,8 +521,8 @@ async function detectQuote(cu: JS.CompilationUnit): Promise<QuoteChar> {
601
521
  return specifierQuote;
602
522
  }
603
523
 
604
- const prettier = getStyle(StyleKind.PrettierStyle, cu) as PrettierStyle | undefined;
605
- if (prettier?.kind === StyleKind.PrettierStyle && !prettier.ignored) {
524
+ const prettier = getPrettierStyle(cu);
525
+ if (prettier && !prettier.ignored) {
606
526
  const singleQuote = prettier.config.singleQuote;
607
527
  if (typeof singleQuote === 'boolean') {
608
528
  return singleQuote ? "'" : '"';
@@ -612,6 +532,33 @@ async function detectQuote(cu: JS.CompilationUnit): Promise<QuoteChar> {
612
532
  return double > single ? '"' : "'";
613
533
  }
614
534
 
535
+ /**
536
+ * Lays out the import statement `AddImport` wrote, so that the file's own style — a Prettier
537
+ * configuration's brace spacing and print width, say — reaches it.
538
+ */
539
+ async function formatImport<P>(cu: JS.CompilationUnit, id: UUID, p: P): Promise<JS.CompilationUnit> {
540
+ return await new class extends JavaScriptVisitor<P> {
541
+ override async visitImportDeclaration(jsImport: JS.Import, p: P): Promise<J | undefined> {
542
+ return jsImport.id === id ? await autoFormat(jsImport, p, undefined, this.cursor.parent) : jsImport;
543
+ }
544
+ }().visit(cu, p) as JS.CompilationUnit;
545
+ }
546
+
547
+ /**
548
+ * As {@link formatImport}, for an import that gained specifiers. Short of a Prettier configuration
549
+ * deciding the whole line, the statement's own text says more about its spacing than a
550
+ * repository-wide style does, and the merge reads it directly.
551
+ */
552
+ async function formatMergedImport<P>(cu: JS.CompilationUnit, id: UUID, p: P): Promise<JS.CompilationUnit> {
553
+ return getPrettierStyle(cu)?.ignored === false ? formatImport(cu, id, p) : cu;
554
+ }
555
+
556
+ /**
557
+ * Adds the import {@link AddImportOptions} describes, under the name derived there. An import
558
+ * already in the file answers where it binds that same name, or where the request expressed no
559
+ * preference at all. Reuse under some other name belongs to {@link bindImport}, whose caller
560
+ * learns which name came back; a recipe driving this visitor is stuck with the one it derived.
561
+ */
615
562
  export class AddImport<P> extends JavaScriptVisitor<P> {
616
563
  readonly module: string;
617
564
  /** Set when the caller supplied the module specifier as a literal; printed verbatim. */
@@ -858,20 +805,20 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
858
805
  }
859
806
  }
860
807
 
861
- // Add the import using the appropriate style
862
- if (importStyle === ImportStyle.CommonJS) {
863
- // TODO: Implement CommonJS require creation
864
- // For now, fall back to ES6 imports
865
- // return this.addCommonJSRequire(compilationUnit, p);
808
+ // TODO: create a `require` here. Until then the request goes unserved, since `import` would
809
+ // make a file that binds its modules with `require` an ES module and change how everything
810
+ // in it loads — `maybeBind` refuses it for the same reason. An explicit ES6 `style`
811
+ // overrides, which is how a caller converting the file to ESM asks for one.
812
+ if (importStyle === ImportStyle.CommonJS && isCommonJs(compilationUnit)) {
813
+ return compilationUnit;
866
814
  }
867
815
 
868
816
  // Add ES6 import (handles ES6Named, ES6Namespace, ES6Default)
869
- return this.produceJavaScript(compilationUnit, p, async draft => {
870
- // Find the position to insert the import
871
- const insertionIndex = this.findImportInsertionIndex(compilationUnit);
872
-
873
- const newImport = await this.createImportStatement(compilationUnit, insertionIndex, p);
817
+ // Find the position to insert the import
818
+ const insertionIndex = this.findImportInsertionIndex(compilationUnit);
819
+ const newImport = await this.createImportStatement(compilationUnit, insertionIndex, p);
874
820
 
821
+ const withImport = await this.produceJavaScript(compilationUnit, p, async draft => {
875
822
  // Insert the import at the appropriate position
876
823
  // Create semicolon marker for the import statement
877
824
  const semicolonMarkers = markers({
@@ -928,6 +875,8 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
928
875
  }
929
876
  }
930
877
  });
878
+
879
+ return formatImport(withImport, newImport.id, p);
931
880
  }
932
881
 
933
882
  /**
@@ -971,7 +920,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
971
920
  }
972
921
 
973
922
  // We found a matching import with named bindings - merge into it
974
- return this.produceJavaScript(compilationUnit, p, async draft => {
923
+ return formatMergedImport(await this.produceJavaScript(compilationUnit, p, async draft => {
975
924
  const namedImports = importClause.namedBindings as JS.NamedImports;
976
925
  const existingElements = namedImports.elements.elements;
977
926
 
@@ -1044,13 +993,13 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1044
993
  draft.statements = compilationUnit.statements.map((s, idx) =>
1045
994
  idx === i ? {...s, element: updatedImport} : s
1046
995
  );
1047
- });
996
+ }), jsImport.id, p);
1048
997
  }
1049
998
 
1050
999
  // Case 2: Default import without named bindings - add named bindings
1051
1000
  // Transform: import React from 'react' -> import React, { useState } from 'react'
1052
1001
  if (importClause.name && !importClause.namedBindings) {
1053
- return this.produceJavaScript(compilationUnit, p, async draft => {
1002
+ return formatMergedImport(await this.produceJavaScript(compilationUnit, p, async draft => {
1054
1003
  const newSpecifier = this.createImportSpecifier();
1055
1004
 
1056
1005
  // Get the spaces style for brace spacing
@@ -1099,7 +1048,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1099
1048
  draft.statements = compilationUnit.statements.map((s, idx) =>
1100
1049
  idx === i ? {...s, element: updatedImport} : s
1101
1050
  );
1102
- });
1051
+ }), jsImport.id, p);
1103
1052
  }
1104
1053
  }
1105
1054
  }
@@ -1428,10 +1377,8 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1428
1377
  // Determine the appropriate prefix (spacing before the import)
1429
1378
  const prefix = this.determineImportPrefix(compilationUnit, insertionIndex);
1430
1379
 
1431
- // Create the module specifier
1432
- // For side-effect imports, use emptySpace since space comes from LeftPadded.before
1433
- // For regular imports with import clause, use emptySpace since space comes from LeftPadded.before
1434
- // However, the printer expects the space after 'from' in the literal's prefix
1380
+ // Whitespace throughout follows the parser's placement, so that formatting the result is a
1381
+ // no-op where the file's style already agrees with it.
1435
1382
  // Note: value is the unquoted module name; valueSource and unicodeEscapes are its printed form
1436
1383
  let valueSource = this.moduleValueSource;
1437
1384
  if (valueSource === undefined) {
@@ -1441,7 +1388,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1441
1388
  const moduleSpecifier: J.Literal = {
1442
1389
  id: randomId(),
1443
1390
  kind: J.Kind.Literal,
1444
- prefix: this.sideEffectOnly ? emptySpace : singleSpace,
1391
+ prefix: singleSpace,
1445
1392
  markers: emptyMarkers,
1446
1393
  value: this.module,
1447
1394
  valueSource,
@@ -1481,7 +1428,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1481
1428
  const namespaceBinding: JS.Alias = {
1482
1429
  id: randomId(),
1483
1430
  kind: JS.Kind.Alias,
1484
- prefix: singleSpace,
1431
+ prefix: this.typeOnly ? singleSpace : emptySpace,
1485
1432
  markers: emptyMarkers,
1486
1433
  propertyName: rightPadded(propertyName, singleSpace),
1487
1434
  alias: aliasIdentifier
@@ -1490,7 +1437,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1490
1437
  importClause = {
1491
1438
  id: randomId(),
1492
1439
  kind: JS.Kind.ImportClause,
1493
- prefix: this.typeOnly ? singleSpace : emptySpace,
1440
+ prefix: singleSpace,
1494
1441
  markers: emptyMarkers,
1495
1442
  typeOnly: this.typeOnly,
1496
1443
  name: undefined,
@@ -1502,7 +1449,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1502
1449
  const defaultName: J.Identifier = {
1503
1450
  id: randomId(),
1504
1451
  kind: J.Kind.Identifier,
1505
- prefix: singleSpace,
1452
+ prefix: this.typeOnly ? singleSpace : emptySpace,
1506
1453
  markers: emptyMarkers,
1507
1454
  annotations: [],
1508
1455
  simpleName: this.bindingName!,
@@ -1513,10 +1460,10 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1513
1460
  importClause = {
1514
1461
  id: randomId(),
1515
1462
  kind: JS.Kind.ImportClause,
1516
- prefix: this.typeOnly ? singleSpace : emptySpace,
1463
+ prefix: singleSpace,
1517
1464
  markers: emptyMarkers,
1518
1465
  typeOnly: this.typeOnly,
1519
- name: rightPadded(defaultName, emptySpace),
1466
+ name: rightPadded(defaultName, singleSpace),
1520
1467
  namedBindings: undefined
1521
1468
  };
1522
1469
  } else {
@@ -1534,11 +1481,11 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1534
1481
  const namedImports: JS.NamedImports = {
1535
1482
  id: randomId(),
1536
1483
  kind: JS.Kind.NamedImports,
1537
- prefix: singleSpace,
1484
+ prefix: emptySpace,
1538
1485
  markers: emptyMarkers,
1539
1486
  elements: {
1540
1487
  kind: J.Kind.Container,
1541
- before: emptySpace,
1488
+ before: this.typeOnly ? singleSpace : emptySpace,
1542
1489
  elements: [rightPadded(importSpecWithSpacing, braceSpace)],
1543
1490
  markers: emptyMarkers
1544
1491
  }
@@ -1547,7 +1494,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1547
1494
  importClause = {
1548
1495
  id: randomId(),
1549
1496
  kind: JS.Kind.ImportClause,
1550
- prefix: this.typeOnly ? singleSpace : emptySpace,
1497
+ prefix: singleSpace,
1551
1498
  markers: emptyMarkers,
1552
1499
  typeOnly: this.typeOnly,
1553
1500
  name: undefined,
@@ -1564,7 +1511,9 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1564
1511
  importClause,
1565
1512
  moduleSpecifier: {
1566
1513
  kind: J.Kind.LeftPadded,
1567
- before: singleSpace,
1514
+ // Bindings end at `}`, so the space before `from` sits here; a default import's name
1515
+ // and a side-effect import's literal carry their own
1516
+ before: importClause?.namedBindings ? singleSpace : emptySpace,
1568
1517
  element: moduleSpecifier,
1569
1518
  markers: emptyMarkers
1570
1519
  },
@@ -1694,3 +1643,569 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1694
1643
 
1695
1644
 
1696
1645
  }
1646
+
1647
+ /**
1648
+ * The local name `jsImport` binds `member` of `module` to, or `undefined` where it binds
1649
+ * something else. `'default'` and an absent `member` both mean the default import, matching
1650
+ * {@link memberName}.
1651
+ */
1652
+ function importBinds(jsImport: JS.Import, module: string, member: string | undefined): string | undefined {
1653
+ const specifier = jsImport.moduleSpecifier?.element;
1654
+ if (specifier?.kind !== J.Kind.Literal || (specifier as J.Literal).value !== module) {
1655
+ return undefined;
1656
+ }
1657
+ const importClause = jsImport.importClause;
1658
+ if (!importClause) {
1659
+ return undefined;
1660
+ }
1661
+ const key = memberName(member);
1662
+ if (key === undefined) {
1663
+ const nameElem = importClause.name?.element;
1664
+ return nameElem && isIdentifier(nameElem) ? nameElem.simpleName : undefined;
1665
+ }
1666
+ if (key === '*') {
1667
+ const namedBindings = importClause.namedBindings;
1668
+ return namedBindings?.kind === JS.Kind.Alias && isIdentifier((namedBindings as JS.Alias).alias)
1669
+ ? ((namedBindings as JS.Alias).alias as J.Identifier).simpleName
1670
+ : undefined;
1671
+ }
1672
+ const namedBindings = importClause.namedBindings;
1673
+ if (namedBindings?.kind !== JS.Kind.NamedImports) {
1674
+ return undefined;
1675
+ }
1676
+ for (const elem of (namedBindings as JS.NamedImports).elements.elements) {
1677
+ const specifierNode = elem.element.specifier;
1678
+ if (!namedSpecifierImports(specifierNode, key)) {
1679
+ continue;
1680
+ }
1681
+ if (isIdentifier(specifierNode)) {
1682
+ return specifierNode.simpleName;
1683
+ }
1684
+ const alias = (specifierNode as JS.Alias).alias;
1685
+ if (isIdentifier(alias)) {
1686
+ return alias.simpleName;
1687
+ }
1688
+ }
1689
+ return undefined;
1690
+ }
1691
+
1692
+ /** Whether `specifier` imports the member `key`, under whatever local name it binds it to. */
1693
+ function namedSpecifierImports(specifier: JS.ImportSpecifier["specifier"], key: string): boolean {
1694
+ if (isIdentifier(specifier)) {
1695
+ return specifier.simpleName === key;
1696
+ }
1697
+ if (specifier.kind === JS.Kind.Alias) {
1698
+ const propertyName = (specifier as JS.Alias).propertyName.element;
1699
+ return isIdentifier(propertyName) && propertyName.simpleName === key;
1700
+ }
1701
+ return false;
1702
+ }
1703
+
1704
+ /** Whether the named specifier binding `key` carries its own inline `type`, as in `{type a, b}`. */
1705
+ function namedSpecifierIsTypeOnly(imp: JS.Import, key: string): boolean {
1706
+ const namedBindings = imp.importClause?.namedBindings;
1707
+ if (namedBindings?.kind !== JS.Kind.NamedImports) {
1708
+ return false;
1709
+ }
1710
+ for (const elem of (namedBindings as JS.NamedImports).elements.elements) {
1711
+ if (namedSpecifierImports(elem.element.specifier, key)) {
1712
+ return elem.element.importType.element;
1713
+ }
1714
+ }
1715
+ return false;
1716
+ }
1717
+
1718
+ /** How many named specifiers `jsImport` carries, `0` for a default, namespace or side-effect import. */
1719
+ function namedImportCount(jsImport: JS.Import): number {
1720
+ const namedBindings = jsImport.importClause?.namedBindings;
1721
+ return namedBindings?.kind === JS.Kind.NamedImports ? (namedBindings as JS.NamedImports).elements.elements.length : 0;
1722
+ }
1723
+
1724
+ /**
1725
+ * Whether `jsImport`'s clause binds exactly one thing — a default, a namespace, or one named
1726
+ * specifier. That is the only shape a module-only move can rewrite in place: whichever binding
1727
+ * `importBinds` already matched is this one, so nothing else the clause carries goes along with it.
1728
+ */
1729
+ function isOnlyMember(jsImport: JS.Import): boolean {
1730
+ const importClause = jsImport.importClause;
1731
+ if (!importClause) {
1732
+ return false;
1733
+ }
1734
+ const hasDefault = importClause.name !== undefined;
1735
+ const hasNamespace = importClause.namedBindings?.kind === JS.Kind.Alias;
1736
+ return (hasDefault ? 1 : 0) + (hasNamespace ? 1 : 0) + namedImportCount(jsImport) === 1;
1737
+ }
1738
+
1739
+ export interface ExistingImportBinding {
1740
+ localName: string;
1741
+ onlyMemberOfStatement: boolean;
1742
+
1743
+ /** Whether the source states this local name — `import {a as b}` — or takes it from the member. */
1744
+ aliased: boolean;
1745
+ }
1746
+
1747
+ /**
1748
+ * The existing binding for `member` of `module`, read from `cu`'s own import statements — what
1749
+ * `maybeRebind` reads before committing to a `RebindImport` edit.
1750
+ */
1751
+ export function existingImportBinding(
1752
+ cu: JS.CompilationUnit,
1753
+ module: string,
1754
+ member: string | undefined
1755
+ ): ExistingImportBinding | undefined {
1756
+ for (const stmt of cu.statements) {
1757
+ const element = stmt.element;
1758
+ if (element?.kind !== JS.Kind.Import) {
1759
+ continue;
1760
+ }
1761
+ const localName = importBinds(element as JS.Import, module, member);
1762
+ if (localName !== undefined) {
1763
+ return {
1764
+ localName,
1765
+ onlyMemberOfStatement: isOnlyMember(element as JS.Import),
1766
+ aliased: localName !== memberName(member)
1767
+ };
1768
+ }
1769
+ }
1770
+ return undefined;
1771
+ }
1772
+
1773
+ /**
1774
+ * The `member as local` specifier, for an import binding `member` under `local` or an export
1775
+ * publishing `local`'s binding under that name. `local` itself becomes the alias, keeping the type
1776
+ * attribution it holds, and the alias takes its prefix: that whitespace separates the specifier
1777
+ * from a `type` keyword before it.
1778
+ */
1779
+ function aliasing(local: J.Identifier, member: string): JS.Alias {
1780
+ const propertyName: J.Identifier = {
1781
+ id: randomId(),
1782
+ kind: J.Kind.Identifier,
1783
+ prefix: emptySpace,
1784
+ markers: emptyMarkers,
1785
+ annotations: [],
1786
+ simpleName: member,
1787
+ type: undefined,
1788
+ fieldType: undefined
1789
+ };
1790
+ return {
1791
+ id: randomId(),
1792
+ kind: JS.Kind.Alias,
1793
+ prefix: local.prefix,
1794
+ markers: emptyMarkers,
1795
+ propertyName: rightPadded(propertyName, singleSpace),
1796
+ alias: {...local, prefix: singleSpace}
1797
+ };
1798
+ }
1799
+
1800
+ /**
1801
+ * Drops the binding for `member` from `jsImport`'s clause — the default, the namespace alias, or
1802
+ * one entry of the named list, whichever `member` names — keeping everything else the clause
1803
+ * binds. `ElementRemovalFormatter` carries the dropped binding's prefix onto whatever prints
1804
+ * next, the same way `RemoveImport` keeps formatting sane when trimming a list.
1805
+ */
1806
+ function removeBinding(jsImport: JS.Import, member: string | undefined): JS.Import {
1807
+ const importClause = jsImport.importClause;
1808
+ if (!importClause) {
1809
+ return jsImport;
1810
+ }
1811
+ const key = memberName(member);
1812
+
1813
+ if (key === undefined) {
1814
+ if (!importClause.name) {
1815
+ return jsImport;
1816
+ }
1817
+ const namedBindings = importClause.namedBindings;
1818
+ if (namedBindings?.kind === JS.Kind.NamedImports) {
1819
+ // `NamedImports` keeps the space before its own `{` on the container's `before`,
1820
+ // not on its own prefix, so the removed default's prefix has to land there instead.
1821
+ const namedImports = namedBindings as JS.NamedImports;
1822
+ const updated: JS.NamedImports = {
1823
+ ...namedImports,
1824
+ elements: {...namedImports.elements, before: importClause.name.element.prefix}
1825
+ };
1826
+ return {...jsImport, importClause: {...importClause, name: undefined, namedBindings: updated}};
1827
+ }
1828
+ if (namedBindings) {
1829
+ const formatter = new ElementRemovalFormatter<J>();
1830
+ formatter.markRemoved(importClause.name.element);
1831
+ return {...jsImport, importClause: {...importClause, name: undefined, namedBindings: formatter.processKept(namedBindings)}};
1832
+ }
1833
+ return {...jsImport, importClause: {...importClause, name: undefined}};
1834
+ }
1835
+
1836
+ if (key === '*') {
1837
+ return {...jsImport, importClause: {...importClause, namedBindings: undefined}};
1838
+ }
1839
+
1840
+ if (importClause.namedBindings?.kind !== JS.Kind.NamedImports) {
1841
+ return jsImport;
1842
+ }
1843
+ const namedImports = importClause.namedBindings as JS.NamedImports;
1844
+ const formatter = new ElementRemovalFormatter<JS.ImportSpecifier>();
1845
+ const kept: J.RightPadded<JS.ImportSpecifier>[] = [];
1846
+ for (const entry of namedImports.elements.elements) {
1847
+ if (namedSpecifierImports(entry.element.specifier, key)) {
1848
+ formatter.markRemoved(entry.element);
1849
+ } else {
1850
+ kept.push({...entry, element: formatter.processKept(entry.element)});
1851
+ }
1852
+ }
1853
+ if (kept.length === 0) {
1854
+ // An emptied brace list still prints, as `import D, {} from "m"`, so it goes with its
1855
+ // last member; the caller drops the whole statement when no default remains either.
1856
+ return {...jsImport, importClause: {...importClause, namedBindings: undefined}};
1857
+ }
1858
+ const updatedNamedImports: JS.NamedImports = {...namedImports, elements: {...namedImports.elements, elements: kept}};
1859
+ return {...jsImport, importClause: {...importClause, namedBindings: updatedNamedImports}};
1860
+ }
1861
+
1862
+ /**
1863
+ * Moves the binding `from` names to `to`, binding it under `boundName`. In place when the
1864
+ * statement that carries it binds nothing else — module and member specifier rewritten there
1865
+ * directly; otherwise the old specifier drops and {@link bindImport} queues the replacement.
1866
+ * A `boundName` of its own renames the binding, and the file's references to it follow.
1867
+ *
1868
+ * Not built on `RemoveImport`/`maybeUnbind`: those only drop a binding once nothing references
1869
+ * it, but a rebind moves one that is still in use — removal here has to be unconditional.
1870
+ */
1871
+ export class RebindImport<P> extends JavaScriptVisitor<P> {
1872
+ constructor(
1873
+ readonly from: {module: string; member?: string},
1874
+ readonly to: {module: string; member?: string},
1875
+ readonly localName: string,
1876
+ readonly boundName: string
1877
+ ) {
1878
+ super();
1879
+ }
1880
+
1881
+ private transformedInPlace = false;
1882
+ private typeOnly = false;
1883
+ private readonly movedTypes = new MovedTypes(this.from, this.to);
1884
+ /** Identifiers settled as references to the binding, so a parent need not settle it again. */
1885
+ private readonly references = new Set<string>();
1886
+
1887
+ private get renaming(): boolean {
1888
+ return this.boundName !== this.localName;
1889
+ }
1890
+
1891
+ override async visitJsCompilationUnit(cu: JS.CompilationUnit, p: P): Promise<J | undefined> {
1892
+ const visited = await super.visitJsCompilationUnit(cu, p) as JS.CompilationUnit;
1893
+ if (!this.transformedInPlace) {
1894
+ bindImport(this, {
1895
+ module: this.to.module,
1896
+ member: this.to.member,
1897
+ alias: this.boundName,
1898
+ typeOnly: this.typeOnly,
1899
+ onlyIfReferenced: false
1900
+ });
1901
+ }
1902
+ return visited;
1903
+ }
1904
+
1905
+ override async visitImportDeclaration(jsImport: JS.Import, p: P): Promise<J | undefined> {
1906
+ const imp = await super.visitImportDeclaration(jsImport, p) as JS.Import;
1907
+
1908
+ const key = memberName(this.from.member);
1909
+ // One call moves one binding, and no two imports bind the same local name, so the name
1910
+ // read from the matched statement is what picks it back out of the file.
1911
+ if (importBinds(imp, this.from.module, this.from.member) !== this.localName) {
1912
+ return imp;
1913
+ }
1914
+ // A moved named specifier's own inline `type` marks it type-only even where the clause
1915
+ // it's leaving is not — the replacement needs the same answer to stay type-safe.
1916
+ this.typeOnly = (imp.importClause?.typeOnly ?? false) ||
1917
+ (key !== undefined && key !== '*' && namedSpecifierIsTypeOnly(imp, key));
1918
+
1919
+ if (!isOnlyMember(imp)) {
1920
+ return removeBinding(imp, this.from.member);
1921
+ }
1922
+
1923
+ this.transformedInPlace = true;
1924
+ return produce(imp, draft => {
1925
+ const literal = draft.moduleSpecifier!.element as Draft<J.Literal>;
1926
+ literal.value = this.to.module;
1927
+ const originalSource = literal.valueSource || `"${this.from.module}"`;
1928
+ const quoteChar = originalSource.startsWith("'") ? "'" : '"';
1929
+ literal.valueSource = `${quoteChar}${this.to.module}${quoteChar}`;
1930
+
1931
+ // A default or namespace import carries its local name on the clause itself; a named
1932
+ // one states the member alongside it, in the specifier.
1933
+ if (key === undefined || key === '*') {
1934
+ if (this.boundName !== this.localName) {
1935
+ renameClauseBinding(draft.importClause, key, this.boundName);
1936
+ }
1937
+ return;
1938
+ }
1939
+ rewriteNamedSpecifier(draft.importClause, key, memberName(this.to.member) ?? key, this.boundName);
1940
+ });
1941
+ }
1942
+
1943
+ override async visitIdentifier(identifier: J.Identifier, p: P): Promise<J | undefined> {
1944
+ if (!this.referencesBinding(identifier)) {
1945
+ return super.visitIdentifier(identifier, p);
1946
+ }
1947
+ this.references.add(identifier.id);
1948
+ // The name follows the binding, and so does what the name is attributed to; an aliased
1949
+ // binding keeps its name and still resolves somewhere new.
1950
+ const type = await this.movedTypes.visit(identifier.type, undefined);
1951
+ const fieldType = await this.movedTypes.visit(identifier.fieldType, undefined) as Type.Variable | undefined;
1952
+ return !this.renaming && type === identifier.type && fieldType === identifier.fieldType
1953
+ ? identifier
1954
+ : {...identifier, simpleName: this.boundName, type, fieldType} as J.Identifier;
1955
+ }
1956
+
1957
+ override async visitMethodInvocation(method: J.MethodInvocation, p: P): Promise<J | undefined> {
1958
+ const m = await super.visitMethodInvocation(method, p) as J.MethodInvocation;
1959
+ if (!this.referencesMovedBinding(m.name)) {
1960
+ return m;
1961
+ }
1962
+ const methodType = await this.movedTypes.visit(m.methodType, undefined) as Type.Method | undefined;
1963
+ return methodType === m.methodType ? m : {...m, methodType} as J.MethodInvocation;
1964
+ }
1965
+
1966
+ override async visitNewClass(newClass: J.NewClass, p: P): Promise<J | undefined> {
1967
+ const nc = await super.visitNewClass(newClass, p) as J.NewClass;
1968
+ if (!this.referencesMovedBinding(nc.class)) {
1969
+ return nc;
1970
+ }
1971
+ const type = await this.movedTypes.visit(nc.type, undefined);
1972
+ const methodType = await this.movedTypes.visit(nc.methodType, undefined) as Type.Method | undefined;
1973
+ const constructorType = await this.movedTypes.visit(nc.constructorType, undefined) as Type.Method | undefined;
1974
+ return type === nc.type && methodType === nc.methodType && constructorType === nc.constructorType
1975
+ ? nc
1976
+ : {...nc, type, methodType, constructorType} as J.NewClass;
1977
+ }
1978
+
1979
+ override async visitFunctionCall(functionCall: JS.FunctionCall, p: P): Promise<J | undefined> {
1980
+ const fc = await super.visitFunctionCall(functionCall, p) as JS.FunctionCall;
1981
+ if (!this.referencesMovedBinding(fc.function?.element)) {
1982
+ return fc;
1983
+ }
1984
+ const methodType = await this.movedTypes.visit(fc.methodType, undefined) as Type.Method | undefined;
1985
+ return methodType === fc.methodType ? fc : {...fc, methodType} as JS.FunctionCall;
1986
+ }
1987
+
1988
+ /**
1989
+ * Whether `callee` is the identifier `visitIdentifier` settled as a reference to the binding.
1990
+ * A rewrite keeps the identifier's id, so the one it settled is the one still standing here.
1991
+ */
1992
+ private referencesMovedBinding(callee: J | undefined): boolean {
1993
+ return callee?.kind === J.Kind.Identifier && this.references.has(callee.id);
1994
+ }
1995
+
1996
+ /**
1997
+ * A decorator and the other type-name positions reach the tree through this hook, which the
1998
+ * base visitor holds closed; a reference in one of them is a reference like any other.
1999
+ */
2000
+ protected override async visitTypeName<N extends NameTree>(nameTree: N, p: P): Promise<N> {
2001
+ return await this.visit(nameTree, p) as N;
2002
+ }
2003
+
2004
+ /**
2005
+ * An export specifier's own name is the module's public surface, which its consumers read and
2006
+ * the rename leaves alone. See CLAUDE.md: Which name a rebind binds.
2007
+ */
2008
+ override async visitExportSpecifier(exportSpecifier: JS.ExportSpecifier, p: P): Promise<J | undefined> {
2009
+ if (!this.renaming) {
2010
+ return super.visitExportSpecifier(exportSpecifier, p);
2011
+ }
2012
+ // `export {a} from "m"` names `m`'s member rather than anything this file binds.
2013
+ const from = this.cursor.firstEnclosing(
2014
+ (v): v is JS.ExportDeclaration => (v as J | undefined)?.kind === JS.Kind.ExportDeclaration);
2015
+ if (from?.moduleSpecifier !== undefined) {
2016
+ return exportSpecifier;
2017
+ }
2018
+ const specifier = exportSpecifier.specifier;
2019
+ if (specifier.kind === J.Kind.Identifier && (specifier as J.Identifier).simpleName === this.localName) {
2020
+ return {...exportSpecifier, specifier: aliasing(specifier as J.Identifier, this.boundName)} as JS.ExportSpecifier;
2021
+ }
2022
+ if (specifier.kind === JS.Kind.Alias) {
2023
+ const alias = specifier as JS.Alias;
2024
+ const propertyName = alias.propertyName.element;
2025
+ return propertyName.kind === J.Kind.Identifier && (propertyName as J.Identifier).simpleName === this.localName
2026
+ ? {
2027
+ ...exportSpecifier,
2028
+ specifier: {
2029
+ ...alias,
2030
+ propertyName: {...alias.propertyName, element: {...propertyName, simpleName: this.boundName}}
2031
+ }
2032
+ } as JS.ExportSpecifier
2033
+ : exportSpecifier;
2034
+ }
2035
+ return super.visitExportSpecifier(exportSpecifier, p);
2036
+ }
2037
+
2038
+ /**
2039
+ * Whether the identifier at the cursor stands for the moved binding: the right name, in a
2040
+ * position that references rather than declares, reaching the module scope that binds it.
2041
+ */
2042
+ private referencesBinding(identifier: J.Identifier): boolean {
2043
+ return identifier.simpleName === this.localName &&
2044
+ // The specifier binding the name is the one edit that is not a reference to it.
2045
+ !this.cursor.firstEnclosing((v): v is JS.Import => (v as J | undefined)?.kind === JS.Kind.Import) &&
2046
+ isValueReference(this.cursor, identifier) &&
2047
+ scopeOf(this.cursor).declaringScope(this.localName)?.kind === JS.Kind.CompilationUnit;
2048
+ }
2049
+
2050
+ /** A shorthand property's name slot is also the reference to the binding. */
2051
+ override async visitPropertyAssignment(propertyAssignment: JS.PropertyAssignment, p: P): Promise<J | undefined> {
2052
+ const name = propertyAssignment.name.element;
2053
+ if (this.renaming && propertyAssignment.initializer === undefined &&
2054
+ name.kind === J.Kind.Identifier && (name as J.Identifier).simpleName === this.localName &&
2055
+ scopeOf(this.cursor).declaringScope(this.localName)?.kind === JS.Kind.CompilationUnit) {
2056
+ // The key names a property rather than the binding, so it carries no attribution,
2057
+ // the same way `aliasing` builds a property name that stands for nothing.
2058
+ return {
2059
+ ...propertyAssignment,
2060
+ name: {...propertyAssignment.name, element: {...name, type: undefined, fieldType: undefined}},
2061
+ assigmentToken: JS.PropertyAssignment.Token.Colon,
2062
+ initializer: {...(name as J.Identifier), prefix: singleSpace, simpleName: this.boundName}
2063
+ } as JS.PropertyAssignment;
2064
+ }
2065
+ return super.visitPropertyAssignment(propertyAssignment, p);
2066
+ }
2067
+ }
2068
+
2069
+ /**
2070
+ * Rewrites the attribution a move invalidates, onto the module and member it moved to. Applied at
2071
+ * a reference to the moved binding. See CLAUDE.md: What a rebind's attribution follows.
2072
+ */
2073
+ class MovedTypes extends TypeVisitor<undefined> {
2074
+ private readonly onPath = new Set<Type>();
2075
+ private readonly answered = new Map<Type, Type | undefined>();
2076
+ private readonly renamed: ReadonlyMap<string, string>;
2077
+ private readonly fromMember?: string;
2078
+ private readonly toMember?: string;
2079
+ private readonly toModule: string;
2080
+
2081
+ constructor(from: {module: string; member?: string}, to: {module: string; member?: string}) {
2082
+ super();
2083
+ // Where no member is named the two keys coincide, and both name the module.
2084
+ this.renamed = new Map([[from.module, to.module], [qualifiedName(from), qualifiedName(to)]]);
2085
+ this.fromMember = declaredMember(from);
2086
+ this.toMember = declaredMember(to);
2087
+ this.toModule = to.module;
2088
+ }
2089
+
2090
+ /**
2091
+ * A type reached while it is still being visited is a cycle — a class holds a method whose
2092
+ * declaring type is that class — and answers with itself, which is what ends the walk. Every
2093
+ * reference to a binding shares one type, so a completed walk is remembered for the next.
2094
+ */
2095
+ override async visit<T extends Type>(type: T | undefined, p: undefined): Promise<T | undefined> {
2096
+ if (type === undefined || this.onPath.has(type)) {
2097
+ return type;
2098
+ }
2099
+ if (this.answered.has(type)) {
2100
+ return this.answered.get(type) as T | undefined;
2101
+ }
2102
+ this.onPath.add(type);
2103
+ try {
2104
+ const answer = await super.visit(type, p);
2105
+ // Only the type a walk entered at is remembered: one reached inside it may have met
2106
+ // a back edge, which answers with the type itself and so stands for the path it was on.
2107
+ if (this.onPath.size === 1) {
2108
+ this.answered.set(type, answer);
2109
+ }
2110
+ return answer;
2111
+ } finally {
2112
+ this.onPath.delete(type);
2113
+ }
2114
+ }
2115
+
2116
+ protected override async visitClass(aClass: Type.Class, p: undefined): Promise<Type | undefined> {
2117
+ const visited = await super.visitClass(aClass, p) as Type.Class;
2118
+ const moved = this.renamed.get(visited.fullyQualifiedName);
2119
+ return moved === undefined || moved === visited.fullyQualifiedName
2120
+ ? visited
2121
+ : {...visited, fullyQualifiedName: moved} as Type.Class;
2122
+ }
2123
+
2124
+ protected override async visitMethod(method: Type.Method, p: undefined): Promise<Type | undefined> {
2125
+ const visited = await super.visitMethod(method, p) as Type.Method;
2126
+ return this.declaresMovedMember(visited.name, visited.declaringType)
2127
+ ? {...visited, name: this.toMember} as Type.Method
2128
+ : visited;
2129
+ }
2130
+
2131
+ protected override async visitVariable(variable: Type.Variable, p: undefined): Promise<Type | undefined> {
2132
+ const visited = await super.visitVariable(variable, p) as Type.Variable;
2133
+ return this.declaresMovedMember(visited.name, visited.owner)
2134
+ ? {...visited, name: this.toMember} as Type.Variable
2135
+ : visited;
2136
+ }
2137
+
2138
+ /**
2139
+ * Whether `name`, declared on `owner`, is the member that moved. `owner` has already followed
2140
+ * the move by the time this runs, so it is the module moved *to* that it has to name. A
2141
+ * default or namespace binding declares no member, so there is no name for one to take.
2142
+ */
2143
+ private declaresMovedMember(name: string, owner: Type | undefined): boolean {
2144
+ return this.toMember !== undefined && name === this.fromMember && this.toMember !== this.fromMember &&
2145
+ owner !== undefined && Type.isFullyQualified(owner) &&
2146
+ Type.FullyQualified.getFullyQualifiedName(owner) === this.toModule;
2147
+ }
2148
+ }
2149
+
2150
+ /** A member's qualified name, or the module's own where no member is named. */
2151
+ function qualifiedName(binding: {module: string; member?: string}): string {
2152
+ const key = memberName(binding.member);
2153
+ return key === undefined || key === '*' ? binding.module : `${binding.module}.${key}`;
2154
+ }
2155
+
2156
+ /** The member the module declares, where the binding names one: a whole module declares none. */
2157
+ function declaredMember(binding: {module: string; member?: string}): string | undefined {
2158
+ const key = memberName(binding.member);
2159
+ return key === undefined || key === '*' ? undefined : key;
2160
+ }
2161
+
2162
+ /** Renames the identifier a default (`key` undefined) or namespace clause binds its module under. */
2163
+ function renameClauseBinding(
2164
+ importClause: Draft<JS.ImportClause> | undefined,
2165
+ key: string | undefined,
2166
+ boundName: string
2167
+ ): void {
2168
+ const bound = key === undefined
2169
+ ? importClause?.name?.element
2170
+ : (importClause?.namedBindings as Draft<JS.Alias> | undefined)?.alias;
2171
+ if (bound?.kind === J.Kind.Identifier) {
2172
+ (bound as Draft<J.Identifier>).simpleName = boundName;
2173
+ }
2174
+ }
2175
+
2176
+ /**
2177
+ * Rewrites the specifier importing `key` to import `member` under `boundName` — a bare `{member}`
2178
+ * where the two agree, since an alias saying the same thing is noise the source never had.
2179
+ */
2180
+ function rewriteNamedSpecifier(
2181
+ importClause: Draft<JS.ImportClause> | undefined,
2182
+ key: string,
2183
+ member: string,
2184
+ boundName: string
2185
+ ): void {
2186
+ if (importClause?.namedBindings?.kind !== JS.Kind.NamedImports) {
2187
+ return;
2188
+ }
2189
+ for (const elem of (importClause.namedBindings as Draft<JS.NamedImports>).elements.elements) {
2190
+ const specifier = elem.element;
2191
+ const node = specifier.specifier;
2192
+ if (!namedSpecifierImports(node as JS.ImportSpecifier["specifier"], key)) {
2193
+ continue;
2194
+ }
2195
+ const alias = node.kind === JS.Kind.Alias ? node as Draft<JS.Alias> : undefined;
2196
+ const local = (alias?.alias ?? node) as Draft<J.Identifier>;
2197
+ if (local.kind !== J.Kind.Identifier) {
2198
+ continue;
2199
+ }
2200
+ if (boundName === member) {
2201
+ // An alias node holds the specifier's leading whitespace, so the bare identifier
2202
+ // standing in for it takes that prefix.
2203
+ specifier.specifier = {...local, prefix: (alias ?? local).prefix, simpleName: member};
2204
+ } else if (alias) {
2205
+ (alias.propertyName.element as Draft<J.Identifier>).simpleName = member;
2206
+ local.simpleName = boundName;
2207
+ } else {
2208
+ specifier.specifier = aliasing({...local, simpleName: boundName}, member) as Draft<JS.Alias>;
2209
+ }
2210
+ }
2211
+ }