@openrewrite/rewrite 8.66.0-20251027-112229 → 8.66.0-20251027-133754
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/tree.d.ts +1 -0
- package/dist/java/tree.d.ts.map +1 -1
- package/dist/java/tree.js +6 -1
- package/dist/java/tree.js.map +1 -1
- package/dist/javascript/add-import.d.ts +102 -0
- package/dist/javascript/add-import.d.ts.map +1 -0
- package/dist/javascript/add-import.js +789 -0
- package/dist/javascript/add-import.js.map +1 -0
- package/dist/javascript/index.d.ts +1 -0
- package/dist/javascript/index.d.ts.map +1 -1
- package/dist/javascript/index.js +1 -0
- package/dist/javascript/index.js.map +1 -1
- package/dist/javascript/type-mapping.js +1 -1
- package/dist/javascript/type-mapping.js.map +1 -1
- package/dist/version.txt +1 -1
- package/package.json +1 -1
- package/src/java/tree.ts +6 -0
- package/src/javascript/add-import.ts +871 -0
- package/src/javascript/index.ts +1 -0
- package/src/javascript/type-mapping.ts +1 -1
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
import {JavaScriptVisitor} from "./visitor";
|
|
2
|
+
import {J, emptySpace, rightPadded, space, Statement, singleSpace, Type} from "../java";
|
|
3
|
+
import {JS} from "./tree";
|
|
4
|
+
import {randomId} from "../uuid";
|
|
5
|
+
import {emptyMarkers, markers} from "../markers";
|
|
6
|
+
import {ExecutionContext} from "../execution";
|
|
7
|
+
|
|
8
|
+
export enum ImportStyle {
|
|
9
|
+
ES6Named, // import { x } from 'module'
|
|
10
|
+
ES6Namespace, // import * as x from 'module'
|
|
11
|
+
ES6Default, // import x from 'module'
|
|
12
|
+
CommonJS // const x = require('module')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AddImportOptions {
|
|
16
|
+
/** The module name (e.g., 'fs') to import from */
|
|
17
|
+
target: string;
|
|
18
|
+
|
|
19
|
+
/** Optionally, the specific member to import from the module.
|
|
20
|
+
* If not specified, adds a default import or namespace import */
|
|
21
|
+
member?: string;
|
|
22
|
+
|
|
23
|
+
/** Optional alias for the imported member */
|
|
24
|
+
alias?: string;
|
|
25
|
+
|
|
26
|
+
/** If true, only add the import if the member is actually used in the file. Default: true */
|
|
27
|
+
onlyIfReferenced?: boolean;
|
|
28
|
+
|
|
29
|
+
/** Optional import style to use. If not specified, auto-detects from file and existing imports */
|
|
30
|
+
style?: ImportStyle;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Register an AddImport visitor to add an import statement to a JavaScript/TypeScript file
|
|
35
|
+
* @param visitor The visitor to add the import addition to
|
|
36
|
+
* @param options Configuration options for the import to add
|
|
37
|
+
*/
|
|
38
|
+
export function maybeAddImport(
|
|
39
|
+
visitor: JavaScriptVisitor<any>,
|
|
40
|
+
options: AddImportOptions
|
|
41
|
+
) {
|
|
42
|
+
for (const v of visitor.afterVisit || []) {
|
|
43
|
+
if (v instanceof AddImport &&
|
|
44
|
+
v.target === options.target &&
|
|
45
|
+
v.member === options.member &&
|
|
46
|
+
v.alias === options.alias) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
visitor.afterVisit.push(new AddImport(options));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class AddImport<P> extends JavaScriptVisitor<P> {
|
|
54
|
+
readonly target: string;
|
|
55
|
+
readonly member?: string;
|
|
56
|
+
readonly alias?: string;
|
|
57
|
+
readonly onlyIfReferenced: boolean;
|
|
58
|
+
readonly style?: ImportStyle;
|
|
59
|
+
|
|
60
|
+
constructor(options: AddImportOptions) {
|
|
61
|
+
super();
|
|
62
|
+
this.target = options.target;
|
|
63
|
+
this.member = options.member;
|
|
64
|
+
this.alias = options.alias;
|
|
65
|
+
this.onlyIfReferenced = options.onlyIfReferenced ?? true;
|
|
66
|
+
this.style = options.style;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Extract module name from a module specifier literal
|
|
71
|
+
*/
|
|
72
|
+
private getModuleName(moduleSpecifier: J): string | undefined {
|
|
73
|
+
if (moduleSpecifier.kind !== J.Kind.Literal) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return (moduleSpecifier as J.Literal).value?.toString();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if a method invocation is a require() call
|
|
81
|
+
*/
|
|
82
|
+
private isRequireCall(methodInv: J.MethodInvocation): boolean {
|
|
83
|
+
return methodInv.name?.kind === J.Kind.Identifier &&
|
|
84
|
+
(methodInv.name as J.Identifier).simpleName === 'require';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Determine the appropriate import style based on file type and existing imports
|
|
89
|
+
*/
|
|
90
|
+
private determineImportStyle(compilationUnit: JS.CompilationUnit): ImportStyle {
|
|
91
|
+
// If style was explicitly provided, use it
|
|
92
|
+
if (this.style !== undefined) {
|
|
93
|
+
return this.style;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check the file extension from sourcePath
|
|
97
|
+
const sourcePath = compilationUnit.sourcePath;
|
|
98
|
+
const isTypeScript = sourcePath.endsWith('.ts') ||
|
|
99
|
+
sourcePath.endsWith('.tsx') ||
|
|
100
|
+
sourcePath.endsWith('.mts') ||
|
|
101
|
+
sourcePath.endsWith('.cts');
|
|
102
|
+
|
|
103
|
+
// Check for .cjs extension - must use CommonJS
|
|
104
|
+
if (sourcePath.endsWith('.cjs')) {
|
|
105
|
+
return ImportStyle.CommonJS;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// First, check if there's already an import from the same module
|
|
109
|
+
// and match that style
|
|
110
|
+
const existingStyleForModule = this.findExistingImportStyleForModule(compilationUnit);
|
|
111
|
+
if (existingStyleForModule !== null) {
|
|
112
|
+
return existingStyleForModule;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// For .mjs or TypeScript, prefer ES6
|
|
116
|
+
if (sourcePath.endsWith('.mjs') || isTypeScript) {
|
|
117
|
+
// If we're importing a member, use named imports
|
|
118
|
+
if (this.member !== undefined) {
|
|
119
|
+
return ImportStyle.ES6Named;
|
|
120
|
+
}
|
|
121
|
+
// Otherwise default import
|
|
122
|
+
return ImportStyle.ES6Default;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// For .js files, check what style is predominantly being used
|
|
126
|
+
let hasNamedImports = false;
|
|
127
|
+
let hasNamespaceImports = false;
|
|
128
|
+
let hasDefaultImports = false;
|
|
129
|
+
let hasCommonJSRequires = false;
|
|
130
|
+
|
|
131
|
+
for (const stmt of compilationUnit.statements) {
|
|
132
|
+
const statement = stmt.element;
|
|
133
|
+
|
|
134
|
+
// Check for ES6 imports
|
|
135
|
+
if (statement?.kind === JS.Kind.Import) {
|
|
136
|
+
const jsImport = statement as JS.Import;
|
|
137
|
+
const importClause = jsImport.importClause;
|
|
138
|
+
|
|
139
|
+
if (importClause) {
|
|
140
|
+
// Check for named bindings
|
|
141
|
+
if (importClause.namedBindings) {
|
|
142
|
+
if (importClause.namedBindings.kind === JS.Kind.NamedImports) {
|
|
143
|
+
hasNamedImports = true;
|
|
144
|
+
} else if (importClause.namedBindings.kind === J.Kind.Identifier ||
|
|
145
|
+
importClause.namedBindings.kind === JS.Kind.Alias) {
|
|
146
|
+
// import * as x from 'module'
|
|
147
|
+
hasNamespaceImports = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check for default import
|
|
152
|
+
if (importClause.name) {
|
|
153
|
+
hasDefaultImports = true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check for CommonJS requires
|
|
159
|
+
if (statement?.kind === J.Kind.VariableDeclarations) {
|
|
160
|
+
const varDecl = statement as J.VariableDeclarations;
|
|
161
|
+
if (varDecl.variables.length === 1) {
|
|
162
|
+
const namedVar = varDecl.variables[0].element;
|
|
163
|
+
const initializer = namedVar?.initializer?.element;
|
|
164
|
+
if (initializer?.kind === J.Kind.MethodInvocation &&
|
|
165
|
+
this.isRequireCall(initializer as J.MethodInvocation)) {
|
|
166
|
+
hasCommonJSRequires = true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Prefer matching the predominant style
|
|
173
|
+
// If file uses CommonJS, stick with it
|
|
174
|
+
if (hasCommonJSRequires && !hasNamedImports && !hasNamespaceImports && !hasDefaultImports) {
|
|
175
|
+
return ImportStyle.CommonJS;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// If importing a member, prefer named imports if they exist in the file
|
|
179
|
+
if (this.member !== undefined) {
|
|
180
|
+
if (hasNamedImports) {
|
|
181
|
+
return ImportStyle.ES6Named;
|
|
182
|
+
}
|
|
183
|
+
if (hasNamespaceImports) {
|
|
184
|
+
return ImportStyle.ES6Namespace;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// For default/whole module imports
|
|
189
|
+
if (this.member === undefined) {
|
|
190
|
+
if (hasNamespaceImports) {
|
|
191
|
+
return ImportStyle.ES6Namespace;
|
|
192
|
+
}
|
|
193
|
+
if (hasDefaultImports) {
|
|
194
|
+
return ImportStyle.ES6Default;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Default to named imports for members, default imports for modules
|
|
199
|
+
return this.member !== undefined ? ImportStyle.ES6Named : ImportStyle.ES6Default;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Find the import style used for an existing import from the same module
|
|
204
|
+
*/
|
|
205
|
+
private findExistingImportStyleForModule(compilationUnit: JS.CompilationUnit): ImportStyle | null {
|
|
206
|
+
for (const stmt of compilationUnit.statements) {
|
|
207
|
+
const statement = stmt.element;
|
|
208
|
+
|
|
209
|
+
// Check ES6 imports
|
|
210
|
+
if (statement?.kind === JS.Kind.Import) {
|
|
211
|
+
const jsImport = statement as JS.Import;
|
|
212
|
+
const moduleSpecifier = jsImport.moduleSpecifier?.element;
|
|
213
|
+
|
|
214
|
+
if (moduleSpecifier) {
|
|
215
|
+
const moduleName = this.getModuleName(moduleSpecifier);
|
|
216
|
+
|
|
217
|
+
if (moduleName === this.target) {
|
|
218
|
+
const importClause = jsImport.importClause;
|
|
219
|
+
if (importClause?.namedBindings) {
|
|
220
|
+
if (importClause.namedBindings.kind === JS.Kind.NamedImports) {
|
|
221
|
+
return ImportStyle.ES6Named;
|
|
222
|
+
} else {
|
|
223
|
+
return ImportStyle.ES6Namespace;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (importClause?.name) {
|
|
227
|
+
return ImportStyle.ES6Default;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Check CommonJS requires
|
|
234
|
+
if (statement?.kind === J.Kind.VariableDeclarations) {
|
|
235
|
+
const varDecl = statement as J.VariableDeclarations;
|
|
236
|
+
if (varDecl.variables.length === 1) {
|
|
237
|
+
const namedVar = varDecl.variables[0].element;
|
|
238
|
+
const initializer = namedVar?.initializer?.element;
|
|
239
|
+
|
|
240
|
+
if (initializer?.kind === J.Kind.MethodInvocation &&
|
|
241
|
+
this.isRequireCall(initializer as J.MethodInvocation)) {
|
|
242
|
+
const moduleName = this.getModuleNameFromRequire(initializer as J.MethodInvocation);
|
|
243
|
+
if (moduleName === this.target) {
|
|
244
|
+
return ImportStyle.CommonJS;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
override async visitJsCompilationUnit(compilationUnit: JS.CompilationUnit, p: P): Promise<J | undefined> {
|
|
255
|
+
// First, check if the import already exists
|
|
256
|
+
const hasImport = await this.checkImportExists(compilationUnit);
|
|
257
|
+
if (hasImport) {
|
|
258
|
+
return compilationUnit;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// If onlyIfReferenced is true, check if the identifier is actually used
|
|
262
|
+
if (this.onlyIfReferenced) {
|
|
263
|
+
const isReferenced = await this.checkIdentifierReferenced(compilationUnit);
|
|
264
|
+
if (!isReferenced) {
|
|
265
|
+
return compilationUnit;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Determine the appropriate import style
|
|
270
|
+
const importStyle = this.determineImportStyle(compilationUnit);
|
|
271
|
+
|
|
272
|
+
// For ES6 named imports, check if we can merge into an existing import from the same module
|
|
273
|
+
if (importStyle === ImportStyle.ES6Named && this.member !== undefined) {
|
|
274
|
+
const mergedCu = await this.tryMergeIntoExistingImport(compilationUnit, p);
|
|
275
|
+
if (mergedCu !== compilationUnit) {
|
|
276
|
+
return mergedCu;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Add the import using the appropriate style
|
|
281
|
+
if (importStyle === ImportStyle.CommonJS) {
|
|
282
|
+
// TODO: Implement CommonJS require creation
|
|
283
|
+
// For now, fall back to ES6 imports
|
|
284
|
+
// return this.addCommonJSRequire(compilationUnit, p);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Add ES6 import (handles ES6Named, ES6Namespace, ES6Default)
|
|
288
|
+
return this.produceJavaScript<JS.CompilationUnit>(compilationUnit, p, async draft => {
|
|
289
|
+
// Find the position to insert the import
|
|
290
|
+
const insertionIndex = this.findImportInsertionIndex(compilationUnit);
|
|
291
|
+
|
|
292
|
+
const newImport = await this.createImportStatement(compilationUnit, insertionIndex, p);
|
|
293
|
+
|
|
294
|
+
// Insert the import at the appropriate position
|
|
295
|
+
// Create semicolon marker for the import statement
|
|
296
|
+
const semicolonMarkers = markers({
|
|
297
|
+
kind: J.Markers.Semicolon,
|
|
298
|
+
id: randomId()
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (insertionIndex === 0) {
|
|
302
|
+
// Insert at the beginning
|
|
303
|
+
// The `after` space should be empty since semicolon is printed after it
|
|
304
|
+
// The spacing comes from updating the next statement's prefix
|
|
305
|
+
const updatedStatements = compilationUnit.statements.length > 0
|
|
306
|
+
? [
|
|
307
|
+
rightPadded(newImport, emptySpace, semicolonMarkers),
|
|
308
|
+
{
|
|
309
|
+
...compilationUnit.statements[0],
|
|
310
|
+
element: compilationUnit.statements[0].element
|
|
311
|
+
? {...compilationUnit.statements[0].element, prefix: space("\n\n")}
|
|
312
|
+
: undefined
|
|
313
|
+
} as J.RightPadded<Statement>,
|
|
314
|
+
...compilationUnit.statements.slice(1)
|
|
315
|
+
]
|
|
316
|
+
: [rightPadded(newImport, emptySpace, semicolonMarkers)];
|
|
317
|
+
|
|
318
|
+
draft.statements = updatedStatements;
|
|
319
|
+
} else {
|
|
320
|
+
// Insert after existing imports
|
|
321
|
+
const before = compilationUnit.statements.slice(0, insertionIndex);
|
|
322
|
+
const after = compilationUnit.statements.slice(insertionIndex);
|
|
323
|
+
|
|
324
|
+
//The `after` space is empty, spacing comes from next statement's prefix
|
|
325
|
+
// Ensure the next statement has at least one newline in its prefix
|
|
326
|
+
if (after.length > 0 && after[0].element) {
|
|
327
|
+
const currentPrefix = after[0].element.prefix;
|
|
328
|
+
const needsNewline = !currentPrefix.whitespace.includes('\n');
|
|
329
|
+
|
|
330
|
+
const updatedNextStatement = needsNewline ? {
|
|
331
|
+
...after[0],
|
|
332
|
+
element: {...after[0].element, prefix: space("\n" + currentPrefix.whitespace)}
|
|
333
|
+
} : after[0];
|
|
334
|
+
|
|
335
|
+
draft.statements = [
|
|
336
|
+
...before,
|
|
337
|
+
rightPadded(newImport, emptySpace, semicolonMarkers),
|
|
338
|
+
updatedNextStatement,
|
|
339
|
+
...after.slice(1)
|
|
340
|
+
];
|
|
341
|
+
} else {
|
|
342
|
+
draft.statements = [
|
|
343
|
+
...before,
|
|
344
|
+
rightPadded(newImport, emptySpace, semicolonMarkers),
|
|
345
|
+
...after
|
|
346
|
+
];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Try to merge the new member into an existing import from the same module
|
|
354
|
+
*/
|
|
355
|
+
private async tryMergeIntoExistingImport(compilationUnit: JS.CompilationUnit, p: P): Promise<JS.CompilationUnit> {
|
|
356
|
+
for (let i = 0; i < compilationUnit.statements.length; i++) {
|
|
357
|
+
const stmt = compilationUnit.statements[i];
|
|
358
|
+
const statement = stmt.element;
|
|
359
|
+
|
|
360
|
+
if (statement?.kind === JS.Kind.Import) {
|
|
361
|
+
const jsImport = statement as JS.Import;
|
|
362
|
+
const moduleSpecifier = jsImport.moduleSpecifier?.element;
|
|
363
|
+
|
|
364
|
+
if (!moduleSpecifier) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const moduleName = this.getModuleName(moduleSpecifier);
|
|
369
|
+
|
|
370
|
+
// Check if this is an import from our target module
|
|
371
|
+
if (moduleName !== this.target) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const importClause = jsImport.importClause;
|
|
376
|
+
if (!importClause || !importClause.namedBindings) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Only merge into NamedImports, not namespace imports
|
|
381
|
+
if (importClause.namedBindings.kind !== JS.Kind.NamedImports) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// We found a matching import with named bindings - merge into it
|
|
386
|
+
return this.produceJavaScript<JS.CompilationUnit>(compilationUnit, p, async draft => {
|
|
387
|
+
const namedImports = importClause.namedBindings as JS.NamedImports;
|
|
388
|
+
|
|
389
|
+
// Create the new specifier with a space prefix (since it's not the first element)
|
|
390
|
+
const newSpecifierBase = this.createImportSpecifier();
|
|
391
|
+
const newSpecifier = {...newSpecifierBase, prefix: singleSpace};
|
|
392
|
+
|
|
393
|
+
// Add the new specifier to the elements
|
|
394
|
+
const updatedNamedImports: JS.NamedImports = await this.produceJavaScript<JS.NamedImports>(
|
|
395
|
+
namedImports, p, async namedDraft => {
|
|
396
|
+
namedDraft.elements = {
|
|
397
|
+
...namedImports.elements,
|
|
398
|
+
elements: [
|
|
399
|
+
...namedImports.elements.elements,
|
|
400
|
+
rightPadded(newSpecifier, emptySpace)
|
|
401
|
+
]
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// Update the import with the new named imports
|
|
407
|
+
const updatedImport: JS.Import = await this.produceJavaScript<JS.Import>(
|
|
408
|
+
jsImport, p, async importDraft => {
|
|
409
|
+
importDraft.importClause = await this.produceJavaScript<JS.ImportClause>(
|
|
410
|
+
importClause, p, async clauseDraft => {
|
|
411
|
+
clauseDraft.namedBindings = updatedNamedImports;
|
|
412
|
+
}
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
// Replace the statement in the compilation unit
|
|
418
|
+
draft.statements = compilationUnit.statements.map((s, idx) =>
|
|
419
|
+
idx === i ? {...s, element: updatedImport} : s
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return compilationUnit;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Check if the import already exists in the compilation unit
|
|
430
|
+
*/
|
|
431
|
+
private async checkImportExists(compilationUnit: JS.CompilationUnit): Promise<boolean> {
|
|
432
|
+
for (const stmt of compilationUnit.statements) {
|
|
433
|
+
const statement = stmt.element;
|
|
434
|
+
|
|
435
|
+
// Check ES6 imports
|
|
436
|
+
if (statement?.kind === JS.Kind.Import) {
|
|
437
|
+
const jsImport = statement as JS.Import;
|
|
438
|
+
if (this.isMatchingImport(jsImport)) {
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Check CommonJS require statements
|
|
444
|
+
if (statement?.kind === J.Kind.VariableDeclarations) {
|
|
445
|
+
const varDecl = statement as J.VariableDeclarations;
|
|
446
|
+
if (this.isMatchingRequire(varDecl)) {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Check if the import matches what we're trying to add
|
|
456
|
+
*/
|
|
457
|
+
private isMatchingImport(jsImport: JS.Import): boolean {
|
|
458
|
+
// Check module specifier
|
|
459
|
+
const moduleSpecifier = jsImport.moduleSpecifier?.element;
|
|
460
|
+
if (!moduleSpecifier) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const moduleName = this.getModuleName(moduleSpecifier);
|
|
465
|
+
if (moduleName !== this.target) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const importClause = jsImport.importClause;
|
|
470
|
+
if (!importClause) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Check if the specific member or default import already exists
|
|
475
|
+
if (this.member === undefined) {
|
|
476
|
+
// We're adding a default import, check if one exists
|
|
477
|
+
return importClause.name !== undefined;
|
|
478
|
+
} else {
|
|
479
|
+
// We're adding a named import, check if it exists
|
|
480
|
+
const namedBindings = importClause.namedBindings;
|
|
481
|
+
if (!namedBindings) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (namedBindings.kind === JS.Kind.NamedImports) {
|
|
486
|
+
const namedImports = namedBindings as JS.NamedImports;
|
|
487
|
+
for (const elem of namedImports.elements.elements) {
|
|
488
|
+
if (elem.element?.kind === JS.Kind.ImportSpecifier) {
|
|
489
|
+
const specifier = elem.element as JS.ImportSpecifier;
|
|
490
|
+
const importName = this.getImportName(specifier);
|
|
491
|
+
const aliasName = this.getImportAlias(specifier);
|
|
492
|
+
|
|
493
|
+
if (importName === this.member && aliasName === this.alias) {
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Check if this is a matching CommonJS require statement
|
|
506
|
+
*/
|
|
507
|
+
private isMatchingRequire(varDecl: J.VariableDeclarations): boolean {
|
|
508
|
+
if (varDecl.variables.length !== 1) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const namedVar = varDecl.variables[0].element;
|
|
513
|
+
if (!namedVar) {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const initializer = namedVar.initializer?.element;
|
|
518
|
+
if (!initializer || initializer.kind !== J.Kind.MethodInvocation) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const methodInv = initializer as J.MethodInvocation;
|
|
523
|
+
if (!this.isRequireCall(methodInv)) {
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const moduleName = this.getModuleNameFromRequire(methodInv);
|
|
528
|
+
if (moduleName !== this.target) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Check if the variable name matches what we're trying to add
|
|
533
|
+
const pattern = namedVar.name;
|
|
534
|
+
if (this.member === undefined && pattern?.kind === J.Kind.Identifier) {
|
|
535
|
+
// Default import style: const fs = require('fs')
|
|
536
|
+
return true;
|
|
537
|
+
} else if (this.member !== undefined && pattern?.kind === JS.Kind.ObjectBindingPattern) {
|
|
538
|
+
// Destructured import: const { member } = require('module')
|
|
539
|
+
const objectPattern = pattern as JS.ObjectBindingPattern;
|
|
540
|
+
for (const elem of objectPattern.bindings.elements) {
|
|
541
|
+
if (elem.element?.kind === JS.Kind.BindingElement) {
|
|
542
|
+
const bindingElem = elem.element as JS.BindingElement;
|
|
543
|
+
const name = (bindingElem.name as J.Identifier)?.simpleName;
|
|
544
|
+
if (name === (this.alias || this.member)) {
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Check if the identifier is actually referenced in the file
|
|
556
|
+
*/
|
|
557
|
+
private async checkIdentifierReferenced(compilationUnit: JS.CompilationUnit): Promise<boolean> {
|
|
558
|
+
// Use type attribution to detect if the identifier is referenced
|
|
559
|
+
// Map of module name -> Set of member names used from that module
|
|
560
|
+
const usedImports = new Map<string, Set<string>>();
|
|
561
|
+
|
|
562
|
+
// Helper to record usage of a method from a module
|
|
563
|
+
const recordMethodUsage = (methodType: Type.Method) => {
|
|
564
|
+
const moduleName = Type.FullyQualified.getFullyQualifiedName(methodType.declaringType);
|
|
565
|
+
if (moduleName) {
|
|
566
|
+
if (!usedImports.has(moduleName)) {
|
|
567
|
+
usedImports.set(moduleName, new Set());
|
|
568
|
+
}
|
|
569
|
+
usedImports.get(moduleName)!.add(methodType.name);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// Create a visitor to collect used identifiers with their type attribution
|
|
574
|
+
const collector = new class extends JavaScriptVisitor<ExecutionContext> {
|
|
575
|
+
override async visitIdentifier(identifier: J.Identifier, p: ExecutionContext): Promise<J | undefined> {
|
|
576
|
+
const type = identifier.type;
|
|
577
|
+
if (type && Type.isMethod(type)) {
|
|
578
|
+
recordMethodUsage(type as Type.Method);
|
|
579
|
+
}
|
|
580
|
+
return super.visitIdentifier(identifier, p);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
override async visitMethodInvocation(methodInvocation: J.MethodInvocation, p: ExecutionContext): Promise<J | undefined> {
|
|
584
|
+
if (methodInvocation.methodType) {
|
|
585
|
+
recordMethodUsage(methodInvocation.methodType);
|
|
586
|
+
}
|
|
587
|
+
return super.visitMethodInvocation(methodInvocation, p);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
override async visitFunctionCall(functionCall: JS.FunctionCall, p: ExecutionContext): Promise<J | undefined> {
|
|
591
|
+
if (functionCall.methodType) {
|
|
592
|
+
recordMethodUsage(functionCall.methodType);
|
|
593
|
+
}
|
|
594
|
+
return super.visitFunctionCall(functionCall, p);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
override async visitFieldAccess(fieldAccess: J.FieldAccess, p: ExecutionContext): Promise<J | undefined> {
|
|
598
|
+
const type = fieldAccess.type;
|
|
599
|
+
if (type && Type.isMethod(type)) {
|
|
600
|
+
recordMethodUsage(type as Type.Method);
|
|
601
|
+
}
|
|
602
|
+
return super.visitFieldAccess(fieldAccess, p);
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
await collector.visit(compilationUnit, new ExecutionContext());
|
|
607
|
+
|
|
608
|
+
// Check if our target import is used based on type attribution
|
|
609
|
+
const moduleMembers = usedImports.get(this.target);
|
|
610
|
+
if (!moduleMembers) {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// For specific members, check if that member is used; otherwise check if any member is used
|
|
615
|
+
return this.member ? moduleMembers.has(this.member) : moduleMembers.size > 0;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Create a new import statement
|
|
620
|
+
*/
|
|
621
|
+
private async createImportStatement(compilationUnit: JS.CompilationUnit, insertionIndex: number, p: P): Promise<JS.Import> {
|
|
622
|
+
// Determine the appropriate prefix (spacing before the import)
|
|
623
|
+
const prefix = this.determineImportPrefix(compilationUnit, insertionIndex);
|
|
624
|
+
|
|
625
|
+
// Create the module specifier
|
|
626
|
+
const moduleSpecifier: J.Literal = {
|
|
627
|
+
id: randomId(),
|
|
628
|
+
kind: J.Kind.Literal,
|
|
629
|
+
prefix: singleSpace,
|
|
630
|
+
markers: emptyMarkers,
|
|
631
|
+
value: `'${this.target}'`,
|
|
632
|
+
valueSource: `'${this.target}'`,
|
|
633
|
+
unicodeEscapes: [],
|
|
634
|
+
type: undefined
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
let importClause: JS.ImportClause | undefined;
|
|
638
|
+
|
|
639
|
+
if (this.member === undefined) {
|
|
640
|
+
// Default import: import target from 'module'
|
|
641
|
+
const defaultName: J.Identifier = {
|
|
642
|
+
id: randomId(),
|
|
643
|
+
kind: J.Kind.Identifier,
|
|
644
|
+
prefix: singleSpace,
|
|
645
|
+
markers: emptyMarkers,
|
|
646
|
+
annotations: [],
|
|
647
|
+
simpleName: this.alias || this.target,
|
|
648
|
+
type: undefined,
|
|
649
|
+
fieldType: undefined
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
importClause = {
|
|
653
|
+
id: randomId(),
|
|
654
|
+
kind: JS.Kind.ImportClause,
|
|
655
|
+
prefix: emptySpace,
|
|
656
|
+
markers: emptyMarkers,
|
|
657
|
+
typeOnly: false,
|
|
658
|
+
name: rightPadded(defaultName, emptySpace),
|
|
659
|
+
namedBindings: undefined
|
|
660
|
+
};
|
|
661
|
+
} else {
|
|
662
|
+
// Named import: import { member } from 'module'
|
|
663
|
+
const importSpec = this.createImportSpecifier();
|
|
664
|
+
|
|
665
|
+
const namedImports: JS.NamedImports = {
|
|
666
|
+
id: randomId(),
|
|
667
|
+
kind: JS.Kind.NamedImports,
|
|
668
|
+
prefix: singleSpace,
|
|
669
|
+
markers: emptyMarkers,
|
|
670
|
+
elements: {
|
|
671
|
+
kind: J.Kind.Container,
|
|
672
|
+
before: emptySpace,
|
|
673
|
+
elements: [rightPadded(importSpec, emptySpace)],
|
|
674
|
+
markers: emptyMarkers
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
importClause = {
|
|
679
|
+
id: randomId(),
|
|
680
|
+
kind: JS.Kind.ImportClause,
|
|
681
|
+
prefix: emptySpace,
|
|
682
|
+
markers: emptyMarkers,
|
|
683
|
+
typeOnly: false,
|
|
684
|
+
name: undefined,
|
|
685
|
+
namedBindings: namedImports
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const jsImport: JS.Import = {
|
|
690
|
+
id: randomId(),
|
|
691
|
+
kind: JS.Kind.Import,
|
|
692
|
+
prefix,
|
|
693
|
+
markers: emptyMarkers,
|
|
694
|
+
modifiers: [],
|
|
695
|
+
importClause,
|
|
696
|
+
moduleSpecifier: {
|
|
697
|
+
kind: J.Kind.LeftPadded,
|
|
698
|
+
before: singleSpace,
|
|
699
|
+
element: moduleSpecifier,
|
|
700
|
+
markers: emptyMarkers
|
|
701
|
+
},
|
|
702
|
+
initializer: undefined
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
return jsImport;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Create an import specifier for a named import
|
|
710
|
+
*/
|
|
711
|
+
private createImportSpecifier(): JS.ImportSpecifier {
|
|
712
|
+
let specifier: J.Identifier | JS.Alias;
|
|
713
|
+
|
|
714
|
+
if (this.alias) {
|
|
715
|
+
// Aliased import: import { member as alias } from 'module'
|
|
716
|
+
const propertyName: J.Identifier = {
|
|
717
|
+
id: randomId(),
|
|
718
|
+
kind: J.Kind.Identifier,
|
|
719
|
+
prefix: emptySpace,
|
|
720
|
+
markers: emptyMarkers,
|
|
721
|
+
annotations: [],
|
|
722
|
+
simpleName: this.member!,
|
|
723
|
+
type: undefined,
|
|
724
|
+
fieldType: undefined
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
const aliasName: J.Identifier = {
|
|
728
|
+
id: randomId(),
|
|
729
|
+
kind: J.Kind.Identifier,
|
|
730
|
+
prefix: singleSpace,
|
|
731
|
+
markers: emptyMarkers,
|
|
732
|
+
annotations: [],
|
|
733
|
+
simpleName: this.alias,
|
|
734
|
+
type: undefined,
|
|
735
|
+
fieldType: undefined
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
specifier = {
|
|
739
|
+
id: randomId(),
|
|
740
|
+
kind: JS.Kind.Alias,
|
|
741
|
+
prefix: emptySpace,
|
|
742
|
+
markers: emptyMarkers,
|
|
743
|
+
propertyName: rightPadded(propertyName, singleSpace),
|
|
744
|
+
alias: aliasName
|
|
745
|
+
};
|
|
746
|
+
} else {
|
|
747
|
+
// Regular import: import { member } from 'module'
|
|
748
|
+
specifier = {
|
|
749
|
+
id: randomId(),
|
|
750
|
+
kind: J.Kind.Identifier,
|
|
751
|
+
prefix: emptySpace,
|
|
752
|
+
markers: emptyMarkers,
|
|
753
|
+
annotations: [],
|
|
754
|
+
simpleName: this.member!,
|
|
755
|
+
type: undefined,
|
|
756
|
+
fieldType: undefined
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return {
|
|
761
|
+
id: randomId(),
|
|
762
|
+
kind: JS.Kind.ImportSpecifier,
|
|
763
|
+
prefix: emptySpace,
|
|
764
|
+
markers: emptyMarkers,
|
|
765
|
+
importType: {
|
|
766
|
+
kind: J.Kind.LeftPadded,
|
|
767
|
+
before: emptySpace,
|
|
768
|
+
element: false,
|
|
769
|
+
markers: emptyMarkers
|
|
770
|
+
},
|
|
771
|
+
specifier
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Determine the appropriate spacing before the import statement
|
|
777
|
+
*/
|
|
778
|
+
private determineImportPrefix(compilationUnit: JS.CompilationUnit, insertionIndex: number): J.Space {
|
|
779
|
+
// If inserting at the beginning (index 0), use the prefix of the first statement
|
|
780
|
+
// but only the whitespace part (preserve comments on the original first statement)
|
|
781
|
+
if (insertionIndex === 0 && compilationUnit.statements.length > 0) {
|
|
782
|
+
const firstPrefix = compilationUnit.statements[0].element?.prefix;
|
|
783
|
+
if (firstPrefix) {
|
|
784
|
+
// Keep only whitespace, not comments
|
|
785
|
+
return {
|
|
786
|
+
kind: J.Kind.Space,
|
|
787
|
+
comments: [],
|
|
788
|
+
whitespace: firstPrefix.whitespace
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
return emptySpace;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// If inserting after other statements, ensure we have at least one newline
|
|
795
|
+
// to separate from the previous statement
|
|
796
|
+
return space("\n");
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Find the index where the new import should be inserted
|
|
801
|
+
*/
|
|
802
|
+
private findImportInsertionIndex(compilationUnit: JS.CompilationUnit): number {
|
|
803
|
+
let lastImportIndex = -1;
|
|
804
|
+
|
|
805
|
+
for (let i = 0; i < compilationUnit.statements.length; i++) {
|
|
806
|
+
const statement = compilationUnit.statements[i].element;
|
|
807
|
+
if (statement?.kind === JS.Kind.Import) {
|
|
808
|
+
lastImportIndex = i;
|
|
809
|
+
} else if (lastImportIndex >= 0) {
|
|
810
|
+
// We've found a non-import after imports, insert after the last import
|
|
811
|
+
return lastImportIndex + 1;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// If we found imports, insert after them
|
|
816
|
+
if (lastImportIndex >= 0) {
|
|
817
|
+
return lastImportIndex + 1;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// No imports found, insert at the beginning
|
|
821
|
+
return 0;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Get the module name from a require() call
|
|
826
|
+
*/
|
|
827
|
+
private getModuleNameFromRequire(methodInv: J.MethodInvocation): string | undefined {
|
|
828
|
+
const args = methodInv.arguments?.elements;
|
|
829
|
+
if (!args || args.length === 0) {
|
|
830
|
+
return undefined;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const firstArg = args[0].element;
|
|
834
|
+
if (!firstArg || firstArg.kind !== J.Kind.Literal || typeof (firstArg as J.Literal).value !== 'string') {
|
|
835
|
+
return undefined;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return (firstArg as J.Literal).value?.toString();
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Get the import name from an import specifier
|
|
843
|
+
*/
|
|
844
|
+
private getImportName(specifier: JS.ImportSpecifier): string {
|
|
845
|
+
const spec = specifier.specifier;
|
|
846
|
+
if (spec?.kind === JS.Kind.Alias) {
|
|
847
|
+
const alias = spec as JS.Alias;
|
|
848
|
+
const propertyName = alias.propertyName.element;
|
|
849
|
+
if (propertyName?.kind === J.Kind.Identifier) {
|
|
850
|
+
return (propertyName as J.Identifier).simpleName;
|
|
851
|
+
}
|
|
852
|
+
} else if (spec?.kind === J.Kind.Identifier) {
|
|
853
|
+
return (spec as J.Identifier).simpleName;
|
|
854
|
+
}
|
|
855
|
+
return '';
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Get the import alias from an import specifier
|
|
860
|
+
*/
|
|
861
|
+
private getImportAlias(specifier: JS.ImportSpecifier): string | undefined {
|
|
862
|
+
const spec = specifier.specifier;
|
|
863
|
+
if (spec?.kind === JS.Kind.Alias) {
|
|
864
|
+
const alias = spec as JS.Alias;
|
|
865
|
+
if (alias.alias?.kind === J.Kind.Identifier) {
|
|
866
|
+
return (alias.alias as J.Identifier).simpleName;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return undefined;
|
|
870
|
+
}
|
|
871
|
+
}
|