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

moveToNewFile: Update namespace imports (#24612) #24657

Merged
2 commits merged into from
Jun 4, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions src/services/findAllReferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -710,16 +710,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
4 changes: 2 additions & 2 deletions src/services/refactors/extractSymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,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 @@ -1001,7 +1001,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
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
67 changes: 67 additions & 0 deletions src/services/refactors/moveToNewFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ namespace ts.refactor {
if (sourceFile === oldFile) continue;
for (const statement of sourceFile.statements) {
forEachImportInStatement(statement, importNode => {
if (checker.getSymbolAtLocation(moduleSpecifierFromImport(importNode)) !== oldFile.symbol) return;

const shouldMove = (name: Identifier): boolean => {
const symbol = isBindingElement(name.parent)
? getPropertySymbolFromBindingElement(checker, name.parent as BindingElement & { name: Identifier })
Expand All @@ -163,11 +165,76 @@ namespace ts.refactor {
const newModuleSpecifier = combinePaths(getDirectoryPath(moduleSpecifierFromImport(importNode).text), newModuleName);
const newImportDeclaration = filterImport(importNode, createLiteral(newModuleSpecifier), shouldMove);
if (newImportDeclaration) changes.insertNodeAfter(sourceFile, statement, newImportDeclaration);

const ns = getNamespaceLikeImport(importNode);
if (ns) updateNamespaceLikeImport(changes, sourceFile, checker, movedSymbols, newModuleName, newModuleSpecifier, ns, importNode);
});
}
}
}

function getNamespaceLikeImport(node: SupportedImport): Identifier | undefined {
switch (node.kind) {
case SyntaxKind.ImportDeclaration:
return node.importClause && node.importClause.namedBindings && node.importClause.namedBindings.kind === SyntaxKind.NamespaceImport ?
node.importClause.namedBindings.name : undefined;
case SyntaxKind.ImportEqualsDeclaration:
return node.name;
case SyntaxKind.VariableDeclaration:
return tryCast(node.name, isIdentifier);
default:
return Debug.assertNever(node);
}
}

function updateNamespaceLikeImport(
changes: textChanges.ChangeTracker,
sourceFile: SourceFile,
checker: TypeChecker,
movedSymbols: ReadonlySymbolSet,
newModuleName: string,
newModuleSpecifier: string,
oldImportId: Identifier,
oldImportNode: SupportedImport,
): void {
const preferredNewNamespaceName = codefix.moduleSpecifierToValidIdentifier(newModuleName, ScriptTarget.ESNext);
let needUniqueName = false;
const toChange: Identifier[] = [];
FindAllReferences.Core.eachSymbolReferenceInFile(oldImportId, checker, sourceFile, ref => {
if (!isPropertyAccessExpression(ref.parent)) return;
needUniqueName = needUniqueName || !!checker.resolveName(preferredNewNamespaceName, ref, SymbolFlags.All, /*excludeGlobals*/ true);
if (movedSymbols.has(checker.getSymbolAtLocation(ref.parent.name)!)) {
toChange.push(ref);
}
});

if (toChange.length) {
const newNamespaceName = needUniqueName ? getUniqueName(preferredNewNamespaceName, sourceFile) : preferredNewNamespaceName;
for (const ref of toChange) {
changes.replaceNode(sourceFile, ref, createIdentifier(newNamespaceName));
}
changes.insertNodeAfter(sourceFile, oldImportNode, updateNamespaceLikeImportNode(oldImportNode, newModuleName, newModuleSpecifier));
}
}

function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: string, newModuleSpecifier: string): Node {
const newNamespaceId = createIdentifier(newNamespaceName);
const newModuleString = createLiteral(newModuleSpecifier);
switch (node.kind) {
case SyntaxKind.ImportDeclaration:
return createImportDeclaration(
/*decorators*/ undefined, /*modifiers*/ undefined,
createImportClause(/*name*/ undefined, createNamespaceImport(newNamespaceId)),
newModuleString);
case SyntaxKind.ImportEqualsDeclaration:
return createImportEqualsDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, newNamespaceId, createExternalModuleReference(newModuleString));
case SyntaxKind.VariableDeclaration:
return createVariableDeclaration(newNamespaceId, /*type*/ undefined, createRequireCall(newModuleString));
default:
return Debug.assertNever(node);
}
}

function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike {
return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier
: i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression
Expand Down
15 changes: 12 additions & 3 deletions src/services/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1596,9 +1596,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 @@ -1617,7 +1617,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 @@ -1635,4 +1635,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
49 changes: 49 additions & 0 deletions tests/cases/fourslash/moveToNewFile_namespaceImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/// <reference path='fourslash.ts' />

// @allowJs: true

// @Filename: /a.ts
////[|export const x = 0;|]
////export const y = 0;

// @Filename: /b.ts
////import * as a from "./a";
////a.x;
////a.y;

// @Filename: /c.ts
////import a = require("./a");
////a.x;
////a.y;

// @Filename: /d.js
////const a = require("./a");
////a.x;
////a.y;

verify.moveToNewFile({
newFileContents: {
"/a.ts":
`export const y = 0;`,

"/x.ts":
`export const x = 0;`,

"/b.ts":
`import * as a from "./a";
import * as x from "./x";
x.x;
a.y;`,

"/c.ts":
`import a = require("./a");
import x = require("./x");
x.x;
a.y;`,

"/d.js":
`const a = require("./a"), x = require("./x");
x.x;
a.y;`,
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ edit.applyRefactor({
actionDescription: "Generate 'get' and 'set' accessors",
newContent: `class A {
private _a: number = 1;
public get /*RENAME*/a_2(): number {
public get /*RENAME*/a_1(): number {
return this._a;
}
public set a_2(value: number) {
public set a_1(value: number) {
this._a = value;
}
private _a_1: string = "foo";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ edit.applyRefactor({
actionDescription: "Generate 'get' and 'set' accessors",
newContent: `class A {
private _a: string;
public get /*RENAME*/a_1(): string {
public get /*RENAME*/a(): string {
return this._a;
}
public set a_1(value: string) {
public set a(value: string) {
this._a = value;
}
}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ edit.applyRefactor({
actionDescription: "Generate 'get' and 'set' accessors",
newContent: `class A {
private _a: string;
public get /*RENAME*/a_1(): string {
public get /*RENAME*/a(): string {
return this._a;
}
public set a_1(value: string) {
public set a(value: string) {
this._a = value;
}
}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ edit.applyRefactor({
actionDescription: "Generate 'get' and 'set' accessors",
newContent: `class A {
private _a: string;
public get /*RENAME*/a_1(): string {
public get /*RENAME*/a(): string {
return this._a;
}
public set a_1(value: string) {
public set a(value: string) {
this._a = value;
}
}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ edit.applyRefactor({
actionDescription: "Generate 'get' and 'set' accessors",
newContent: `class A {
private _a: string;
protected get /*RENAME*/a_1(): string {
protected get /*RENAME*/a(): string {
return this._a;
}
protected set a_1(value: string) {
protected set a(value: string) {
this._a = value;
}
}`,
Expand Down