@openrewrite/rewrite 8.91.0 → 8.91.1

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 (116) 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 +39 -20
  10. package/dist/javascript/add-import.d.ts.map +1 -1
  11. package/dist/javascript/add-import.js +513 -243
  12. package/dist/javascript/add-import.js.map +1 -1
  13. package/dist/javascript/assertions.d.ts +7 -0
  14. package/dist/javascript/assertions.d.ts.map +1 -1
  15. package/dist/javascript/assertions.js +31 -5
  16. package/dist/javascript/assertions.js.map +1 -1
  17. package/dist/javascript/comparator.d.ts +5 -0
  18. package/dist/javascript/comparator.d.ts.map +1 -1
  19. package/dist/javascript/comparator.js +34 -9
  20. package/dist/javascript/comparator.js.map +1 -1
  21. package/dist/javascript/format/format.d.ts.map +1 -1
  22. package/dist/javascript/format/format.js +8 -9
  23. package/dist/javascript/format/format.js.map +1 -1
  24. package/dist/javascript/format/tabs-and-indents-visitor.d.ts +2 -0
  25. package/dist/javascript/format/tabs-and-indents-visitor.d.ts.map +1 -1
  26. package/dist/javascript/format/tabs-and-indents-visitor.js +17 -11
  27. package/dist/javascript/format/tabs-and-indents-visitor.js.map +1 -1
  28. package/dist/javascript/node-resolution-result.d.ts +19 -1
  29. package/dist/javascript/node-resolution-result.d.ts.map +1 -1
  30. package/dist/javascript/node-resolution-result.js +37 -2
  31. package/dist/javascript/node-resolution-result.js.map +1 -1
  32. package/dist/javascript/package-exported-types.d.ts.map +1 -1
  33. package/dist/javascript/package-exported-types.js +56 -72
  34. package/dist/javascript/package-exported-types.js.map +1 -1
  35. package/dist/javascript/parser.d.ts.map +1 -1
  36. package/dist/javascript/parser.js +10 -3
  37. package/dist/javascript/parser.js.map +1 -1
  38. package/dist/javascript/recipes/change-import.d.ts.map +1 -1
  39. package/dist/javascript/recipes/change-import.js +4 -8
  40. package/dist/javascript/recipes/change-import.js.map +1 -1
  41. package/dist/javascript/style.d.ts.map +1 -1
  42. package/dist/javascript/style.js +3 -10
  43. package/dist/javascript/style.js.map +1 -1
  44. package/dist/javascript/templating/bindings.d.ts +19 -0
  45. package/dist/javascript/templating/bindings.d.ts.map +1 -0
  46. package/dist/javascript/templating/bindings.js +121 -0
  47. package/dist/javascript/templating/bindings.js.map +1 -0
  48. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  49. package/dist/javascript/templating/comparator.js +5 -6
  50. package/dist/javascript/templating/comparator.js.map +1 -1
  51. package/dist/javascript/templating/engine.d.ts +18 -7
  52. package/dist/javascript/templating/engine.d.ts.map +1 -1
  53. package/dist/javascript/templating/engine.js +70 -42
  54. package/dist/javascript/templating/engine.js.map +1 -1
  55. package/dist/javascript/templating/index.d.ts +1 -1
  56. package/dist/javascript/templating/index.d.ts.map +1 -1
  57. package/dist/javascript/templating/index.js.map +1 -1
  58. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  59. package/dist/javascript/templating/pattern.js +13 -10
  60. package/dist/javascript/templating/pattern.js.map +1 -1
  61. package/dist/javascript/templating/placeholder-replacement.d.ts +2 -0
  62. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  63. package/dist/javascript/templating/placeholder-replacement.js +14 -5
  64. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  65. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  66. package/dist/javascript/templating/rewrite.js +14 -18
  67. package/dist/javascript/templating/rewrite.js.map +1 -1
  68. package/dist/javascript/templating/template.d.ts +8 -0
  69. package/dist/javascript/templating/template.d.ts.map +1 -1
  70. package/dist/javascript/templating/template.js +43 -3
  71. package/dist/javascript/templating/template.js.map +1 -1
  72. package/dist/javascript/templating/types.d.ts +47 -1
  73. package/dist/javascript/templating/types.d.ts.map +1 -1
  74. package/dist/javascript/templating/utils.d.ts +5 -0
  75. package/dist/javascript/templating/utils.d.ts.map +1 -1
  76. package/dist/javascript/templating/utils.js +17 -0
  77. package/dist/javascript/templating/utils.js.map +1 -1
  78. package/dist/javascript/type-mapping.d.ts +1 -0
  79. package/dist/javascript/type-mapping.d.ts.map +1 -1
  80. package/dist/javascript/type-mapping.js +52 -2
  81. package/dist/javascript/type-mapping.js.map +1 -1
  82. package/dist/rewrite-javascript-version.txt +1 -1
  83. package/dist/style.d.ts +2 -0
  84. package/dist/style.d.ts.map +1 -1
  85. package/dist/style.js +5 -0
  86. package/dist/style.js.map +1 -1
  87. package/dist/test/rewrite-test.d.ts +12 -0
  88. package/dist/test/rewrite-test.d.ts.map +1 -1
  89. package/dist/test/rewrite-test.js +40 -3
  90. package/dist/test/rewrite-test.js.map +1 -1
  91. package/package.json +1 -1
  92. package/src/java/formatting-utils.ts +14 -10
  93. package/src/java/type.ts +5 -0
  94. package/src/javascript/add-import.ts +590 -230
  95. package/src/javascript/assertions.ts +33 -5
  96. package/src/javascript/comparator.ts +39 -10
  97. package/src/javascript/format/format.ts +9 -10
  98. package/src/javascript/format/tabs-and-indents-visitor.ts +18 -12
  99. package/src/javascript/node-resolution-result.ts +59 -4
  100. package/src/javascript/package-exported-types.ts +60 -74
  101. package/src/javascript/parser.ts +10 -3
  102. package/src/javascript/recipes/change-import.ts +4 -7
  103. package/src/javascript/style.ts +3 -11
  104. package/src/javascript/templating/bindings.ts +129 -0
  105. package/src/javascript/templating/comparator.ts +5 -6
  106. package/src/javascript/templating/engine.ts +82 -44
  107. package/src/javascript/templating/index.ts +2 -0
  108. package/src/javascript/templating/pattern.ts +16 -15
  109. package/src/javascript/templating/placeholder-replacement.ts +16 -5
  110. package/src/javascript/templating/rewrite.ts +16 -18
  111. package/src/javascript/templating/template.ts +45 -3
  112. package/src/javascript/templating/types.ts +53 -1
  113. package/src/javascript/templating/utils.ts +17 -0
  114. package/src/javascript/type-mapping.ts +54 -2
  115. package/src/style.ts +5 -0
  116. package/src/test/rewrite-test.ts +61 -3
@@ -1,9 +1,12 @@
1
1
  import {JavaScriptVisitor} from "./visitor";
2
2
  import {emptySpace, J, rightPadded, singleSpace, space, Statement, Type} from "../java";
3
- import {JS} from "./tree";
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 {Cursor} from "../tree";
8
+
9
+ export type QuoteChar = "'" | '"';
7
10
 
8
11
  export enum ImportStyle {
9
12
  ES6Named, // import { x } from 'module'
@@ -13,8 +16,10 @@ export enum ImportStyle {
13
16
  }
14
17
 
15
18
  export interface AddImportOptions {
16
- /** The module name (e.g., 'fs', 'react') to import from */
17
- module: string;
19
+ /** 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. */
22
+ module: string | J.Literal;
18
23
 
19
24
  /** Optionally, the specific member to import from the module.
20
25
  * If not specified, adds a default import or namespace import.
@@ -28,9 +33,16 @@ export interface AddImportOptions {
28
33
 
29
34
  /** Optional alias for the imported member.
30
35
  * Required when member is 'default' or '*'.
36
+ * Taken verbatim, never deconflicted: a caller that names an alias may already have
37
+ * emitted code using it.
31
38
  * Cannot be combined with `sideEffectOnly`. */
32
39
  alias?: string;
33
40
 
41
+ /** Preferred local name, deconflicted if the file already binds it. Stands in for `alias` where
42
+ * `member` is 'default' or '*' and the caller wants a name it does not insist on.
43
+ * Cannot be combined with `sideEffectOnly`. */
44
+ preferredName?: string;
45
+
34
46
  /** If true, only add the import if the member is actually used in the file. Default: true
35
47
  * Cannot be combined with `sideEffectOnly`. */
36
48
  onlyIfReferenced?: boolean;
@@ -46,12 +58,20 @@ export interface AddImportOptions {
46
58
 
47
59
  /** Optional import style to use. If not specified, auto-detects from file and existing imports */
48
60
  style?: ImportStyle;
61
+
62
+ /** 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. */
64
+ quoteStyle?: QuoteChar;
49
65
  }
50
66
 
51
67
  /**
52
68
  * Register an AddImport visitor to add an import statement to a JavaScript/TypeScript file
53
69
  * @param visitor The visitor to add the import addition to
54
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`.
55
75
  *
56
76
  * @example
57
77
  * // Add a named import
@@ -73,68 +93,562 @@ export interface AddImportOptions {
73
93
  * // Add a side-effect import
74
94
  * maybeAddImport(visitor, { module: 'core-js/stable', sideEffectOnly: true });
75
95
  */
96
+ export function maybeAddImport(
97
+ 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;
76
104
  export function maybeAddImport(
77
105
  visitor: JavaScriptVisitor<any>,
78
106
  options: AddImportOptions
79
- ) {
107
+ ): string | undefined;
108
+ export function maybeAddImport(
109
+ visitor: JavaScriptVisitor<any>,
110
+ options: AddImportOptions
111
+ ): string | undefined {
112
+ validate(options);
113
+ const module = moduleNameOf(options.module);
114
+ const sideEffectOnly = options.sideEffectOnly ?? false;
115
+ const typeOnly = options.typeOnly ?? false;
116
+
117
+ // A queued import binds the module as much as one already in the file, so it answers a later
118
+ // request the same way; a name a merged request never emits would be referenced but not bound.
80
119
  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;
120
+ if (!(v instanceof AddImport) || v.module !== module ||
121
+ v.sideEffectOnly !== sideEffectOnly || v.typeOnly !== typeOnly) {
122
+ continue;
123
+ }
124
+ // How a specifier prints, or what name would be nice, does not say which binding is wanted.
125
+ // A pinned alias does: it asks for a binding of its own, which only the same request answers.
126
+ const answers = options.alias
127
+ ? v.alias === options.alias && v.member === options.member
128
+ : memberName(v.member) === memberName(options.member);
129
+ if (answers) {
130
+ return v.bindingName;
131
+ }
132
+ }
133
+
134
+ if (sideEffectOnly) {
135
+ visitor.afterVisit.push(new AddImport(options));
136
+ return undefined;
137
+ }
138
+
139
+ // A pinned alias is the whole answer, so the file has no say and nothing below needs to read it.
140
+ if (options.alias) {
141
+ visitor.afterVisit.push(new AddImport(options, options.alias));
142
+ return options.alias;
143
+ }
144
+
145
+ const derived = derivedName(options);
146
+ const cu = compilationUnitOf(visitor);
147
+ if (!cu) {
148
+ visitor.afterVisit.push(new AddImport(options, derived));
149
+ return derived;
150
+ }
151
+
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) {
159
+ if (binding.module === module && binding.member === memberName(options.member) &&
160
+ binding.typeOnly === typeOnly &&
161
+ (options.preferredName !== undefined || anyNameAnswers(options) ||
162
+ binding.name === derived)) {
163
+ return binding.name;
88
164
  }
89
165
  }
90
- visitor.afterVisit.push(new AddImport(options));
166
+
167
+ const name = deconflict(derived, takenNames(bindings, visitor));
168
+ visitor.afterVisit.push(new AddImport(options, name));
169
+ return name;
91
170
  }
92
171
 
93
- export class AddImport<P> extends JavaScriptVisitor<P> {
94
- readonly module: string;
95
- readonly member?: string;
96
- readonly alias?: string;
97
- readonly onlyIfReferenced: boolean;
98
- readonly sideEffectOnly: boolean;
99
- readonly typeOnly: boolean;
100
- readonly style?: ImportStyle;
172
+ /** Rejects a request on its own terms, whatever the queue already holds. */
173
+ function validate(options: AddImportOptions): void {
174
+ // Validate that a name is provided when member is 'default'
175
+ if (options.member === 'default' && !options.alias && !options.preferredName) {
176
+ throw new Error("When member is 'default', the alias parameter is required");
177
+ }
101
178
 
102
- constructor(options: AddImportOptions) {
103
- super();
179
+ // Validate that a name is provided when member is '*' (namespace import)
180
+ if (options.member === '*' && !options.alias && !options.preferredName) {
181
+ throw new Error("When member is '*', the alias parameter is required");
182
+ }
104
183
 
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");
184
+ // Validate that sideEffectOnly is not combined with incompatible options
185
+ if (options.sideEffectOnly) {
186
+ if (options.member !== undefined) {
187
+ throw new Error("Cannot combine sideEffectOnly with member");
188
+ }
189
+ if (options.alias !== undefined) {
190
+ throw new Error("Cannot combine sideEffectOnly with alias");
191
+ }
192
+ if (options.preferredName !== undefined) {
193
+ throw new Error("Cannot combine sideEffectOnly with preferredName");
108
194
  }
195
+ if (options.onlyIfReferenced !== undefined) {
196
+ throw new Error("Cannot combine sideEffectOnly with onlyIfReferenced");
197
+ }
198
+ if (options.typeOnly) {
199
+ throw new Error("Cannot combine sideEffectOnly with typeOnly");
200
+ }
201
+ }
202
+ }
203
+
204
+ /**
205
+ * A request that named no local name and asks for no member of the module. Having expressed no
206
+ * preference it has none to disappoint, so whatever the file already calls that module answers it.
207
+ */
208
+ function anyNameAnswers(options: AddImportOptions): boolean {
209
+ return !options.sideEffectOnly && memberName(options.member) === undefined &&
210
+ options.alias === undefined && options.preferredName === undefined;
211
+ }
212
+
213
+ /** The local name a request asks for, before the file has a say in it. */
214
+ function derivedName(options: AddImportOptions): string {
215
+ return options.alias ?? options.preferredName ?? memberName(options.member) ?? moduleNameOf(options.module);
216
+ }
217
+
218
+ /** `'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 {
220
+ return member === 'default' ? undefined : member;
221
+ }
109
222
 
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");
223
+ /** A name introduced into the file's module scope, and where it came from when that is an import. */
224
+ interface ModuleScopeBinding {
225
+ name: string;
226
+ module?: string;
227
+ /** Carries the {@link AddImportOptions.member} spelling, so `undefined` means a default import. */
228
+ member?: string;
229
+ typeOnly?: boolean;
230
+ }
231
+
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});
113
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[] {
280
+ const bindings: ModuleScopeBinding[] = [];
114
281
 
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");
282
+ const declaredBy = (name: J | undefined): void => {
283
+ for (const bound of patternNames(name)) {
284
+ bindings.push({name: bound.name});
285
+ }
286
+ };
287
+
288
+ const declaredByVariables = (varDecl: J.VariableDeclarations): void => {
289
+ for (const variable of varDecl.variables) {
290
+ const required = requiredModule(variable.element?.initializer?.element);
291
+ if (required !== undefined) {
292
+ bindings.push(...requireBindings(variable.element?.name, required));
293
+ } else {
294
+ declaredBy(variable.element?.name);
119
295
  }
120
- if (options.alias !== undefined) {
121
- throw new Error("Cannot combine sideEffectOnly with alias");
296
+ }
297
+ };
298
+
299
+ for (const stmt of statements) {
300
+ 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;
327
+ }
328
+ }
329
+
330
+ return bindings;
331
+ }
332
+
333
+ /** The module a `require('...')` initializer names, `undefined` for an initializer that is anything else. */
334
+ function requiredModule(initializer: J | undefined): string | undefined {
335
+ return initializer?.kind === J.Kind.MethodInvocation
336
+ ? requiredModuleOf(initializer as J.MethodInvocation)
337
+ : undefined;
338
+ }
339
+
340
+ /** A `require(...)` call, whatever it is passed. `obj.require('x')` selects a method rather than loading a module. */
341
+ function isRequireCall(methodInv: J.MethodInvocation): boolean {
342
+ return !methodInv.select && methodInv.name?.kind === J.Kind.Identifier &&
343
+ (methodInv.name as J.Identifier).simpleName === 'require';
344
+ }
345
+
346
+ /**
347
+ * The module a `require(...)` call loads. `obj.require('x')` selects a method rather than loading a
348
+ * module, and a specifier that is not a string literal names none that can be read here.
349
+ */
350
+ function requiredModuleOf(methodInv: J.MethodInvocation): string | undefined {
351
+ if (!isRequireCall(methodInv)) {
352
+ return undefined;
353
+ }
354
+ const argument = methodInv.arguments?.elements[0]?.element;
355
+ return argument?.kind === J.Kind.Literal && typeof (argument as J.Literal).value === 'string'
356
+ ? moduleNameOf(argument as J.Literal)
357
+ : undefined;
358
+ }
359
+
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
+ /** A `require` binds a module the way an import does, so the pool records it the same way. */
403
+ function requireBindings(pattern: J | undefined, module: string): ModuleScopeBinding[] {
404
+ // A whole-module require binds no member, exactly as a default import does.
405
+ if (pattern?.kind === J.Kind.Identifier) {
406
+ return [{name: (pattern as J.Identifier).simpleName, module, member: undefined, typeOnly: false}];
407
+ }
408
+ return patternNames(pattern).map(bound => bound.member === undefined
409
+ ? {name: bound.name}
410
+ : {name: bound.name, module, member: bound.member, typeOnly: false});
411
+ }
412
+
413
+ /** The member a specifier imports and the name it binds it under, which are the same absent an alias. */
414
+ function specifierBinding(specifier: JS.ImportSpecifier): { name: string; member: string } | undefined {
415
+ if (specifier.specifier?.kind === J.Kind.Identifier) {
416
+ const name = (specifier.specifier as J.Identifier).simpleName;
417
+ return {name, member: name};
418
+ }
419
+ if (specifier.specifier?.kind !== JS.Kind.Alias) {
420
+ return undefined;
421
+ }
422
+ const alias = specifier.specifier as JS.Alias;
423
+ return alias.propertyName?.element?.kind === J.Kind.Identifier && alias.alias?.kind === J.Kind.Identifier
424
+ ? {name: (alias.alias as J.Identifier).simpleName, member: (alias.propertyName.element as J.Identifier).simpleName}
425
+ : undefined;
426
+ }
427
+
428
+ function importBindings(jsImport: JS.Import): ModuleScopeBinding[] {
429
+ const moduleSpecifier = jsImport.moduleSpecifier?.element;
430
+ const module = moduleSpecifier?.kind === J.Kind.Literal
431
+ ? moduleNameOf(moduleSpecifier as J.Literal)
432
+ : undefined;
433
+ const importClause = jsImport.importClause;
434
+ if (!importClause) {
435
+ return [];
436
+ }
437
+
438
+ const typeOnly = importClause.typeOnly;
439
+ const bindings: ModuleScopeBinding[] = [];
440
+
441
+ if (importClause.name?.element?.kind === J.Kind.Identifier) {
442
+ bindings.push({name: (importClause.name.element as J.Identifier).simpleName, module, member: undefined, typeOnly});
443
+ }
444
+
445
+ const namedBindings = importClause.namedBindings;
446
+ if (namedBindings?.kind === J.Kind.Identifier) {
447
+ bindings.push({name: (namedBindings as J.Identifier).simpleName, module, member: '*', typeOnly});
448
+ } else if (namedBindings?.kind === JS.Kind.Alias) {
449
+ const alias = (namedBindings as JS.Alias).alias;
450
+ if (alias?.kind === J.Kind.Identifier) {
451
+ bindings.push({name: (alias as J.Identifier).simpleName, module, member: '*', typeOnly});
452
+ }
453
+ } else if (namedBindings?.kind === JS.Kind.NamedImports) {
454
+ for (const elem of (namedBindings as JS.NamedImports).elements.elements) {
455
+ const bound = elem.element?.kind === JS.Kind.ImportSpecifier
456
+ ? specifierBinding(elem.element as JS.ImportSpecifier)
457
+ : undefined;
458
+ if (bound) {
459
+ bindings.push({...bound, module, typeOnly});
122
460
  }
123
- if (options.onlyIfReferenced !== undefined) {
124
- throw new Error("Cannot combine sideEffectOnly with onlyIfReferenced");
461
+ }
462
+ }
463
+
464
+ return bindings;
465
+ }
466
+
467
+ /**
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.
471
+ */
472
+ function takenNames(bindings: ModuleScopeBinding[], visitor: JavaScriptVisitor<any>): Set<string> {
473
+ const taken = new Set<string>();
474
+
475
+ for (const v of visitor.afterVisit || []) {
476
+ if (v instanceof AddImport && v.bindingName) {
477
+ taken.add(v.bindingName);
478
+ }
479
+ }
480
+
481
+ for (const binding of bindings) {
482
+ taken.add(binding.name);
483
+ }
484
+
485
+ return taken;
486
+ }
487
+
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}`;
497
+ }
498
+
499
+ /**
500
+ * The parser lifts surrogate pairs out of `valueSource` into `unicodeEscapes` and, when it does,
501
+ * sets `value` to the quoted source (`parser.ts` `mapLiteral`), so neither field alone is the
502
+ * module name. Reuniting them and stripping the quotes yields it for either shape of literal.
503
+ */
504
+ function moduleNameOf(module: string | J.Literal): string {
505
+ if (typeof module === 'string') {
506
+ return module;
507
+ }
508
+ const source = module.valueSource;
509
+ if (source === undefined) {
510
+ return String(module.value);
511
+ }
512
+ let restored = '';
513
+ let cut = 0;
514
+ for (const escape of module.unicodeEscapes ?? []) {
515
+ restored += source.slice(cut, escape.valueSourceIndex) + String.fromCharCode(parseInt(escape.codePoint, 16));
516
+ cut = escape.valueSourceIndex;
517
+ }
518
+ restored += source.slice(cut);
519
+ const quote = restored.charAt(0);
520
+ return (quote === "'" || quote === '"') && restored.endsWith(quote) && restored.length > 1
521
+ ? restored.slice(1, -1)
522
+ : restored;
523
+ }
524
+
525
+ function quoteOf(expression: J | undefined): QuoteChar | undefined {
526
+ if (expression?.kind !== J.Kind.Literal) {
527
+ return undefined;
528
+ }
529
+ const source = (expression as J.Literal).valueSource;
530
+ const quote = source?.charAt(0);
531
+ // JsxText carries raw text as its `valueSource`, so a leading quote alone does not make a
532
+ // literal quote-delimited; requiring a matching pair keeps prose out of the tally.
533
+ return (quote === "'" || quote === '"') && source!.length > 1 && source!.endsWith(quote)
534
+ ? quote
535
+ : undefined;
536
+ }
537
+
538
+ /**
539
+ * Pick the quote character for a module specifier being added to `cu`, so that it agrees
540
+ * with the file it lands in. An existing specifier is the most direct precedent; a Prettier
541
+ * config states intent even where the file itself has not been formatted to match.
542
+ */
543
+ async function detectQuote(cu: JS.CompilationUnit): Promise<QuoteChar> {
544
+ // Static imports are top-level, so the highest-ranked signal needs no traversal to find.
545
+ for (const statement of cu.statements) {
546
+ const element = statement.element;
547
+ const topLevelQuote = element?.kind === JS.Kind.Import
548
+ ? quoteOf((element as JS.Import).moduleSpecifier?.element)
549
+ : element?.kind === JS.Kind.ExportDeclaration
550
+ ? quoteOf((element as JS.ExportDeclaration).moduleSpecifier?.element)
551
+ : undefined;
552
+ if (topLevelQuote) {
553
+ return topLevelQuote;
554
+ }
555
+ }
556
+
557
+ let importQuote: QuoteChar | undefined;
558
+ let requireQuote: QuoteChar | undefined;
559
+ let single = 0;
560
+ let double = 0;
561
+
562
+ await new class extends JavaScriptVisitor<void> {
563
+ override async visitImportDeclaration(jsImport: JS.Import, p: void): Promise<J | undefined> {
564
+ importQuote ??= quoteOf(jsImport.moduleSpecifier?.element);
565
+ return super.visitImportDeclaration(jsImport, p);
566
+ }
567
+
568
+ override async visitExportDeclaration(exportDeclaration: JS.ExportDeclaration, p: void): Promise<J | undefined> {
569
+ importQuote ??= quoteOf(exportDeclaration.moduleSpecifier?.element);
570
+ return super.visitExportDeclaration(exportDeclaration, p);
571
+ }
572
+
573
+ override async visitMethodInvocation(method: J.MethodInvocation, p: void): Promise<J | undefined> {
574
+ if (!method.select && method.name?.kind === J.Kind.Identifier &&
575
+ (method.name as J.Identifier).simpleName === 'require') {
576
+ requireQuote ??= quoteOf(method.arguments?.elements[0]?.element);
125
577
  }
126
- if (options.typeOnly) {
127
- throw new Error("Cannot combine sideEffectOnly with typeOnly");
578
+ return super.visitMethodInvocation(method, p);
579
+ }
580
+
581
+ override async visitJsxAttribute(attribute: JSX.Attribute, p: void): Promise<J | undefined> {
582
+ // JSX attribute quoting answers to Prettier's `jsxSingleQuote`, so it is no evidence
583
+ // about the quote style of ordinary strings.
584
+ return attribute;
585
+ }
586
+
587
+ override async visitLiteral(literal: J.Literal, p: void): Promise<J | undefined> {
588
+ const quote = quoteOf(literal);
589
+ if (quote === "'") {
590
+ single++;
591
+ } else if (quote === '"') {
592
+ double++;
128
593
  }
594
+ return super.visitLiteral(literal, p);
595
+ }
596
+ }().visit(cu, undefined);
597
+
598
+ // Imports outrank requires; that ordering is what makes the import-only scan above sound.
599
+ const specifierQuote = importQuote ?? requireQuote;
600
+ if (specifierQuote) {
601
+ return specifierQuote;
602
+ }
603
+
604
+ const prettier = getStyle(StyleKind.PrettierStyle, cu) as PrettierStyle | undefined;
605
+ if (prettier?.kind === StyleKind.PrettierStyle && !prettier.ignored) {
606
+ const singleQuote = prettier.config.singleQuote;
607
+ if (typeof singleQuote === 'boolean') {
608
+ return singleQuote ? "'" : '"';
129
609
  }
610
+ }
611
+
612
+ return double > single ? '"' : "'";
613
+ }
614
+
615
+ export class AddImport<P> extends JavaScriptVisitor<P> {
616
+ readonly module: string;
617
+ /** Set when the caller supplied the module specifier as a literal; printed verbatim. */
618
+ readonly moduleValueSource?: string;
619
+ readonly moduleUnicodeEscapes?: J.LiteralUnicodeEscape[];
620
+ readonly member?: string;
621
+ readonly alias?: string;
622
+ readonly onlyIfReferenced: boolean;
623
+ readonly sideEffectOnly: boolean;
624
+ readonly typeOnly: boolean;
625
+ readonly style?: ImportStyle;
626
+ readonly quoteStyle?: QuoteChar;
627
+ /** The local name this import binds; `undefined` for a side-effect import. */
628
+ readonly bindingName?: string;
629
+ /**
630
+ * A default import the request named no local name for, so whatever the file already calls
631
+ * that module answers it.
632
+ */
633
+ readonly anyNameAnswers: boolean;
634
+
635
+ constructor(options: AddImportOptions, bindingName?: string) {
636
+ super();
637
+
638
+ validate(options);
130
639
 
131
- this.module = options.module;
640
+ this.module = moduleNameOf(options.module);
641
+ this.moduleValueSource = typeof options.module === 'string' ? undefined : options.module.valueSource;
642
+ this.moduleUnicodeEscapes = typeof options.module === 'string' ? undefined : options.module.unicodeEscapes;
132
643
  this.member = options.member;
133
644
  this.alias = options.alias;
134
645
  this.onlyIfReferenced = options.onlyIfReferenced ?? true;
135
646
  this.sideEffectOnly = options.sideEffectOnly ?? false;
136
647
  this.typeOnly = options.typeOnly ?? false;
137
648
  this.style = options.style;
649
+ this.quoteStyle = options.quoteStyle;
650
+ this.bindingName = this.sideEffectOnly ? undefined : bindingName ?? derivedName(options);
651
+ this.anyNameAnswers = anyNameAnswers(options);
138
652
  }
139
653
 
140
654
  /**
@@ -144,16 +658,9 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
144
658
  if (moduleSpecifier.kind !== J.Kind.Literal) {
145
659
  return undefined;
146
660
  }
147
- return (moduleSpecifier as J.Literal).value?.toString();
661
+ return moduleNameOf(moduleSpecifier as J.Literal);
148
662
  }
149
663
 
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
664
 
158
665
  /**
159
666
  * Determine the appropriate import style based on file type and existing imports
@@ -233,7 +740,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
233
740
  const namedVar = varDecl.variables[0].element;
234
741
  const initializer = namedVar?.initializer?.element;
235
742
  if (initializer?.kind === J.Kind.MethodInvocation &&
236
- this.isRequireCall(initializer as J.MethodInvocation)) {
743
+ isRequireCall(initializer as J.MethodInvocation)) {
237
744
  hasCommonJSRequires = true;
238
745
  }
239
746
  }
@@ -310,12 +817,8 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
310
817
  const namedVar = varDecl.variables[0].element;
311
818
  const initializer = namedVar?.initializer?.element;
312
819
 
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
- }
820
+ if (requiredModule(initializer) === this.module) {
821
+ return ImportStyle.CommonJS;
319
822
  }
320
823
  }
321
824
  }
@@ -473,10 +976,10 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
473
976
  const existingElements = namedImports.elements.elements;
474
977
 
475
978
  // Find the correct insertion position (alphabetical, case-insensitive)
476
- const newName = (this.alias || this.member!).toLowerCase();
979
+ const newName = this.bindingName!.toLowerCase();
477
980
  let insertIndex = existingElements.findIndex(elem => {
478
981
  if (elem.element?.kind === JS.Kind.ImportSpecifier) {
479
- const name = this.getImportAlias(elem.element) || this.getImportName(elem.element);
982
+ const name = specifierBinding(elem.element as JS.ImportSpecifier)?.name ?? '';
480
983
  return newName.localeCompare(name.toLowerCase()) < 0;
481
984
  }
482
985
  return false;
@@ -634,91 +1137,27 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
634
1137
  * Check if the import matches what we're trying to add
635
1138
  */
636
1139
  private isMatchingImport(jsImport: JS.Import): boolean {
637
- // Check module specifier
638
1140
  const moduleSpecifier = jsImport.moduleSpecifier?.element;
639
- if (!moduleSpecifier) {
640
- return false;
641
- }
642
-
643
- const moduleName = this.getModuleName(moduleSpecifier);
644
- if (moduleName !== this.module) {
1141
+ if (!moduleSpecifier || this.getModuleName(moduleSpecifier) !== this.module) {
645
1142
  return false;
646
1143
  }
647
1144
 
1145
+ // A side-effect import is the clause-less one, and no import carrying bindings stands in
1146
+ // for it — nor it for them.
648
1147
  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;
1148
+ if (!importClause || this.sideEffectOnly) {
1149
+ return !importClause && this.sideEffectOnly;
660
1150
  }
661
1151
 
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
- }
1152
+ return importBindings(jsImport).some(binding => this.answeredBy(binding));
1153
+ }
720
1154
 
721
- return false;
1155
+ /** Whether a binding the file already has serves this request. */
1156
+ private answeredBy(binding: ModuleScopeBinding): boolean {
1157
+ return binding.module === this.module &&
1158
+ binding.member === memberName(this.member) &&
1159
+ binding.typeOnly === this.typeOnly &&
1160
+ (this.anyNameAnswers || binding.name === this.bindingName);
722
1161
  }
723
1162
 
724
1163
  /**
@@ -728,52 +1167,12 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
728
1167
  if (varDecl.variables.length !== 1) {
729
1168
  return false;
730
1169
  }
731
-
732
1170
  const namedVar = varDecl.variables[0].element;
733
- if (!namedVar) {
1171
+ const module = requiredModule(namedVar?.initializer?.element);
1172
+ if (module !== this.module) {
734
1173
  return false;
735
1174
  }
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;
1175
+ return requireBindings(namedVar!.name, module).some(binding => this.answeredBy(binding));
777
1176
  }
778
1177
 
779
1178
  /**
@@ -899,7 +1298,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
899
1298
  }
900
1299
 
901
1300
  // Step 2: Look for references that match
902
- const targetName = this.alias || this.member;
1301
+ const targetName = this.bindingName;
903
1302
  const targetModule = this.module;
904
1303
  let found = false;
905
1304
  const self = this;
@@ -1033,15 +1432,20 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1033
1432
  // For side-effect imports, use emptySpace since space comes from LeftPadded.before
1034
1433
  // For regular imports with import clause, use emptySpace since space comes from LeftPadded.before
1035
1434
  // 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
1435
+ // Note: value is the unquoted module name; valueSource and unicodeEscapes are its printed form
1436
+ let valueSource = this.moduleValueSource;
1437
+ if (valueSource === undefined) {
1438
+ const quote = this.quoteStyle ?? await detectQuote(compilationUnit);
1439
+ valueSource = `${quote}${this.module}${quote}`;
1440
+ }
1037
1441
  const moduleSpecifier: J.Literal = {
1038
1442
  id: randomId(),
1039
1443
  kind: J.Kind.Literal,
1040
1444
  prefix: this.sideEffectOnly ? emptySpace : singleSpace,
1041
1445
  markers: emptyMarkers,
1042
1446
  value: this.module,
1043
- valueSource: `'${this.module}'`,
1044
- unicodeEscapes: [],
1447
+ valueSource,
1448
+ unicodeEscapes: this.moduleUnicodeEscapes,
1045
1449
  type: undefined
1046
1450
  };
1047
1451
 
@@ -1069,7 +1473,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1069
1473
  prefix: singleSpace,
1070
1474
  markers: emptyMarkers,
1071
1475
  annotations: [],
1072
- simpleName: this.alias!,
1476
+ simpleName: this.bindingName!,
1073
1477
  type: undefined,
1074
1478
  fieldType: undefined
1075
1479
  };
@@ -1101,7 +1505,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1101
1505
  prefix: singleSpace,
1102
1506
  markers: emptyMarkers,
1103
1507
  annotations: [],
1104
- simpleName: this.alias || this.module,
1508
+ simpleName: this.bindingName!,
1105
1509
  type: undefined,
1106
1510
  fieldType: undefined
1107
1511
  };
@@ -1176,7 +1580,8 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1176
1580
  private createImportSpecifier(): JS.ImportSpecifier {
1177
1581
  let specifier: J.Identifier | JS.Alias;
1178
1582
 
1179
- if (this.alias) {
1583
+ // An alias equal to the member says nothing `{member}` alone does not.
1584
+ if (this.bindingName !== this.member) {
1180
1585
  // Aliased import: import { member as alias } from 'module'
1181
1586
  const propertyName: J.Identifier = {
1182
1587
  id: randomId(),
@@ -1195,7 +1600,7 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1195
1600
  prefix: singleSpace,
1196
1601
  markers: emptyMarkers,
1197
1602
  annotations: [],
1198
- simpleName: this.alias,
1603
+ simpleName: this.bindingName!,
1199
1604
  type: undefined,
1200
1605
  fieldType: undefined
1201
1606
  };
@@ -1286,51 +1691,6 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1286
1691
  return 0;
1287
1692
  }
1288
1693
 
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;
1296
- }
1297
1694
 
1298
- const firstArg = args[0].element;
1299
- if (!firstArg || firstArg.kind !== J.Kind.Literal || typeof (firstArg as J.Literal).value !== 'string') {
1300
- return undefined;
1301
- }
1302
-
1303
- return (firstArg as J.Literal).value?.toString();
1304
- }
1305
1695
 
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;
1319
- }
1320
- return '';
1321
- }
1322
-
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
- }
1333
- }
1334
- return undefined;
1335
- }
1336
1696
  }