@openrewrite/rewrite 8.91.1 → 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 (96) hide show
  1. package/dist/javascript/add-import.d.ts +68 -34
  2. package/dist/javascript/add-import.d.ts.map +1 -1
  3. package/dist/javascript/add-import.js +322 -160
  4. package/dist/javascript/add-import.js.map +1 -1
  5. package/dist/javascript/amd.d.ts +138 -0
  6. package/dist/javascript/amd.d.ts.map +1 -0
  7. package/dist/javascript/amd.js +768 -0
  8. package/dist/javascript/amd.js.map +1 -0
  9. package/dist/javascript/binding.d.ts +94 -0
  10. package/dist/javascript/binding.d.ts.map +1 -0
  11. package/dist/javascript/binding.js +335 -0
  12. package/dist/javascript/binding.js.map +1 -0
  13. package/dist/javascript/index.d.ts +8 -1
  14. package/dist/javascript/index.d.ts.map +1 -1
  15. package/dist/javascript/index.js +26 -1
  16. package/dist/javascript/index.js.map +1 -1
  17. package/dist/javascript/parser-utils.d.ts.map +1 -1
  18. package/dist/javascript/parser-utils.js +2 -0
  19. package/dist/javascript/parser-utils.js.map +1 -1
  20. package/dist/javascript/parser.d.ts +7 -1
  21. package/dist/javascript/parser.d.ts.map +1 -1
  22. package/dist/javascript/parser.js +7 -1
  23. package/dist/javascript/parser.js.map +1 -1
  24. package/dist/javascript/recipes/change-import.d.ts +2 -4
  25. package/dist/javascript/recipes/change-import.d.ts.map +1 -1
  26. package/dist/javascript/recipes/change-import.js +8 -240
  27. package/dist/javascript/recipes/change-import.js.map +1 -1
  28. package/dist/javascript/remove-import.d.ts +2 -28
  29. package/dist/javascript/remove-import.d.ts.map +1 -1
  30. package/dist/javascript/remove-import.js +19 -57
  31. package/dist/javascript/remove-import.js.map +1 -1
  32. package/dist/javascript/scope.d.ts +49 -0
  33. package/dist/javascript/scope.d.ts.map +1 -0
  34. package/dist/javascript/scope.js +324 -0
  35. package/dist/javascript/scope.js.map +1 -0
  36. package/dist/javascript/templating/bindings.d.ts +0 -11
  37. package/dist/javascript/templating/bindings.d.ts.map +1 -1
  38. package/dist/javascript/templating/bindings.js +0 -29
  39. package/dist/javascript/templating/bindings.js.map +1 -1
  40. package/dist/javascript/templating/comparator.d.ts +10 -0
  41. package/dist/javascript/templating/comparator.d.ts.map +1 -1
  42. package/dist/javascript/templating/comparator.js +43 -14
  43. package/dist/javascript/templating/comparator.js.map +1 -1
  44. package/dist/javascript/templating/engine.d.ts +17 -4
  45. package/dist/javascript/templating/engine.d.ts.map +1 -1
  46. package/dist/javascript/templating/engine.js +43 -17
  47. package/dist/javascript/templating/engine.js.map +1 -1
  48. package/dist/javascript/templating/index.d.ts +1 -1
  49. package/dist/javascript/templating/index.d.ts.map +1 -1
  50. package/dist/javascript/templating/index.js.map +1 -1
  51. package/dist/javascript/templating/pattern.d.ts.map +1 -1
  52. package/dist/javascript/templating/pattern.js +2 -2
  53. package/dist/javascript/templating/pattern.js.map +1 -1
  54. package/dist/javascript/templating/placeholder-replacement.d.ts +2 -0
  55. package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
  56. package/dist/javascript/templating/placeholder-replacement.js +10 -5
  57. package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
  58. package/dist/javascript/templating/precedence.d.ts +12 -4
  59. package/dist/javascript/templating/precedence.d.ts.map +1 -1
  60. package/dist/javascript/templating/precedence.js +18 -11
  61. package/dist/javascript/templating/precedence.js.map +1 -1
  62. package/dist/javascript/templating/rewrite.d.ts.map +1 -1
  63. package/dist/javascript/templating/rewrite.js +7 -1
  64. package/dist/javascript/templating/rewrite.js.map +1 -1
  65. package/dist/javascript/templating/template.d.ts +7 -1
  66. package/dist/javascript/templating/template.d.ts.map +1 -1
  67. package/dist/javascript/templating/template.js +48 -28
  68. package/dist/javascript/templating/template.js.map +1 -1
  69. package/dist/javascript/templating/types.d.ts +10 -18
  70. package/dist/javascript/templating/types.d.ts.map +1 -1
  71. package/dist/javascript/templating/utils.d.ts +1 -1
  72. package/dist/javascript/templating/utils.d.ts.map +1 -1
  73. package/dist/javascript/templating/utils.js +3 -2
  74. package/dist/javascript/templating/utils.js.map +1 -1
  75. package/dist/rewrite-javascript-version.txt +1 -1
  76. package/package.json +2 -2
  77. package/src/javascript/add-import.ts +342 -198
  78. package/src/javascript/amd.ts +915 -0
  79. package/src/javascript/binding.ts +449 -0
  80. package/src/javascript/index.ts +12 -1
  81. package/src/javascript/parser-utils.ts +2 -0
  82. package/src/javascript/parser.ts +13 -0
  83. package/src/javascript/recipes/change-import.ts +10 -260
  84. package/src/javascript/remove-import.ts +20 -56
  85. package/src/javascript/scope.ts +338 -0
  86. package/src/javascript/templating/bindings.ts +0 -30
  87. package/src/javascript/templating/comparator.ts +52 -14
  88. package/src/javascript/templating/engine.ts +70 -28
  89. package/src/javascript/templating/index.ts +0 -1
  90. package/src/javascript/templating/pattern.ts +4 -2
  91. package/src/javascript/templating/placeholder-replacement.ts +16 -6
  92. package/src/javascript/templating/precedence.ts +21 -13
  93. package/src/javascript/templating/rewrite.ts +9 -1
  94. package/src/javascript/templating/template.ts +62 -29
  95. package/src/javascript/templating/types.ts +11 -18
  96. package/src/javascript/templating/utils.ts +4 -2
@@ -1,10 +1,11 @@
1
1
  import {JavaScriptVisitor} from "./visitor";
2
- import {emptySpace, J, rightPadded, singleSpace, space, Statement, Type} from "../java";
2
+ import {ElementRemovalFormatter, emptySpace, isIdentifier, J, rightPadded, singleSpace, space, Statement, Type} from "../java";
3
3
  import {JS, JSX} from "./tree";
4
4
  import {randomId} from "../uuid";
5
5
  import {emptyMarkers, markers} from "../markers";
6
6
  import {getStyle, PrettierStyle, SpacesStyle, StyleKind} from "./style";
7
- import {Cursor} from "../tree";
7
+ import {bindingNames, compilationUnitOf, cursorOf, declarationsOf, deconflict, namesDeclaredIn} from "./scope";
8
+ import {create as produce, Draft} from "mutative";
8
9
 
9
10
  export type QuoteChar = "'" | '"';
10
11
 
@@ -65,49 +66,15 @@ export interface AddImportOptions {
65
66
  }
66
67
 
67
68
  /**
68
- * Register an AddImport visitor to add an import statement to a JavaScript/TypeScript file
69
- * @param visitor The visitor to add the import addition to
70
- * @param options Configuration options for the import to add
71
- * @returns The local name the module is bound to: an existing binding's where one answers this
72
- * request, otherwise the name the new import will use, suffixed if the file already binds it. `onlyIfReferenced` defaults to true, so
73
- * the import may never appear, and the name is then what it would have gone by. A side-effect
74
- * import binds no name and returns `undefined`.
75
- *
76
- * @example
77
- * // Add a named import
78
- * maybeAddImport(visitor, { module: 'fs', member: 'readFile' });
79
- *
80
- * @example
81
- * // Add a default import using the 'default' member specifier
82
- * maybeAddImport(visitor, { module: 'react', member: 'default', alias: 'React' });
83
- *
84
- * @example
85
- * // Add a default import (legacy way, without specifying member)
86
- * maybeAddImport(visitor, { module: 'react', alias: 'React' });
87
- *
88
- * @example
89
- * // Add a namespace import
90
- * maybeAddImport(visitor, { module: 'crypto', member: '*', alias: 'crypto' });
91
- *
92
- * @example
93
- * // Add a side-effect import
94
- * maybeAddImport(visitor, { module: 'core-js/stable', sideEffectOnly: true });
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.
95
73
  */
96
- export function maybeAddImport(
97
- visitor: JavaScriptVisitor<any>,
98
- options: AddImportOptions & { sideEffectOnly: true }
99
- ): undefined;
100
- export function maybeAddImport(
74
+ export function bindImport(
101
75
  visitor: JavaScriptVisitor<any>,
102
- options: AddImportOptions & { sideEffectOnly?: false }
103
- ): string;
104
- export function maybeAddImport(
105
- visitor: JavaScriptVisitor<any>,
106
- options: AddImportOptions
107
- ): string | undefined;
108
- export function maybeAddImport(
109
- visitor: JavaScriptVisitor<any>,
110
- options: AddImportOptions
76
+ options: AddImportOptions,
77
+ refuseCreate?: boolean
111
78
  ): string | undefined {
112
79
  validate(options);
113
80
  const module = moduleNameOf(options.module);
@@ -132,30 +99,38 @@ export function maybeAddImport(
132
99
  }
133
100
 
134
101
  if (sideEffectOnly) {
102
+ if (refuseCreate) {
103
+ return undefined;
104
+ }
135
105
  visitor.afterVisit.push(new AddImport(options));
136
106
  return undefined;
137
107
  }
138
108
 
139
109
  // A pinned alias is the whole answer, so the file has no say and nothing below needs to read it.
140
110
  if (options.alias) {
111
+ if (refuseCreate) {
112
+ return undefined;
113
+ }
141
114
  visitor.afterVisit.push(new AddImport(options, options.alias));
142
115
  return options.alias;
143
116
  }
144
117
 
145
118
  const derived = derivedName(options);
146
- const cu = compilationUnitOf(visitor);
119
+ const cursor = cursorOf(visitor);
120
+ const cu = cursor && compilationUnitOf(cursor);
147
121
  if (!cu) {
122
+ if (refuseCreate) {
123
+ return undefined;
124
+ }
148
125
  visitor.afterVisit.push(new AddImport(options, derived));
149
126
  return derived;
150
127
  }
151
128
 
152
- const bindings = bindingsInScope(cu, cursorOf(visitor));
153
-
154
129
  // An import already serving this request answers it; queuing one would, on the next cycle,
155
130
  // derive a suffixed name from the binding this call just added. A caller that named a preference
156
131
  // takes whatever comes back; one that did not assumes the name it derived, so a binding under
157
132
  // any other name would leave the references it emits unbound.
158
- for (const binding of bindings) {
133
+ for (const binding of moduleScopeBindings(cu)) {
159
134
  if (binding.module === module && binding.member === memberName(options.member) &&
160
135
  binding.typeOnly === typeOnly &&
161
136
  (options.preferredName !== undefined || anyNameAnswers(options) ||
@@ -164,7 +139,15 @@ export function maybeAddImport(
164
139
  }
165
140
  }
166
141
 
167
- const name = deconflict(derived, takenNames(bindings, visitor));
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));
168
151
  visitor.afterVisit.push(new AddImport(options, name));
169
152
  return name;
170
153
  }
@@ -216,12 +199,12 @@ function derivedName(options: AddImportOptions): string {
216
199
  }
217
200
 
218
201
  /** `'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 {
202
+ export function memberName(member: string | undefined): string | undefined {
220
203
  return member === 'default' ? undefined : member;
221
204
  }
222
205
 
223
206
  /** A name introduced into the file's module scope, and where it came from when that is an import. */
224
- interface ModuleScopeBinding {
207
+ export interface ModuleScopeBinding {
225
208
  name: string;
226
209
  module?: string;
227
210
  /** Carries the {@link AddImportOptions.member} spelling, so `undefined` means a default import. */
@@ -229,101 +212,27 @@ interface ModuleScopeBinding {
229
212
  typeOnly?: boolean;
230
213
  }
231
214
 
232
- function cursorOf(visitor: JavaScriptVisitor<any>): Cursor | undefined {
233
- return (visitor as unknown as { cursor?: Cursor }).cursor;
234
- }
235
-
236
- function compilationUnitOf(visitor: JavaScriptVisitor<any>): JS.CompilationUnit | undefined {
237
- // `cursor` is protected on `TreeVisitor`, and the `maybeAddImport`/`maybeRemoveImport`
238
- // API is free functions, so reaching it takes a cast.
239
- return cursorOf(visitor)?.firstEnclosing((v): v is JS.CompilationUnit => v?.kind === JS.Kind.CompilationUnit);
240
- }
241
-
242
- /** Every name in scope at `cursor`: what the file binds, and what each block enclosing it declares. */
243
- function bindingsInScope(cu: JS.CompilationUnit, cursor: Cursor | undefined): ModuleScopeBinding[] {
244
- const bindings = statementBindings(cu.statements);
245
- // A local shadows an import for the code the template lands in, so a name a block declares is
246
- // taken there even though the module never answers for it.
247
- for (let c = cursor; c && c.value !== cu; c = c.parent) {
248
- for (const name of scopeNames(c.value)) {
249
- bindings.push({name});
250
- }
251
- }
252
- return bindings;
253
- }
254
-
255
- /** The names a scope introduces directly: a block's declarations, or a function's parameters. */
256
- function scopeNames(scope: unknown): string[] {
257
- const node = scope as J | undefined;
258
- if (node?.kind === J.Kind.Block) {
259
- return statementBindings((node as J.Block).statements).map(binding => binding.name);
260
- }
261
- if (node?.kind === J.Kind.MethodDeclaration) {
262
- return (node as J.MethodDeclaration).parameters.elements
263
- .flatMap(param => param.element?.kind === J.Kind.VariableDeclarations
264
- ? (param.element as J.VariableDeclarations).variables.flatMap(v => patternNames(v.element?.name))
265
- : patternNames(param.element))
266
- .map(bound => bound.name);
267
- }
268
- if (node?.kind === J.Kind.Lambda) {
269
- return (node as J.Lambda).parameters.parameters
270
- .flatMap(param => param.element?.kind === J.Kind.VariableDeclarations
271
- ? (param.element as J.VariableDeclarations).variables.flatMap(v => patternNames(v.element?.name))
272
- : patternNames(param.element))
273
- .map(bound => bound.name);
274
- }
275
- return [];
276
- }
277
-
278
- /** Imports are top-level statements and so is anything that can shadow one, so a flat scan sees every name. */
279
- function statementBindings(statements: J.RightPadded<Statement>[]): ModuleScopeBinding[] {
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[] {
280
217
  const bindings: ModuleScopeBinding[] = [];
281
218
 
282
- const declaredBy = (name: J | undefined): void => {
283
- for (const bound of patternNames(name)) {
284
- bindings.push({name: bound.name});
285
- }
286
- };
287
-
288
219
  const declaredByVariables = (varDecl: J.VariableDeclarations): void => {
289
220
  for (const variable of varDecl.variables) {
290
221
  const required = requiredModule(variable.element?.initializer?.element);
291
222
  if (required !== undefined) {
292
223
  bindings.push(...requireBindings(variable.element?.name, required));
293
- } else {
294
- declaredBy(variable.element?.name);
295
224
  }
296
225
  }
297
226
  };
298
227
 
299
- for (const stmt of statements) {
228
+ for (const stmt of cu.statements) {
300
229
  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;
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);
327
236
  }
328
237
  }
329
238
 
@@ -347,7 +256,7 @@ function isRequireCall(methodInv: J.MethodInvocation): boolean {
347
256
  * The module a `require(...)` call loads. `obj.require('x')` selects a method rather than loading a
348
257
  * module, and a specifier that is not a string literal names none that can be read here.
349
258
  */
350
- function requiredModuleOf(methodInv: J.MethodInvocation): string | undefined {
259
+ export function requiredModuleOf(methodInv: J.MethodInvocation): string | undefined {
351
260
  if (!isRequireCall(methodInv)) {
352
261
  return undefined;
353
262
  }
@@ -357,55 +266,13 @@ function requiredModuleOf(methodInv: J.MethodInvocation): string | undefined {
357
266
  : undefined;
358
267
  }
359
268
 
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
269
  /** A `require` binds a module the way an import does, so the pool records it the same way. */
403
270
  function requireBindings(pattern: J | undefined, module: string): ModuleScopeBinding[] {
404
271
  // A whole-module require binds no member, exactly as a default import does.
405
272
  if (pattern?.kind === J.Kind.Identifier) {
406
273
  return [{name: (pattern as J.Identifier).simpleName, module, member: undefined, typeOnly: false}];
407
274
  }
408
- return patternNames(pattern).map(bound => bound.member === undefined
275
+ return bindingNames(pattern).map(bound => bound.member === undefined
409
276
  ? {name: bound.name}
410
277
  : {name: bound.name, module, member: bound.member, typeOnly: false});
411
278
  }
@@ -465,12 +332,12 @@ function importBindings(jsImport: JS.Import): ModuleScopeBinding[] {
465
332
  }
466
333
 
467
334
  /**
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.
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.
471
338
  */
472
- function takenNames(bindings: ModuleScopeBinding[], visitor: JavaScriptVisitor<any>): Set<string> {
473
- const taken = new Set<string>();
339
+ function takenNames(inScope: ReadonlySet<string>, visitor: JavaScriptVisitor<any>): Set<string> {
340
+ const taken = new Set<string>(inScope);
474
341
 
475
342
  for (const v of visitor.afterVisit || []) {
476
343
  if (v instanceof AddImport && v.bindingName) {
@@ -478,30 +345,15 @@ function takenNames(bindings: ModuleScopeBinding[], visitor: JavaScriptVisitor<a
478
345
  }
479
346
  }
480
347
 
481
- for (const binding of bindings) {
482
- taken.add(binding.name);
483
- }
484
-
485
348
  return taken;
486
349
  }
487
350
 
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
351
  /**
500
352
  * The parser lifts surrogate pairs out of `valueSource` into `unicodeEscapes` and, when it does,
501
353
  * sets `value` to the quoted source (`parser.ts` `mapLiteral`), so neither field alone is the
502
354
  * module name. Reuniting them and stripping the quotes yields it for either shape of literal.
503
355
  */
504
- function moduleNameOf(module: string | J.Literal): string {
356
+ export function moduleNameOf(module: string | J.Literal): string {
505
357
  if (typeof module === 'string') {
506
358
  return module;
507
359
  }
@@ -1694,3 +1546,295 @@ export class AddImport<P> extends JavaScriptVisitor<P> {
1694
1546
 
1695
1547
 
1696
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;
1590
+ }
1591
+ }
1592
+ return undefined;
1593
+ }
1594
+
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;
1616
+ }
1617
+ }
1618
+ return false;
1619
+ }
1620
+
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;
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
+ }
1641
+
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)};
1664
+ }
1665
+ }
1666
+ return undefined;
1667
+ }
1668
+
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;
1711
+ }
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
+ });
1839
+ }
1840
+ }