-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Fix namespace name conflict detection in "Convert named imports to namespace import" action #45019
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -138,13 +138,36 @@ namespace ts.refactor { | |
const importDecl = toConvert.parent.parent; | ||
const { moduleSpecifier } = importDecl; | ||
|
||
const toConvertSymbols: Set<Symbol> = new Set(); | ||
toConvert.elements.forEach(namedImport => { | ||
const symbol = checker.getSymbolAtLocation(namedImport.name); | ||
if (symbol) { | ||
toConvertSymbols.add(symbol); | ||
} | ||
}); | ||
const preferredName = moduleSpecifier && isStringLiteral(moduleSpecifier) ? codefix.moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module"; | ||
const namespaceNameConflicts = toConvert.elements.some(element => | ||
FindAllReferences.Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id => | ||
!!checker.resolveName(preferredName, id, SymbolFlags.All, /*excludeGlobals*/ true)) || false); | ||
function hasNamespaceNameConflict(namedImport: ImportSpecifier): boolean { | ||
// We need to check if the preferred namespace name (`preferredName`) we'd like to use in the refactored code will present a name conflict. | ||
// 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. | ||
// We are going to use the namespace name in the scopes the named imports being refactored are referenced, | ||
// so we look for conflicts by looking at every reference to those named imports. | ||
return !!FindAllReferences.Core.eachSymbolReferenceInFile(namedImport.name, checker, sourceFile, id => { | ||
const symbol = checker.resolveName(preferredName, id, SymbolFlags.All, /*excludeGlobals*/ true); | ||
if (symbol) { // There already is a symbol with the same name as the preferred namespace name. | ||
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... | ||
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. | ||
} | ||
return true; // `preferredName` resolves to any other symbol, which will be present in the refactored code and so poses a name conflict. | ||
} | ||
return false; // There is no symbol with the same name as the preferred namespace name, so no conflict. | ||
}); | ||
} | ||
const namespaceNameConflicts = toConvert.elements.some(hasNamespaceNameConflict); | ||
const namespaceImportName = namespaceNameConflicts ? getUniqueName(preferredName, sourceFile) : preferredName; | ||
|
||
const neededNamedImports: ImportSpecifier[] = []; | ||
// Imports that need to be kept as named imports in the refactored code, to avoid changing the semantics. | ||
// 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 }`. | ||
const neededNamedImports: Set<ImportSpecifier> = new Set(); | ||
|
||
for (const element of toConvert.elements) { | ||
const propertyName = (element.propertyName || element.name).text; | ||
|
@@ -153,10 +176,8 @@ namespace ts.refactor { | |
if (isShorthandPropertyAssignment(id.parent)) { | ||
changes.replaceNode(sourceFile, id.parent, factory.createPropertyAssignment(id.text, access)); | ||
} | ||
else if (isExportSpecifier(id.parent) && !id.parent.propertyName) { | ||
if (!neededNamedImports.some(n => n.name === element.name)) { | ||
neededNamedImports.push(factory.createImportSpecifier(element.propertyName && factory.createIdentifier(element.propertyName.text), factory.createIdentifier(element.name.text))); | ||
} | ||
else if (isExportSpecifier(id.parent)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Old code disregarded named export clauses that aliased the reference to the named import, so code such as import { a } from "m";
export { a as b }; was in fact incorrectly refactored into import { a } from "m";
export { m.a as b }; |
||
neededNamedImports.add(element); | ||
} | ||
else { | ||
changes.replaceNode(sourceFile, id, access); | ||
|
@@ -165,8 +186,10 @@ namespace ts.refactor { | |
} | ||
|
||
changes.replaceNode(sourceFile, toConvert, factory.createNamespaceImport(factory.createIdentifier(namespaceImportName))); | ||
if (neededNamedImports.length) { | ||
changes.insertNodeAfter(sourceFile, toConvert.parent.parent, updateImport(importDecl, /*defaultImportName*/ undefined, neededNamedImports)); | ||
if (neededNamedImports.size) { | ||
const newNamedImports: ImportSpecifier[] = arrayFrom(neededNamedImports.values()).map(element => | ||
factory.createImportSpecifier(element.propertyName && factory.createIdentifier(element.propertyName.text), factory.createIdentifier(element.name.text))); | ||
changes.insertNodeAfter(sourceFile, toConvert.parent.parent, updateImport(importDecl, /*defaultImportName*/ undefined, newNamedImports)); | ||
} | ||
} | ||
|
||
|
19 changes: 19 additions & 0 deletions
19
tests/cases/fourslash/refactorConvertImport_namedToNamespace1.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
/////*a*/import { a, b, foo } from "./foo";/*b*/ | ||
////a; | ||
////b; | ||
////foo; | ||
|
||
|
||
goTo.select("a", "b"); | ||
edit.applyRefactor({ | ||
refactorName: "Convert import", | ||
actionName: "Convert named imports to namespace import", | ||
actionDescription: "Convert named imports to namespace import", | ||
newContent: | ||
`import * as foo from "./foo"; | ||
foo.a; | ||
foo.b; | ||
foo.foo;`, | ||
}); |
31 changes: 31 additions & 0 deletions
31
tests/cases/fourslash/refactorConvertImport_namedToNamespace2.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
////import foo, /*a*/{ a, b, c as d }/*b*/ from "./foo"; | ||
////a; | ||
////b; | ||
////d; | ||
////foo; | ||
////export default a; | ||
////export { b }; | ||
////export { d }; | ||
////export { foo }; | ||
|
||
|
||
goTo.select("a", "b"); | ||
edit.applyRefactor({ | ||
refactorName: "Convert import", | ||
actionName: "Convert named imports to namespace import", | ||
actionDescription: "Convert named imports to namespace import", | ||
triggerReason: "invoked", | ||
newContent: | ||
`import foo, * as foo_1 from "./foo"; | ||
import { b, c as d } from "./foo"; | ||
foo_1.a; | ||
foo_1.b; | ||
foo_1.c; | ||
foo; | ||
export default foo_1.a; | ||
export { b }; | ||
export { d }; | ||
export { foo };`, | ||
}); |
30 changes: 30 additions & 0 deletions
30
tests/cases/fourslash/refactorConvertImport_namedToNamespace3.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
/////*a*/import { a, b } from "./foo"/*b*/; | ||
////a; | ||
////b; | ||
////function newScope() { | ||
//// const foo = "foo"; | ||
//// foo; | ||
//// return a; | ||
////} | ||
////newScope(); | ||
|
||
|
||
goTo.select("a", "b"); | ||
edit.applyRefactor({ | ||
refactorName: "Convert import", | ||
actionName: "Convert named imports to namespace import", | ||
actionDescription: "Convert named imports to namespace import", | ||
triggerReason: "invoked", | ||
newContent: | ||
`import * as foo_1 from "./foo"; | ||
foo_1.a; | ||
foo_1.b; | ||
function newScope() { | ||
const foo = "foo"; | ||
foo; | ||
return foo_1.a; | ||
} | ||
newScope();`, | ||
}); |
30 changes: 30 additions & 0 deletions
30
tests/cases/fourslash/refactorConvertImport_namedToNamespace4.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
/////*a*/import { a, b, c as d } from "./foo"/*b*/; | ||
////a; | ||
////b; | ||
////d; | ||
////export default a; | ||
////export { b }; | ||
////export { d }; | ||
////export { d as e }; | ||
////export { b as f }; | ||
|
||
goTo.select("a", "b"); | ||
edit.applyRefactor({ | ||
refactorName: "Convert import", | ||
actionName: "Convert named imports to namespace import", | ||
actionDescription: "Convert named imports to namespace import", | ||
triggerReason: "invoked", | ||
newContent: | ||
`import * as foo from "./foo"; | ||
import { b, c as d } from "./foo"; | ||
foo.a; | ||
foo.b; | ||
foo.c; | ||
export default foo.a; | ||
export { b }; | ||
export { d }; | ||
export { d as e }; | ||
export { b as f };`, | ||
}); |
24 changes: 24 additions & 0 deletions
24
tests/cases/fourslash/refactorConvertImport_namedToNamespace5.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
/////*a*/import { a, b, foo } from "./foo";/*b*/ | ||
////a; | ||
////b; | ||
////foo; | ||
////export { foo }; | ||
////export { foo as bar }; | ||
|
||
goTo.select("a", "b"); | ||
edit.applyRefactor({ | ||
refactorName: "Convert import", | ||
actionName: "Convert named imports to namespace import", | ||
actionDescription: "Convert named imports to namespace import", | ||
triggerReason: "invoked", | ||
newContent: | ||
`import * as foo_1 from "./foo"; | ||
import { foo } from "./foo"; | ||
foo_1.a; | ||
foo_1.b; | ||
foo_1.foo; | ||
export { foo }; | ||
export { foo as bar };`, | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to use a set instead of an array. The old code used the array as a set by comparing the named imports'
name
properties (of typeIdentifiers
) (element.name
in for loop below), but it pushed into the array a node with a newly-createdIdentifier
asname
, so in fact this new node'sname
would never be equal to the named imports'name
, and therefore the same named import could be repeated, as in:would become