Skip to content

Commit 3358f13

Browse files
authored
Fix namespace name conflict detection in "Convert named imports to namespace import" action (microsoft#45019)
* fix namespace conflict detection
1 parent 2b8296b commit 3358f13

6 files changed

+167
-10
lines changed

src/services/refactors/convertImport.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,36 @@ namespace ts.refactor {
138138
const importDecl = toConvert.parent.parent;
139139
const { moduleSpecifier } = importDecl;
140140

141+
const toConvertSymbols: Set<Symbol> = new Set();
142+
toConvert.elements.forEach(namedImport => {
143+
const symbol = checker.getSymbolAtLocation(namedImport.name);
144+
if (symbol) {
145+
toConvertSymbols.add(symbol);
146+
}
147+
});
141148
const preferredName = moduleSpecifier && isStringLiteral(moduleSpecifier) ? codefix.moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module";
142-
const namespaceNameConflicts = toConvert.elements.some(element =>
143-
FindAllReferences.Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id =>
144-
!!checker.resolveName(preferredName, id, SymbolFlags.All, /*excludeGlobals*/ true)) || false);
149+
function hasNamespaceNameConflict(namedImport: ImportSpecifier): boolean {
150+
// We need to check if the preferred namespace name (`preferredName`) we'd like to use in the refactored code will present a name conflict.
151+
// A name conflict means that, in a scope where we would like to use the preferred namespace name, there already exists a symbol with that name in that scope.
152+
// We are going to use the namespace name in the scopes the named imports being refactored are referenced,
153+
// so we look for conflicts by looking at every reference to those named imports.
154+
return !!FindAllReferences.Core.eachSymbolReferenceInFile(namedImport.name, checker, sourceFile, id => {
155+
const symbol = checker.resolveName(preferredName, id, SymbolFlags.All, /*excludeGlobals*/ true);
156+
if (symbol) { // There already is a symbol with the same name as the preferred namespace name.
157+
if (toConvertSymbols.has(symbol)) { // `preferredName` resolves to a symbol for one of the named import references we are going to transform into namespace import references...
158+
return isExportSpecifier(id.parent); // ...but if this reference is an export specifier, it will not be transformed, so it is a conflict; otherwise, it will be renamed and is not a conflict.
159+
}
160+
return true; // `preferredName` resolves to any other symbol, which will be present in the refactored code and so poses a name conflict.
161+
}
162+
return false; // There is no symbol with the same name as the preferred namespace name, so no conflict.
163+
});
164+
}
165+
const namespaceNameConflicts = toConvert.elements.some(hasNamespaceNameConflict);
145166
const namespaceImportName = namespaceNameConflicts ? getUniqueName(preferredName, sourceFile) : preferredName;
146167

147-
const neededNamedImports: ImportSpecifier[] = [];
168+
// Imports that need to be kept as named imports in the refactored code, to avoid changing the semantics.
169+
// More specifically, those are named imports that appear in named exports in the original code, e.g. `a` in `import { a } from "m"; export { a }`.
170+
const neededNamedImports: Set<ImportSpecifier> = new Set();
148171

149172
for (const element of toConvert.elements) {
150173
const propertyName = (element.propertyName || element.name).text;
@@ -153,10 +176,8 @@ namespace ts.refactor {
153176
if (isShorthandPropertyAssignment(id.parent)) {
154177
changes.replaceNode(sourceFile, id.parent, factory.createPropertyAssignment(id.text, access));
155178
}
156-
else if (isExportSpecifier(id.parent) && !id.parent.propertyName) {
157-
if (!neededNamedImports.some(n => n.name === element.name)) {
158-
neededNamedImports.push(factory.createImportSpecifier(element.propertyName && factory.createIdentifier(element.propertyName.text), factory.createIdentifier(element.name.text)));
159-
}
179+
else if (isExportSpecifier(id.parent)) {
180+
neededNamedImports.add(element);
160181
}
161182
else {
162183
changes.replaceNode(sourceFile, id, access);
@@ -165,8 +186,10 @@ namespace ts.refactor {
165186
}
166187

167188
changes.replaceNode(sourceFile, toConvert, factory.createNamespaceImport(factory.createIdentifier(namespaceImportName)));
168-
if (neededNamedImports.length) {
169-
changes.insertNodeAfter(sourceFile, toConvert.parent.parent, updateImport(importDecl, /*defaultImportName*/ undefined, neededNamedImports));
189+
if (neededNamedImports.size) {
190+
const newNamedImports: ImportSpecifier[] = arrayFrom(neededNamedImports.values()).map(element =>
191+
factory.createImportSpecifier(element.propertyName && factory.createIdentifier(element.propertyName.text), factory.createIdentifier(element.name.text)));
192+
changes.insertNodeAfter(sourceFile, toConvert.parent.parent, updateImport(importDecl, /*defaultImportName*/ undefined, newNamedImports));
170193
}
171194
}
172195

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
/////*a*/import { a, b, foo } from "./foo";/*b*/
4+
////a;
5+
////b;
6+
////foo;
7+
8+
9+
goTo.select("a", "b");
10+
edit.applyRefactor({
11+
refactorName: "Convert import",
12+
actionName: "Convert named imports to namespace import",
13+
actionDescription: "Convert named imports to namespace import",
14+
newContent:
15+
`import * as foo from "./foo";
16+
foo.a;
17+
foo.b;
18+
foo.foo;`,
19+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////import foo, /*a*/{ a, b, c as d }/*b*/ from "./foo";
4+
////a;
5+
////b;
6+
////d;
7+
////foo;
8+
////export default a;
9+
////export { b };
10+
////export { d };
11+
////export { foo };
12+
13+
14+
goTo.select("a", "b");
15+
edit.applyRefactor({
16+
refactorName: "Convert import",
17+
actionName: "Convert named imports to namespace import",
18+
actionDescription: "Convert named imports to namespace import",
19+
triggerReason: "invoked",
20+
newContent:
21+
`import foo, * as foo_1 from "./foo";
22+
import { b, c as d } from "./foo";
23+
foo_1.a;
24+
foo_1.b;
25+
foo_1.c;
26+
foo;
27+
export default foo_1.a;
28+
export { b };
29+
export { d };
30+
export { foo };`,
31+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
/////*a*/import { a, b } from "./foo"/*b*/;
4+
////a;
5+
////b;
6+
////function newScope() {
7+
//// const foo = "foo";
8+
//// foo;
9+
//// return a;
10+
////}
11+
////newScope();
12+
13+
14+
goTo.select("a", "b");
15+
edit.applyRefactor({
16+
refactorName: "Convert import",
17+
actionName: "Convert named imports to namespace import",
18+
actionDescription: "Convert named imports to namespace import",
19+
triggerReason: "invoked",
20+
newContent:
21+
`import * as foo_1 from "./foo";
22+
foo_1.a;
23+
foo_1.b;
24+
function newScope() {
25+
const foo = "foo";
26+
foo;
27+
return foo_1.a;
28+
}
29+
newScope();`,
30+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
/////*a*/import { a, b, c as d } from "./foo"/*b*/;
4+
////a;
5+
////b;
6+
////d;
7+
////export default a;
8+
////export { b };
9+
////export { d };
10+
////export { d as e };
11+
////export { b as f };
12+
13+
goTo.select("a", "b");
14+
edit.applyRefactor({
15+
refactorName: "Convert import",
16+
actionName: "Convert named imports to namespace import",
17+
actionDescription: "Convert named imports to namespace import",
18+
triggerReason: "invoked",
19+
newContent:
20+
`import * as foo from "./foo";
21+
import { b, c as d } from "./foo";
22+
foo.a;
23+
foo.b;
24+
foo.c;
25+
export default foo.a;
26+
export { b };
27+
export { d };
28+
export { d as e };
29+
export { b as f };`,
30+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
/////*a*/import { a, b, foo } from "./foo";/*b*/
4+
////a;
5+
////b;
6+
////foo;
7+
////export { foo };
8+
////export { foo as bar };
9+
10+
goTo.select("a", "b");
11+
edit.applyRefactor({
12+
refactorName: "Convert import",
13+
actionName: "Convert named imports to namespace import",
14+
actionDescription: "Convert named imports to namespace import",
15+
triggerReason: "invoked",
16+
newContent:
17+
`import * as foo_1 from "./foo";
18+
import { foo } from "./foo";
19+
foo_1.a;
20+
foo_1.b;
21+
foo_1.foo;
22+
export { foo };
23+
export { foo as bar };`,
24+
});

0 commit comments

Comments
 (0)