@openrewrite/rewrite 8.91.0 → 8.91.2

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 (153) hide show
  1. package/dist/java/formatting-utils.d.ts +8 -5
  2. package/dist/java/formatting-utils.d.ts.map +1 -1
  3. package/dist/java/formatting-utils.js +14 -10
  4. package/dist/java/formatting-utils.js.map +1 -1
  5. package/dist/java/type.d.ts +2 -0
  6. package/dist/java/type.d.ts.map +1 -1
  7. package/dist/java/type.js +5 -0
  8. package/dist/java/type.js.map +1 -1
  9. package/dist/javascript/add-import.d.ts +96 -43
  10. package/dist/javascript/add-import.d.ts.map +1 -1
  11. package/dist/javascript/add-import.js +668 -236
  12. package/dist/javascript/add-import.js.map +1 -1
  13. package/dist/javascript/amd.d.ts +138 -0
  14. package/dist/javascript/amd.d.ts.map +1 -0
  15. package/dist/javascript/amd.js +768 -0
  16. package/dist/javascript/amd.js.map +1 -0
  17. package/dist/javascript/assertions.d.ts +7 -0
  18. package/dist/javascript/assertions.d.ts.map +1 -1
  19. package/dist/javascript/assertions.js +31 -5
  20. package/dist/javascript/assertions.js.map +1 -1
  21. package/dist/javascript/binding.d.ts +94 -0
  22. package/dist/javascript/binding.d.ts.map +1 -0
  23. package/dist/javascript/binding.js +335 -0
  24. package/dist/javascript/binding.js.map +1 -0
  25. package/dist/javascript/comparator.d.ts +5 -0
  26. package/dist/javascript/comparator.d.ts.map +1 -1
  27. package/dist/javascript/comparator.js +34 -9
  28. package/dist/javascript/comparator.js.map +1 -1
  29. package/dist/javascript/format/format.d.ts.map +1 -1
  30. package/dist/javascript/format/format.js +8 -9
  31. package/dist/javascript/format/format.js.map +1 -1
  32. package/dist/javascript/format/tabs-and-indents-visitor.d.ts +2 -0
  33. package/dist/javascript/format/tabs-and-indents-visitor.d.ts.map +1 -1
  34. package/dist/javascript/format/tabs-and-indents-visitor.js +17 -11
  35. package/dist/javascript/format/tabs-and-indents-visitor.js.map +1 -1
  36. package/dist/javascript/index.d.ts +8 -1
  37. package/dist/javascript/index.d.ts.map +1 -1
  38. package/dist/javascript/index.js +26 -1
  39. package/dist/javascript/index.js.map +1 -1
  40. package/dist/javascript/node-resolution-result.d.ts +19 -1
  41. package/dist/javascript/node-resolution-result.d.ts.map +1 -1
  42. package/dist/javascript/node-resolution-result.js +37 -2
  43. package/dist/javascript/node-resolution-result.js.map +1 -1
  44. package/dist/javascript/package-exported-types.d.ts.map +1 -1
  45. package/dist/javascript/package-exported-types.js +56 -72
  46. package/dist/javascript/package-exported-types.js.map +1 -1
  47. package/dist/javascript/parser-utils.d.ts.map +1 -1
  48. package/dist/javascript/parser-utils.js +2 -0
  49. package/dist/javascript/parser-utils.js.map +1 -1
  50. package/dist/javascript/parser.d.ts +7 -1
  51. package/dist/javascript/parser.d.ts.map +1 -1
  52. package/dist/javascript/parser.js +17 -4
  53. package/dist/javascript/parser.js.map +1 -1
  54. package/dist/javascript/recipes/change-import.d.ts +2 -4
  55. package/dist/javascript/recipes/change-import.d.ts.map +1 -1
  56. package/dist/javascript/recipes/change-import.js +8 -244
  57. package/dist/javascript/recipes/change-import.js.map +1 -1
  58. package/dist/javascript/remove-import.d.ts +2 -28
  59. package/dist/javascript/remove-import.d.ts.map +1 -1
  60. package/dist/javascript/remove-import.js +19 -57
  61. package/dist/javascript/remove-import.js.map +1 -1
  62. package/dist/javascript/scope.d.ts +49 -0
  63. package/dist/javascript/scope.d.ts.map +1 -0
  64. package/dist/javascript/scope.js +324 -0
  65. package/dist/javascript/scope.js.map +1 -0
  66. package/dist/javascript/style.d.ts.map +1 -1
  67. package/dist/javascript/style.js +3 -10
  68. package/dist/javascript/style.js.map +1 -1
  69. package/dist/javascript/templating/bindings.d.ts +8 -0
  70. package/dist/javascript/templating/bindings.d.ts.map +1 -0
  71. package/dist/javascript/templating/bindings.js +92 -0
  72. package/dist/javascript/templating/bindings.js.map +1 -0
  73. package/dist/javascript/templating/comparator.d.ts +10 -0
  74. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  75. package/dist/javascript/templating/comparator.js +48 -20
  76. package/dist/javascript/templating/comparator.js.map +1 -1
  77. package/dist/javascript/templating/engine.d.ts +34 -10
  78. package/dist/javascript/templating/engine.d.ts.map +1 -1
  79. package/dist/javascript/templating/engine.js +109 -55
  80. package/dist/javascript/templating/engine.js.map +1 -1
  81. package/dist/javascript/templating/index.d.ts +1 -1
  82. package/dist/javascript/templating/index.d.ts.map +1 -1
  83. package/dist/javascript/templating/index.js.map +1 -1
  84. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  85. package/dist/javascript/templating/pattern.js +13 -10
  86. package/dist/javascript/templating/pattern.js.map +1 -1
  87. package/dist/javascript/templating/placeholder-replacement.d.ts +4 -0
  88. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  89. package/dist/javascript/templating/placeholder-replacement.js +24 -10
  90. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  91. package/dist/javascript/templating/precedence.d.ts +12 -4
  92. package/dist/javascript/templating/precedence.d.ts.map +1 -1
  93. package/dist/javascript/templating/precedence.js +18 -11
  94. package/dist/javascript/templating/precedence.js.map +1 -1
  95. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  96. package/dist/javascript/templating/rewrite.js +19 -17
  97. package/dist/javascript/templating/rewrite.js.map +1 -1
  98. package/dist/javascript/templating/template.d.ts +14 -0
  99. package/dist/javascript/templating/template.d.ts.map +1 -1
  100. package/dist/javascript/templating/template.js +63 -3
  101. package/dist/javascript/templating/template.js.map +1 -1
  102. package/dist/javascript/templating/types.d.ts +39 -1
  103. package/dist/javascript/templating/types.d.ts.map +1 -1
  104. package/dist/javascript/templating/utils.d.ts +6 -1
  105. package/dist/javascript/templating/utils.d.ts.map +1 -1
  106. package/dist/javascript/templating/utils.js +20 -2
  107. package/dist/javascript/templating/utils.js.map +1 -1
  108. package/dist/javascript/type-mapping.d.ts +1 -0
  109. package/dist/javascript/type-mapping.d.ts.map +1 -1
  110. package/dist/javascript/type-mapping.js +52 -2
  111. package/dist/javascript/type-mapping.js.map +1 -1
  112. package/dist/rewrite-javascript-version.txt +1 -1
  113. package/dist/style.d.ts +2 -0
  114. package/dist/style.d.ts.map +1 -1
  115. package/dist/style.js +5 -0
  116. package/dist/style.js.map +1 -1
  117. package/dist/test/rewrite-test.d.ts +12 -0
  118. package/dist/test/rewrite-test.d.ts.map +1 -1
  119. package/dist/test/rewrite-test.js +40 -3
  120. package/dist/test/rewrite-test.js.map +1 -1
  121. package/package.json +2 -2
  122. package/src/java/formatting-utils.ts +14 -10
  123. package/src/java/type.ts +5 -0
  124. package/src/javascript/add-import.ts +751 -247
  125. package/src/javascript/amd.ts +915 -0
  126. package/src/javascript/assertions.ts +33 -5
  127. package/src/javascript/binding.ts +449 -0
  128. package/src/javascript/comparator.ts +39 -10
  129. package/src/javascript/format/format.ts +9 -10
  130. package/src/javascript/format/tabs-and-indents-visitor.ts +18 -12
  131. package/src/javascript/index.ts +12 -1
  132. package/src/javascript/node-resolution-result.ts +59 -4
  133. package/src/javascript/package-exported-types.ts +60 -74
  134. package/src/javascript/parser-utils.ts +2 -0
  135. package/src/javascript/parser.ts +23 -3
  136. package/src/javascript/recipes/change-import.ts +10 -263
  137. package/src/javascript/remove-import.ts +20 -56
  138. package/src/javascript/scope.ts +338 -0
  139. package/src/javascript/style.ts +3 -11
  140. package/src/javascript/templating/bindings.ts +99 -0
  141. package/src/javascript/templating/comparator.ts +57 -20
  142. package/src/javascript/templating/engine.ts +148 -68
  143. package/src/javascript/templating/index.ts +1 -0
  144. package/src/javascript/templating/pattern.ts +19 -16
  145. package/src/javascript/templating/placeholder-replacement.ts +32 -11
  146. package/src/javascript/templating/precedence.ts +21 -13
  147. package/src/javascript/templating/rewrite.ts +23 -17
  148. package/src/javascript/templating/template.ts +84 -9
  149. package/src/javascript/templating/types.ts +46 -1
  150. package/src/javascript/templating/utils.ts +21 -2
  151. package/src/javascript/type-mapping.ts +54 -2
  152. package/src/style.ts +5 -0
  153. package/src/test/rewrite-test.ts +61 -3
@@ -1,9 +1,13 @@
1
1
  import {JavaScriptVisitor} from "./visitor";
2
- import {emptySpace, J, rightPadded, singleSpace, space, Statement, Type} from "../java";
3
- import {JS} from "./tree";
2
+ import {ElementRemovalFormatter, emptySpace, isIdentifier, J, rightPadded, singleSpace, space, Statement, Type} from "../java";
3
+ import {JS, JSX} from "./tree";
4
4
  import {randomId} from "../uuid";
5
5
  import {emptyMarkers, markers} from "../markers";
6
- import {getStyle, SpacesStyle, StyleKind} from "./style";
6
+ import {getStyle, PrettierStyle, SpacesStyle, StyleKind} from "./style";
7
+ import {bindingNames, compilationUnitOf, cursorOf, declarationsOf, deconflict, namesDeclaredIn} from "./scope";
8
+ import {create as produce, Draft} from "mutative";
9
+
10
+ export type QuoteChar = "'" | '"';
7
11
 
8
12
  export enum ImportStyle {
9
13
  ES6Named, // import { x } from 'module'
@@ -13,8 +17,10 @@ export enum ImportStyle {
13
17
  }
14
18
 
15
19
  export interface AddImportOptions {
16
- /** The module name (e.g., 'fs', 'react') to import from */
17
- module: string;
20
+ /** The module name (e.g., 'fs', 'react') to import from.
21
+ * Pass a `J.Literal` to reuse its source form verbatim, which carries the quoting,
22
+ * escapes and unicode form of a specifier being moved from elsewhere in the source. */
23
+ module: string | J.Literal;
18
24
 
19
25
  /** Optionally, the specific member to import from the module.
20
26
  * If not specified, adds a default import or namespace import.
@@ -28,9 +34,16 @@ export interface AddImportOptions {
28
34
 
29
35
  /** Optional alias for the imported member.
30
36
  * Required when member is 'default' or '*'.
37
+ * Taken verbatim, never deconflicted: a caller that names an alias may already have
38
+ * emitted code using it.
31
39
  * Cannot be combined with `sideEffectOnly`. */
32
40
  alias?: string;
33
41
 
42
+ /** Preferred local name, deconflicted if the file already binds it. Stands in for `alias` where
43
+ * `member` is 'default' or '*' and the caller wants a name it does not insist on.
44
+ * Cannot be combined with `sideEffectOnly`. */
45
+ preferredName?: string;
46
+
34
47
  /** If true, only add the import if the member is actually used in the file. Default: true
35
48
  * Cannot be combined with `sideEffectOnly`. */
36
49
  onlyIfReferenced?: boolean;
@@ -46,95 +59,448 @@ export interface AddImportOptions {
46
59
 
47
60
  /** Optional import style to use. If not specified, auto-detects from file and existing imports */
48
61
  style?: ImportStyle;
62
+
63
+ /** Quote character for the module specifier. If not specified, detected from the file.
64
+ * A `J.Literal` module carries its own quoting and takes precedence over this. */
65
+ quoteStyle?: QuoteChar;
49
66
  }
50
67
 
51
68
  /**
52
- * Register an AddImport visitor to add an import statement to a JavaScript/TypeScript file
53
- * @param visitor The visitor to add the import addition to
54
- * @param options Configuration options for the import to add
55
- *
56
- * @example
57
- * // Add a named import
58
- * maybeAddImport(visitor, { module: 'fs', member: 'readFile' });
59
- *
60
- * @example
61
- * // Add a default import using the 'default' member specifier
62
- * maybeAddImport(visitor, { module: 'react', member: 'default', alias: 'React' });
63
- *
64
- * @example
65
- * // Add a default import (legacy way, without specifying member)
66
- * maybeAddImport(visitor, { module: 'react', alias: 'React' });
67
- *
68
- * @example
69
- * // Add a namespace import
70
- * maybeAddImport(visitor, { module: 'crypto', member: '*', alias: 'crypto' });
71
- *
72
- * @example
73
- * // Add a side-effect import
74
- * maybeAddImport(visitor, { module: 'core-js/stable', sideEffectOnly: true });
69
+ * Ensures an import for `options.module` exists, queuing an `AddImport` edit where none already
70
+ * serves the request. `maybeBind`'s ESM/CommonJS lane is this function; its own JSDoc carries the
71
+ * return-value contract. `refuseCreate` answers `undefined` instead of queuing a new import,
72
+ * without affecting whether an existing binding answers the request first.
75
73
  */
76
- export function maybeAddImport(
74
+ export function bindImport(
77
75
  visitor: JavaScriptVisitor<any>,
78
- options: AddImportOptions
79
- ) {
76
+ options: AddImportOptions,
77
+ refuseCreate?: boolean
78
+ ): string | undefined {
79
+ validate(options);
80
+ const module = moduleNameOf(options.module);
81
+ const sideEffectOnly = options.sideEffectOnly ?? false;
82
+ const typeOnly = options.typeOnly ?? false;
83
+
84
+ // A queued import binds the module as much as one already in the file, so it answers a later
85
+ // request the same way; a name a merged request never emits would be referenced but not bound.
80
86
  for (const v of visitor.afterVisit || []) {
81
- if (v instanceof AddImport &&
82
- v.module === options.module &&
83
- v.member === options.member &&
84
- v.alias === options.alias &&
85
- v.sideEffectOnly === (options.sideEffectOnly ?? false) &&
86
- v.typeOnly === (options.typeOnly ?? false)) {
87
- return;
87
+ if (!(v instanceof AddImport) || v.module !== module ||
88
+ v.sideEffectOnly !== sideEffectOnly || v.typeOnly !== typeOnly) {
89
+ continue;
90
+ }
91
+ // How a specifier prints, or what name would be nice, does not say which binding is wanted.
92
+ // A pinned alias does: it asks for a binding of its own, which only the same request answers.
93
+ const answers = options.alias
94
+ ? v.alias === options.alias && v.member === options.member
95
+ : memberName(v.member) === memberName(options.member);
96
+ if (answers) {
97
+ return v.bindingName;
98
+ }
99
+ }
100
+
101
+ if (sideEffectOnly) {
102
+ if (refuseCreate) {
103
+ return undefined;
104
+ }
105
+ visitor.afterVisit.push(new AddImport(options));
106
+ return undefined;
107
+ }
108
+
109
+ // A pinned alias is the whole answer, so the file has no say and nothing below needs to read it.
110
+ if (options.alias) {
111
+ if (refuseCreate) {
112
+ return undefined;
113
+ }
114
+ visitor.afterVisit.push(new AddImport(options, options.alias));
115
+ return options.alias;
116
+ }
117
+
118
+ const derived = derivedName(options);
119
+ const cursor = cursorOf(visitor);
120
+ const cu = cursor && compilationUnitOf(cursor);
121
+ if (!cu) {
122
+ if (refuseCreate) {
123
+ return undefined;
124
+ }
125
+ visitor.afterVisit.push(new AddImport(options, derived));
126
+ return derived;
127
+ }
128
+
129
+ // An import already serving this request answers it; queuing one would, on the next cycle,
130
+ // derive a suffixed name from the binding this call just added. A caller that named a preference
131
+ // takes whatever comes back; one that did not assumes the name it derived, so a binding under
132
+ // any other name would leave the references it emits unbound.
133
+ for (const binding of moduleScopeBindings(cu)) {
134
+ if (binding.module === module && binding.member === memberName(options.member) &&
135
+ binding.typeOnly === typeOnly &&
136
+ (options.preferredName !== undefined || anyNameAnswers(options) ||
137
+ binding.name === derived)) {
138
+ return binding.name;
139
+ }
140
+ }
141
+
142
+ if (refuseCreate) {
143
+ return undefined;
144
+ }
145
+
146
+ // Only the module scope answers for a name, but any scope in the file occupies one. The queue
147
+ // gives every later request for this module the name chosen here, so it has to clear the scopes
148
+ // those references will sit in, which are not known yet.
149
+ const taken = takenNames(namesDeclaredIn(cu), visitor);
150
+ const name = deconflict(derived, candidate => taken.has(candidate));
151
+ visitor.afterVisit.push(new AddImport(options, name));
152
+ return name;
153
+ }
154
+
155
+ /** Rejects a request on its own terms, whatever the queue already holds. */
156
+ function validate(options: AddImportOptions): void {
157
+ // Validate that a name is provided when member is 'default'
158
+ if (options.member === 'default' && !options.alias && !options.preferredName) {
159
+ throw new Error("When member is 'default', the alias parameter is required");
160
+ }
161
+
162
+ // Validate that a name is provided when member is '*' (namespace import)
163
+ if (options.member === '*' && !options.alias && !options.preferredName) {
164
+ throw new Error("When member is '*', the alias parameter is required");
165
+ }
166
+
167
+ // Validate that sideEffectOnly is not combined with incompatible options
168
+ if (options.sideEffectOnly) {
169
+ if (options.member !== undefined) {
170
+ throw new Error("Cannot combine sideEffectOnly with member");
171
+ }
172
+ if (options.alias !== undefined) {
173
+ throw new Error("Cannot combine sideEffectOnly with alias");
88
174
  }
175
+ if (options.preferredName !== undefined) {
176
+ throw new Error("Cannot combine sideEffectOnly with preferredName");
177
+ }
178
+ if (options.onlyIfReferenced !== undefined) {
179
+ throw new Error("Cannot combine sideEffectOnly with onlyIfReferenced");
180
+ }
181
+ if (options.typeOnly) {
182
+ throw new Error("Cannot combine sideEffectOnly with typeOnly");
183
+ }
184
+ }
185
+ }
186
+
187
+ /**
188
+ * A request that named no local name and asks for no member of the module. Having expressed no
189
+ * preference it has none to disappoint, so whatever the file already calls that module answers it.
190
+ */
191
+ function anyNameAnswers(options: AddImportOptions): boolean {
192
+ return !options.sideEffectOnly && memberName(options.member) === undefined &&
193
+ options.alias === undefined && options.preferredName === undefined;
194
+ }
195
+
196
+ /** The local name a request asks for, before the file has a say in it. */
197
+ function derivedName(options: AddImportOptions): string {
198
+ return options.alias ?? options.preferredName ?? memberName(options.member) ?? moduleNameOf(options.module);
199
+ }
200
+
201
+ /** `'default'` and an absent member both name a default import, which binds no member name of its own. */
202
+ export function memberName(member: string | undefined): string | undefined {
203
+ return member === 'default' ? undefined : member;
204
+ }
205
+
206
+ /** A name introduced into the file's module scope, and where it came from when that is an import. */
207
+ export interface ModuleScopeBinding {
208
+ name: string;
209
+ module?: string;
210
+ /** Carries the {@link AddImportOptions.member} spelling, so `undefined` means a default import. */
211
+ member?: string;
212
+ typeOnly?: boolean;
213
+ }
214
+
215
+ /** What the file's imports and `require`s bind at module scope, and the module each name comes from. */
216
+ export function moduleScopeBindings(cu: JS.CompilationUnit): ModuleScopeBinding[] {
217
+ const bindings: ModuleScopeBinding[] = [];
218
+
219
+ const declaredByVariables = (varDecl: J.VariableDeclarations): void => {
220
+ for (const variable of varDecl.variables) {
221
+ const required = requiredModule(variable.element?.initializer?.element);
222
+ if (required !== undefined) {
223
+ bindings.push(...requireBindings(variable.element?.name, required));
224
+ }
225
+ }
226
+ };
227
+
228
+ for (const stmt of cu.statements) {
229
+ const statement = stmt.element;
230
+ if (statement?.kind === JS.Kind.Import) {
231
+ bindings.push(...importBindings(statement as JS.Import));
232
+ continue;
233
+ }
234
+ for (const declaration of declarationsOf(statement)) {
235
+ declaredByVariables(declaration);
236
+ }
237
+ }
238
+
239
+ return bindings;
240
+ }
241
+
242
+ /** The module a `require('...')` initializer names, `undefined` for an initializer that is anything else. */
243
+ function requiredModule(initializer: J | undefined): string | undefined {
244
+ return initializer?.kind === J.Kind.MethodInvocation
245
+ ? requiredModuleOf(initializer as J.MethodInvocation)
246
+ : undefined;
247
+ }
248
+
249
+ /** A `require(...)` call, whatever it is passed. `obj.require('x')` selects a method rather than loading a module. */
250
+ function isRequireCall(methodInv: J.MethodInvocation): boolean {
251
+ return !methodInv.select && methodInv.name?.kind === J.Kind.Identifier &&
252
+ (methodInv.name as J.Identifier).simpleName === 'require';
253
+ }
254
+
255
+ /**
256
+ * The module a `require(...)` call loads. `obj.require('x')` selects a method rather than loading a
257
+ * module, and a specifier that is not a string literal names none that can be read here.
258
+ */
259
+ export function requiredModuleOf(methodInv: J.MethodInvocation): string | undefined {
260
+ if (!isRequireCall(methodInv)) {
261
+ return undefined;
262
+ }
263
+ const argument = methodInv.arguments?.elements[0]?.element;
264
+ return argument?.kind === J.Kind.Literal && typeof (argument as J.Literal).value === 'string'
265
+ ? moduleNameOf(argument as J.Literal)
266
+ : undefined;
267
+ }
268
+
269
+ /** A `require` binds a module the way an import does, so the pool records it the same way. */
270
+ function requireBindings(pattern: J | undefined, module: string): ModuleScopeBinding[] {
271
+ // A whole-module require binds no member, exactly as a default import does.
272
+ if (pattern?.kind === J.Kind.Identifier) {
273
+ return [{name: (pattern as J.Identifier).simpleName, module, member: undefined, typeOnly: false}];
89
274
  }
90
- visitor.afterVisit.push(new AddImport(options));
275
+ return bindingNames(pattern).map(bound => bound.member === undefined
276
+ ? {name: bound.name}
277
+ : {name: bound.name, module, member: bound.member, typeOnly: false});
278
+ }
279
+
280
+ /** The member a specifier imports and the name it binds it under, which are the same absent an alias. */
281
+ function specifierBinding(specifier: JS.ImportSpecifier): { name: string; member: string } | undefined {
282
+ if (specifier.specifier?.kind === J.Kind.Identifier) {
283
+ const name = (specifier.specifier as J.Identifier).simpleName;
284
+ return {name, member: name};
285
+ }
286
+ if (specifier.specifier?.kind !== JS.Kind.Alias) {
287
+ return undefined;
288
+ }
289
+ const alias = specifier.specifier as JS.Alias;
290
+ return alias.propertyName?.element?.kind === J.Kind.Identifier && alias.alias?.kind === J.Kind.Identifier
291
+ ? {name: (alias.alias as J.Identifier).simpleName, member: (alias.propertyName.element as J.Identifier).simpleName}
292
+ : undefined;
293
+ }
294
+
295
+ function importBindings(jsImport: JS.Import): ModuleScopeBinding[] {
296
+ const moduleSpecifier = jsImport.moduleSpecifier?.element;
297
+ const module = moduleSpecifier?.kind === J.Kind.Literal
298
+ ? moduleNameOf(moduleSpecifier as J.Literal)
299
+ : undefined;
300
+ const importClause = jsImport.importClause;
301
+ if (!importClause) {
302
+ return [];
303
+ }
304
+
305
+ const typeOnly = importClause.typeOnly;
306
+ const bindings: ModuleScopeBinding[] = [];
307
+
308
+ if (importClause.name?.element?.kind === J.Kind.Identifier) {
309
+ bindings.push({name: (importClause.name.element as J.Identifier).simpleName, module, member: undefined, typeOnly});
310
+ }
311
+
312
+ const namedBindings = importClause.namedBindings;
313
+ if (namedBindings?.kind === J.Kind.Identifier) {
314
+ bindings.push({name: (namedBindings as J.Identifier).simpleName, module, member: '*', typeOnly});
315
+ } else if (namedBindings?.kind === JS.Kind.Alias) {
316
+ const alias = (namedBindings as JS.Alias).alias;
317
+ if (alias?.kind === J.Kind.Identifier) {
318
+ bindings.push({name: (alias as J.Identifier).simpleName, module, member: '*', typeOnly});
319
+ }
320
+ } else if (namedBindings?.kind === JS.Kind.NamedImports) {
321
+ for (const elem of (namedBindings as JS.NamedImports).elements.elements) {
322
+ const bound = elem.element?.kind === JS.Kind.ImportSpecifier
323
+ ? specifierBinding(elem.element as JS.ImportSpecifier)
324
+ : undefined;
325
+ if (bound) {
326
+ bindings.push({...bound, module, typeOnly});
327
+ }
328
+ }
329
+ }
330
+
331
+ return bindings;
332
+ }
333
+
334
+ /**
335
+ * Names in scope, plus those pending `AddImport`s on the `afterVisit` queue have claimed. A queued
336
+ * `RemoveImport` does not free one: it removes only what the file leaves unused, and binding a name
337
+ * it keeps is an error, where an unnecessary suffix merely reads oddly.
338
+ */
339
+ function takenNames(inScope: ReadonlySet<string>, visitor: JavaScriptVisitor<any>): Set<string> {
340
+ const taken = new Set<string>(inScope);
341
+
342
+ for (const v of visitor.afterVisit || []) {
343
+ if (v instanceof AddImport && v.bindingName) {
344
+ taken.add(v.bindingName);
345
+ }
346
+ }
347
+
348
+ return taken;
349
+ }
350
+
351
+ /**
352
+ * The parser lifts surrogate pairs out of `valueSource` into `unicodeEscapes` and, when it does,
353
+ * sets `value` to the quoted source (`parser.ts` `mapLiteral`), so neither field alone is the
354
+ * module name. Reuniting them and stripping the quotes yields it for either shape of literal.
355
+ */
356
+ export function moduleNameOf(module: string | J.Literal): string {
357
+ if (typeof module === 'string') {
358
+ return module;
359
+ }
360
+ const source = module.valueSource;
361
+ if (source === undefined) {
362
+ return String(module.value);
363
+ }
364
+ let restored = '';
365
+ let cut = 0;
366
+ for (const escape of module.unicodeEscapes ?? []) {
367
+ restored += source.slice(cut, escape.valueSourceIndex) + String.fromCharCode(parseInt(escape.codePoint, 16));
368
+ cut = escape.valueSourceIndex;
369
+ }
370
+ restored += source.slice(cut);
371
+ const quote = restored.charAt(0);
372
+ return (quote === "'" || quote === '"') && restored.endsWith(quote) && restored.length > 1
373
+ ? restored.slice(1, -1)
374
+ : restored;
375
+ }
376
+
377
+ function quoteOf(expression: J | undefined): QuoteChar | undefined {
378
+ if (expression?.kind !== J.Kind.Literal) {
379
+ return undefined;
380
+ }
381
+ const source = (expression as J.Literal).valueSource;
382
+ const quote = source?.charAt(0);
383
+ // JsxText carries raw text as its `valueSource`, so a leading quote alone does not make a
384
+ // literal quote-delimited; requiring a matching pair keeps prose out of the tally.
385
+ return (quote === "'" || quote === '"') && source!.length > 1 && source!.endsWith(quote)
386
+ ? quote
387
+ : undefined;
388
+ }
389
+
390
+ /**
391
+ * Pick the quote character for a module specifier being added to `cu`, so that it agrees
392
+ * with the file it lands in. An existing specifier is the most direct precedent; a Prettier
393
+ * config states intent even where the file itself has not been formatted to match.
394
+ */
395
+ async function detectQuote(cu: JS.CompilationUnit): Promise<QuoteChar> {
396
+ // Static imports are top-level, so the highest-ranked signal needs no traversal to find.
397
+ for (const statement of cu.statements) {
398
+ const element = statement.element;
399
+ const topLevelQuote = element?.kind === JS.Kind.Import
400
+ ? quoteOf((element as JS.Import).moduleSpecifier?.element)
401
+ : element?.kind === JS.Kind.ExportDeclaration
402
+ ? quoteOf((element as JS.ExportDeclaration).moduleSpecifier?.element)
403
+ : undefined;
404
+ if (topLevelQuote) {
405
+ return topLevelQuote;
406
+ }
407
+ }
408
+
409
+ let importQuote: QuoteChar | undefined;
410
+ let requireQuote: QuoteChar | undefined;
411
+ let single = 0;
412
+ let double = 0;
413
+
414
+ await new class extends JavaScriptVisitor<void> {
415
+ override async visitImportDeclaration(jsImport: JS.Import, p: void): Promise<J | undefined> {
416
+ importQuote ??= quoteOf(jsImport.moduleSpecifier?.element);
417
+ return super.visitImportDeclaration(jsImport, p);
418
+ }
419
+
420
+ override async visitExportDeclaration(exportDeclaration: JS.ExportDeclaration, p: void): Promise<J | undefined> {
421
+ importQuote ??= quoteOf(exportDeclaration.moduleSpecifier?.element);
422
+ return super.visitExportDeclaration(exportDeclaration, p);
423
+ }
424
+
425
+ override async visitMethodInvocation(method: J.MethodInvocation, p: void): Promise<J | undefined> {
426
+ if (!method.select && method.name?.kind === J.Kind.Identifier &&
427
+ (method.name as J.Identifier).simpleName === 'require') {
428
+ requireQuote ??= quoteOf(method.arguments?.elements[0]?.element);
429
+ }
430
+ return super.visitMethodInvocation(method, p);
431
+ }
432
+
433
+ override async visitJsxAttribute(attribute: JSX.Attribute, p: void): Promise<J | undefined> {
434
+ // JSX attribute quoting answers to Prettier's `jsxSingleQuote`, so it is no evidence
435
+ // about the quote style of ordinary strings.
436
+ return attribute;
437
+ }
438
+
439
+ override async visitLiteral(literal: J.Literal, p: void): Promise<J | undefined> {
440
+ const quote = quoteOf(literal);
441
+ if (quote === "'") {
442
+ single++;
443
+ } else if (quote === '"') {
444
+ double++;
445
+ }
446
+ return super.visitLiteral(literal, p);
447
+ }
448
+ }().visit(cu, undefined);
449
+
450
+ // Imports outrank requires; that ordering is what makes the import-only scan above sound.
451
+ const specifierQuote = importQuote ?? requireQuote;
452
+ if (specifierQuote) {
453
+ return specifierQuote;
454
+ }
455
+
456
+ const prettier = getStyle(StyleKind.PrettierStyle, cu) as PrettierStyle | undefined;
457
+ if (prettier?.kind === StyleKind.PrettierStyle && !prettier.ignored) {
458
+ const singleQuote = prettier.config.singleQuote;
459
+ if (typeof singleQuote === 'boolean') {
460
+ return singleQuote ? "'" : '"';
461
+ }
462
+ }
463
+
464
+ return double > single ? '"' : "'";
91
465
  }
92
466
 
93
467
  export class AddImport<P> extends JavaScriptVisitor<P> {
94
468
  readonly module: string;
469
+ /** Set when the caller supplied the module specifier as a literal; printed verbatim. */
470
+ readonly moduleValueSource?: string;
471
+ readonly moduleUnicodeEscapes?: J.LiteralUnicodeEscape[];
95
472
  readonly member?: string;
96
473
  readonly alias?: string;
97
474
  readonly onlyIfReferenced: boolean;
98
475
  readonly sideEffectOnly: boolean;
99
476
  readonly typeOnly: boolean;
100
477
  readonly style?: ImportStyle;
478
+ readonly quoteStyle?: QuoteChar;
479
+ /** The local name this import binds; `undefined` for a side-effect import. */
480
+ readonly bindingName?: string;
481
+ /**
482
+ * A default import the request named no local name for, so whatever the file already calls
483
+ * that module answers it.
484
+ */
485
+ readonly anyNameAnswers: boolean;
101
486
 
102
- constructor(options: AddImportOptions) {
487
+ constructor(options: AddImportOptions, bindingName?: string) {
103
488
  super();
104
489
 
105
- // Validate that alias is provided when member is 'default'
106
- if (options.member === 'default' && !options.alias) {
107
- throw new Error("When member is 'default', the alias parameter is required");
108
- }
109
-
110
- // Validate that alias is provided when member is '*' (namespace import)
111
- if (options.member === '*' && !options.alias) {
112
- throw new Error("When member is '*', the alias parameter is required");
113
- }
490
+ validate(options);
114
491
 
115
- // Validate that sideEffectOnly is not combined with incompatible options
116
- if (options.sideEffectOnly) {
117
- if (options.member !== undefined) {
118
- throw new Error("Cannot combine sideEffectOnly with member");
119
- }
120
- if (options.alias !== undefined) {
121
- throw new Error("Cannot combine sideEffectOnly with alias");
122
- }
123
- if (options.onlyIfReferenced !== undefined) {
124
- throw new Error("Cannot combine sideEffectOnly with onlyIfReferenced");
125
- }
126
- if (options.typeOnly) {
127
- throw new Error("Cannot combine sideEffectOnly with typeOnly");
128
- }
129
- }
130
-
131
- this.module = options.module;
492
+ this.module = moduleNameOf(options.module);
493
+ this.moduleValueSource = typeof options.module === 'string' ? undefined : options.module.valueSource;
494
+ this.moduleUnicodeEscapes = typeof options.module === 'string' ? undefined : options.module.unicodeEscapes;
132
495
  this.member = options.member;
133
496
  this.alias = options.alias;
134
497
  this.onlyIfReferenced = options.onlyIfReferenced ?? true;
135
498
  this.sideEffectOnly = options.sideEffectOnly ?? false;
136
499
  this.typeOnly = options.typeOnly ?? false;
137
500
  this.style = options.style;
501
+ this.quoteStyle = options.quoteStyle;
502
+ this.bindingName = this.sideEffectOnly ? undefined : bindingName ?? derivedName(options);
503
+ this.anyNameAnswers = anyNameAnswers(options);
138
504
  }
139
505
 
140
506
  /**
@@ -144,16 +510,9 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
144
510
  if (moduleSpecifier.kind !== J.Kind.Literal) {
145
511
  return undefined;
146
512
  }
147
- return (moduleSpecifier as J.Literal).value?.toString();
513
+ return moduleNameOf(moduleSpecifier as J.Literal);
148
514
  }
149
515
 
150
- /**
151
- * Check if a method invocation is a require() call
152
- */
153
- private isRequireCall(methodInv: J.MethodInvocation): boolean {
154
- return methodInv.name?.kind === J.Kind.Identifier &&
155
- (methodInv.name as J.Identifier).simpleName === 'require';
156
- }
157
516
 
158
517
  /**
159
518
  * Determine the appropriate import style based on file type and existing imports
@@ -233,7 +592,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
233
592
  const namedVar = varDecl.variables[0].element;
234
593
  const initializer = namedVar?.initializer?.element;
235
594
  if (initializer?.kind === J.Kind.MethodInvocation &&
236
- this.isRequireCall(initializer as J.MethodInvocation)) {
595
+ isRequireCall(initializer as J.MethodInvocation)) {
237
596
  hasCommonJSRequires = true;
238
597
  }
239
598
  }
@@ -310,12 +669,8 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
310
669
  const namedVar = varDecl.variables[0].element;
311
670
  const initializer = namedVar?.initializer?.element;
312
671
 
313
- if (initializer?.kind === J.Kind.MethodInvocation &&
314
- this.isRequireCall(initializer as J.MethodInvocation)) {
315
- const moduleName = this.getModuleNameFromRequire(initializer as J.MethodInvocation);
316
- if (moduleName === this.module) {
317
- return ImportStyle.CommonJS;
318
- }
672
+ if (requiredModule(initializer) === this.module) {
673
+ return ImportStyle.CommonJS;
319
674
  }
320
675
  }
321
676
  }
@@ -473,10 +828,10 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
473
828
  const existingElements = namedImports.elements.elements;
474
829
 
475
830
  // Find the correct insertion position (alphabetical, case-insensitive)
476
- const newName = (this.alias || this.member!).toLowerCase();
831
+ const newName = this.bindingName!.toLowerCase();
477
832
  let insertIndex = existingElements.findIndex(elem => {
478
833
  if (elem.element?.kind === JS.Kind.ImportSpecifier) {
479
- const name = this.getImportAlias(elem.element) || this.getImportName(elem.element);
834
+ const name = specifierBinding(elem.element as JS.ImportSpecifier)?.name ?? '';
480
835
  return newName.localeCompare(name.toLowerCase()) < 0;
481
836
  }
482
837
  return false;
@@ -634,91 +989,27 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
634
989
  * Check if the import matches what we're trying to add
635
990
  */
636
991
  private isMatchingImport(jsImport: JS.Import): boolean {
637
- // Check module specifier
638
992
  const moduleSpecifier = jsImport.moduleSpecifier?.element;
639
- if (!moduleSpecifier) {
640
- return false;
641
- }
642
-
643
- const moduleName = this.getModuleName(moduleSpecifier);
644
- if (moduleName !== this.module) {
993
+ if (!moduleSpecifier || this.getModuleName(moduleSpecifier) !== this.module) {
645
994
  return false;
646
995
  }
647
996
 
997
+ // A side-effect import is the clause-less one, and no import carrying bindings stands in
998
+ // for it — nor it for them.
648
999
  const importClause = jsImport.importClause;
649
-
650
- // Handle side-effect imports (no import clause)
651
- if (!importClause) {
652
- // If we're trying to add a side-effect import and one already exists, it's a match
653
- return this.sideEffectOnly;
654
- }
655
-
656
- // If we're adding a side-effect import but there's an existing import with bindings,
657
- // it's not a match (side-effect import should be separate)
658
- if (this.sideEffectOnly) {
659
- return false;
1000
+ if (!importClause || this.sideEffectOnly) {
1001
+ return !importClause && this.sideEffectOnly;
660
1002
  }
661
1003
 
662
- // Check if the typeOnly flag matches - type-only and value imports are separate
663
- if (importClause.typeOnly !== this.typeOnly) {
664
- return false;
665
- }
666
-
667
- // Check if the specific member or default import already exists
668
- if (this.member === '*') {
669
- // We're adding a namespace import, check if one exists
670
- const namedBindings = importClause.namedBindings;
671
- if (!namedBindings) {
672
- return false;
673
- }
674
-
675
- // Namespace imports can be represented as J.Identifier or JS.Alias
676
- if (namedBindings.kind === J.Kind.Identifier) {
677
- const identifier = namedBindings as J.Identifier;
678
- return identifier.simpleName === this.alias;
679
- } else if (namedBindings.kind === JS.Kind.Alias) {
680
- const alias = namedBindings as JS.Alias;
681
- if (alias.alias?.kind === J.Kind.Identifier) {
682
- return (alias.alias as J.Identifier).simpleName === this.alias;
683
- }
684
- }
685
- return false;
686
- } else if (this.member === undefined || this.member === 'default') {
687
- // We're adding a default import, check if one exists
688
- // For member === 'default', also verify the alias matches if specified
689
- if (importClause.name === undefined) {
690
- return false;
691
- }
692
- // If we have an alias, check that it matches
693
- if (this.alias && importClause.name.element?.kind === J.Kind.Identifier) {
694
- const existingName = (importClause.name.element as J.Identifier).simpleName;
695
- return existingName === this.alias;
696
- }
697
- return true;
698
- } else {
699
- // We're adding a named import, check if it exists
700
- const namedBindings = importClause.namedBindings;
701
- if (!namedBindings) {
702
- return false;
703
- }
704
-
705
- if (namedBindings.kind === JS.Kind.NamedImports) {
706
- const namedImports = namedBindings as JS.NamedImports;
707
- for (const elem of namedImports.elements.elements) {
708
- if (elem.element?.kind === JS.Kind.ImportSpecifier) {
709
- const specifier = elem.element as JS.ImportSpecifier;
710
- const importName = this.getImportName(specifier);
711
- const aliasName = this.getImportAlias(specifier);
712
-
713
- if (importName === this.member && aliasName === this.alias) {
714
- return true;
715
- }
716
- }
717
- }
718
- }
719
- }
1004
+ return importBindings(jsImport).some(binding => this.answeredBy(binding));
1005
+ }
720
1006
 
721
- return false;
1007
+ /** Whether a binding the file already has serves this request. */
1008
+ private answeredBy(binding: ModuleScopeBinding): boolean {
1009
+ return binding.module === this.module &&
1010
+ binding.member === memberName(this.member) &&
1011
+ binding.typeOnly === this.typeOnly &&
1012
+ (this.anyNameAnswers || binding.name === this.bindingName);
722
1013
  }
723
1014
 
724
1015
  /**
@@ -728,52 +1019,12 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
728
1019
  if (varDecl.variables.length !== 1) {
729
1020
  return false;
730
1021
  }
731
-
732
1022
  const namedVar = varDecl.variables[0].element;
733
- if (!namedVar) {
1023
+ const module = requiredModule(namedVar?.initializer?.element);
1024
+ if (module !== this.module) {
734
1025
  return false;
735
1026
  }
736
-
737
- const initializer = namedVar.initializer?.element;
738
- if (!initializer || initializer.kind !== J.Kind.MethodInvocation) {
739
- return false;
740
- }
741
-
742
- const methodInv = initializer as J.MethodInvocation;
743
- if (!this.isRequireCall(methodInv)) {
744
- return false;
745
- }
746
-
747
- const moduleName = this.getModuleNameFromRequire(methodInv);
748
- if (moduleName !== this.module) {
749
- return false;
750
- }
751
-
752
- // Check if the variable name matches what we're trying to add
753
- const pattern = namedVar.name;
754
- if ((this.member === undefined || this.member === 'default') && pattern?.kind === J.Kind.Identifier) {
755
- // Default import style: const fs = require('fs')
756
- // For member === 'default', also check the alias matches if specified
757
- if (this.alias) {
758
- const varName = (pattern as J.Identifier).simpleName;
759
- return varName === this.alias;
760
- }
761
- return true;
762
- } else if (this.member !== undefined && this.member !== 'default' && pattern?.kind === JS.Kind.ObjectBindingPattern) {
763
- // Destructured import: const { member } = require('module')
764
- const objectPattern = pattern as JS.ObjectBindingPattern;
765
- for (const elem of objectPattern.bindings.elements) {
766
- if (elem.element?.kind === JS.Kind.BindingElement) {
767
- const bindingElem = elem.element as JS.BindingElement;
768
- const name = (bindingElem.name as J.Identifier)?.simpleName;
769
- if (name === (this.alias || this.member)) {
770
- return true;
771
- }
772
- }
773
- }
774
- }
775
-
776
- return false;
1027
+ return requireBindings(namedVar!.name, module).some(binding => this.answeredBy(binding));
777
1028
  }
778
1029
 
779
1030
  /**
@@ -899,7 +1150,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
899
1150
  }
900
1151
 
901
1152
  // Step 2: Look for references that match
902
- const targetName = this.alias || this.member;
1153
+ const targetName = this.bindingName;
903
1154
  const targetModule = this.module;
904
1155
  let found = false;
905
1156
  const self = this;
@@ -1033,15 +1284,20 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1033
1284
  // For side-effect imports, use emptySpace since space comes from LeftPadded.before
1034
1285
  // For regular imports with import clause, use emptySpace since space comes from LeftPadded.before
1035
1286
  // However, the printer expects the space after 'from' in the literal's prefix
1036
- // Note: value contains the unquoted string, valueSource contains the quoted version for printing
1287
+ // Note: value is the unquoted module name; valueSource and unicodeEscapes are its printed form
1288
+ let valueSource = this.moduleValueSource;
1289
+ if (valueSource === undefined) {
1290
+ const quote = this.quoteStyle ?? await detectQuote(compilationUnit);
1291
+ valueSource = `${quote}${this.module}${quote}`;
1292
+ }
1037
1293
  const moduleSpecifier: J.Literal = {
1038
1294
  id: randomId(),
1039
1295
  kind: J.Kind.Literal,
1040
1296
  prefix: this.sideEffectOnly ? emptySpace : singleSpace,
1041
1297
  markers: emptyMarkers,
1042
1298
  value: this.module,
1043
- valueSource: `'${this.module}'`,
1044
- unicodeEscapes: [],
1299
+ valueSource,
1300
+ unicodeEscapes: this.moduleUnicodeEscapes,
1045
1301
  type: undefined
1046
1302
  };
1047
1303
 
@@ -1069,7 +1325,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1069
1325
  prefix: singleSpace,
1070
1326
  markers: emptyMarkers,
1071
1327
  annotations: [],
1072
- simpleName: this.alias!,
1328
+ simpleName: this.bindingName!,
1073
1329
  type: undefined,
1074
1330
  fieldType: undefined
1075
1331
  };
@@ -1101,7 +1357,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1101
1357
  prefix: singleSpace,
1102
1358
  markers: emptyMarkers,
1103
1359
  annotations: [],
1104
- simpleName: this.alias || this.module,
1360
+ simpleName: this.bindingName!,
1105
1361
  type: undefined,
1106
1362
  fieldType: undefined
1107
1363
  };
@@ -1176,7 +1432,8 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1176
1432
  private createImportSpecifier(): JS.ImportSpecifier {
1177
1433
  let specifier: J.Identifier | JS.Alias;
1178
1434
 
1179
- if (this.alias) {
1435
+ // An alias equal to the member says nothing `{member}` alone does not.
1436
+ if (this.bindingName !== this.member) {
1180
1437
  // Aliased import: import { member as alias } from 'module'
1181
1438
  const propertyName: J.Identifier = {
1182
1439
  id: randomId(),
@@ -1195,7 +1452,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1195
1452
  prefix: singleSpace,
1196
1453
  markers: emptyMarkers,
1197
1454
  annotations: [],
1198
- simpleName: this.alias,
1455
+ simpleName: this.bindingName!,
1199
1456
  type: undefined,
1200
1457
  fieldType: undefined
1201
1458
  };
@@ -1286,51 +1543,298 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1286
1543
  return 0;
1287
1544
  }
1288
1545
 
1289
- /**
1290
- * Get the module name from a require() call
1291
- */
1292
- private getModuleNameFromRequire(methodInv: J.MethodInvocation): string | undefined {
1293
- const args = methodInv.arguments?.elements;
1294
- if (!args || args.length === 0) {
1295
- return undefined;
1546
+
1547
+
1548
+ }
1549
+
1550
+ /**
1551
+ * The local name `jsImport` binds `member` of `module` to, or `undefined` where it binds
1552
+ * something else. `'default'` and an absent `member` both mean the default import, matching
1553
+ * {@link memberName}.
1554
+ */
1555
+ function importBinds(jsImport: JS.Import, module: string, member: string | undefined): string | undefined {
1556
+ const specifier = jsImport.moduleSpecifier?.element;
1557
+ if (specifier?.kind !== J.Kind.Literal || (specifier as J.Literal).value !== module) {
1558
+ return undefined;
1559
+ }
1560
+ const importClause = jsImport.importClause;
1561
+ if (!importClause) {
1562
+ return undefined;
1563
+ }
1564
+ const key = memberName(member);
1565
+ if (key === undefined) {
1566
+ const nameElem = importClause.name?.element;
1567
+ return nameElem && isIdentifier(nameElem) ? nameElem.simpleName : undefined;
1568
+ }
1569
+ if (key === '*') {
1570
+ const namedBindings = importClause.namedBindings;
1571
+ return namedBindings?.kind === JS.Kind.Alias && isIdentifier((namedBindings as JS.Alias).alias)
1572
+ ? ((namedBindings as JS.Alias).alias as J.Identifier).simpleName
1573
+ : undefined;
1574
+ }
1575
+ const namedBindings = importClause.namedBindings;
1576
+ if (namedBindings?.kind !== JS.Kind.NamedImports) {
1577
+ return undefined;
1578
+ }
1579
+ for (const elem of (namedBindings as JS.NamedImports).elements.elements) {
1580
+ const specifierNode = elem.element.specifier;
1581
+ if (!namedSpecifierImports(specifierNode, key)) {
1582
+ continue;
1583
+ }
1584
+ if (isIdentifier(specifierNode)) {
1585
+ return specifierNode.simpleName;
1586
+ }
1587
+ const alias = (specifierNode as JS.Alias).alias;
1588
+ if (isIdentifier(alias)) {
1589
+ return alias.simpleName;
1296
1590
  }
1591
+ }
1592
+ return undefined;
1593
+ }
1297
1594
 
1298
- const firstArg = args[0].element;
1299
- if (!firstArg || firstArg.kind !== J.Kind.Literal || typeof (firstArg as J.Literal).value !== 'string') {
1300
- return undefined;
1595
+ /** Whether `specifier` imports the member `key`, under whatever local name it binds it to. */
1596
+ function namedSpecifierImports(specifier: JS.ImportSpecifier["specifier"], key: string): boolean {
1597
+ if (isIdentifier(specifier)) {
1598
+ return specifier.simpleName === key;
1599
+ }
1600
+ if (specifier.kind === JS.Kind.Alias) {
1601
+ const propertyName = (specifier as JS.Alias).propertyName.element;
1602
+ return isIdentifier(propertyName) && propertyName.simpleName === key;
1603
+ }
1604
+ return false;
1605
+ }
1606
+
1607
+ /** Whether the named specifier binding `key` carries its own inline `type`, as in `{type a, b}`. */
1608
+ function namedSpecifierIsTypeOnly(imp: JS.Import, key: string): boolean {
1609
+ const namedBindings = imp.importClause?.namedBindings;
1610
+ if (namedBindings?.kind !== JS.Kind.NamedImports) {
1611
+ return false;
1612
+ }
1613
+ for (const elem of (namedBindings as JS.NamedImports).elements.elements) {
1614
+ if (namedSpecifierImports(elem.element.specifier, key)) {
1615
+ return elem.element.importType.element;
1301
1616
  }
1617
+ }
1618
+ return false;
1619
+ }
1302
1620
 
1303
- return (firstArg as J.Literal).value?.toString();
1621
+ /** How many named specifiers `jsImport` carries, `0` for a default, namespace or side-effect import. */
1622
+ function namedImportCount(jsImport: JS.Import): number {
1623
+ const namedBindings = jsImport.importClause?.namedBindings;
1624
+ return namedBindings?.kind === JS.Kind.NamedImports ? (namedBindings as JS.NamedImports).elements.elements.length : 0;
1625
+ }
1626
+
1627
+ /**
1628
+ * Whether `jsImport`'s clause binds exactly one thing — a default, a namespace, or one named
1629
+ * specifier. That is the only shape a module-only move can rewrite in place: whichever binding
1630
+ * `importBinds` already matched is this one, so nothing else the clause carries goes along with it.
1631
+ */
1632
+ function isOnlyMember(jsImport: JS.Import): boolean {
1633
+ const importClause = jsImport.importClause;
1634
+ if (!importClause) {
1635
+ return false;
1304
1636
  }
1637
+ const hasDefault = importClause.name !== undefined;
1638
+ const hasNamespace = importClause.namedBindings?.kind === JS.Kind.Alias;
1639
+ return (hasDefault ? 1 : 0) + (hasNamespace ? 1 : 0) + namedImportCount(jsImport) === 1;
1640
+ }
1305
1641
 
1306
- /**
1307
- * Get the import name from an import specifier
1308
- */
1309
- private getImportName(specifier: JS.ImportSpecifier): string {
1310
- const spec = specifier.specifier;
1311
- if (spec?.kind === JS.Kind.Alias) {
1312
- const alias = spec as JS.Alias;
1313
- const propertyName = alias.propertyName.element;
1314
- if (propertyName?.kind === J.Kind.Identifier) {
1315
- return (propertyName as J.Identifier).simpleName;
1316
- }
1317
- } else if (spec?.kind === J.Kind.Identifier) {
1318
- return (spec as J.Identifier).simpleName;
1642
+ export interface ExistingImportBinding {
1643
+ localName: string;
1644
+ onlyMemberOfStatement: boolean;
1645
+ }
1646
+
1647
+ /**
1648
+ * The existing binding for `member` of `module`, read from `cu`'s own import statements — what
1649
+ * `maybeRebind` reads before committing to a `RebindImport` edit.
1650
+ */
1651
+ export function existingImportBinding(
1652
+ cu: JS.CompilationUnit,
1653
+ module: string,
1654
+ member: string | undefined
1655
+ ): ExistingImportBinding | undefined {
1656
+ for (const stmt of cu.statements) {
1657
+ const element = stmt.element;
1658
+ if (element?.kind !== JS.Kind.Import) {
1659
+ continue;
1660
+ }
1661
+ const localName = importBinds(element as JS.Import, module, member);
1662
+ if (localName !== undefined) {
1663
+ return {localName, onlyMemberOfStatement: isOnlyMember(element as JS.Import)};
1319
1664
  }
1320
- return '';
1321
1665
  }
1666
+ return undefined;
1667
+ }
1322
1668
 
1323
- /**
1324
- * Get the import alias from an import specifier
1325
- */
1326
- private getImportAlias(specifier: JS.ImportSpecifier): string | undefined {
1327
- const spec = specifier.specifier;
1328
- if (spec?.kind === JS.Kind.Alias) {
1329
- const alias = spec as JS.Alias;
1330
- if (alias.alias?.kind === J.Kind.Identifier) {
1331
- return (alias.alias as J.Identifier).simpleName;
1332
- }
1669
+ /**
1670
+ * Binds `member` under the name `local` already carries, so the file's references to it still
1671
+ * resolve. `local` itself becomes the alias, keeping the type attribution it holds, and the alias
1672
+ * takes its prefix: that whitespace separates the specifier from a `type` keyword before it.
1673
+ */
1674
+ function aliasing(local: J.Identifier, member: string): JS.Alias {
1675
+ const propertyName: J.Identifier = {
1676
+ id: randomId(),
1677
+ kind: J.Kind.Identifier,
1678
+ prefix: emptySpace,
1679
+ markers: emptyMarkers,
1680
+ annotations: [],
1681
+ simpleName: member,
1682
+ type: undefined,
1683
+ fieldType: undefined
1684
+ };
1685
+ return {
1686
+ id: randomId(),
1687
+ kind: JS.Kind.Alias,
1688
+ prefix: local.prefix,
1689
+ markers: emptyMarkers,
1690
+ propertyName: rightPadded(propertyName, singleSpace),
1691
+ alias: {...local, prefix: singleSpace}
1692
+ };
1693
+ }
1694
+
1695
+ /**
1696
+ * Drops the binding for `member` from `jsImport`'s clause — the default, the namespace alias, or
1697
+ * one entry of the named list, whichever `member` names — keeping everything else the clause
1698
+ * binds. `ElementRemovalFormatter` carries the dropped binding's prefix onto whatever prints
1699
+ * next, the same way `RemoveImport` keeps formatting sane when trimming a list.
1700
+ */
1701
+ function removeBinding(jsImport: JS.Import, member: string | undefined): JS.Import {
1702
+ const importClause = jsImport.importClause;
1703
+ if (!importClause) {
1704
+ return jsImport;
1705
+ }
1706
+ const key = memberName(member);
1707
+
1708
+ if (key === undefined) {
1709
+ if (!importClause.name) {
1710
+ return jsImport;
1333
1711
  }
1334
- return undefined;
1712
+ const namedBindings = importClause.namedBindings;
1713
+ if (namedBindings?.kind === JS.Kind.NamedImports) {
1714
+ // `NamedImports` keeps the space before its own `{` on the container's `before`,
1715
+ // not on its own prefix, so the removed default's prefix has to land there instead.
1716
+ const namedImports = namedBindings as JS.NamedImports;
1717
+ const updated: JS.NamedImports = {
1718
+ ...namedImports,
1719
+ elements: {...namedImports.elements, before: importClause.name.element.prefix}
1720
+ };
1721
+ return {...jsImport, importClause: {...importClause, name: undefined, namedBindings: updated}};
1722
+ }
1723
+ if (namedBindings) {
1724
+ const formatter = new ElementRemovalFormatter<J>();
1725
+ formatter.markRemoved(importClause.name.element);
1726
+ return {...jsImport, importClause: {...importClause, name: undefined, namedBindings: formatter.processKept(namedBindings)}};
1727
+ }
1728
+ return {...jsImport, importClause: {...importClause, name: undefined}};
1729
+ }
1730
+
1731
+ if (key === '*') {
1732
+ return {...jsImport, importClause: {...importClause, namedBindings: undefined}};
1733
+ }
1734
+
1735
+ if (importClause.namedBindings?.kind !== JS.Kind.NamedImports) {
1736
+ return jsImport;
1737
+ }
1738
+ const namedImports = importClause.namedBindings as JS.NamedImports;
1739
+ const formatter = new ElementRemovalFormatter<JS.ImportSpecifier>();
1740
+ const kept: J.RightPadded<JS.ImportSpecifier>[] = [];
1741
+ for (const entry of namedImports.elements.elements) {
1742
+ if (namedSpecifierImports(entry.element.specifier, key)) {
1743
+ formatter.markRemoved(entry.element);
1744
+ } else {
1745
+ kept.push({...entry, element: formatter.processKept(entry.element)});
1746
+ }
1747
+ }
1748
+ if (kept.length === 0) {
1749
+ // An emptied brace list still prints, as `import D, {} from "m"`, so it goes with its
1750
+ // last member; the caller drops the whole statement when no default remains either.
1751
+ return {...jsImport, importClause: {...importClause, namedBindings: undefined}};
1752
+ }
1753
+ const updatedNamedImports: JS.NamedImports = {...namedImports, elements: {...namedImports.elements, elements: kept}};
1754
+ return {...jsImport, importClause: {...importClause, namedBindings: updatedNamedImports}};
1755
+ }
1756
+
1757
+ /**
1758
+ * Moves the binding `from` names to `to`, keeping the local name it already had. In place when
1759
+ * the statement that carries it binds nothing else — module and member specifier rewritten there
1760
+ * directly; otherwise the old specifier drops and {@link bindImport} queues the replacement,
1761
+ * aliased to the preserved name.
1762
+ *
1763
+ * Not built on `RemoveImport`/`maybeUnbind`: those only drop a binding once nothing references
1764
+ * it, but a rebind moves one that is still in use — removal here has to be unconditional.
1765
+ */
1766
+ export class RebindImport<P> extends JavaScriptVisitor<P> {
1767
+ constructor(
1768
+ readonly from: {module: string; member?: string},
1769
+ readonly to: {module: string; member?: string},
1770
+ readonly localName: string
1771
+ ) {
1772
+ super();
1773
+ }
1774
+
1775
+ private transformedInPlace = false;
1776
+ private typeOnly = false;
1777
+
1778
+ override async visitJsCompilationUnit(cu: JS.CompilationUnit, p: P): Promise<J | undefined> {
1779
+ const visited = await super.visitJsCompilationUnit(cu, p) as JS.CompilationUnit;
1780
+ if (!this.transformedInPlace) {
1781
+ bindImport(this, {
1782
+ module: this.to.module,
1783
+ member: this.to.member,
1784
+ alias: this.localName,
1785
+ typeOnly: this.typeOnly,
1786
+ onlyIfReferenced: false
1787
+ });
1788
+ }
1789
+ return visited;
1790
+ }
1791
+
1792
+ override async visitImportDeclaration(jsImport: JS.Import, p: P): Promise<J | undefined> {
1793
+ const imp = await super.visitImportDeclaration(jsImport, p) as JS.Import;
1794
+
1795
+ const key = memberName(this.from.member);
1796
+ if (importBinds(imp, this.from.module, this.from.member) === undefined) {
1797
+ return imp;
1798
+ }
1799
+ // A moved named specifier's own inline `type` marks it type-only even where the clause
1800
+ // it's leaving is not — the replacement needs the same answer to stay type-safe.
1801
+ this.typeOnly = (imp.importClause?.typeOnly ?? false) ||
1802
+ (key !== undefined && key !== '*' && namedSpecifierIsTypeOnly(imp, key));
1803
+
1804
+ if (!isOnlyMember(imp)) {
1805
+ return removeBinding(imp, this.from.member);
1806
+ }
1807
+
1808
+ this.transformedInPlace = true;
1809
+ return produce(imp, draft => {
1810
+ const literal = draft.moduleSpecifier!.element as Draft<J.Literal>;
1811
+ literal.value = this.to.module;
1812
+ const originalSource = literal.valueSource || `"${this.from.module}"`;
1813
+ const quoteChar = originalSource.startsWith("'") ? "'" : '"';
1814
+ literal.valueSource = `${quoteChar}${this.to.module}${quoteChar}`;
1815
+
1816
+ // A named specifier's local name has to stay put in the source, since it is what the
1817
+ // rest of the file already reads; default and namespace imports carry that name on
1818
+ // the clause itself, which needs no edit for a module-only move.
1819
+ const toKey = memberName(this.to.member);
1820
+ if (key !== undefined && key !== '*' && toKey !== undefined && toKey !== key) {
1821
+ const importClause = draft.importClause;
1822
+ if (importClause?.namedBindings?.kind === JS.Kind.NamedImports) {
1823
+ const namedImports = importClause.namedBindings as Draft<JS.NamedImports>;
1824
+ for (const elem of namedImports.elements.elements) {
1825
+ const specifier = elem.element;
1826
+ if (specifier.specifier.kind === J.Kind.Identifier && specifier.specifier.simpleName === key) {
1827
+ specifier.specifier = aliasing(specifier.specifier as Draft<J.Identifier>, toKey) as Draft<JS.Alias>;
1828
+ } else if (specifier.specifier.kind === JS.Kind.Alias) {
1829
+ const aliasNode = specifier.specifier as Draft<JS.Alias>;
1830
+ const propertyName = aliasNode.propertyName.element;
1831
+ if (propertyName.kind === J.Kind.Identifier && propertyName.simpleName === key) {
1832
+ propertyName.simpleName = toKey;
1833
+ }
1834
+ }
1835
+ }
1836
+ }
1837
+ }
1838
+ });
1335
1839
  }
1336
1840
  }