Skip to content

Commit 5c278ce

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 f4141ac commit 5c278ce

File tree

6 files changed

+130
-164
lines changed

6 files changed

+130
-164
lines changed

src/compiler/core.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1905,6 +1905,11 @@ namespace ts {
19051905
Comparison.EqualTo;
19061906
}
19071907

1908+
/** True is greater than false. */
1909+
export function compareBooleans(a: boolean, b: boolean): Comparison {
1910+
return compareValues(a ? 1 : 0, b ? 1 : 0);
1911+
}
1912+
19081913
function compareMessageText(text1: string | DiagnosticMessageChain, text2: string | DiagnosticMessageChain): Comparison {
19091914
while (text1 && text2) {
19101915
// We still have both chains.

src/harness/unittests/organizeImports.ts

+25-30
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));
@@ -286,6 +264,25 @@ D();
286264
},
287265
libFile);
288266

267+
// tslint:disable no-invalid-template-strings
268+
testOrganizeImports("MoveToTop_Invalid",
269+
{
270+
path: "/test.ts",
271+
content: `
272+
import { F1, F2 } from "lib";
273+
F1();
274+
F2();
275+
import * as NS from "lib";
276+
NS.F1();
277+
import b from ${"`${'lib'}`"};
278+
import a from ${"`${'lib'}`"};
279+
import D from "lib";
280+
D();
281+
`,
282+
},
283+
libFile);
284+
// tslint:enable no-invalid-template-strings
285+
289286
testOrganizeImports("CoalesceTrivia",
290287
{
291288
path: "/test.ts",
@@ -322,15 +319,13 @@ F2();
322319
assert.equal(testPath, changes[0].fileName);
323320

324321
Harness.Baseline.runBaseline(baselinePath, () => {
325-
const data: string[] = [];
326-
data.push(`// ==ORIGINAL==`);
327-
data.push(testContent);
328-
329-
data.push(`// ==ORGANIZED==`);
330322
const newText = textChanges.applyChanges(testContent, changes[0].textChanges);
331-
data.push(newText);
332-
333-
return data.join(newLineCharacter);
323+
return [
324+
"// ==ORIGINAL==",
325+
testContent,
326+
"// ==ORGANIZED==",
327+
newText,
328+
].join(newLineCharacter);
334329
});
335330
}
336331

src/services/organizeImports.ts

+76-133
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,14 @@ 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-
}
67-
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;
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);
9559
});
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-
}
10960
}
11061

11162
function getExternalModuleName(specifier: Expression) {
@@ -123,11 +74,13 @@ namespace ts.OrganizeImports {
12374
const groups: ImportDeclaration[][] = [];
12475

12576
let groupName: string | undefined = getExternalModuleName(sortedImports[0].moduleSpecifier);
77+
Debug.assert(groupName !== undefined);
12678
let group: ImportDeclaration[] = [];
12779

12880
for (const importDeclaration of sortedImports) {
12981
const moduleName = getExternalModuleName(importDeclaration.moduleSpecifier);
130-
if (moduleName && moduleName === groupName) {
82+
Debug.assert(moduleName !== undefined);
83+
if (moduleName === groupName) {
13184
group.push(importDeclaration);
13285
}
13386
else if (group.length) {
@@ -159,38 +112,10 @@ namespace ts.OrganizeImports {
159112
const groupedImports = groupSortedImports(sortedImports);
160113
for (const importGroup of groupedImports) {
161114

162-
let seenImportWithoutClause = false;
115+
const { importWithoutClause, defaultImports, namespaceImports, namedImports } = getImportParts(importGroup);
163116

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-
}
185-
186-
if (namedBindings) {
187-
if (isNamespaceImport(namedBindings)) {
188-
namespaceImports.push(namedBindings);
189-
}
190-
else {
191-
namedImports.push(namedBindings);
192-
}
193-
}
117+
if (importWithoutClause) {
118+
coalescedImports.push(importWithoutClause);
194119
}
195120

196121
// Normally, we don't combine default and namespace imports, but it would be silly to
@@ -204,8 +129,6 @@ namespace ts.OrganizeImports {
204129
continue;
205130
}
206131

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

211134
for (const namespaceImport of sortedNamespaceImports) {
@@ -218,7 +141,7 @@ namespace ts.OrganizeImports {
218141
continue;
219142
}
220143

221-
let newDefaultImport: Identifier = undefined;
144+
let newDefaultImport: Identifier | undefined;
222145
const newImportSpecifiers: ImportSpecifier[] = [];
223146
if (defaultImports.length === 1) {
224147
newDefaultImport = defaultImports[0];
@@ -230,18 +153,11 @@ namespace ts.OrganizeImports {
230153
}
231154
}
232155

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

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-
});
158+
const sortedImportSpecifiers = stableSort(newImportSpecifiers, (s1, s2) =>
159+
compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name) ||
160+
compareIdentifiers(s1.name, s2.name));
245161

246162
const importClause = defaultImports.length > 0
247163
? defaultImports[0].parent as ImportClause
@@ -259,19 +175,46 @@ namespace ts.OrganizeImports {
259175

260176
return coalescedImports;
261177

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;
178+
function getImportParts(importGroup: ReadonlyArray<ImportDeclaration>) {
179+
let importWithoutClause: ImportDeclaration | undefined;
180+
const defaultImports: Identifier[] = [];
181+
const namespaceImports: NamespaceImport[] = [];
182+
const namedImports: NamedImports[] = [];
183+
184+
for (const importDeclaration of importGroup) {
185+
if (importDeclaration.importClause === undefined) {
186+
// Only the first such import is interesting - the others are redundant.
187+
// Note: Unfortunately, we will lose trivia that was on this node.
188+
importWithoutClause = importWithoutClause || importDeclaration;
189+
continue;
190+
}
191+
192+
const { name, namedBindings } = importDeclaration.importClause;
193+
194+
if (name) {
195+
defaultImports.push(name);
196+
}
197+
198+
if (namedBindings) {
199+
if (isNamespaceImport(namedBindings)) {
200+
namespaceImports.push(namedBindings);
201+
}
202+
else {
203+
namedImports.push(namedBindings);
204+
}
205+
}
206+
}
207+
208+
return {
209+
importWithoutClause,
210+
defaultImports,
211+
namespaceImports,
212+
namedImports,
213+
};
214+
}
215+
216+
function compareIdentifiers(s1: Identifier, s2: Identifier) {
217+
return compareStringsCaseSensitive(s1.text, s2.text);
275218
}
276219

277220
function updateImportDeclarationAndClause(

src/services/services.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1855,7 +1855,7 @@ namespace ts {
18551855
const sourceFile = getValidSourceFile(scope.fileName);
18561856
const formatContext = formatting.getFormatContext(formatOptions);
18571857

1858-
return OrganizeImports.organizeImports(sourceFile, formatContext, host, cancellationToken);
1858+
return OrganizeImports.organizeImports(sourceFile, formatContext, host);
18591859
}
18601860

18611861
function applyCodeActionCommand(action: CodeActionCommand): Promise<ApplyCodeActionCommandResult>;

0 commit comments

Comments
 (0)