@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.
- package/dist/javascript/add-import.d.ts +68 -34
- package/dist/javascript/add-import.d.ts.map +1 -1
- package/dist/javascript/add-import.js +322 -160
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/amd.d.ts +138 -0
- package/dist/javascript/amd.d.ts.map +1 -0
- package/dist/javascript/amd.js +768 -0
- package/dist/javascript/amd.js.map +1 -0
- package/dist/javascript/binding.d.ts +94 -0
- package/dist/javascript/binding.d.ts.map +1 -0
- package/dist/javascript/binding.js +335 -0
- package/dist/javascript/binding.js.map +1 -0
- package/dist/javascript/index.d.ts +8 -1
- package/dist/javascript/index.d.ts.map +1 -1
- package/dist/javascript/index.js +26 -1
- package/dist/javascript/index.js.map +1 -1
- package/dist/javascript/parser-utils.d.ts.map +1 -1
- package/dist/javascript/parser-utils.js +2 -0
- package/dist/javascript/parser-utils.js.map +1 -1
- package/dist/javascript/parser.d.ts +7 -1
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +7 -1
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/recipes/change-import.d.ts +2 -4
- package/dist/javascript/recipes/change-import.d.ts.map +1 -1
- package/dist/javascript/recipes/change-import.js +8 -240
- package/dist/javascript/recipes/change-import.js.map +1 -1
- package/dist/javascript/remove-import.d.ts +2 -28
- package/dist/javascript/remove-import.d.ts.map +1 -1
- package/dist/javascript/remove-import.js +19 -57
- package/dist/javascript/remove-import.js.map +1 -1
- package/dist/javascript/scope.d.ts +49 -0
- package/dist/javascript/scope.d.ts.map +1 -0
- package/dist/javascript/scope.js +324 -0
- package/dist/javascript/scope.js.map +1 -0
- package/dist/javascript/templating/bindings.d.ts +0 -11
- package/dist/javascript/templating/bindings.d.ts.map +1 -1
- package/dist/javascript/templating/bindings.js +0 -29
- package/dist/javascript/templating/bindings.js.map +1 -1
- package/dist/javascript/templating/comparator.d.ts +10 -0
- package/dist/javascript/templating/comparator.d.ts.map +1 -1
- package/dist/javascript/templating/comparator.js +43 -14
- package/dist/javascript/templating/comparator.js.map +1 -1
- package/dist/javascript/templating/engine.d.ts +17 -4
- package/dist/javascript/templating/engine.d.ts.map +1 -1
- package/dist/javascript/templating/engine.js +43 -17
- package/dist/javascript/templating/engine.js.map +1 -1
- package/dist/javascript/templating/index.d.ts +1 -1
- package/dist/javascript/templating/index.d.ts.map +1 -1
- package/dist/javascript/templating/index.js.map +1 -1
- package/dist/javascript/templating/pattern.d.ts.map +1 -1
- package/dist/javascript/templating/pattern.js +2 -2
- package/dist/javascript/templating/pattern.js.map +1 -1
- package/dist/javascript/templating/placeholder-replacement.d.ts +2 -0
- package/dist/javascript/templating/placeholder-replacement.d.ts.map +1 -1
- package/dist/javascript/templating/placeholder-replacement.js +10 -5
- package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
- package/dist/javascript/templating/precedence.d.ts +12 -4
- package/dist/javascript/templating/precedence.d.ts.map +1 -1
- package/dist/javascript/templating/precedence.js +18 -11
- package/dist/javascript/templating/precedence.js.map +1 -1
- package/dist/javascript/templating/rewrite.d.ts.map +1 -1
- package/dist/javascript/templating/rewrite.js +7 -1
- package/dist/javascript/templating/rewrite.js.map +1 -1
- package/dist/javascript/templating/template.d.ts +7 -1
- package/dist/javascript/templating/template.d.ts.map +1 -1
- package/dist/javascript/templating/template.js +48 -28
- package/dist/javascript/templating/template.js.map +1 -1
- package/dist/javascript/templating/types.d.ts +10 -18
- package/dist/javascript/templating/types.d.ts.map +1 -1
- package/dist/javascript/templating/utils.d.ts +1 -1
- package/dist/javascript/templating/utils.d.ts.map +1 -1
- package/dist/javascript/templating/utils.js +3 -2
- package/dist/javascript/templating/utils.js.map +1 -1
- package/dist/rewrite-javascript-version.txt +1 -1
- package/package.json +2 -2
- package/src/javascript/add-import.ts +342 -198
- package/src/javascript/amd.ts +915 -0
- package/src/javascript/binding.ts +449 -0
- package/src/javascript/index.ts +12 -1
- package/src/javascript/parser-utils.ts +2 -0
- package/src/javascript/parser.ts +13 -0
- package/src/javascript/recipes/change-import.ts +10 -260
- package/src/javascript/remove-import.ts +20 -56
- package/src/javascript/scope.ts +338 -0
- package/src/javascript/templating/bindings.ts +0 -30
- package/src/javascript/templating/comparator.ts +52 -14
- package/src/javascript/templating/engine.ts +70 -28
- package/src/javascript/templating/index.ts +0 -1
- package/src/javascript/templating/pattern.ts +4 -2
- package/src/javascript/templating/placeholder-replacement.ts +16 -6
- package/src/javascript/templating/precedence.ts +21 -13
- package/src/javascript/templating/rewrite.ts +9 -1
- package/src/javascript/templating/template.ts +62 -29
- package/src/javascript/templating/types.ts +11 -18
- 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 {
|
|
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
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
|
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
|
|
103
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
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
|
|
469
|
-
*
|
|
470
|
-
*
|
|
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(
|
|
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
|
+
}
|