Skip to content

Commit 89e004f

Browse files
authored
add support for autoimports/moveToFile to generate aliased named imports (microsoft#59885)
1 parent 0d7763e commit 89e004f

File tree

5 files changed

+197
-16
lines changed

5 files changed

+197
-16
lines changed

src/services/codefixes/importFixes.ts

+32-16
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ export function createImportAdder(sourceFile: SourceFile | FutureSourceFile, pro
241241
interface AddToExistingState {
242242
readonly importClauseOrBindingPattern: ImportClause | ObjectBindingPattern;
243243
defaultImport: Import | undefined;
244-
readonly namedImports: Map<string, AddAsTypeOnly>;
244+
readonly namedImports: Map<string, { addAsTypeOnly: AddAsTypeOnly; propertyName?: string | undefined; }>;
245245
}
246246

247247
function createImportAdderWorker(sourceFile: SourceFile | FutureSourceFile, program: Program, useAutoImportProvider: boolean, preferences: UserPreferences, host: LanguageServiceHost, cancellationToken: CancellationToken | undefined): ImportAdder {
@@ -290,15 +290,28 @@ function createImportAdderWorker(sourceFile: SourceFile | FutureSourceFile, prog
290290
let fix = getImportFixForSymbol(sourceFile, exportInfo, program, /*position*/ undefined, !!isValidTypeOnlyUseSite, useRequire, host, preferences);
291291
if (fix) {
292292
const localName = tryCast(referenceImport?.name, isIdentifier)?.text ?? symbolName;
293+
let addAsTypeOnly: AddAsTypeOnly | undefined;
294+
let propertyName: string | undefined;
293295
if (
294296
referenceImport
295297
&& isTypeOnlyImportDeclaration(referenceImport)
296298
&& (fix.kind === ImportFixKind.AddNew || fix.kind === ImportFixKind.AddToExisting)
297299
&& fix.addAsTypeOnly === AddAsTypeOnly.Allowed
298300
) {
299301
// Copy the type-only status from the reference import
300-
fix = { ...fix, addAsTypeOnly: AddAsTypeOnly.Required };
302+
addAsTypeOnly = AddAsTypeOnly.Required;
301303
}
304+
305+
if (exportedSymbol.name !== localName) {
306+
// checks if the symbol was aliased at the referenced import
307+
propertyName = exportedSymbol.name;
308+
}
309+
310+
fix = {
311+
...fix,
312+
...(addAsTypeOnly === undefined ? {} : { addAsTypeOnly }),
313+
...(propertyName === undefined ? {} : { propertyName }),
314+
};
302315
addImport({ fix, symbolName: localName ?? symbolName, errorIdentifierText: undefined });
303316
}
304317
}
@@ -375,14 +388,14 @@ function createImportAdderWorker(sourceFile: SourceFile | FutureSourceFile, prog
375388
importType.push(fix);
376389
break;
377390
case ImportFixKind.AddToExisting: {
378-
const { importClauseOrBindingPattern, importKind, addAsTypeOnly } = fix;
391+
const { importClauseOrBindingPattern, importKind, addAsTypeOnly, propertyName } = fix;
379392
let entry = addToExisting.get(importClauseOrBindingPattern);
380393
if (!entry) {
381394
addToExisting.set(importClauseOrBindingPattern, entry = { importClauseOrBindingPattern, defaultImport: undefined, namedImports: new Map() });
382395
}
383396
if (importKind === ImportKind.Named) {
384-
const prevValue = entry?.namedImports.get(symbolName);
385-
entry.namedImports.set(symbolName, reduceAddAsTypeOnlyValues(prevValue, addAsTypeOnly));
397+
const prevTypeOnly = entry?.namedImports.get(symbolName)?.addAsTypeOnly;
398+
entry.namedImports.set(symbolName, { addAsTypeOnly: reduceAddAsTypeOnlyValues(prevTypeOnly, addAsTypeOnly), propertyName });
386399
}
387400
else {
388401
Debug.assert(entry.defaultImport === undefined || entry.defaultImport.name === symbolName, "(Add to Existing) Default import should be missing or match symbolName");
@@ -394,7 +407,7 @@ function createImportAdderWorker(sourceFile: SourceFile | FutureSourceFile, prog
394407
break;
395408
}
396409
case ImportFixKind.AddNew: {
397-
const { moduleSpecifier, importKind, useRequire, addAsTypeOnly } = fix;
410+
const { moduleSpecifier, importKind, useRequire, addAsTypeOnly, propertyName } = fix;
398411
const entry = getNewImportEntry(moduleSpecifier, importKind, useRequire, addAsTypeOnly);
399412
Debug.assert(entry.useRequire === useRequire, "(Add new) Tried to add an `import` and a `require` for the same module");
400413

@@ -405,12 +418,12 @@ function createImportAdderWorker(sourceFile: SourceFile | FutureSourceFile, prog
405418
break;
406419
case ImportKind.Named:
407420
const prevValue = (entry.namedImports ||= new Map()).get(symbolName);
408-
entry.namedImports.set(symbolName, reduceAddAsTypeOnlyValues(prevValue, addAsTypeOnly));
421+
entry.namedImports.set(symbolName, [reduceAddAsTypeOnlyValues(prevValue, addAsTypeOnly), propertyName]);
409422
break;
410423
case ImportKind.CommonJS:
411424
if (compilerOptions.verbatimModuleSyntax) {
412425
const prevValue = (entry.namedImports ||= new Map()).get(symbolName);
413-
entry.namedImports.set(symbolName, reduceAddAsTypeOnlyValues(prevValue, addAsTypeOnly));
426+
entry.namedImports.set(symbolName, [reduceAddAsTypeOnlyValues(prevValue, addAsTypeOnly), propertyName]);
414427
}
415428
else {
416429
Debug.assert(entry.namespaceLikeImport === undefined || entry.namespaceLikeImport.name === symbolName, "Namespacelike import shoudl be missing or match symbolName");
@@ -582,7 +595,7 @@ function createImportAdderWorker(sourceFile: SourceFile | FutureSourceFile, prog
582595
sourceFile as SourceFile,
583596
importClauseOrBindingPattern,
584597
defaultImport,
585-
arrayFrom(namedImports.entries(), ([name, addAsTypeOnly]) => ({ addAsTypeOnly, name })),
598+
arrayFrom(namedImports.entries(), ([name, { addAsTypeOnly, propertyName }]) => ({ addAsTypeOnly, propertyName, name })),
586599
importSpecifiersToRemoveWhileAdding,
587600
preferences,
588601
);
@@ -596,7 +609,7 @@ function createImportAdderWorker(sourceFile: SourceFile | FutureSourceFile, prog
596609
moduleSpecifier,
597610
quotePreference,
598611
defaultImport,
599-
namedImports && arrayFrom(namedImports.entries(), ([name, addAsTypeOnly]) => ({ addAsTypeOnly, name })),
612+
namedImports && arrayFrom(namedImports.entries(), ([name, [addAsTypeOnly, propertyName]]) => ({ addAsTypeOnly, propertyName, name })),
600613
namespaceLikeImport,
601614
compilerOptions,
602615
preferences,
@@ -772,11 +785,13 @@ interface FixAddToExistingImport extends ImportFixBase {
772785
readonly importClauseOrBindingPattern: ImportClause | ObjectBindingPattern;
773786
readonly importKind: ImportKind.Default | ImportKind.Named;
774787
readonly addAsTypeOnly: AddAsTypeOnly;
788+
readonly propertyName?: string;
775789
}
776790
interface FixAddNewImport extends ImportFixBase {
777791
readonly kind: ImportFixKind.AddNew;
778792
readonly importKind: ImportKind;
779793
readonly addAsTypeOnly: AddAsTypeOnly;
794+
readonly propertyName?: string;
780795
readonly useRequire: boolean;
781796
readonly qualification?: Qualification;
782797
}
@@ -1794,7 +1809,7 @@ function doAddExistingFix(
17941809
factory.createObjectBindingPattern([
17951810
...clause.elements.filter(e => !removeExistingImportSpecifiers.has(e)),
17961811
...defaultImport ? [factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ "default", defaultImport.name)] : emptyArray,
1797-
...namedImports.map(i => factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, i.name)),
1812+
...namedImports.map(i => factory.createBindingElement(/*dotDotDotToken*/ undefined, i.propertyName, i.name)),
17981813
]),
17991814
);
18001815
return;
@@ -1803,7 +1818,7 @@ function doAddExistingFix(
18031818
addElementToBindingPattern(clause, defaultImport.name, "default");
18041819
}
18051820
for (const specifier of namedImports) {
1806-
addElementToBindingPattern(clause, specifier.name, /*propertyName*/ undefined);
1821+
addElementToBindingPattern(clause, specifier.name, specifier.propertyName);
18071822
}
18081823
return;
18091824
}
@@ -1823,7 +1838,7 @@ function doAddExistingFix(
18231838
namedImports.map(namedImport =>
18241839
factory.createImportSpecifier(
18251840
(!clause.isTypeOnly || promoteFromTypeOnly) && shouldUseTypeOnly(namedImport, preferences),
1826-
/*propertyName*/ undefined,
1841+
namedImport.propertyName === undefined ? undefined : factory.createIdentifier(namedImport.propertyName),
18271842
factory.createIdentifier(namedImport.name),
18281843
)
18291844
),
@@ -1919,11 +1934,12 @@ function getImportTypePrefix(moduleSpecifier: string, quotePreference: QuotePref
19191934
interface Import {
19201935
readonly name: string;
19211936
readonly addAsTypeOnly: AddAsTypeOnly;
1937+
readonly propertyName?: string; // Use when needing to generate an `ImportSpecifier with a `propertyName`; the name preceding "as" keyword (undefined when "as" is absent)
19221938
}
19231939

19241940
interface ImportsCollection {
19251941
readonly defaultImport?: Import;
1926-
readonly namedImports?: Map<string, AddAsTypeOnly>;
1942+
readonly namedImports?: Map<string, [AddAsTypeOnly, /*propertyName*/ string?]>;
19271943
readonly namespaceLikeImport?: {
19281944
readonly importKind: ImportKind.CommonJS | ImportKind.Namespace;
19291945
readonly name: string;
@@ -1964,7 +1980,7 @@ function getNewImports(
19641980
namedImports?.map(namedImport =>
19651981
factory.createImportSpecifier(
19661982
!topLevelTypeOnly && shouldUseTypeOnly(namedImport, preferences),
1967-
/*propertyName*/ undefined,
1983+
namedImport.propertyName === undefined ? undefined : factory.createIdentifier(namedImport.propertyName),
19681984
factory.createIdentifier(namedImport.name),
19691985
)
19701986
),
@@ -2003,7 +2019,7 @@ function getNewRequires(moduleSpecifier: string, quotePreference: QuotePreferenc
20032019
let statements: RequireVariableStatement | readonly RequireVariableStatement[] | undefined;
20042020
// const { default: foo, bar, etc } = require('./mod');
20052021
if (defaultImport || namedImports?.length) {
2006-
const bindingElements = namedImports?.map(({ name }) => factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, name)) || [];
2022+
const bindingElements = namedImports?.map(({ name, propertyName }) => factory.createBindingElement(/*dotDotDotToken*/ undefined, propertyName, name)) || [];
20072023
if (defaultImport) {
20082024
bindingElements.unshift(factory.createBindingElement(/*dotDotDotToken*/ undefined, "default", defaultImport.name));
20092025
}
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
/// <reference path='fourslash.ts' />
3+
4+
5+
// @filename: /producer.ts
6+
//// export function doit() {}
7+
8+
// @filename: /test.ts
9+
//// import { doit as doit2 } from "./producer";
10+
////
11+
//// class Another {}
12+
////
13+
//// [|class Consumer {
14+
//// constructor() {
15+
//// doit2();
16+
//// }
17+
//// }|]
18+
19+
// @filename: /consumer.ts
20+
////
21+
22+
23+
verify.moveToFile({
24+
newFileContents: {
25+
"/test.ts":
26+
`
27+
class Another {}
28+
29+
`,
30+
31+
"/consumer.ts":
32+
`import { doit as doit2 } from "./producer";
33+
34+
35+
class Consumer {
36+
constructor() {
37+
doit2();
38+
}
39+
}
40+
`
41+
},
42+
interactiveRefactorArguments: { targetFile: "/consumer.ts" },
43+
});
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
/// <reference path='fourslash.ts' />
3+
4+
5+
// @filename: /producer.ts
6+
//// export function doit() {};
7+
//// export const x = 1;
8+
9+
// @filename: /test.ts
10+
//// import { doit as doit2 } from "./producer";
11+
////
12+
//// class Another {}
13+
////
14+
//// [|class Consumer {
15+
//// constructor() {
16+
//// doit2();
17+
//// }
18+
//// }|]
19+
20+
// @filename: /consumer.ts
21+
//// import { x } from "./producer";
22+
//// x;
23+
24+
verify.moveToFile({
25+
newFileContents: {
26+
"/test.ts":
27+
`
28+
class Another {}
29+
30+
`,
31+
32+
"/consumer.ts":
33+
`import { doit as doit2, x } from "./producer";
34+
x;
35+
class Consumer {
36+
constructor() {
37+
doit2();
38+
}
39+
}
40+
`
41+
},
42+
interactiveRefactorArguments: { targetFile: "/consumer.ts" },
43+
});
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
2+
/// <reference path='fourslash.ts' />
3+
4+
5+
// @filename: /producer.ts
6+
//// export function doit() {};
7+
8+
// @filename: /test.ts
9+
//// import { doit as doit2 } from "./producer";
10+
////
11+
//// class Another {}
12+
////
13+
//// [|class Consumer {
14+
//// constructor() {
15+
//// doit2();
16+
//// }
17+
//// }|]
18+
19+
// @filename: /consumer.ts
20+
//// import { doit } from "./producer"; // existing import does not change when alias imported
21+
//// doit();
22+
23+
verify.moveToFile({
24+
newFileContents: {
25+
"/test.ts":
26+
`
27+
class Another {}
28+
29+
`,
30+
31+
"/consumer.ts":
32+
`import { doit } from "./producer"; // existing import does not change when alias imported
33+
doit();
34+
class Consumer {
35+
constructor() {
36+
doit2();
37+
}
38+
}
39+
`
40+
},
41+
interactiveRefactorArguments: { targetFile: "/consumer.ts" },
42+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
/// <reference path='fourslash.ts' />
3+
4+
5+
// @filename: /producer.ts
6+
//// export function doit() {}
7+
8+
// @filename: /test.ts
9+
//// import { doit as doit2 } from "./producer";
10+
////
11+
//// class Another {}
12+
////
13+
//// [|class Consumer {
14+
//// constructor() {
15+
//// doit2();
16+
//// }
17+
//// }|]
18+
19+
verify.moveToNewFile({
20+
newFileContents: {
21+
"/test.ts":
22+
`
23+
class Another {}
24+
25+
`,
26+
27+
"/Consumer.ts":
28+
`import { doit as doit2 } from "./producer";
29+
30+
class Consumer {
31+
constructor() {
32+
doit2();
33+
}
34+
}
35+
`
36+
}
37+
});

0 commit comments

Comments
 (0)