Skip to content

Commit 5bb8921

Browse files
author
Andy Hanson
committed
Don't use forEachFreeIdentifier
1 parent 4cc0474 commit 5bb8921

13 files changed

+93
-79
lines changed

src/compiler/utilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ namespace ts {
224224

225225
/**
226226
* Returns a value indicating whether a name is unique globally or within the current file.
227-
* Note: This does not consider whether a name appears as a free identifier or not. For that, use `forEachFreeIdentifier`.
227+
* 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`.
228228
*/
229229
export function isFileLevelUniqueName(sourceFile: SourceFile, name: string, hasGlobalName?: PrintHandlers["hasGlobalName"]): boolean {
230230
return !(hasGlobalName && hasGlobalName(name)) && !sourceFile.identifiers.has(name);

src/services/codefixes/convertToEs6Module.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,29 @@ namespace ts.codefix {
441441
return map;
442442
}
443443

444+
/**
445+
* A free identifier is an identifier that can be accessed through name lookup as a local variable.
446+
* In the expression `x.y`, `x` is a free identifier, but `y` is not.
447+
*/
448+
function forEachFreeIdentifier(node: Node, cb: (id: Identifier) => void): void {
449+
if (isIdentifier(node) && isFreeIdentifier(node)) cb(node);
450+
node.forEachChild(child => forEachFreeIdentifier(child, cb));
451+
}
452+
453+
function isFreeIdentifier(node: Identifier): boolean {
454+
const { parent } = node;
455+
switch (parent.kind) {
456+
case SyntaxKind.PropertyAccessExpression:
457+
return (parent as PropertyAccessExpression).name !== node;
458+
case SyntaxKind.BindingElement:
459+
return (parent as BindingElement).propertyName !== node;
460+
case SyntaxKind.ImportSpecifier:
461+
return (parent as ImportSpecifier).propertyName !== node;
462+
default:
463+
return true;
464+
}
465+
}
466+
444467
// Node helpers
445468

446469
function functionExpressionToDeclaration(name: string | undefined, additionalModifiers: ReadonlyArray<Modifier>, fn: FunctionExpression | ArrowFunction | MethodDeclaration): FunctionDeclaration {

src/services/refactors/convertImport.ts

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,58 +30,72 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor {
3030
}
3131

3232
function doChange(sourceFile: SourceFile, program: Program, changes: textChanges.ChangeTracker, toConvert: NamedImportBindings): void {
33-
const usedIdentifiers = createMap<true>();
34-
forEachFreeIdentifier(sourceFile, id => usedIdentifiers.set(id.text, true));
3533
const checker = program.getTypeChecker();
36-
3734
if (toConvert.kind === SyntaxKind.NamespaceImport) {
38-
doChangeNamespaceToNamed(sourceFile, checker, changes, toConvert, usedIdentifiers, getAllowSyntheticDefaultImports(program.getCompilerOptions()));
35+
doChangeNamespaceToNamed(sourceFile, checker, changes, toConvert, getAllowSyntheticDefaultImports(program.getCompilerOptions()));
3936
}
4037
else {
41-
doChangeNamedToNamespace(sourceFile, checker, changes, toConvert, usedIdentifiers);
38+
doChangeNamedToNamespace(sourceFile, checker, changes, toConvert);
4239
}
4340
}
4441

45-
function doChangeNamespaceToNamed(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamespaceImport, usedIdentifiers: ReadonlyMap<true>, allowSyntheticDefaultImports: boolean): void {
46-
// We may need to change `mod.x` to `_x` to avoid a name conflict.
47-
const exportNameToImportName = createMap<string>();
42+
function doChangeNamespaceToNamed(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamespaceImport, allowSyntheticDefaultImports: boolean): void {
4843
let usedAsNamespaceOrDefault = false;
4944

45+
const nodesToReplace: PropertyAccessExpression[] = [];
46+
const conflictingNames = createMap<true>();
47+
5048
FindAllReferences.Core.eachSymbolReferenceInFile(toConvert.name, checker, sourceFile, id => {
5149
if (!isPropertyAccessExpression(id.parent)) {
5250
usedAsNamespaceOrDefault = true;
5351
}
5452
else {
5553
const parent = cast(id.parent, isPropertyAccessExpression);
5654
const exportName = parent.name.text;
57-
let name = exportNameToImportName.get(exportName);
58-
if (name === undefined) {
59-
exportNameToImportName.set(exportName, name = generateName(exportName, usedIdentifiers));
55+
if (checker.resolveName(exportName, id, SymbolFlags.All, /*excludeGlobals*/ true)) {
56+
conflictingNames.set(exportName, true);
6057
}
6158
Debug.assert(parent.expression === id);
62-
changes.replaceNode(sourceFile, parent, createIdentifier(name));
59+
nodesToReplace.push(parent);
6360
}
6461
});
6562

66-
const elements: ImportSpecifier[] = [];
63+
// We may need to change `mod.x` to `_x` to avoid a name conflict.
64+
const exportNameToImportName = createMap<string>();
65+
66+
for (const propertyAccess of nodesToReplace) {
67+
const exportName = propertyAccess.name.text;
68+
let importName = exportNameToImportName.get(exportName);
69+
if (importName === undefined) {
70+
exportNameToImportName.set(exportName, importName = conflictingNames.has(exportName) ? getUniqueName(exportName, sourceFile) : exportName);
71+
}
72+
changes.replaceNode(sourceFile, propertyAccess, createIdentifier(importName));
73+
}
74+
75+
const importSpecifiers: ImportSpecifier[] = [];
6776
exportNameToImportName.forEach((name, propertyName) => {
68-
elements.push(createImportSpecifier(name === propertyName ? undefined : createIdentifier(propertyName), createIdentifier(name)));
77+
importSpecifiers.push(createImportSpecifier(name === propertyName ? undefined : createIdentifier(propertyName), createIdentifier(name)));
6978
});
7079

7180
const importDecl = toConvert.parent.parent;
7281
if (usedAsNamespaceOrDefault && !allowSyntheticDefaultImports) {
7382
// Need to leave the namespace import alone
74-
changes.insertNodeAfter(sourceFile, importDecl, updateImport(importDecl, /*defaultImportName*/ undefined, elements));
83+
changes.insertNodeAfter(sourceFile, importDecl, updateImport(importDecl, /*defaultImportName*/ undefined, importSpecifiers));
7584
}
7685
else {
77-
changes.replaceNode(sourceFile, importDecl, updateImport(importDecl, usedAsNamespaceOrDefault ? createIdentifier(toConvert.name.text) : undefined, elements));
86+
changes.replaceNode(sourceFile, importDecl, updateImport(importDecl, usedAsNamespaceOrDefault ? createIdentifier(toConvert.name.text) : undefined, importSpecifiers));
7887
}
7988
}
8089

81-
function doChangeNamedToNamespace(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamedImports, usedIdentifiers: ReadonlyMap<true>): void {
82-
const { moduleSpecifier } = toConvert.parent.parent;
83-
// We know the user is using at least ScriptTarget.ES6, and moduleSpecifierToValidIdentifier only cares if we're using ES5+, so just set ScriptTarget.ESNext
84-
const namespaceImportName = generateName(moduleSpecifier && isStringLiteral(moduleSpecifier) ? codefix.moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module", usedIdentifiers);
90+
function doChangeNamedToNamespace(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, toConvert: NamedImports): void {
91+
const importDecl = toConvert.parent.parent;
92+
const { moduleSpecifier } = importDecl;
93+
94+
const preferredName = moduleSpecifier && isStringLiteral(moduleSpecifier) ? codefix.moduleSpecifierToValidIdentifier(moduleSpecifier.text, ScriptTarget.ESNext) : "module";
95+
const namespaceNameConflicts = toConvert.elements.some(element =>
96+
FindAllReferences.Core.eachSymbolReferenceInFile(element.name, checker, sourceFile, id =>
97+
!!checker.resolveName(preferredName, id, SymbolFlags.All, /*excludeGlobals*/ true)) || false);
98+
const namespaceImportName = namespaceNameConflicts ? getUniqueName(preferredName, sourceFile) : preferredName;
8599

86100
const neededNamedImports: ImportSpecifier[] = [];
87101

@@ -105,7 +119,6 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor {
105119

106120
changes.replaceNode(sourceFile, toConvert, createNamespaceImport(createIdentifier(namespaceImportName)));
107121
if (neededNamedImports.length) {
108-
const importDecl = toConvert.parent.parent;
109122
changes.insertNodeAfter(sourceFile, toConvert.parent.parent, updateImport(importDecl, /*defaultImportName*/ undefined, neededNamedImports));
110123
}
111124
}
@@ -114,11 +127,4 @@ namespace ts.refactor.generateGetAccessorAndSetAccessor {
114127
return createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined,
115128
createImportClause(defaultImportName, elements && elements.length ? createNamedImports(elements) : undefined), old.moduleSpecifier);
116129
}
117-
118-
function generateName(name: string, usedIdentifiers: ReadonlyMap<true>): string {
119-
while (usedIdentifiers.has(name)) {
120-
name = `_${name}`;
121-
}
122-
return name;
123-
}
124130
}

src/services/refactors/extractSymbol.ts

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

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

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

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

10111011
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/utilities.ts

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,29 +1326,6 @@ namespace ts {
13261326
return textSpanContainsPosition(span, node.getStart(file)) &&
13271327
node.getEnd() <= textSpanEnd(span);
13281328
}
1329-
1330-
/**
1331-
* A free identifier is an identifier that can be accessed through name lookup as a local variable.
1332-
* In the expression `x.y`, `x` is a free identifier, but `y` is not.
1333-
*/
1334-
export function forEachFreeIdentifier(node: Node, cb: (id: Identifier) => void): void {
1335-
if (isIdentifier(node) && isFreeIdentifier(node)) cb(node);
1336-
node.forEachChild(child => forEachFreeIdentifier(child, cb));
1337-
}
1338-
1339-
function isFreeIdentifier(node: Identifier): boolean {
1340-
const { parent } = node;
1341-
switch (parent.kind) {
1342-
case SyntaxKind.PropertyAccessExpression:
1343-
return (parent as PropertyAccessExpression).name !== node;
1344-
case SyntaxKind.BindingElement:
1345-
return (parent as BindingElement).propertyName !== node;
1346-
case SyntaxKind.ImportSpecifier:
1347-
return (parent as ImportSpecifier).propertyName !== node;
1348-
default:
1349-
return true;
1350-
}
1351-
}
13521329
}
13531330

13541331
// Display-part writer helpers
@@ -1650,9 +1627,9 @@ namespace ts {
16501627
}
16511628

16521629
/* @internal */
1653-
export function getUniqueName(baseName: string, fileText: string): string {
1630+
export function getUniqueName(baseName: string, sourceFile: SourceFile): string {
16541631
let nameText = baseName;
1655-
for (let i = 1; stringContains(fileText, nameText); i++) {
1632+
for (let i = 1; !isFileLevelUniqueName(sourceFile, nameText); i++) {
16561633
nameText = `${baseName}_${i}`;
16571634
}
16581635
return nameText;
@@ -1671,7 +1648,7 @@ namespace ts {
16711648
Debug.assert(fileName === renameFilename);
16721649
for (const change of textChanges) {
16731650
const { span, newText } = change;
1674-
const index = newText.indexOf(name);
1651+
const index = indexInTextChange(newText, name);
16751652
if (index !== -1) {
16761653
lastPos = span.start + delta + index;
16771654

@@ -1689,4 +1666,12 @@ namespace ts {
16891666
Debug.assert(lastPos >= 0);
16901667
return lastPos;
16911668
}
1669+
1670+
function indexInTextChange(change: string, name: string): number {
1671+
if (startsWith(change, name)) return 0;
1672+
// Add a " " to avoid references inside words
1673+
let idx = change.indexOf(" " + name);
1674+
if (idx === -1) idx = change.indexOf('"' + name);
1675+
return idx === -1 ? -1 : idx + 1;
1676+
}
16921677
}

tests/cases/fourslash/refactorConvertImport_namedToNamespace.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ edit.applyRefactor({
1313
actionName: "Convert named imports to namespace import",
1414
actionDescription: "Convert named imports to namespace import",
1515
newContent:
16-
// TODO: GH#23781 (_m.y shouldn't be indented)
17-
`import * as _m from "m";
16+
// TODO: GH#23781 (m_1.y shouldn't be indented)
17+
`import * as m_1 from "m";
1818
import { x } from "m";
1919
const m = 0;
20-
const o = { x: _m.x };
20+
const o = { x: m_1.x };
2121
export { x }; // Need a named import for this
22-
_m.y;
23-
const n: _m.T = 0;`,
22+
m_1.y;
23+
const n: m_1.T = 0;`,
2424
});

tests/cases/fourslash/refactorConvertImport_namespaceToNamed.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ edit.applyRefactor({
1111
actionName: "Convert namespace import to named imports",
1212
actionDescription: "Convert namespace import to named imports",
1313
newContent:
14-
`import { a as _a, b } from "m";
14+
`import { a as a_1, b } from "m";
1515
const a = 0;
16-
_a;
16+
a_1;
1717
b;`,
1818
});

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: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/// <reference path='fourslash.ts' />
22

3-
//// class A {
4-
//// /*a*/public _a: string;/*b*/
5-
//// }
3+
////class A {
4+
//// /*a*/public _a: string;/*b*/
5+
////}
66

77
goTo.select("a", "b");
88
edit.applyRefactor({
@@ -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)