Skip to content

Commit a398e71

Browse files
committed
Address PR feedback
Eliminate cancellation token Add organizeImports.ts to tsconfig.json Simplify ts.OrganizeImports.organizeImports Simplify sortImports Semantic change: all invalid module specifiers are now considered to be equal. Simplify comparisons using || Pull out imports with invalid modules specifiers ...for separate processing. They are tacked on to the end of the organized imports in their original order. Bonus: downstream functions can now assume imports have valid module specifiers. Rename baseline folder with leading lowercase Simplify coalesceImports Remove some unnecessary null checks Simplify baseline generation
1 parent af8c76b commit a398e71

File tree

10 files changed

+129
-164
lines changed

10 files changed

+129
-164
lines changed

src/compiler/core.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1897,6 +1897,11 @@ namespace ts {
18971897
Comparison.EqualTo;
18981898
}
18991899

1900+
/** True is greater than false. */
1901+
export function compareBooleans(a: boolean, b: boolean): Comparison {
1902+
return compareValues(a ? 1 : 0, b ? 1 : 0);
1903+
}
1904+
19001905
function compareMessageText(text1: string | DiagnosticMessageChain, text2: string | DiagnosticMessageChain): Comparison {
19011906
while (text1 && text2) {
19021907
// We still have both chains.

src/harness/unittests/organizeImports.ts

+24-31
Original file line numberDiff line numberDiff line change
@@ -54,34 +54,12 @@ namespace ts {
5454
`import x from "./lib2";`);
5555
});
5656

57-
it("Sort - invalid vs invalid", () => {
58-
assertSortsBefore(
59-
// tslint:disable-next-line no-invalid-template-strings
60-
"import y from `${'lib1'}`;",
61-
// tslint:disable-next-line no-invalid-template-strings
62-
"import x from `${'lib2'}`;");
63-
});
64-
6557
it("Sort - relative vs non-relative", () => {
6658
assertSortsBefore(
6759
`import y from "lib";`,
6860
`import x from "./lib";`);
6961
});
7062

71-
it("Sort - non-relative vs invalid", () => {
72-
assertSortsBefore(
73-
`import y from "lib";`,
74-
// tslint:disable-next-line no-invalid-template-strings
75-
"import x from `${'lib'}`;");
76-
});
77-
78-
it("Sort - relative vs invalid", () => {
79-
assertSortsBefore(
80-
`import y from "./lib";`,
81-
// tslint:disable-next-line no-invalid-template-strings
82-
"import x from `${'lib'}`;");
83-
});
84-
8563
function assertUnaffectedBySort(...importStrings: string[]) {
8664
const unsortedImports1 = parseImports(...importStrings);
8765
assertListEqual(unsortedImports1, OrganizeImports.sortImports(unsortedImports1));
@@ -282,6 +260,23 @@ import * as NS from "lib";
282260
NS.F1();
283261
import D from "lib";
284262
D();
263+
`,
264+
},
265+
libFile);
266+
267+
testOrganizeImports("MoveToTop_Invalid",
268+
{
269+
path: "/test.ts",
270+
content: `
271+
import { F1, F2 } from "lib";
272+
F1();
273+
F2();
274+
import * as NS from "lib";
275+
NS.F1();
276+
import b from ${"`${'lib'}`"};
277+
import a from ${"`${'lib'}`"};
278+
import D from "lib";
279+
D();
285280
`,
286281
},
287282
libFile);
@@ -311,7 +306,7 @@ F2();
311306
{ path: "/lib2.ts", content: "" });
312307

313308
function testOrganizeImports(testName: string, testFile: TestFSWithWatch.FileOrFolder, ...otherFiles: TestFSWithWatch.FileOrFolder[]) {
314-
it(testName, () => runBaseline(`OrganizeImports/${testName}.ts`, testFile, ...otherFiles));
309+
it(testName, () => runBaseline(`organizeImports/${testName}.ts`, testFile, ...otherFiles));
315310
}
316311

317312
function runBaseline(baselinePath: string, testFile: TestFSWithWatch.FileOrFolder, ...otherFiles: TestFSWithWatch.FileOrFolder[]) {
@@ -322,15 +317,13 @@ F2();
322317
assert.equal(testPath, changes[0].fileName);
323318

324319
Harness.Baseline.runBaseline(baselinePath, () => {
325-
const data: string[] = [];
326-
data.push(`// ==ORIGINAL==`);
327-
data.push(testContent);
328-
329-
data.push(`// ==ORGANIZED==`);
330320
const newText = textChanges.applyChanges(testContent, changes[0].textChanges);
331-
data.push(newText);
332-
333-
return data.join(newLineCharacter);
321+
return [
322+
"// ==ORIGINAL==",
323+
testContent,
324+
"// ==ORGANIZED==",
325+
newText,
326+
].join(newLineCharacter);
334327
});
335328
}
336329

src/services/organizeImports.ts

+76-132
Original file line numberDiff line numberDiff line change
@@ -3,56 +3,44 @@ namespace ts.OrganizeImports {
33
export function organizeImports(
44
sourceFile: SourceFile,
55
formatContext: formatting.FormatContext,
6-
host: LanguageServiceHost,
7-
cancellationToken: CancellationToken) {
6+
host: LanguageServiceHost) {
87

9-
// All of the (old) ImportDeclarations in the file, in syntactic order.
10-
const oldImportDecls: ImportDeclaration[] = [];
8+
// TODO (https://github.com/Microsoft/TypeScript/issues/10020): sort *within* ambient modules (find using isAmbientModule)
119

12-
forEachChild(sourceFile, node => {
13-
cancellationToken.throwIfCancellationRequested();
14-
if (isImportDeclaration(node)) {
15-
oldImportDecls.push(node);
16-
}
17-
// TODO (https://github.com/Microsoft/TypeScript/issues/10020): sort *within* ambient modules (find using isAmbientModule)
18-
});
10+
// All of the old ImportDeclarations in the file, in syntactic order.
11+
const oldImportDecls = sourceFile.statements.filter(isImportDeclaration);
1912

2013
if (oldImportDecls.length === 0) {
2114
return [];
2215
}
2316

24-
const usedImportDecls = removeUnusedImports(oldImportDecls);
25-
cancellationToken.throwIfCancellationRequested();
26-
const sortedImportDecls = sortImports(usedImportDecls);
27-
cancellationToken.throwIfCancellationRequested();
28-
const coalescedImportDecls = coalesceImports(sortedImportDecls);
29-
cancellationToken.throwIfCancellationRequested();
17+
const oldValidImportDecls = oldImportDecls.filter(importDecl => getExternalModuleName(importDecl.moduleSpecifier));
18+
const oldInvalidImportDecls = oldImportDecls.filter(importDecl => !getExternalModuleName(importDecl.moduleSpecifier));
3019

31-
// All of the (new) ImportDeclarations in the file, in sorted order.
32-
const newImportDecls = coalescedImportDecls;
20+
// All of the new ImportDeclarations in the file, in sorted order.
21+
const newImportDecls = coalesceImports(sortImports(removeUnusedImports(oldValidImportDecls))).concat(oldInvalidImportDecls);
3322

3423
const changeTracker = textChanges.ChangeTracker.fromContext({ host, formatContext });
3524

36-
// NB: Stopping before i === 0
37-
for (let i = oldImportDecls.length - 1; i > 0; i--) {
38-
changeTracker.deleteNode(sourceFile, oldImportDecls[i]);
39-
}
40-
25+
// Delete or replace the first import.
4126
if (newImportDecls.length === 0) {
4227
changeTracker.deleteNode(sourceFile, oldImportDecls[0]);
4328
}
4429
else {
45-
// Delete the surrounding trivia because it will have been retained in newImportDecls.
46-
const replaceOptions = {
30+
// Note: Delete the surrounding trivia because it will have been retained in newImportDecls.
31+
changeTracker.replaceNodeWithNodes(sourceFile, oldImportDecls[0], newImportDecls, {
4732
useNonAdjustedStartPosition: false,
4833
useNonAdjustedEndPosition: false,
4934
suffix: getNewLineOrDefaultFromHost(host, formatContext.options),
50-
};
51-
changeTracker.replaceNodeWithNodes(sourceFile, oldImportDecls[0], newImportDecls, replaceOptions);
35+
});
5236
}
5337

54-
const changes = changeTracker.getChanges();
55-
return changes;
38+
// Delete any subsequent imports.
39+
for (let i = 1; i < oldImportDecls.length; i++) {
40+
changeTracker.deleteNode(sourceFile, oldImportDecls[i]);
41+
}
42+
43+
return changeTracker.getChanges();
5644
}
5745

5846
function removeUnusedImports(oldImports: ReadonlyArray<ImportDeclaration>) {
@@ -61,51 +49,15 @@ namespace ts.OrganizeImports {
6149

6250
/* @internal */ // Internal for testing
6351
export function sortImports(oldImports: ReadonlyArray<ImportDeclaration>) {
64-
if (oldImports.length < 2) {
65-
return oldImports;
66-
}
52+
return stableSort(oldImports, (import1, import2) => {
53+
const name1 = getExternalModuleName(import1.moduleSpecifier);
54+
const name2 = getExternalModuleName(import2.moduleSpecifier);
55+
Debug.assert(name1 !== undefined);
56+
Debug.assert(name2 !== undefined);
57+
return compareBooleans(isExternalModuleNameRelative(name1), isExternalModuleNameRelative(name2)) ||
58+
compareStringsCaseSensitive(name1, name2);
6759

68-
// NB: declaration order determines sort order
69-
const enum ModuleNameKind {
70-
NonRelative,
71-
Relative,
72-
Invalid,
73-
}
74-
75-
const importRecords = oldImports.map(createImportRecord);
76-
77-
const sortedRecords = stableSort(importRecords, (import1, import2) => {
78-
const { name: name1, kind: kind1 } = import1;
79-
const { name: name2, kind: kind2 } = import2;
80-
81-
if (kind1 !== kind2) {
82-
return kind1 < kind2
83-
? Comparison.LessThan
84-
: Comparison.GreaterThan;
85-
}
86-
87-
// Note that we're using simple equality, retaining case-sensitivity.
88-
if (name1 !== name2) {
89-
return name1 < name2
90-
? Comparison.LessThan
91-
: Comparison.GreaterThan;
92-
}
93-
94-
return Comparison.EqualTo;
9560
});
96-
97-
return sortedRecords.map(r => r.importDeclaration);
98-
99-
function createImportRecord(importDeclaration: ImportDeclaration) {
100-
const specifier = importDeclaration.moduleSpecifier;
101-
const name = getExternalModuleName(specifier);
102-
if (name) {
103-
const isRelative = isExternalModuleNameRelative(name);
104-
return { importDeclaration, name, kind: isRelative ? ModuleNameKind.Relative : ModuleNameKind.NonRelative };
105-
}
106-
107-
return { importDeclaration, name: specifier.getText(), kind: ModuleNameKind.Invalid };
108-
}
10961
}
11062

11163
function getExternalModuleName(specifier: Expression) {
@@ -123,11 +75,13 @@ namespace ts.OrganizeImports {
12375
const groups: ImportDeclaration[][] = [];
12476

12577
let groupName: string | undefined = getExternalModuleName(sortedImports[0].moduleSpecifier);
78+
Debug.assert(groupName !== undefined);
12679
let group: ImportDeclaration[] = [];
12780

12881
for (const importDeclaration of sortedImports) {
12982
const moduleName = getExternalModuleName(importDeclaration.moduleSpecifier);
130-
if (moduleName && moduleName === groupName) {
83+
Debug.assert(moduleName !== undefined);
84+
if (moduleName === groupName) {
13185
group.push(importDeclaration);
13286
}
13387
else if (group.length) {
@@ -159,38 +113,10 @@ namespace ts.OrganizeImports {
159113
const groupedImports = groupSortedImports(sortedImports);
160114
for (const importGroup of groupedImports) {
161115

162-
let seenImportWithoutClause = false;
163-
164-
const defaultImports: Identifier[] = [];
165-
const namespaceImports: NamespaceImport[] = [];
166-
const namedImports: NamedImports[] = [];
167-
168-
for (const importDeclaration of importGroup) {
169-
if (importDeclaration.importClause === undefined) {
170-
// Only the first such import is interesting - the others are redundant.
171-
// Note: Unfortunately, we will lose trivia that was on this node.
172-
if (!seenImportWithoutClause) {
173-
coalescedImports.push(importDeclaration);
174-
}
175-
176-
seenImportWithoutClause = true;
177-
continue;
178-
}
179-
180-
const { name, namedBindings } = importDeclaration.importClause;
181-
182-
if (name) {
183-
defaultImports.push(name);
184-
}
116+
const { importWithoutClause, defaultImports, namespaceImports, namedImports } = getImportParts(importGroup);
185117

186-
if (namedBindings) {
187-
if (isNamespaceImport(namedBindings)) {
188-
namespaceImports.push(namedBindings);
189-
}
190-
else {
191-
namedImports.push(namedBindings);
192-
}
193-
}
118+
if (importWithoutClause) {
119+
coalescedImports.push(importWithoutClause);
194120
}
195121

196122
// Normally, we don't combine default and namespace imports, but it would be silly to
@@ -204,8 +130,6 @@ namespace ts.OrganizeImports {
204130
continue;
205131
}
206132

207-
// For convenience, we cheat and do a little sorting during coalescing.
208-
// Seems reasonable since we're restructuring so much anyway.
209133
const sortedNamespaceImports = stableSort(namespaceImports, (n1, n2) => compareIdentifiers(n1.name, n2.name));
210134

211135
for (const namespaceImport of sortedNamespaceImports) {
@@ -218,7 +142,7 @@ namespace ts.OrganizeImports {
218142
continue;
219143
}
220144

221-
let newDefaultImport: Identifier = undefined;
145+
let newDefaultImport: Identifier | undefined;
222146
const newImportSpecifiers: ImportSpecifier[] = [];
223147
if (defaultImports.length === 1) {
224148
newDefaultImport = defaultImports[0];
@@ -230,18 +154,11 @@ namespace ts.OrganizeImports {
230154
}
231155
}
232156

233-
for (const namedImport of namedImports) {
234-
for (const specifier of namedImport.elements) {
235-
newImportSpecifiers.push(specifier);
236-
}
237-
}
157+
newImportSpecifiers.push(...flatMap(namedImports, n => n.elements));
238158

239-
const sortedImportSpecifiers = stableSort(newImportSpecifiers, (s1, s2) => {
240-
const nameComparison = compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name);
241-
return nameComparison !== Comparison.EqualTo
242-
? nameComparison
243-
: compareIdentifiers(s1.name, s2.name);
244-
});
159+
const sortedImportSpecifiers = stableSort(newImportSpecifiers, (s1, s2) =>
160+
compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name) ||
161+
compareIdentifiers(s1.name, s2.name));
245162

246163
const importClause = defaultImports.length > 0
247164
? defaultImports[0].parent as ImportClause
@@ -259,19 +176,46 @@ namespace ts.OrganizeImports {
259176

260177
return coalescedImports;
261178

262-
// `undefined` is the min value.
263-
function compareIdentifiers(s1: Identifier | undefined, s2: Identifier | undefined) {
264-
return s1 === undefined
265-
? s2 === undefined
266-
? Comparison.EqualTo
267-
: Comparison.LessThan
268-
: s2 === undefined
269-
? Comparison.GreaterThan
270-
: s1.text < s2.text
271-
? Comparison.LessThan
272-
: s1.text > s2.text
273-
? Comparison.GreaterThan
274-
: Comparison.EqualTo;
179+
function getImportParts(importGroup: ReadonlyArray<ImportDeclaration>) {
180+
let importWithoutClause: ImportDeclaration | undefined;
181+
const defaultImports: Identifier[] = [];
182+
const namespaceImports: NamespaceImport[] = [];
183+
const namedImports: NamedImports[] = [];
184+
185+
for (const importDeclaration of importGroup) {
186+
if (importDeclaration.importClause === undefined) {
187+
// Only the first such import is interesting - the others are redundant.
188+
// Note: Unfortunately, we will lose trivia that was on this node.
189+
importWithoutClause = importWithoutClause || importDeclaration;
190+
continue;
191+
}
192+
193+
const { name, namedBindings } = importDeclaration.importClause;
194+
195+
if (name) {
196+
defaultImports.push(name);
197+
}
198+
199+
if (namedBindings) {
200+
if (isNamespaceImport(namedBindings)) {
201+
namespaceImports.push(namedBindings);
202+
}
203+
else {
204+
namedImports.push(namedBindings);
205+
}
206+
}
207+
}
208+
209+
return {
210+
importWithoutClause,
211+
defaultImports,
212+
namespaceImports,
213+
namedImports,
214+
}
215+
}
216+
217+
function compareIdentifiers(s1: Identifier, s2: Identifier) {
218+
return compareStringsCaseSensitive(s1.text, s2.text);
275219
}
276220

277221
function updateImportDeclarationAndClause(

0 commit comments

Comments
 (0)