Skip to content
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

Add refactor to convert namespace to named imports and back #24469

Merged
6 commits merged into from
May 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -4289,5 +4289,13 @@
"Convert '{0}' to mapped object type": {
"category": "Message",
"code": 95055
},
"Convert namespace import to named imports": {
"category": "Message",
"code": 95056
},
"Convert named imports to namespace import": {
"category": "Message",
"code": 95057
}
}
8 changes: 4 additions & 4 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,11 @@ namespace ts {
}

/**
* Returns a value indicating whether a name is unique globally or within the current file
* Returns a value indicating whether a name is unique globally or within the current file.
* Note: This does not consider whether a name appears as a free identifier or not, so at the expression `x.y` this includes both `x` and `y`.
*/
export function isFileLevelUniqueName(currentSourceFile: SourceFile, name: string, hasGlobalName?: PrintHandlers["hasGlobalName"]): boolean {
return !(hasGlobalName && hasGlobalName(name))
&& !currentSourceFile.identifiers.has(name);
export function isFileLevelUniqueName(sourceFile: SourceFile, name: string, hasGlobalName?: PrintHandlers["hasGlobalName"]): boolean {
return !(hasGlobalName && hasGlobalName(name)) && !sourceFile.identifiers.has(name);
}

// Returns true if this node is missing from the actual source code. A 'missing' node is different
Expand Down
1 change: 1 addition & 0 deletions src/harness/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"../services/codefixes/useDefaultImport.ts",
"../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts",
"../services/codefixes/convertToMappedObjectType.ts",
"../services/refactors/convertImport.ts",
"../services/refactors/extractSymbol.ts",
"../services/refactors/generateGetAccessorAndSetAccessor.ts",
"../services/refactors/moveToNewFile.ts",
Expand Down
1 change: 1 addition & 0 deletions src/server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"../services/codefixes/useDefaultImport.ts",
"../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts",
"../services/codefixes/convertToMappedObjectType.ts",
"../services/refactors/convertImport.ts",
"../services/refactors/extractSymbol.ts",
"../services/refactors/generateGetAccessorAndSetAccessor.ts",
"../services/refactors/moveToNewFile.ts",
Expand Down
1 change: 1 addition & 0 deletions src/server/tsconfig.library.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
"../services/codefixes/useDefaultImport.ts",
"../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts",
"../services/codefixes/convertToMappedObjectType.ts",
"../services/refactors/convertImport.ts",
"../services/refactors/extractSymbol.ts",
"../services/refactors/generateGetAccessorAndSetAccessor.ts",
"../services/refactors/moveToNewFile.ts",
Expand Down
18 changes: 12 additions & 6 deletions src/services/codefixes/convertToEs6Module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,22 +437,28 @@ namespace ts.codefix {
type FreeIdentifiers = ReadonlyMap<ReadonlyArray<Identifier>>;
function collectFreeIdentifiers(file: SourceFile): FreeIdentifiers {
const map = createMultiMap<Identifier>();
file.forEachChild(function recur(node) {
if (isIdentifier(node) && isFreeIdentifier(node)) {
map.add(node.text, node);
}
node.forEachChild(recur);
});
forEachFreeIdentifier(file, id => map.add(id.text, id));
return map;
}

/**
* 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.
*/
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;
}
Expand Down
23 changes: 15 additions & 8 deletions src/services/findAllReferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,16 +712,23 @@ namespace ts.FindAllReferences.Core {
}

/** Used as a quick check for whether a symbol is used at all in a file (besides its definition). */
export function isSymbolReferencedInFile(definition: Identifier, checker: TypeChecker, sourceFile: SourceFile) {
export function isSymbolReferencedInFile(definition: Identifier, checker: TypeChecker, sourceFile: SourceFile): boolean {
return eachSymbolReferenceInFile(definition, checker, sourceFile, () => true) || false;
}

export function eachSymbolReferenceInFile<T>(definition: Identifier, checker: TypeChecker, sourceFile: SourceFile, cb: (token: Identifier) => T): T | undefined {
const symbol = checker.getSymbolAtLocation(definition);
if (!symbol) return true; // Be lenient with invalid code.
return getPossibleSymbolReferenceNodes(sourceFile, symbol.name).some(token => {
if (!isIdentifier(token) || token === definition || token.escapedText !== definition.escapedText) return false;
const referenceSymbol = checker.getSymbolAtLocation(token)!;
return referenceSymbol === symbol
if (!symbol) return undefined;
for (const token of getPossibleSymbolReferenceNodes(sourceFile, symbol.name)) {
if (!isIdentifier(token) || token === definition || token.escapedText !== definition.escapedText) continue;
const referenceSymbol: Symbol = checker.getSymbolAtLocation(token)!; // See GH#19955 for why the type annotation is necessary
if (referenceSymbol === symbol
|| checker.getShorthandAssignmentValueSymbol(token.parent) === symbol
|| isExportSpecifier(token.parent) && getLocalSymbolForExportSpecifier(token, referenceSymbol, token.parent, checker) === symbol;
});
|| isExportSpecifier(token.parent) && getLocalSymbolForExportSpecifier(token, referenceSymbol, token.parent, checker) === symbol) {
const res = cb(token);
if (res) return res;
}
}
}

function getPossibleSymbolReferenceNodes(sourceFile: SourceFile, symbolName: string, container: Node = sourceFile): ReadonlyArray<Node> {
Expand Down
130 changes: 130 additions & 0 deletions src/services/refactors/convertImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/* @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;
Copy link
Contributor

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 in import d, * as ns from "./mod" should not trigger any action..

Copy link
Author

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 just d and not the entire import declaration.

}

function doChange(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, toConvert: NamedImportBindings): void {
const checker = program.getTypeChecker();
if (toConvert.kind === SyntaxKind.NamespaceImport) {
doChangeNamespaceToNamed(sourceFile, checker, changes, toConvert, getAllowSyntheticDefaultImports(program.getCompilerOptions()));
}
else {
doChangeNamedToNamespace(sourceFile, checker, changes, toConvert);
}
}

function doChangeNamespaceToNamed(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamespaceImport, allowSyntheticDefaultImports: boolean): void {
let usedAsNamespaceOrDefault = false;

const nodesToReplace: PropertyAccessExpression[] = [];
const conflictingNames = createMap<true>();

FindAllReferences.Core.eachSymbolReferenceInFile(toConvert.name, checker, sourceFile, id => {
if (!isPropertyAccessExpression(id.parent)) {
usedAsNamespaceOrDefault = true;
}
else {
const parent = cast(id.parent, isPropertyAccessExpression);
const exportName = parent.name.text;
if (checker.resolveName(exportName, id, SymbolFlags.All, /*excludeGlobals*/ true)) {
conflictingNames.set(exportName, true);
}
Debug.assert(parent.expression === id);
nodesToReplace.push(parent);
}
});

// We may need to change `mod.x` to `_x` to avoid a name conflict.
const exportNameToImportName = createMap<string>();

for (const propertyAccess of nodesToReplace) {
const exportName = propertyAccess.name.text;
let importName = exportNameToImportName.get(exportName);
if (importName === undefined) {
exportNameToImportName.set(exportName, importName = conflictingNames.has(exportName) ? getUniqueName(exportName, sourceFile) : exportName);
}
changes.replaceNode(sourceFile, propertyAccess, createIdentifier(importName));
}

const importSpecifiers: ImportSpecifier[] = [];
exportNameToImportName.forEach((name, propertyName) => {
importSpecifiers.push(createImportSpecifier(name === propertyName ? undefined : createIdentifier(propertyName), createIdentifier(name)));
});

const importDecl = toConvert.parent.parent;
if (usedAsNamespaceOrDefault && !allowSyntheticDefaultImports) {
// Need to leave the namespace import alone
changes.insertNodeAfter(sourceFile, importDecl, updateImport(importDecl, /*defaultImportName*/ undefined, importSpecifiers));
}
else {
changes.replaceNode(sourceFile, importDecl, updateImport(importDecl, usedAsNamespaceOrDefault ? createIdentifier(toConvert.name.text) : undefined, importSpecifiers));
}
}

function doChangeNamedToNamespace(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamedImports): void {
const importDecl = toConvert.parent.parent;
const { moduleSpecifier } = importDecl;

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);
const namespaceImportName = namespaceNameConflicts ? getUniqueName(preferredName, sourceFile) : preferredName;

const neededNamedImports: ImportSpecifier[] = [];

for (const element of toConvert.elements) {
const propertyName = (element.propertyName || element.name).text;
FindAllReferences.Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id => {
const access = createPropertyAccess(createIdentifier(namespaceImportName), propertyName);
if (isShorthandPropertyAssignment(id.parent)) {
changes.replaceNode(sourceFile, id.parent, createPropertyAssignment(id.text, access));
}
else if (isExportSpecifier(id.parent) && !id.parent.propertyName) {
if (!neededNamedImports.some(n => n.name === element.name)) {
neededNamedImports.push(createImportSpecifier(element.propertyName && createIdentifier(element.propertyName.text), createIdentifier(element.name.text)));
}
}
else {
changes.replaceNode(sourceFile, id, access);
}
});
}

changes.replaceNode(sourceFile, toConvert, createNamespaceImport(createIdentifier(namespaceImportName)));
if (neededNamedImports.length) {
changes.insertNodeAfter(sourceFile, toConvert.parent.parent, updateImport(importDecl, /*defaultImportName*/ undefined, neededNamedImports));
}
}

function updateImport(old: ImportDeclaration, defaultImportName: Identifier | undefined, elements: ReadonlyArray<ImportSpecifier> | undefined): ImportDeclaration {
return createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined,
createImportClause(defaultImportName, elements && elements.length ? createNamedImports(elements) : undefined), old.moduleSpecifier);
}
}
21 changes: 2 additions & 19 deletions src/services/refactors/extractSymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,7 @@ namespace ts.refactor.extractSymbol {

// Make a unique name for the extracted function
const file = scope.getSourceFile();
const functionNameText = getUniqueName(isClassLike(scope) ? "newMethod" : "newFunction", file.text);
const functionNameText = getUniqueName(isClassLike(scope) ? "newMethod" : "newFunction", file);
const isJS = isInJavaScriptFile(scope);

const functionName = createIdentifier(functionNameText);
Expand Down Expand Up @@ -1005,7 +1005,7 @@ namespace ts.refactor.extractSymbol {

// Make a unique name for the extracted variable
const file = scope.getSourceFile();
const localNameText = getUniqueName(isClassLike(scope) ? "newProperty" : "newLocal", file.text);
const localNameText = getUniqueName(isClassLike(scope) ? "newProperty" : "newLocal", file);
const isJS = isInJavaScriptFile(scope);

const variableType = isJS || !checker.isContextSensitive(node)
Expand Down Expand Up @@ -1744,23 +1744,6 @@ namespace ts.refactor.extractSymbol {
}
}

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);
}

/**
* Computes whether or not a node represents an expression in a position where it could
* be extracted.
Expand Down
4 changes: 2 additions & 2 deletions src/services/refactors/generateGetAccessorAndSetAccessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor {

const name = declaration.name.text;
const startWithUnderscore = startsWithUnderscore(name);
const fieldName = createPropertyName(startWithUnderscore ? name : getUniqueName(`_${name}`, file.text), declaration.name);
const accessorName = createPropertyName(startWithUnderscore ? getUniqueName(name.substring(1), file.text) : name, declaration.name);
const fieldName = createPropertyName(startWithUnderscore ? name : getUniqueName(`_${name}`, file), declaration.name);
const accessorName = createPropertyName(startWithUnderscore ? getUniqueName(name.substring(1), file) : name, declaration.name);
return {
isStatic: hasStaticModifier(declaration),
isReadonly: hasReadonlyModifier(declaration),
Expand Down
1 change: 1 addition & 0 deletions src/services/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
"codefixes/useDefaultImport.ts",
"codefixes/fixAddModuleReferTypeMissingTypeof.ts",
"codefixes/convertToMappedObjectType.ts",
"refactors/convertImport.ts",
"refactors/extractSymbol.ts",
"refactors/generateGetAccessorAndSetAccessor.ts",
"refactors/moveToNewFile.ts",
Expand Down
32 changes: 29 additions & 3 deletions src/services/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,23 @@ 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);
}
}

// Display-part writer helpers
Expand Down Expand Up @@ -1610,9 +1627,9 @@ namespace ts {
}

/* @internal */
export function getUniqueName(baseName: string, fileText: string): string {
export function getUniqueName(baseName: string, sourceFile: SourceFile): string {
let nameText = baseName;
for (let i = 1; stringContains(fileText, nameText); i++) {
for (let i = 1; !isFileLevelUniqueName(sourceFile, nameText); i++) {
nameText = `${baseName}_${i}`;
}
return nameText;
Expand All @@ -1631,7 +1648,7 @@ namespace ts {
Debug.assert(fileName === renameFilename);
for (const change of textChanges) {
const { span, newText } = change;
const index = newText.indexOf(name);
const index = indexInTextChange(newText, name);
if (index !== -1) {
lastPos = span.start + delta + index;

Expand All @@ -1649,4 +1666,13 @@ namespace ts {
Debug.assert(lastPos >= 0);
return lastPos;
}

function indexInTextChange(change: string, name: string): number {
if (startsWith(change, name)) return 0;
// Add a " " to avoid references inside words
let idx = change.indexOf(" " + name);
if (idx === -1) idx = change.indexOf("." + name);
if (idx === -1) idx = change.indexOf('"' + name);
return idx === -1 ? -1 : idx + 1;
}
}
4 changes: 2 additions & 2 deletions tests/cases/fourslash/extract-method-uniqueName.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference path='fourslash.ts' />

////// newFunction
////const newFunction = 0;
/////*start*/1 + 1/*end*/;

goTo.select('start', 'end')
Expand All @@ -9,7 +9,7 @@ edit.applyRefactor({
actionName: "function_scope_0",
actionDescription: "Extract to function in global scope",
newContent:
`// newFunction
`const newFunction = 0;
/*RENAME*/newFunction_1();

function newFunction_1() {
Expand Down