Skip to content

Commit 4e17e77

Browse files
author
Andy
authored
moveToNewFile: Update namespace imports (#24612) (#24657)
* moveToNewFile: Update namespace imports (#24612) * Port needed changes from #24469
1 parent 3bf21be commit 4e17e77

12 files changed

+159
-27
lines changed

src/services/findAllReferences.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -710,16 +710,23 @@ namespace ts.FindAllReferences.Core {
710710
}
711711

712712
/** Used as a quick check for whether a symbol is used at all in a file (besides its definition). */
713-
export function isSymbolReferencedInFile(definition: Identifier, checker: TypeChecker, sourceFile: SourceFile) {
713+
export function isSymbolReferencedInFile(definition: Identifier, checker: TypeChecker, sourceFile: SourceFile): boolean {
714+
return eachSymbolReferenceInFile(definition, checker, sourceFile, () => true) || false;
715+
}
716+
717+
export function eachSymbolReferenceInFile<T>(definition: Identifier, checker: TypeChecker, sourceFile: SourceFile, cb: (token: Identifier) => T): T | undefined {
714718
const symbol = checker.getSymbolAtLocation(definition);
715-
if (!symbol) return true; // Be lenient with invalid code.
716-
return getPossibleSymbolReferenceNodes(sourceFile, symbol.name).some(token => {
717-
if (!isIdentifier(token) || token === definition || token.escapedText !== definition.escapedText) return false;
718-
const referenceSymbol = checker.getSymbolAtLocation(token);
719-
return referenceSymbol === symbol
719+
if (!symbol) return undefined;
720+
for (const token of getPossibleSymbolReferenceNodes(sourceFile, symbol.name)) {
721+
if (!isIdentifier(token) || token === definition || token.escapedText !== definition.escapedText) continue;
722+
const referenceSymbol: Symbol = checker.getSymbolAtLocation(token)!; // See GH#19955 for why the type annotation is necessary
723+
if (referenceSymbol === symbol
720724
|| checker.getShorthandAssignmentValueSymbol(token.parent) === symbol
721-
|| isExportSpecifier(token.parent) && getLocalSymbolForExportSpecifier(token, referenceSymbol, token.parent, checker) === symbol;
722-
});
725+
|| isExportSpecifier(token.parent) && getLocalSymbolForExportSpecifier(token, referenceSymbol, token.parent, checker) === symbol) {
726+
const res = cb(token);
727+
if (res) return res;
728+
}
729+
}
723730
}
724731

725732
function getPossibleSymbolReferenceNodes(sourceFile: SourceFile, symbolName: string, container: Node = sourceFile): ReadonlyArray<Node> {

src/services/refactors/extractSymbol.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,7 @@ namespace ts.refactor.extractSymbol {
714714

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

720720
const functionName = createIdentifier(functionNameText);
@@ -1001,7 +1001,7 @@ namespace ts.refactor.extractSymbol {
10011001

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

10071007
const variableType = isJS || !checker.isContextSensitive(node)

src/services/refactors/generateGetAccessorAndSetAccessor.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor {
129129

130130
const name = declaration.name.text;
131131
const startWithUnderscore = startsWithUnderscore(name);
132-
const fieldName = createPropertyName(startWithUnderscore ? name : getUniqueName(`_${name}`, file.text), declaration.name);
133-
const accessorName = createPropertyName(startWithUnderscore ? getUniqueName(name.substring(1), file.text) : name, declaration.name);
132+
const fieldName = createPropertyName(startWithUnderscore ? name : getUniqueName(`_${name}`, file), declaration.name);
133+
const accessorName = createPropertyName(startWithUnderscore ? getUniqueName(name.substring(1), file) : name, declaration.name);
134134
return {
135135
isStatic: hasStaticModifier(declaration),
136136
isReadonly: hasReadonlyModifier(declaration),

src/services/refactors/moveToNewFile.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ namespace ts.refactor {
153153
if (sourceFile === oldFile) continue;
154154
for (const statement of sourceFile.statements) {
155155
forEachImportInStatement(statement, importNode => {
156+
if (checker.getSymbolAtLocation(moduleSpecifierFromImport(importNode)) !== oldFile.symbol) return;
157+
156158
const shouldMove = (name: Identifier): boolean => {
157159
const symbol = isBindingElement(name.parent)
158160
? getPropertySymbolFromBindingElement(checker, name.parent as BindingElement & { name: Identifier })
@@ -163,11 +165,76 @@ namespace ts.refactor {
163165
const newModuleSpecifier = combinePaths(getDirectoryPath(moduleSpecifierFromImport(importNode).text), newModuleName);
164166
const newImportDeclaration = filterImport(importNode, createLiteral(newModuleSpecifier), shouldMove);
165167
if (newImportDeclaration) changes.insertNodeAfter(sourceFile, statement, newImportDeclaration);
168+
169+
const ns = getNamespaceLikeImport(importNode);
170+
if (ns) updateNamespaceLikeImport(changes, sourceFile, checker, movedSymbols, newModuleName, newModuleSpecifier, ns, importNode);
166171
});
167172
}
168173
}
169174
}
170175

176+
function getNamespaceLikeImport(node: SupportedImport): Identifier | undefined {
177+
switch (node.kind) {
178+
case SyntaxKind.ImportDeclaration:
179+
return node.importClause && node.importClause.namedBindings && node.importClause.namedBindings.kind === SyntaxKind.NamespaceImport ?
180+
node.importClause.namedBindings.name : undefined;
181+
case SyntaxKind.ImportEqualsDeclaration:
182+
return node.name;
183+
case SyntaxKind.VariableDeclaration:
184+
return tryCast(node.name, isIdentifier);
185+
default:
186+
return Debug.assertNever(node);
187+
}
188+
}
189+
190+
function updateNamespaceLikeImport(
191+
changes: textChanges.ChangeTracker,
192+
sourceFile: SourceFile,
193+
checker: TypeChecker,
194+
movedSymbols: ReadonlySymbolSet,
195+
newModuleName: string,
196+
newModuleSpecifier: string,
197+
oldImportId: Identifier,
198+
oldImportNode: SupportedImport,
199+
): void {
200+
const preferredNewNamespaceName = codefix.moduleSpecifierToValidIdentifier(newModuleName, ScriptTarget.ESNext);
201+
let needUniqueName = false;
202+
const toChange: Identifier[] = [];
203+
FindAllReferences.Core.eachSymbolReferenceInFile(oldImportId, checker, sourceFile, ref => {
204+
if (!isPropertyAccessExpression(ref.parent)) return;
205+
needUniqueName = needUniqueName || !!checker.resolveName(preferredNewNamespaceName, ref, SymbolFlags.All, /*excludeGlobals*/ true);
206+
if (movedSymbols.has(checker.getSymbolAtLocation(ref.parent.name)!)) {
207+
toChange.push(ref);
208+
}
209+
});
210+
211+
if (toChange.length) {
212+
const newNamespaceName = needUniqueName ? getUniqueName(preferredNewNamespaceName, sourceFile) : preferredNewNamespaceName;
213+
for (const ref of toChange) {
214+
changes.replaceNode(sourceFile, ref, createIdentifier(newNamespaceName));
215+
}
216+
changes.insertNodeAfter(sourceFile, oldImportNode, updateNamespaceLikeImportNode(oldImportNode, newModuleName, newModuleSpecifier));
217+
}
218+
}
219+
220+
function updateNamespaceLikeImportNode(node: SupportedImport, newNamespaceName: string, newModuleSpecifier: string): Node {
221+
const newNamespaceId = createIdentifier(newNamespaceName);
222+
const newModuleString = createLiteral(newModuleSpecifier);
223+
switch (node.kind) {
224+
case SyntaxKind.ImportDeclaration:
225+
return createImportDeclaration(
226+
/*decorators*/ undefined, /*modifiers*/ undefined,
227+
createImportClause(/*name*/ undefined, createNamespaceImport(newNamespaceId)),
228+
newModuleString);
229+
case SyntaxKind.ImportEqualsDeclaration:
230+
return createImportEqualsDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, newNamespaceId, createExternalModuleReference(newModuleString));
231+
case SyntaxKind.VariableDeclaration:
232+
return createVariableDeclaration(newNamespaceId, /*type*/ undefined, createRequireCall(newModuleString));
233+
default:
234+
return Debug.assertNever(node);
235+
}
236+
}
237+
171238
function moduleSpecifierFromImport(i: SupportedImport): StringLiteralLike {
172239
return (i.kind === SyntaxKind.ImportDeclaration ? i.moduleSpecifier
173240
: i.kind === SyntaxKind.ImportEqualsDeclaration ? i.moduleReference.expression

src/services/utilities.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,9 +1596,9 @@ namespace ts {
15961596
}
15971597

15981598
/* @internal */
1599-
export function getUniqueName(baseName: string, fileText: string): string {
1599+
export function getUniqueName(baseName: string, sourceFile: SourceFile): string {
16001600
let nameText = baseName;
1601-
for (let i = 1; stringContains(fileText, nameText); i++) {
1601+
for (let i = 1; !isFileLevelUniqueName(sourceFile, nameText); i++) {
16021602
nameText = `${baseName}_${i}`;
16031603
}
16041604
return nameText;
@@ -1617,7 +1617,7 @@ namespace ts {
16171617
Debug.assert(fileName === renameFilename);
16181618
for (const change of textChanges) {
16191619
const { span, newText } = change;
1620-
const index = newText.indexOf(name);
1620+
const index = indexInTextChange(newText, name);
16211621
if (index !== -1) {
16221622
lastPos = span.start + delta + index;
16231623

@@ -1635,4 +1635,13 @@ namespace ts {
16351635
Debug.assert(lastPos >= 0);
16361636
return lastPos;
16371637
}
1638+
1639+
function indexInTextChange(change: string, name: string): number {
1640+
if (startsWith(change, name)) return 0;
1641+
// Add a " " to avoid references inside words
1642+
let idx = change.indexOf(" " + name);
1643+
if (idx === -1) idx = change.indexOf("." + name);
1644+
if (idx === -1) idx = change.indexOf('"' + name);
1645+
return idx === -1 ? -1 : idx + 1;
1646+
}
16381647
}

tests/cases/fourslash/extract-method-uniqueName.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference path='fourslash.ts' />
22

3-
////// newFunction
3+
////const newFunction = 0;
44
/////*start*/1 + 1/*end*/;
55

66
goTo.select('start', 'end')
@@ -9,7 +9,7 @@ edit.applyRefactor({
99
actionName: "function_scope_0",
1010
actionDescription: "Extract to function in global scope",
1111
newContent:
12-
`// newFunction
12+
`const newFunction = 0;
1313
/*RENAME*/newFunction_1();
1414
1515
function newFunction_1() {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @allowJs: true
4+
5+
// @Filename: /a.ts
6+
////[|export const x = 0;|]
7+
////export const y = 0;
8+
9+
// @Filename: /b.ts
10+
////import * as a from "./a";
11+
////a.x;
12+
////a.y;
13+
14+
// @Filename: /c.ts
15+
////import a = require("./a");
16+
////a.x;
17+
////a.y;
18+
19+
// @Filename: /d.js
20+
////const a = require("./a");
21+
////a.x;
22+
////a.y;
23+
24+
verify.moveToNewFile({
25+
newFileContents: {
26+
"/a.ts":
27+
`export const y = 0;`,
28+
29+
"/x.ts":
30+
`export const x = 0;`,
31+
32+
"/b.ts":
33+
`import * as a from "./a";
34+
import * as x from "./x";
35+
x.x;
36+
a.y;`,
37+
38+
"/c.ts":
39+
`import a = require("./a");
40+
import x = require("./x");
41+
x.x;
42+
a.y;`,
43+
44+
"/d.js":
45+
`const a = require("./a"), x = require("./x");
46+
x.x;
47+
a.y;`,
48+
},
49+
});

tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess23.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ edit.applyRefactor({
2929
actionDescription: "Generate 'get' and 'set' accessors",
3030
newContent: `class A {
3131
private _a: number = 1;
32-
public get /*RENAME*/a_2(): number {
32+
public get /*RENAME*/a_1(): number {
3333
return this._a;
3434
}
35-
public set a_2(value: number) {
35+
public set a_1(value: number) {
3636
this._a = value;
3737
}
3838
private _a_1: string = "foo";

tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess4.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ edit.applyRefactor({
1111
actionDescription: "Generate 'get' and 'set' accessors",
1212
newContent: `class A {
1313
private _a: string;
14-
public get /*RENAME*/a_1(): string {
14+
public get /*RENAME*/a(): string {
1515
return this._a;
1616
}
17-
public set a_1(value: string) {
17+
public set a(value: string) {
1818
this._a = value;
1919
}
2020
}`,

tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess5.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ edit.applyRefactor({
1111
actionDescription: "Generate 'get' and 'set' accessors",
1212
newContent: `class A {
1313
private _a: string;
14-
public get /*RENAME*/a_1(): string {
14+
public get /*RENAME*/a(): string {
1515
return this._a;
1616
}
17-
public set a_1(value: string) {
17+
public set a(value: string) {
1818
this._a = value;
1919
}
2020
}`,

tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess6.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ edit.applyRefactor({
1111
actionDescription: "Generate 'get' and 'set' accessors",
1212
newContent: `class A {
1313
private _a: string;
14-
public get /*RENAME*/a_1(): string {
14+
public get /*RENAME*/a(): string {
1515
return this._a;
1616
}
17-
public set a_1(value: string) {
17+
public set a(value: string) {
1818
this._a = value;
1919
}
2020
}`,

tests/cases/fourslash/refactorConvertToGetAccessAndSetAccess7.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ edit.applyRefactor({
1111
actionDescription: "Generate 'get' and 'set' accessors",
1212
newContent: `class A {
1313
private _a: string;
14-
protected get /*RENAME*/a_1(): string {
14+
protected get /*RENAME*/a(): string {
1515
return this._a;
1616
}
17-
protected set a_1(value: string) {
17+
protected set a(value: string) {
1818
this._a = value;
1919
}
2020
}`,

0 commit comments

Comments
 (0)