-
Notifications
You must be signed in to change notification settings - Fork 13k
Add refactor to convert namespace to named imports and back #24469
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
Changes from 3 commits
fff5141
bcdec37
61bef3e
4cc0474
5bb8921
bab662d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
/* @internal */ | ||
namespace ts.refactor.generateGetAccessorAndSetAccessor { | ||
const refactorName = "Convert import"; | ||
const actionNameNamespaceToNamed = "Convert namespace import to named imports"; | ||
const actionNameNamedToNamespace = "Convert named imports to namespace import"; | ||
registerRefactor(refactorName, { | ||
getAvailableActions(context): ApplicableRefactorInfo[] | undefined { | ||
const i = getImportToConvert(context); | ||
if (!i) return undefined; | ||
const description = i.kind === SyntaxKind.NamespaceImport ? Diagnostics.Convert_namespace_import_to_named_imports.message : Diagnostics.Convert_named_imports_to_namespace_import.message; | ||
const actionName = i.kind === SyntaxKind.NamespaceImport ? actionNameNamespaceToNamed : actionNameNamedToNamespace; | ||
return [{ name: refactorName, description, actions: [{ name: actionName, description }] }]; | ||
}, | ||
getEditsForAction(context, actionName): RefactorEditInfo { | ||
Debug.assert(actionName === actionNameNamespaceToNamed || actionName === actionNameNamedToNamespace); | ||
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, t, Debug.assertDefined(getImportToConvert(context)))); | ||
return { edits, renameFilename: undefined, renameLocation: undefined }; | ||
} | ||
}); | ||
|
||
// Can convert imports of the form `import * as m from "m";` or `import d, { x, y } from "m";`. | ||
function getImportToConvert(context: RefactorContext): NamedImportBindings | undefined { | ||
const { file } = context; | ||
const span = getRefactorContextSpan(context); | ||
const token = getTokenAtPosition(file, span.start, /*includeJsDocComment*/ false); | ||
const importDecl = getParentNodeInSpan(token, file, span); | ||
if (!importDecl || !isImportDeclaration(importDecl)) return undefined; | ||
const { importClause } = importDecl; | ||
return importClause && importClause.namedBindings; | ||
} | ||
|
||
function doChange(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, toConvert: NamedImportBindings): void { | ||
const usedIdentifiers = createMap<true>(); | ||
forEachFreeIdentifier(sourceFile, id => usedIdentifiers.set(id.text, true)); | ||
|
||
const checker = program.getTypeChecker(); | ||
|
||
if (toConvert.kind === SyntaxKind.NamespaceImport) { | ||
doChangeNamespaceToNamed(sourceFile, checker, changes, toConvert, usedIdentifiers, getAllowSyntheticDefaultImports(program.getCompilerOptions())); | ||
} | ||
else { | ||
doChangeNamedToNamespace(sourceFile, checker, changes, toConvert, usedIdentifiers); | ||
} | ||
} | ||
|
||
function doChangeNamespaceToNamed(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamespaceImport, usedIdentifiers: ReadonlyMap<true>, allowSyntheticDefaultImports: boolean): void { | ||
// We may need to change `mod.x` to `_x` to avoid a name conflict. | ||
const exportNameToImportName = createMap<string>(); | ||
let usedAsNamespaceOrDefault = false; | ||
|
||
FindAllReferences.Core.eachSymbolReferenceInFile(toConvert.name, checker, sourceFile, id => { | ||
if (!isPropertyAccessExpression(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. what if it is used in a call expression.. |
||
usedAsNamespaceOrDefault = true; | ||
} | ||
else { | ||
const parent = cast(id.parent, isPropertyAccessExpression); | ||
const exportName = parent.name.text; | ||
let name = exportNameToImportName.get(exportName); | ||
if (name === undefined) { | ||
exportNameToImportName.set(exportName, name = generateName(exportName, usedIdentifiers)); | ||
} | ||
Debug.assert(parent.expression === id); | ||
changes.replaceNode(sourceFile, parent, createIdentifier(name)); | ||
} | ||
}); | ||
|
||
const elements: ImportSpecifier[] = []; | ||
exportNameToImportName.forEach((name, propertyName) => { | ||
elements.push(createImportSpecifier(name === propertyName ? undefined : createIdentifier(propertyName), createIdentifier(name))); | ||
}); | ||
const makeImportDeclaration = (defaultImportName: Identifier | undefined) => | ||
createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, | ||
createImportClause(defaultImportName, elements.length ? createNamedImports(elements) : undefined), | ||
toConvert.parent.parent.moduleSpecifier); | ||
|
||
if (usedAsNamespaceOrDefault && !allowSyntheticDefaultImports) { | ||
changes.insertNodeAfter(sourceFile, toConvert.parent.parent, makeImportDeclaration(/*defaultImportName*/ undefined)); | ||
} | ||
else { | ||
changes.replaceNode(sourceFile, toConvert.parent.parent, makeImportDeclaration(usedAsNamespaceOrDefault ? createIdentifier(toConvert.name.text) : undefined)); | ||
} | ||
} | ||
|
||
function doChangeNamedToNamespace(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamedImports, usedIdentifiers: ReadonlyMap<true>): void { | ||
const { moduleSpecifier } = toConvert.parent.parent; | ||
// We know the user is using at least ScriptTarget.ES6, and moduleSpecifierToValidIdentifier only cares if we're using ES5+, so just set ScriptTarget.ESNext | ||
const namespaceImportName = generateName(moduleSpecifier && isStringLiteral(moduleSpecifier) ? codefix.moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module", usedIdentifiers); | ||
|
||
changes.replaceNode(sourceFile, toConvert, createNamespaceImport(createIdentifier(namespaceImportName))); | ||
|
||
for (const element of toConvert.elements) { | ||
const propertyName = (element.propertyName || element.name).text; | ||
FindAllReferences.Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id => { | ||
changes.replaceNode(sourceFile, id, createPropertyAccess(createIdentifier(namespaceImportName), propertyName)); | ||
}); | ||
} | ||
} | ||
|
||
function generateName(name: string, usedIdentifiers: ReadonlyMap<true>): string { | ||
while (usedIdentifiers.has(name)) { | ||
name = `_${name}`; | ||
} | ||
return name; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1309,6 +1309,46 @@ namespace ts { | |
return forEachEntry(this.map, pred) || false; | ||
} | ||
} | ||
|
||
export function getParentNodeInSpan(node: Node | undefined, file: SourceFile, span: TextSpan): Node | undefined { | ||
if (!node) return undefined; | ||
|
||
while (node.parent) { | ||
if (isSourceFile(node.parent) || !spanContainsNode(span, node.parent, file)) { | ||
return node; | ||
} | ||
|
||
node = node.parent; | ||
} | ||
} | ||
|
||
function spanContainsNode(span: TextSpan, node: Node, file: SourceFile): boolean { | ||
return textSpanContainsPosition(span, node.getStart(file)) && | ||
node.getEnd() <= textSpanEnd(span); | ||
} | ||
|
||
/** | ||
* A free identifier is an identifier that can be accessed through name lookup as a local variable. | ||
* In the expression `x.y`, `x` is a free identifier, but `y` is not. | ||
*/ | ||
export function forEachFreeIdentifier(node: Node, cb: (id: Identifier) => void): void { | ||
if (isIdentifier(node) && isFreeIdentifier(node)) cb(node); | ||
node.forEachChild(child => forEachFreeIdentifier(child, cb)); | ||
} | ||
|
||
function isFreeIdentifier(node: Identifier): boolean { | ||
const { parent } = node; | ||
switch (parent.kind) { | ||
case SyntaxKind.PropertyAccessExpression: | ||
|
||
return (parent as PropertyAccessExpression).name !== node; | ||
case SyntaxKind.BindingElement: | ||
return (parent as BindingElement).propertyName !== node; | ||
case SyntaxKind.ImportSpecifier: | ||
return (parent as ImportSpecifier).propertyName !== node; | ||
default: | ||
return true; | ||
} | ||
} | ||
} | ||
|
||
// Display-part writer helpers | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
/////*a*/import { x, y as z, T } from "m";/*b*/ | ||
////const m = 0; | ||
////x; | ||
////z; | ||
////const n: T = 0; | ||
|
||
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 _m from "m"; | ||
const m = 0; | ||
_m.x; | ||
_m.y; | ||
const n: _m.T = 0;`, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
/////*a*/import * as m from "m";/*b*/ | ||
////const a = 0; | ||
////m.a; | ||
////m.b; | ||
|
||
goTo.select("a", "b"); | ||
edit.applyRefactor({ | ||
refactorName: "Convert import", | ||
actionName: "Convert namespace import to named imports", | ||
actionDescription: "Convert namespace import to named imports", | ||
newContent: | ||
`import { a as _a, b } from "m"; | ||
const a = 0; | ||
_a; | ||
b;`, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
/////*a*/import * as m from "m";/*b*/ | ||
////m.a; | ||
////m; | ||
|
||
goTo.select("a", "b"); | ||
edit.applyRefactor({ | ||
refactorName: "Convert import", | ||
actionName: "Convert namespace import to named imports", | ||
actionDescription: "Convert namespace import to named imports", | ||
newContent: | ||
`import * as m from "m"; | ||
import { a } from "m"; | ||
a; | ||
m;`, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
////import /*a*/d/*b*/, * as n from "m"; | ||
|
||
goTo.select("a", "b"); | ||
verify.not.refactorAvailable("Convert import"); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
// @allowSyntheticDefaultImports: true | ||
|
||
/////*a*/import * as m from "m";/*b*/ | ||
////m(); | ||
|
||
goTo.select("a", "b"); | ||
edit.applyRefactor({ | ||
refactorName: "Convert import", | ||
actionName: "Convert namespace import to named imports", | ||
actionDescription: "Convert namespace import to named imports", | ||
newContent: | ||
`import m from "m"; | ||
m();`, | ||
}); |
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.
should we verify that the namedBindings is within the span..
e.g. selecting
d
inimport d, * as ns from "./mod"
should not trigger any action..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.
Added a test. If just
d
is selected,getParentNodeInSpan
will return justd
and not the entire import declaration.