@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.
- package/dist/java/formatting-utils.d.ts +8 -5
- package/dist/java/formatting-utils.d.ts.map +1 -1
- package/dist/java/formatting-utils.js +14 -10
- package/dist/java/formatting-utils.js.map +1 -1
- package/dist/java/type.d.ts +2 -0
- package/dist/java/type.d.ts.map +1 -1
- package/dist/java/type.js +5 -0
- package/dist/java/type.js.map +1 -1
- package/dist/javascript/add-import.d.ts +39 -20
- package/dist/javascript/add-import.d.ts.map +1 -1
- package/dist/javascript/add-import.js +513 -243
- package/dist/javascript/add-import.js.map +1 -1
- package/dist/javascript/assertions.d.ts +7 -0
- package/dist/javascript/assertions.d.ts.map +1 -1
- package/dist/javascript/assertions.js +31 -5
- package/dist/javascript/assertions.js.map +1 -1
- package/dist/javascript/comparator.d.ts +5 -0
- package/dist/javascript/comparator.d.ts.map +1 -1
- package/dist/javascript/comparator.js +34 -9
- package/dist/javascript/comparator.js.map +1 -1
- package/dist/javascript/format/format.d.ts.map +1 -1
- package/dist/javascript/format/format.js +8 -9
- package/dist/javascript/format/format.js.map +1 -1
- package/dist/javascript/format/tabs-and-indents-visitor.d.ts +2 -0
- package/dist/javascript/format/tabs-and-indents-visitor.d.ts.map +1 -1
- package/dist/javascript/format/tabs-and-indents-visitor.js +17 -11
- package/dist/javascript/format/tabs-and-indents-visitor.js.map +1 -1
- package/dist/javascript/node-resolution-result.d.ts +19 -1
- package/dist/javascript/node-resolution-result.d.ts.map +1 -1
- package/dist/javascript/node-resolution-result.js +37 -2
- package/dist/javascript/node-resolution-result.js.map +1 -1
- package/dist/javascript/package-exported-types.d.ts.map +1 -1
- package/dist/javascript/package-exported-types.js +56 -72
- package/dist/javascript/package-exported-types.js.map +1 -1
- package/dist/javascript/parser.d.ts.map +1 -1
- package/dist/javascript/parser.js +10 -3
- package/dist/javascript/parser.js.map +1 -1
- package/dist/javascript/recipes/change-import.d.ts.map +1 -1
- package/dist/javascript/recipes/change-import.js +4 -8
- package/dist/javascript/recipes/change-import.js.map +1 -1
- package/dist/javascript/style.d.ts.map +1 -1
- package/dist/javascript/style.js +3 -10
- package/dist/javascript/style.js.map +1 -1
- package/dist/javascript/templating/bindings.d.ts +19 -0
- package/dist/javascript/templating/bindings.d.ts.map +1 -0
- package/dist/javascript/templating/bindings.js +121 -0
- package/dist/javascript/templating/bindings.js.map +1 -0
- package/dist/javascript/templating/comparator.d.ts.map +1 -1
- package/dist/javascript/templating/comparator.js +5 -6
- package/dist/javascript/templating/comparator.js.map +1 -1
- package/dist/javascript/templating/engine.d.ts +18 -7
- package/dist/javascript/templating/engine.d.ts.map +1 -1
- package/dist/javascript/templating/engine.js +70 -42
- 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 +13 -10
- 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 +14 -5
- package/dist/javascript/templating/placeholder-replacement.js.map +1 -1
- package/dist/javascript/templating/rewrite.d.ts.map +1 -1
- package/dist/javascript/templating/rewrite.js +14 -18
- package/dist/javascript/templating/rewrite.js.map +1 -1
- package/dist/javascript/templating/template.d.ts +8 -0
- package/dist/javascript/templating/template.d.ts.map +1 -1
- package/dist/javascript/templating/template.js +43 -3
- package/dist/javascript/templating/template.js.map +1 -1
- package/dist/javascript/templating/types.d.ts +47 -1
- package/dist/javascript/templating/types.d.ts.map +1 -1
- package/dist/javascript/templating/utils.d.ts +5 -0
- package/dist/javascript/templating/utils.d.ts.map +1 -1
- package/dist/javascript/templating/utils.js +17 -0
- package/dist/javascript/templating/utils.js.map +1 -1
- package/dist/javascript/type-mapping.d.ts +1 -0
- package/dist/javascript/type-mapping.d.ts.map +1 -1
- package/dist/javascript/type-mapping.js +52 -2
- package/dist/javascript/type-mapping.js.map +1 -1
- package/dist/rewrite-javascript-version.txt +1 -1
- package/dist/style.d.ts +2 -0
- package/dist/style.d.ts.map +1 -1
- package/dist/style.js +5 -0
- package/dist/style.js.map +1 -1
- package/dist/test/rewrite-test.d.ts +12 -0
- package/dist/test/rewrite-test.d.ts.map +1 -1
- package/dist/test/rewrite-test.js +40 -3
- package/dist/test/rewrite-test.js.map +1 -1
- package/package.json +1 -1
- package/src/java/formatting-utils.ts +14 -10
- package/src/java/type.ts +5 -0
- package/src/javascript/add-import.ts +590 -230
- package/src/javascript/assertions.ts +33 -5
- package/src/javascript/comparator.ts +39 -10
- package/src/javascript/format/format.ts +9 -10
- package/src/javascript/format/tabs-and-indents-visitor.ts +18 -12
- package/src/javascript/node-resolution-result.ts +59 -4
- package/src/javascript/package-exported-types.ts +60 -74
- package/src/javascript/parser.ts +10 -3
- package/src/javascript/recipes/change-import.ts +4 -7
- package/src/javascript/style.ts +3 -11
- package/src/javascript/templating/bindings.ts +129 -0
- package/src/javascript/templating/comparator.ts +5 -6
- package/src/javascript/templating/engine.ts +82 -44
- package/src/javascript/templating/index.ts +2 -0
- package/src/javascript/templating/pattern.ts +16 -15
- package/src/javascript/templating/placeholder-replacement.ts +16 -5
- package/src/javascript/templating/rewrite.ts +16 -18
- package/src/javascript/templating/template.ts +45 -3
- package/src/javascript/templating/types.ts +53 -1
- package/src/javascript/templating/utils.ts +17 -0
- package/src/javascript/type-mapping.ts +54 -2
- package/src/style.ts +5 -0
- 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
|
-
|
|
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.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
314
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
663
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
}
|