From f5421ccd447c9b8605b20c24e0aef2bf69dd5c5c Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Fri, 12 Jan 2024 14:38:18 -0800 Subject: [PATCH 01/33] add failing test --- tests/cases/fourslash/organizeImports22.ts | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/cases/fourslash/organizeImports22.ts diff --git a/tests/cases/fourslash/organizeImports22.ts b/tests/cases/fourslash/organizeImports22.ts new file mode 100644 index 0000000000000..6329b9aeaccbb --- /dev/null +++ b/tests/cases/fourslash/organizeImports22.ts @@ -0,0 +1,31 @@ +/// + +// @filename: /a.ts + +//// import {abc, Abc} from 'b'; +//// import { +//// I, +//// R, +//// M, +//// } from 'a'; +//// console.log(abc, Abc, I, R, M); + + + +verify.organizeImports( +`import { + I, + M, + R, +} from 'a'; +import { abc, Abc } from 'b'; +console.log(abc, Abc, I, R, M);`); + +verify.organizeImports( +`import { + I, + M, + R, +} from 'a'; +import { abc, Abc } from 'b'; +console.log(abc, Abc, I, R, M);`); From fd0290be507c6a6cacc783df1ac6f3fb2b2ca675 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Fri, 12 Jan 2024 14:46:06 -0800 Subject: [PATCH 02/33] more tests --- tests/cases/fourslash/organizeImports22.ts | 13 ++++----- tests/cases/fourslash/organizeImports23.ts | 31 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 tests/cases/fourslash/organizeImports23.ts diff --git a/tests/cases/fourslash/organizeImports22.ts b/tests/cases/fourslash/organizeImports22.ts index 6329b9aeaccbb..9fab52ef0402c 100644 --- a/tests/cases/fourslash/organizeImports22.ts +++ b/tests/cases/fourslash/organizeImports22.ts @@ -1,16 +1,12 @@ /// -// @filename: /a.ts - -//// import {abc, Abc} from 'b'; +//// import {abc, Abc, bc, Bc} from 'b'; //// import { //// I, //// R, //// M, //// } from 'a'; -//// console.log(abc, Abc, I, R, M); - - +//// console.log(abc, Abc, bc, Bc, I, R, M); verify.organizeImports( `import { @@ -19,8 +15,9 @@ verify.organizeImports( R, } from 'a'; import { abc, Abc } from 'b'; -console.log(abc, Abc, I, R, M);`); +console.log(abc, Abc, bc, Bc, I, R, M);`); +// organize already-organized imports to make sure output is stable verify.organizeImports( `import { I, @@ -28,4 +25,4 @@ verify.organizeImports( R, } from 'a'; import { abc, Abc } from 'b'; -console.log(abc, Abc, I, R, M);`); +console.log(abc, Abc, bc, Bc, I, R, M);`); diff --git a/tests/cases/fourslash/organizeImports23.ts b/tests/cases/fourslash/organizeImports23.ts new file mode 100644 index 0000000000000..e13e332a63fea --- /dev/null +++ b/tests/cases/fourslash/organizeImports23.ts @@ -0,0 +1,31 @@ +/// + +//// import {abc, Abc, type bc, type Bc} from 'b'; +//// import { +//// I, +//// R, +//// M, +//// } from 'a'; +//// type x = bc | Bc; +//// console.log(abc, Abc, I, R, M); + +verify.organizeImports( +`import { + I, + M, + R, +} from 'a'; +import { abc, Abc, type bc, type Bc } from 'b'; +type x = bc | Bc; +console.log(abc, Abc, I, R, M);`); + +// organize already-organized imports to make sure output is stable +verify.organizeImports( +`import { + I, + M, + R, +} from 'a'; +import { abc, Abc, type bc, type Bc } from 'b'; +type x = bc | Bc; +console.log(abc, Abc, I, R, M);`); From a0b3a0e10d896c113d58b47a40e7e3503bf298f8 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Tue, 16 Jan 2024 11:27:30 -0800 Subject: [PATCH 03/33] add detection tests --- .../unittests/services/organizeImports.ts | 34 +++++++++++++++++++ tests/cases/fourslash/organizeImports22.ts | 4 +-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index e2970ef0d84a1..6ef992e3f2dcb 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -990,6 +990,40 @@ export * from "lib"; } }); + describe("Detection", () => { + it("Detection-1", () => { + const originalImports = parseImports(`import { abc, Abc } from 'b';`, `import { I, M, R } from 'a';`); + assert.equal( + ts.SortKind.CaseInsensitive, + ts.OrganizeImports.detectSorting(ts.getSourceFileOfNode(originalImports[0]), {}) + ); + }); + + it("Detection-2", () => { + const originalImports = parseImports(`import { abc, Abc } from 'a';`, `import { I, M, R } from 'b';`); + assert.equal( + ts.SortKind.CaseInsensitive, + ts.OrganizeImports.detectSorting(ts.getSourceFileOfNode(originalImports[0]), {}) + ); + }); + + it("Detection-3", () => { + const originalImports = parseImports(`import { I, M, R } from 'a';`,`import { Abc, abc } from 'b';`); + assert.equal( + ts.SortKind.None, + ts.OrganizeImports.detectSorting(ts.getSourceFileOfNode(originalImports[0]), {}) + ); + }); + + it("Detection-4", () => { + const originalImports = parseImports(`import { I, M, R } from 'a';`,`import { abc, Abc } from 'b';`); + assert.equal( + ts.SortKind.CaseInsensitive, + ts.OrganizeImports.detectSorting(ts.getSourceFileOfNode(originalImports[0]), {}) + ); + }); + }); + function parseImports(...importStrings: string[]): readonly ts.ImportDeclaration[] { const sourceFile = ts.createSourceFile("a.ts", importStrings.join("\n"), ts.ScriptTarget.ES2015, /*setParentNodes*/ true, ts.ScriptKind.TS); const imports = ts.filter(sourceFile.statements, ts.isImportDeclaration); diff --git a/tests/cases/fourslash/organizeImports22.ts b/tests/cases/fourslash/organizeImports22.ts index 9fab52ef0402c..b748278ec564e 100644 --- a/tests/cases/fourslash/organizeImports22.ts +++ b/tests/cases/fourslash/organizeImports22.ts @@ -14,7 +14,7 @@ verify.organizeImports( M, R, } from 'a'; -import { abc, Abc } from 'b'; +import { abc, Abc, bc, Bc } from 'b'; console.log(abc, Abc, bc, Bc, I, R, M);`); // organize already-organized imports to make sure output is stable @@ -24,5 +24,5 @@ verify.organizeImports( M, R, } from 'a'; -import { abc, Abc } from 'b'; +import { abc, Abc, bc, Bc } from 'b'; console.log(abc, Abc, bc, Bc, I, R, M);`); From 6f05577d5a9f0bb84bbb7fa7965912f1d1891214 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Tue, 16 Jan 2024 15:45:34 -0800 Subject: [PATCH 04/33] improved test behavior --- tests/cases/fourslash/extract-method32.ts | 2 +- tests/cases/fourslash/organizeImportsType5.ts | 24 +++++++++++++++---- tests/cases/fourslash/organizeImportsType8.ts | 4 ++-- tests/cases/fourslash/organizeImportsType9.ts | 4 ++-- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/tests/cases/fourslash/extract-method32.ts b/tests/cases/fourslash/extract-method32.ts index d2d999fd290ad..229561e9795a1 100644 --- a/tests/cases/fourslash/extract-method32.ts +++ b/tests/cases/fourslash/extract-method32.ts @@ -21,7 +21,7 @@ edit.applyRefactor({ actionName: "function_scope_1", actionDescription: "Extract to function in module scope", newContent: -`import { A, a } from "./a"; +`import { a, A } from "./a"; function foo() { const arg = a; diff --git a/tests/cases/fourslash/organizeImportsType5.ts b/tests/cases/fourslash/organizeImportsType5.ts index 1f8bb8dd602a7..698577ac2cd4a 100644 --- a/tests/cases/fourslash/organizeImportsType5.ts +++ b/tests/cases/fourslash/organizeImportsType5.ts @@ -12,18 +12,32 @@ //// } from './foo'; //// console.log(A, a, B, b, c, C, d, D); -verify.organizeImports( -`import { +verify.organizeImports(`import { type A, + a, + b, b as B, + type c, c as C, - type d as D, + d, + type d as D +} from './foo'; +console.log(A, a, B, b, c, C, d, D);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "inline" } +); + +verify.organizeImports(`import { + type A, a, b, + b as B, type c, - d + c as C, + d, + type d as D } from './foo'; console.log(A, a, B, b, c, C, d, D);`, /*mode*/ undefined, { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "inline" } -); +); \ No newline at end of file diff --git a/tests/cases/fourslash/organizeImportsType8.ts b/tests/cases/fourslash/organizeImportsType8.ts index da43f893431df..d79b2ab2eac30 100644 --- a/tests/cases/fourslash/organizeImportsType8.ts +++ b/tests/cases/fourslash/organizeImportsType8.ts @@ -20,7 +20,7 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type A, type a, b, B } from "foo2";'); verify.organizeImports( -`import { B, b, type A, type a } from "foo2"; +`import { b, B, type A, type a } from "foo2"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "last" } @@ -30,7 +30,7 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type A, type a, b, B } from "foo3";'); verify.organizeImports( -`import { B, b, type A, type a } from "foo3"; +`import { b, B, type A, type a } from "foo3"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: "auto" } diff --git a/tests/cases/fourslash/organizeImportsType9.ts b/tests/cases/fourslash/organizeImportsType9.ts index efe6107e020b6..431be2c724218 100644 --- a/tests/cases/fourslash/organizeImportsType9.ts +++ b/tests/cases/fourslash/organizeImportsType9.ts @@ -19,7 +19,7 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type a, type A, b, B } from "foo2";'); verify.organizeImports( -`import { B, b, type A, type a } from "foo2"; +`import { b, B, type a, type A } from "foo2"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "last" } @@ -27,7 +27,7 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type a, type A, b, B } from "foo3";'); verify.organizeImports( -`import { B, b, type A, type a } from "foo3"; +`import { b, B, type a, type A } from "foo3"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: "auto" } From 40722f5f519b7dbd41f3ce1d3fb3b55c892ac944 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Tue, 16 Jan 2024 15:47:59 -0800 Subject: [PATCH 05/33] improved test behavior --- .../unittests/services/organizeImports.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index 6ef992e3f2dcb..bc219e32369af 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -1022,6 +1022,39 @@ export * from "lib"; ts.OrganizeImports.detectSorting(ts.getSourceFileOfNode(originalImports[0]), {}) ); }); + + it("Detection-5", () => { + const sourceFile = ts.createSourceFile( + "/a.ts", + `import { + Type1, + Type2, + func4, + Type3, + Type4, + Type5, + Type7, + Type8, + Type9, + func1, + func2, + Type6, + func3, + func5, + func6, + func7, + func8, + func9, +} from "foo";`, + ts.ScriptTarget.ES2015, + /*setParentNodes*/ true, + ts.ScriptKind.TS + ); + assert.equal( + ts.SortKind.CaseSensitive, + ts.OrganizeImports.detectSorting(sourceFile, {}) + ); + }); }); function parseImports(...importStrings: string[]): readonly ts.ImportDeclaration[] { From 1dbec4f6d373e42fa97a95f5ddfde81faa3fa98b Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Thu, 25 Jan 2024 12:59:11 -0800 Subject: [PATCH 06/33] add core/utility functions --- src/compiler/core.ts | 40 +++++++++++++++++++++++++++++++++++++++ src/services/utilities.ts | 30 +++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 9738920df7527..8370ffb7b6b19 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -838,6 +838,46 @@ export function sortAndDeduplicate(array: readonly T[], comparer?: Comparer); } +/** + * Merges two sorted arrays so that the resulting array is sorted and has no duplicates. + * @internal + */ +export function mergeAndDeduplicateSorted(array1: readonly T[], array2: readonly T[], comparer: Comparer): SortedReadonlyArray { + if (array1.length === 0) return array2 as any as SortedReadonlyArray; + if (array2.length === 0) return array1 as any as SortedReadonlyArray; + + const finalArray: T[] = []; + let i = 0, j = 0; + while (i < array1.length && j < array2.length) { + switch (comparer(array1[i], array2[j])) { + case Comparison.LessThan: + finalArray.push(array1[i]); + i++; + break; + case Comparison.EqualTo: + finalArray.push(array1[i]); + i++, j++; + break; + case Comparison.GreaterThan: + finalArray.push(array2[j]); + j++; + break; + default: + Debug.fail("not a valid Comparison"); + } + } + if (i === array1.length && j === array2.length) { + return finalArray as any as SortedReadonlyArray; + } + if (i === array1.length) { + return finalArray.concat(array2.slice(j)) as any as SortedReadonlyArray; + } + if (j === array2.length) { + return finalArray.concat(array1.slice(i)) as any as SortedReadonlyArray; + } + return Debug.fail("unreachable"); +} + /** @internal */ export function arrayIsSorted(array: readonly T[], comparer: Comparer) { if (array.length < 2) return true; diff --git a/src/services/utilities.ts b/src/services/utilities.ts index b48367739786d..847249c5887f7 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -4275,3 +4275,33 @@ export function fileShouldUseJavaScriptRequire(file: SourceFile | string, progra } return preferRequire; } + +/** @internal */ +export function getDiffNum(s1: T[], s2: T[]): number { + return getDiffOpt(s1, s2); + + function getDiffOpt(s1: T[], s2: T[]): number { + const diffTable = new Array(s1.length + 1); + diffTable[s1.length] = Array.from({ length: s2.length + 1 }, (_, i) => s2.length - i); + + for (let i = s1.length - 1; i >= 0; i--) { + diffTable[i] = new Array(s2.length + 1); + diffTable[i][s2.length] = s2.length - i; + + for (let j = s2.length - 1; j >= 0; j--) { + if (s1[i] === s2[j]) { + diffTable[i][j] = diffTable[i + 1][j + 1]; + } + else { + // all change types are currently weighted the same + diffTable[i][j] = 1 + Math.min( + diffTable[i + 1][j], + diffTable[i + 1][j + 1], + diffTable[i][j + 1], + ); + } + } + } + return diffTable[0][0]; + } +} From 566f326a06105f37c7cb144ed8607f484f98d06a Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Thu, 25 Jan 2024 15:16:16 -0800 Subject: [PATCH 07/33] update utility docs --- src/services/utilities.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 847249c5887f7..4e22c0ce5a624 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -4276,7 +4276,10 @@ export function fileShouldUseJavaScriptRequire(file: SourceFile | string, progra return preferRequire; } -/** @internal */ +/** + * Does not perform change matching, only returns a the number of the minimum edit + * @internal + */ export function getDiffNum(s1: T[], s2: T[]): number { return getDiffOpt(s1, s2); From 321df23ba3821ab7447a53fc8030a381b36da74e Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Thu, 1 Feb 2024 00:08:44 -0800 Subject: [PATCH 08/33] update behavior in tests to the correct detection --- .../cases/fourslash/organizeImportsType10.ts | 50 +++++++++++++++++++ .../cases/fourslash/organizeImportsType11.ts | 50 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 tests/cases/fourslash/organizeImportsType10.ts create mode 100644 tests/cases/fourslash/organizeImportsType11.ts diff --git a/tests/cases/fourslash/organizeImportsType10.ts b/tests/cases/fourslash/organizeImportsType10.ts new file mode 100644 index 0000000000000..55247daf7ed0f --- /dev/null +++ b/tests/cases/fourslash/organizeImportsType10.ts @@ -0,0 +1,50 @@ +/// + +////import { +//// type Type1, +//// type Type2, +//// func4, +//// type Type3, +//// type Type4, +//// type Type5, +//// type Type7, +//// type Type8, +//// type Type9, +//// func1, +//// func2, +//// type Type6, +//// func3, +//// func5, +//// func6, +//// func7, +//// func8, +//// func9, +////} from "foo"; +////interface Use extends Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9 {} +////console.log(func1, func2, func3, func4, func5, func6, func7, func8, func9); + +verify.organizeImports( +`import { + type Type1, + type Type2, + type Type3, + type Type4, + type Type5, + type Type6, + type Type7, + type Type8, + type Type9, + func1, + func2, + func3, + func4, + func5, + func6, + func7, + func8, + func9, +} from "foo"; +interface Use extends Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9 {} +console.log(func1, func2, func3, func4, func5, func6, func7, func8, func9);`, +/*mode*/ undefined, +{ organizeImportsIgnoreCase: true }); diff --git a/tests/cases/fourslash/organizeImportsType11.ts b/tests/cases/fourslash/organizeImportsType11.ts new file mode 100644 index 0000000000000..89c07d83eaeea --- /dev/null +++ b/tests/cases/fourslash/organizeImportsType11.ts @@ -0,0 +1,50 @@ +/// + +////import { +//// type Type1, +//// type Type2, +//// func4, +//// type Type3, +//// type Type4, +//// type Type5, +//// type Type7, +//// type Type8, +//// type Type9, +//// func1, +//// func2, +//// type Type6, +//// func3, +//// func5, +//// func6, +//// func7, +//// func8, +//// func9, +////} from "foo"; +////interface Use extends Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9 {} +////console.log(func1, func2, func3, func4, func5, func6, func7, func8, func9); + +verify.organizeImports( +`import { + type Type1, + type Type2, + type Type3, + type Type4, + type Type5, + type Type6, + type Type7, + type Type8, + type Type9, + func1, + func2, + func3, + func4, + func5, + func6, + func7, + func8, + func9, +} from "foo"; +interface Use extends Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9 {} +console.log(func1, func2, func3, func4, func5, func6, func7, func8, func9);` +); + \ No newline at end of file From 4ae222d2186189e1a5d96c7dcf6411af9f35272f Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Thu, 1 Feb 2024 00:34:15 -0800 Subject: [PATCH 09/33] update tests since "last" is no longer default --- tests/cases/fourslash/organizeImportsType8.ts | 8 +++----- tests/cases/fourslash/organizeImportsType9.ts | 7 ++++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/cases/fourslash/organizeImportsType8.ts b/tests/cases/fourslash/organizeImportsType8.ts index d79b2ab2eac30..2c63d54cdb38e 100644 --- a/tests/cases/fourslash/organizeImportsType8.ts +++ b/tests/cases/fourslash/organizeImportsType8.ts @@ -26,11 +26,9 @@ console.log(a, b, A, B);`, { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "last" } ); -// default behavior is { organizeImportsTypeOrder: "last" } - edit.replaceLine(0, 'import { type A, type a, b, B } from "foo3";'); verify.organizeImports( -`import { b, B, type A, type a } from "foo3"; +`import { type A, type a, b, B } from "foo3"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: "auto" } @@ -38,7 +36,7 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type A, type a, b, B } from "foo4";'); verify.organizeImports( -`import { b, B, type A, type a } from "foo4"; +`import { type A, type a, b, B } from "foo4"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: true } @@ -46,7 +44,7 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type A, type a, b, B } from "foo5";'); verify.organizeImports( -`import { B, b, type A, type a } from "foo5"; +`import { type A, B, type a, b } from "foo5"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: false } diff --git a/tests/cases/fourslash/organizeImportsType9.ts b/tests/cases/fourslash/organizeImportsType9.ts index 431be2c724218..8bb21b6eaa6b7 100644 --- a/tests/cases/fourslash/organizeImportsType9.ts +++ b/tests/cases/fourslash/organizeImportsType9.ts @@ -27,7 +27,7 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type a, type A, b, B } from "foo3";'); verify.organizeImports( -`import { b, B, type a, type A } from "foo3"; +`import { type a, type A, b, B } from "foo3"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: "auto" } @@ -35,14 +35,15 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type a, type A, b, B } from "foo4";'); verify.organizeImports( -`import { b, B, type a, type A } from "foo4"; +`import { type a, type A, b, B } from "foo4"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: true }); +// the imports are correctly sorted when ignoreCase=false and typeOrder=first edit.replaceLine(0, 'import { type a, type A, b, B } from "foo5";'); verify.organizeImports( -`import { B, b, type A, type a } from "foo5"; +`import { type A, type a, B, b } from "foo5"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: false }); \ No newline at end of file From 76efd1586d1059bb4635ba711a8d9b29ef027053 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Thu, 1 Feb 2024 01:44:35 -0800 Subject: [PATCH 10/33] update remaining tests and baselines --- .../unittests/services/organizeImports.ts | 142 ++++++++++-------- .../reference/organizeImports/detection1.ts | 8 + .../reference/organizeImports/detection2.ts | 8 + .../reference/organizeImports/detection3.ts | 8 + .../reference/organizeImports/detection4.ts | 8 + .../reference/organizeImports/detection5.ts | 44 ++++++ tests/cases/fourslash/extract-method32.ts | 2 +- tests/cases/fourslash/organizeImports22.ts | 2 +- 8 files changed, 159 insertions(+), 63 deletions(-) create mode 100644 tests/baselines/reference/organizeImports/detection1.ts create mode 100644 tests/baselines/reference/organizeImports/detection2.ts create mode 100644 tests/baselines/reference/organizeImports/detection3.ts create mode 100644 tests/baselines/reference/organizeImports/detection4.ts create mode 100644 tests/baselines/reference/organizeImports/detection5.ts diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index bc219e32369af..1c3c469afe497 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -387,6 +387,59 @@ export const Other = 1; assert.isEmpty(changes); }); + testOrganizeImports("detection1", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: `import { abc, Abc } from 'b'; +import { I, M, R } from 'a'; +const x = abc + Abc + I + M + R;`, + }); + + testOrganizeImports("detection2", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: `import { abc, Abc } from 'a'; +import { I, M, R } from 'b'; +const x = abc + Abc + I + M + R;`, + }); + + testOrganizeImports("detection3", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: `import { I, M, R } from 'a'; +import { Abc, abc } from 'b'; +const x = abc + Abc + I + M + R;`, + }); + + testOrganizeImports("detection4", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: `import { I, M, R } from 'a'; +import { abc, Abc } from 'b'; +const x = abc + Abc + I + M + R;`, + }); + + testOrganizeImports("detection5", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: `import { + Type9, + Type2, + Type8, + Type7, + Type5, + Type4, + Type3, + Type1, + func9, + Type6, + func5, + func6, + func8, + func4, + func7, + func3, + func2, + func1, +} from "foo"; +console.log(Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9, func1, func2, func3, func4, func5, func6, func7, func8, func9);`, + }); + testOrganizeImports("Renamed_used", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", content: ` @@ -990,70 +1043,37 @@ export * from "lib"; } }); - describe("Detection", () => { + describe("helper functions", () => { it("Detection-1", () => { - const originalImports = parseImports(`import { abc, Abc } from 'b';`, `import { I, M, R } from 'a';`); - assert.equal( - ts.SortKind.CaseInsensitive, - ts.OrganizeImports.detectSorting(ts.getSourceFileOfNode(originalImports[0]), {}) - ); - }); - - it("Detection-2", () => { - const originalImports = parseImports(`import { abc, Abc } from 'a';`, `import { I, M, R } from 'b';`); - assert.equal( - ts.SortKind.CaseInsensitive, - ts.OrganizeImports.detectSorting(ts.getSourceFileOfNode(originalImports[0]), {}) - ); - }); - - it("Detection-3", () => { - const originalImports = parseImports(`import { I, M, R } from 'a';`,`import { Abc, abc } from 'b';`); - assert.equal( - ts.SortKind.None, - ts.OrganizeImports.detectSorting(ts.getSourceFileOfNode(originalImports[0]), {}) - ); + const originalImports = ["abc", "Abc", "abC"]; + const test2 = ["abc", "Abc", "abC", "b"]; + const test3 = ["abc", "Abc", "abC", "c"]; + const test4 = ["abc", "Abc", "abC", "b", "d", "e"]; + const test5 = ["abc", "b", "Abc", "abC", "d", "e"]; + + assert.equal(ts.getDiffNum(originalImports, originalImports), 0); + assert.equal(ts.getDiffNum(originalImports, test2), 1); + assert.equal(ts.getDiffNum(test2, test3), 1); + assert.equal(ts.getDiffNum(originalImports, test4), 3); + assert.equal(ts.getDiffNum(originalImports, test5), 3); + assert.equal(ts.getDiffNum(test4, test5), 2); }); - it("Detection-4", () => { - const originalImports = parseImports(`import { I, M, R } from 'a';`,`import { abc, Abc } from 'b';`); - assert.equal( - ts.SortKind.CaseInsensitive, - ts.OrganizeImports.detectSorting(ts.getSourceFileOfNode(originalImports[0]), {}) - ); - }); - - it("Detection-5", () => { - const sourceFile = ts.createSourceFile( - "/a.ts", - `import { - Type1, - Type2, - func4, - Type3, - Type4, - Type5, - Type7, - Type8, - Type9, - func1, - func2, - Type6, - func3, - func5, - func6, - func7, - func8, - func9, -} from "foo";`, - ts.ScriptTarget.ES2015, - /*setParentNodes*/ true, - ts.ScriptKind.TS - ); - assert.equal( - ts.SortKind.CaseSensitive, - ts.OrganizeImports.detectSorting(sourceFile, {}) - ); + it("mergededuplicates", () => { + const test = [1, 2, 3, 4]; + const test2 = [1, 2, 3, 4, 5]; + const test3 = [6, 7, 8, 9, 10]; + const test4 = [1, 3, 4, 5, 8, 9]; + const test5 = [1, 2, 4, 6, 7, 8, 9]; + function comparer(x: number, y: number) { + if (x > y) return ts.Comparison.GreaterThan; + if (x < y) return ts.Comparison.LessThan; + return ts.Comparison.EqualTo; + } + assert.sameOrderedMembers(ts.mergeAndDeduplicateSorted(test, test2, comparer) as any as number[], test2); + assert.sameOrderedMembers(ts.mergeAndDeduplicateSorted(test2, test2, comparer) as any as number[], test2); + assert.sameOrderedMembers(ts.mergeAndDeduplicateSorted(test2, test3, comparer) as any as number[], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + assert.sameOrderedMembers(ts.mergeAndDeduplicateSorted(test4, test5, comparer) as any as number[], [1, 2, 3, 4, 5, 6, 7, 8, 9]); }); }); diff --git a/tests/baselines/reference/organizeImports/detection1.ts b/tests/baselines/reference/organizeImports/detection1.ts new file mode 100644 index 0000000000000..74455d34569fc --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection1.ts @@ -0,0 +1,8 @@ +// ==ORIGINAL== +import { abc, Abc } from 'b'; +import { I, M, R } from 'a'; +const x = abc + Abc + I + M + R; +// ==ORGANIZED== +import { I, M, R } from 'a'; +import { abc, Abc } from 'b'; +const x = abc + Abc + I + M + R; \ No newline at end of file diff --git a/tests/baselines/reference/organizeImports/detection2.ts b/tests/baselines/reference/organizeImports/detection2.ts new file mode 100644 index 0000000000000..6adec0e28fd7e --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection2.ts @@ -0,0 +1,8 @@ +// ==ORIGINAL== +import { abc, Abc } from 'a'; +import { I, M, R } from 'b'; +const x = abc + Abc + I + M + R; +// ==ORGANIZED== +import { abc, Abc } from 'a'; +import { I, M, R } from 'b'; +const x = abc + Abc + I + M + R; \ No newline at end of file diff --git a/tests/baselines/reference/organizeImports/detection3.ts b/tests/baselines/reference/organizeImports/detection3.ts new file mode 100644 index 0000000000000..34d52aa97a4d6 --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection3.ts @@ -0,0 +1,8 @@ +// ==ORIGINAL== +import { I, M, R } from 'a'; +import { Abc, abc } from 'b'; +const x = abc + Abc + I + M + R; +// ==ORGANIZED== +import { I, M, R } from 'a'; +import { Abc, abc } from 'b'; +const x = abc + Abc + I + M + R; \ No newline at end of file diff --git a/tests/baselines/reference/organizeImports/detection4.ts b/tests/baselines/reference/organizeImports/detection4.ts new file mode 100644 index 0000000000000..08eee79e68c97 --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection4.ts @@ -0,0 +1,8 @@ +// ==ORIGINAL== +import { I, M, R } from 'a'; +import { abc, Abc } from 'b'; +const x = abc + Abc + I + M + R; +// ==ORGANIZED== +import { I, M, R } from 'a'; +import { abc, Abc } from 'b'; +const x = abc + Abc + I + M + R; \ No newline at end of file diff --git a/tests/baselines/reference/organizeImports/detection5.ts b/tests/baselines/reference/organizeImports/detection5.ts new file mode 100644 index 0000000000000..f4169e4f29522 --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection5.ts @@ -0,0 +1,44 @@ +// ==ORIGINAL== +import { + Type9, + Type2, + Type8, + Type7, + Type5, + Type4, + Type3, + Type1, + func9, + Type6, + func5, + func6, + func8, + func4, + func7, + func3, + func2, + func1, +} from "foo"; +console.log(Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9, func1, func2, func3, func4, func5, func6, func7, func8, func9); +// ==ORGANIZED== +import { + Type1, + Type2, + Type3, + Type4, + Type5, + Type6, + Type7, + Type8, + Type9, + func1, + func2, + func3, + func4, + func5, + func6, + func7, + func8, + func9, +} from "foo"; +console.log(Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9, func1, func2, func3, func4, func5, func6, func7, func8, func9); \ No newline at end of file diff --git a/tests/cases/fourslash/extract-method32.ts b/tests/cases/fourslash/extract-method32.ts index 229561e9795a1..d2d999fd290ad 100644 --- a/tests/cases/fourslash/extract-method32.ts +++ b/tests/cases/fourslash/extract-method32.ts @@ -21,7 +21,7 @@ edit.applyRefactor({ actionName: "function_scope_1", actionDescription: "Extract to function in module scope", newContent: -`import { a, A } from "./a"; +`import { A, a } from "./a"; function foo() { const arg = a; diff --git a/tests/cases/fourslash/organizeImports22.ts b/tests/cases/fourslash/organizeImports22.ts index b748278ec564e..0e3c97d912704 100644 --- a/tests/cases/fourslash/organizeImports22.ts +++ b/tests/cases/fourslash/organizeImports22.ts @@ -16,7 +16,7 @@ verify.organizeImports( } from 'a'; import { abc, Abc, bc, Bc } from 'b'; console.log(abc, Abc, bc, Bc, I, R, M);`); - + // organize already-organized imports to make sure output is stable verify.organizeImports( `import { From 85198ac1fe210de655c1e3221b4860997452e382 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Thu, 1 Feb 2024 01:45:11 -0800 Subject: [PATCH 11/33] first draft: detection implementation --- src/services/organizeImports.ts | 194 ++++++++++++++++++++++++++++++-- 1 file changed, 185 insertions(+), 9 deletions(-) diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index b9c2f7e89096f..f40c3540a677e 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -50,6 +50,7 @@ import { map, MemoizeCache, memoizeCached, + mergeAndDeduplicateSorted, NamedImportBindings, NamedImports, NamespaceImport, @@ -59,6 +60,7 @@ import { Scanner, setEmitFlags, some, + sort, SortKind, SourceFile, stableSort, @@ -68,6 +70,9 @@ import { tryCast, UserPreferences, } from "./_namespaces/ts"; +import { + getDiffNum, +} from "./utilities"; /** * Organize imports by: @@ -87,17 +92,27 @@ export function organizeImports( ): FileTextChanges[] { const changeTracker = textChanges.ChangeTracker.fromContext({ host, formatContext, preferences }); const shouldSort = mode === OrganizeImportsMode.SortAndCombine || mode === OrganizeImportsMode.All; - const shouldCombine = shouldSort; // These are currently inseparable, but I draw a distinction for clarity and in case we add modes in the future. + + // These are currently inseparable, but I draw a distinction for clarity and in case we add modes in the future. + // shouldCombine vs shouldSort is not supported in diff-detection, diff-detection always assumes sorting will combine/deduplicate + const shouldCombine = shouldSort; const shouldRemove = mode === OrganizeImportsMode.RemoveUnused || mode === OrganizeImportsMode.All; // All of the old ImportDeclarations in the file, in syntactic order. const topLevelImportGroupDecls = groupByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration)); - const comparer = getOrganizeImportsComparerWithDetection(preferences, shouldSort ? () => detectSortingWorker(topLevelImportGroupDecls, preferences) === SortKind.CaseInsensitive : undefined); + let { moduleSpecifierComparer, namedImportComparer, typeOrder } = getDetectionByDiff(topLevelImportGroupDecls, preferences); + + const DefaultComparer = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); + const exportComparer = namedImportComparer ?? moduleSpecifierComparer ?? DefaultComparer; + + moduleSpecifierComparer = moduleSpecifierComparer ?? DefaultComparer; + namedImportComparer = namedImportComparer ?? DefaultComparer; + typeOrder = typeOrder ?? preferences.organizeImportsTypeOrder ?? "last"; const processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => { if (shouldRemove) importGroup = removeUnusedImports(importGroup, sourceFile, program); - if (shouldCombine) importGroup = coalesceImportsWorker(importGroup, comparer, sourceFile, preferences); - if (shouldSort) importGroup = stableSort(importGroup, (s1, s2) => compareImportsOrRequireStatements(s1, s2, comparer)); + if (shouldCombine) importGroup = coalesceImportsWorker(importGroup, namedImportComparer ?? DefaultComparer, sourceFile, { organizeImportsTypeOrder: typeOrder }); + if (shouldSort) importGroup = stableSort(importGroup, (s1, s2) => compareImportsOrRequireStatements(s1, s2, moduleSpecifierComparer ?? DefaultComparer)); return importGroup; }; @@ -106,7 +121,7 @@ export function organizeImports( // Exports are always used if (mode !== OrganizeImportsMode.RemoveUnused) { // All of the old ExportDeclarations in the file, in syntactic order. - getTopLevelExportGroups(sourceFile).forEach(exportGroupDecl => organizeImportsWorker(exportGroupDecl, group => coalesceExportsWorker(group, comparer, preferences))); + getTopLevelExportGroups(sourceFile).forEach(exportGroupDecl => organizeImportsWorker(exportGroupDecl, group => coalesceExportsWorker(group, exportComparer, preferences))); } for (const ambientModule of sourceFile.statements.filter(isAmbientModule)) { @@ -118,7 +133,7 @@ export function organizeImports( // Exports are always used if (mode !== OrganizeImportsMode.RemoveUnused) { const ambientModuleExportDecls = ambientModule.body.statements.filter(isExportDeclaration); - organizeImportsWorker(ambientModuleExportDecls, group => coalesceExportsWorker(group, comparer, preferences)); + organizeImportsWorker(ambientModuleExportDecls, group => coalesceExportsWorker(group, exportComparer, { organizeImportsTypeOrder: typeOrder })); } } @@ -143,7 +158,7 @@ export function organizeImports( ? group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier)!) : [oldImportDecls]; const sortedImportGroups = shouldSort - ? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiersWorker(group1[0].moduleSpecifier, group2[0].moduleSpecifier, comparer)) + ? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiersWorker(group1[0].moduleSpecifier, group2[0].moduleSpecifier, moduleSpecifierComparer ?? DefaultComparer)) : oldImportGroups; const newImportDecls = flatMap(sortedImportGroups, importGroup => getExternalModuleName(importGroup[0].moduleSpecifier) || importGroup[0].moduleSpecifier === undefined @@ -754,7 +769,7 @@ export const detectImportSpecifierSorting = memoizeCached((specifiers: readonly const collateCaseInsensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); if (preferences.organizeImportsTypeOrder !== "inline") { - const { type: regularImports, regular: typeImports } = groupBy(specifiers, s => s.isTypeOnly ? "type" : "regular"); + const { regular: regularImports, type: typeImports } = groupBy(specifiers, s => s.isTypeOnly ? "type" : "regular"); const regularCaseSensitivity = regularImports?.length ? detectSortCaseSensitivity(regularImports, specifier => specifier.name.text, collateCaseSensitive, collateCaseInsensitive) : undefined; @@ -876,7 +891,8 @@ export function getOrganizeImportsComparer(preferences: UserPreferences, ignoreC getOrganizeImportsOrdinalStringComparer(ignoreCase); } -function getOrganizeImportsComparerWithDetection(preferences: UserPreferences, detectIgnoreCase?: () => boolean): Comparer { +/** @internal */ +export function getOrganizeImportsComparerWithDetection(preferences: UserPreferences, detectIgnoreCase?: () => boolean): Comparer { const ignoreCase = typeof preferences.organizeImportsIgnoreCase === "boolean" ? preferences.organizeImportsIgnoreCase : detectIgnoreCase?.() ?? false; return getOrganizeImportsComparer(preferences, ignoreCase); } @@ -911,3 +927,163 @@ function getTopLevelExportGroups(sourceFile: SourceFile) { } return flatMap(topLevelExportGroups, exportGroupDecls => groupByNewlineContiguous(sourceFile, exportGroupDecls)); } + +function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], preferences: UserPreferences) { + // attempts to detect an independent sortkind for each of module specifiers, named imports, and named type imports + // comparers should be ordered by default priority (case-insensitive first) + const comparer: { + moduleSpecifierComparer?: Comparer; + namedImportComparer?: Comparer; + typeOrder?: "first" | "last" | "inline"; + } = {}; + + let comparers: Comparer[]; + if (typeof preferences.organizeImportsIgnoreCase === "boolean") { + // both moduleSpecifier and namedImport comparer to the correct case-sensitivity. + // does not yet exit because we still need to detect for type order + comparer.moduleSpecifierComparer = getOrganizeImportsComparer(preferences, preferences.organizeImportsIgnoreCase); + comparer.namedImportComparer = comparer.moduleSpecifierComparer; + comparers = [comparer.moduleSpecifierComparer]; + } + else { + // otherwise, we must test for both case-sensitivity and later, type order + const CASE_INSENSITIVE_COMPARER = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); + const CASE_SENSITIVE_COMPARER = getOrganizeImportsComparer(preferences, /*ignoreCase*/ false); + comparers = [CASE_INSENSITIVE_COMPARER, CASE_SENSITIVE_COMPARER]; + + const moduleSpecifiersByGroup: string[][] = []; + importDeclsByGroup.forEach(importGroup => { + // turns importdeclbygroup into string[][] of module specifiers by group to detect sorting on module specifiers + moduleSpecifiersByGroup.push(importGroup.map(i => getExternalModuleName(i.moduleSpecifier)!)); + }); + comparer.moduleSpecifierComparer = detectCaseSensitivityByDiff(moduleSpecifiersByGroup); + } + + // filter for import declarations with named imports. Will be a flat array of import declarations without separations by group + const importDeclsWithNamed = importDeclsByGroup.map(importGroup => + importGroup.filter(i => { + const namedImports = tryCast(i.importClause?.namedBindings, isNamedImports)?.elements.length! > 1; + return namedImports; + }) + ).flat(); + + // no need for more detection if no named imports + if (importDeclsWithNamed.length === 0) return comparer; + + // TODO combine imports with the same module specifier + + let bothNamedImports = false; + // formats the code in order to detect type order + const namedImportsByDecl = importDeclsWithNamed.map(importDecl => { + const originalNamedImports = tryCast(importDecl.importClause?.namedBindings, isNamedImports)?.elements.map(n => n.name.text); + if (!originalNamedImports) return { originalNamedImports: [] }; + const { regular, type } = groupBy((importDecl.importClause?.namedBindings as NamedImports).elements, s => s.isTypeOnly ? "type" : "regular"); + const regularImportNames = regular?.map(n => n.name.text); + const typeImportNames = type?.map(n => n.name.text); + if (regularImportNames && typeImportNames) { + bothNamedImports = true; + } + return { regularImportNames, typeImportNames, originalNamedImports }; + }); + + const { namedImportComparer, typeOrder } = detectNamedImportOrganizationByDiff(namedImportsByDecl, bothNamedImports); + comparer.namedImportComparer = namedImportComparer; + comparer.typeOrder = typeOrder; + + return comparer; + + function detectCaseSensitivityByDiff(originalGroups: string[][]): Comparer { + // each entry in originalGroups will be sorted and compared against the original entry. + // the total diff of each comparison is the sum of the diffs of all groups + let bestComparer; + let bestDiff = Infinity; + + for (const curComparer of comparers) { + let diffOfCurrentComparer = 0; + + for (const listToSort of originalGroups) { + if (listToSort.length <= 1) continue; + + const sortedList = sort(listToSort, curComparer) as any as string[]; + const diff = getDiffNum(listToSort, sortedList); + diffOfCurrentComparer += diff; + } + + if (diffOfCurrentComparer < bestDiff) { + bestDiff = diffOfCurrentComparer; + bestComparer = curComparer; + } + } + return bestComparer ?? comparers[0]; + } + + interface NamedImportByDecl { + originalNamedImports: string[]; + regularImportNames?: string[]; + typeImportNames?: string[]; + } + function detectNamedImportOrganizationByDiff(originalGroups: NamedImportByDecl[], bothNamedImports: boolean): { namedImportComparer: Comparer; typeOrder?: "first" | "last" | "inline"; } { + // if we don't have any import statements with both named regular and type imports, we do not need to detect a type ordering + if (!bothNamedImports) { + return { namedImportComparer: detectCaseSensitivityByDiff(originalGroups.map(i => i.originalNamedImports)) }; + } + if (preferences.organizeImportsTypeOrder !== undefined) { + switch (preferences.organizeImportsTypeOrder) { + case "first": + return { namedImportComparer: detectCaseSensitivityByDiff(originalGroups.map(i => [i.typeImportNames ?? [], i.regularImportNames ?? []]).flat()), typeOrder: "first" }; + case "last": + return { namedImportComparer: detectCaseSensitivityByDiff(originalGroups.map(i => [i.typeImportNames ?? [], i.regularImportNames ?? []]).flat()), typeOrder: "last" }; + case "inline": + return { namedImportComparer: detectCaseSensitivityByDiff(originalGroups.map(i => i.originalNamedImports)), typeOrder: "inline" }; + } + } + + type TypeOrder = "first" | "last" | "inline"; + + const bestDiff = { first: Infinity, last: Infinity, inline: Infinity }; + const bestComparer = { first: comparers[0], last: comparers[0], inline: comparers[0] }; + + for (const curComparer of comparers) { + const currDiff = { first: 0, last: 0, inline: 0 }; + + for (const { regularImportNames, typeImportNames, originalNamedImports } of originalGroups) { + if (!regularImportNames || !typeImportNames) { + const sortedList = sort(originalNamedImports, curComparer) as any as string[]; + const diff = getDiffNum(originalNamedImports, sortedList); + for (const typeOrder in currDiff) { + currDiff[typeOrder as TypeOrder] += diff; + } + continue; + } + + // ordering + const sortedRegular = sort(regularImportNames, curComparer); + const sortedType = sort(typeImportNames, curComparer); + const sortedInline = mergeAndDeduplicateSorted(sortedRegular, sortedType, curComparer); + + currDiff.inline += getDiffNum(originalNamedImports, sortedInline as any as string[]); + currDiff.first += getDiffNum(originalNamedImports, sortedType.concat(sortedRegular)); + currDiff.last += getDiffNum(originalNamedImports, sortedRegular.concat(sortedType)); + } + for (const key in currDiff) { + const typeOrder = key as TypeOrder; + if (currDiff[typeOrder] < bestDiff[typeOrder]) { + bestDiff[typeOrder] = currDiff[typeOrder]; + bestComparer[typeOrder] = curComparer; + } + } + } + + if (bestDiff.last <= bestDiff.first && bestDiff.last <= bestDiff.inline) { + return { namedImportComparer: bestComparer.last, typeOrder: "last" }; + } + if (bestDiff.inline <= bestDiff.first && bestDiff.inline <= bestDiff.last) { + return { namedImportComparer: bestComparer.inline, typeOrder: "inline" }; + } + if (bestDiff.first <= bestDiff.inline && bestDiff.first <= bestDiff.last) { + return { namedImportComparer: bestComparer.first, typeOrder: "first" }; + } + // hopefully never hit..... + return { namedImportComparer: bestComparer.last, typeOrder: "last" }; + } +} From 4b56b440a2d040834f2b07cc323fd3db7577060f Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Thu, 1 Feb 2024 16:30:48 -0800 Subject: [PATCH 12/33] add detectionByDiff to autoImports, update tests --- src/services/codefixes/importFixes.ts | 66 ++++++++++++------- src/services/organizeImports.ts | 7 +- .../autoImportSortCaseSensitivity1.ts | 2 +- .../cases/fourslash/autoImportTypeImport4.ts | 12 ++-- .../cases/fourslash/autoImportTypeImport5.ts | 16 ++--- .../codeFixInferFromUsageContextualImport1.ts | 2 +- .../importNameCodeFixConvertTypeOnly1.ts | 2 +- .../importNameCodeFix_importType4.ts | 2 +- .../importNameCodeFix_importType7.ts | 39 ++++++----- 9 files changed, 85 insertions(+), 63 deletions(-) diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index a634ac334a02f..110df9fe4669f 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -3,6 +3,7 @@ import { AnyImportOrRequireStatement, AnyImportSyntax, arrayFrom, + arrayIsSorted, CancellationToken, cast, changeAnyExtension, @@ -12,6 +13,7 @@ import { combine, compareBooleans, compareNumberOfDirectorySeparators, + Comparer, compareValues, Comparison, CompilerOptions, @@ -58,6 +60,7 @@ import { hostGetCanonicalFileName, Identifier, ImportClause, + ImportDeclaration, ImportEqualsDeclaration, importFromModuleSpecifier, ImportKind, @@ -124,7 +127,7 @@ import { skipAlias, some, sort, - SortKind, + // SortKind, SourceFile, stableSort, startsWith, @@ -1403,11 +1406,12 @@ function promoteFromTypeOnly( switch (aliasDeclaration.kind) { case SyntaxKind.ImportSpecifier: if (aliasDeclaration.isTypeOnly) { - const sortKind = OrganizeImports.detectImportSpecifierSorting(aliasDeclaration.parent.elements, preferences); - if (aliasDeclaration.parent.elements.length > 1 && sortKind) { + if (aliasDeclaration.parent.elements.length > 1) { const newSpecifier = factory.updateImportSpecifier(aliasDeclaration, /*isTypeOnly*/ false, aliasDeclaration.propertyName, aliasDeclaration.name); - const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); - const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, comparer, preferences); + const moduleSpecifiersByGroup: ImportDeclaration[][] = [[aliasDeclaration.parent.parent.parent]]; + const comparers = OrganizeImports.getDetectionByDiff(moduleSpecifiersByGroup, preferences); + // const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); + const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, comparers.namedImportComparer ?? comparers.moduleSpecifierComparer, { organizeImportsTypeOrder: comparers.typeOrder }); if (insertionIndex !== aliasDeclaration.parent.elements.indexOf(aliasDeclaration)) { changes.delete(sourceFile, aliasDeclaration); changes.insertImportSpecifierAtIndex(sourceFile, newSpecifier, aliasDeclaration.parent, insertionIndex); @@ -1500,21 +1504,38 @@ function doAddExistingFix( // - if the user preference is explicit, use that // - otherwise, if there are enough existing import specifiers in this import to detect unambiguously, use that // - otherwise, detect from other imports in the file - let ignoreCaseForSorting: boolean | undefined; + // let ignoreCaseForSorting: boolean | undefined; + let comparers: { + moduleSpecifierComparer: Comparer; + namedImportComparer?: Comparer; + typeOrder?: "first" | "last" | "inline"; + } = { moduleSpecifierComparer: OrganizeImports.getOrganizeImportsComparer(preferences, typeof preferences.organizeImportsIgnoreCase === "boolean" ? preferences.organizeImportsIgnoreCase : true) }; + if (typeof preferences.organizeImportsIgnoreCase === "boolean") { - ignoreCaseForSorting = preferences.organizeImportsIgnoreCase; + comparers.namedImportComparer = comparers.moduleSpecifierComparer; } - else if (existingSpecifiers) { - const targetImportSorting = OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences); - if (targetImportSorting !== SortKind.Both) { - ignoreCaseForSorting = targetImportSorting === SortKind.CaseInsensitive; - } + if (preferences.organizeImportsTypeOrder) { + comparers.typeOrder = preferences.organizeImportsTypeOrder; } - if (ignoreCaseForSorting === undefined) { - ignoreCaseForSorting = OrganizeImports.detectSorting(sourceFile, preferences) === SortKind.CaseInsensitive; + else if (existingSpecifiers && typeof preferences.organizeImportsIgnoreCase !== "boolean" && !preferences.organizeImportsTypeOrder) { + // const targetImportSorting = OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences); + // if (targetImportSorting !== SortKind.Both) { + // ignoreCaseForSorting = targetImportSorting === SortKind.CaseInsensitive; + // } + const moduleSpecifiersByGroup: ImportDeclaration[][] = [[clause.parent]]; + comparers = OrganizeImports.getDetectionByDiff(moduleSpecifiersByGroup, preferences); } + // if (ignoreCaseForSorting === undefined) { + // ignoreCaseForSorting = OrganizeImports.detectSorting(sourceFile, preferences) === SortKind.CaseInsensitive; + // } - const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, ignoreCaseForSorting); + // const newSpecifier = factory.updateImportSpecifier(aliasDeclaration, /*isTypeOnly*/ false, aliasDeclaration.propertyName, aliasDeclaration.name); + // const moduleSpecifiersByGroup: ImportDeclaration[][] = [[aliasDeclaration.parent.parent.parent]]; + // const comparers = OrganizeImports.getDetectionByDiff(moduleSpecifiersByGroup, preferences); + // // const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); + // const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, comparers.namedImportComparer ?? comparers.moduleSpecifierComparer, { organizeImportsTypeOrder: comparers.typeOrder }); + + // const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, ignoreCaseForSorting); const newSpecifiers = stableSort( namedImports.map(namedImport => factory.createImportSpecifier( @@ -1523,7 +1544,7 @@ function doAddExistingFix( factory.createIdentifier(namedImport.name), ) ), - (s1, s2) => OrganizeImports.compareImportOrExportSpecifiers(s1, s2, comparer), + (s1, s2) => OrganizeImports.compareImportOrExportSpecifiers(s1, s2, comparers.namedImportComparer ?? comparers.moduleSpecifierComparer, { organizeImportsTypeOrder: comparers.typeOrder }), ); // The sorting preference computed earlier may or may not have validated that these particular @@ -1531,15 +1552,12 @@ function doAddExistingFix( // nonsense. So if there are existing specifiers, even if we know the sorting preference, we // need to ensure that the existing specifiers are sorted according to the preference in order // to do a sorted insertion. - const specifierSort = existingSpecifiers?.length && OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences); - if (specifierSort && !(ignoreCaseForSorting && specifierSort === SortKind.CaseSensitive)) { + // const specifierSort = existingSpecifiers?.length && OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences); + + // changed to check if existing specifiers are sorted + if (existingSpecifiers?.length && arrayIsSorted(existingSpecifiers, (s1, s2) => OrganizeImports.compareImportOrExportSpecifiers(s1, s2, comparers.namedImportComparer ?? comparers.moduleSpecifierComparer, { organizeImportsTypeOrder: comparers.typeOrder }))) { for (const spec of newSpecifiers) { - // Organize imports puts type-only import specifiers last, so if we're - // adding a non-type-only specifier and converting all the other ones to - // type-only, there's no need to ask for the insertion index - it's 0. - const insertionIndex = promoteFromTypeOnly && !spec.isTypeOnly - ? 0 - : OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, comparer, preferences); + const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, comparers.namedImportComparer ?? comparers.moduleSpecifierComparer, preferences); changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex); } } diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index 48be32018f44d..a3b9b1e436939 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -943,14 +943,15 @@ function getTopLevelExportGroups(sourceFile: SourceFile) { return flatMap(topLevelExportGroups, exportGroupDecls => groupByNewlineContiguous(sourceFile, exportGroupDecls)); } -function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], preferences: UserPreferences) { +/** @internal */ +export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], preferences: UserPreferences) { // attempts to detect an independent sortkind for each of module specifiers, named imports, and named type imports // comparers should be ordered by default priority (case-insensitive first) const comparer: { - moduleSpecifierComparer?: Comparer; + moduleSpecifierComparer: Comparer; namedImportComparer?: Comparer; typeOrder?: "first" | "last" | "inline"; - } = {}; + } = { moduleSpecifierComparer: getOrganizeImportsComparer(preferences, typeof preferences.organizeImportsIgnoreCase === "boolean" ? preferences.organizeImportsIgnoreCase : true) }; let comparers: Comparer[]; if (typeof preferences.organizeImportsIgnoreCase === "boolean") { diff --git a/tests/cases/fourslash/autoImportSortCaseSensitivity1.ts b/tests/cases/fourslash/autoImportSortCaseSensitivity1.ts index 993b3ec855972..f4120d5beb02e 100644 --- a/tests/cases/fourslash/autoImportSortCaseSensitivity1.ts +++ b/tests/cases/fourslash/autoImportSortCaseSensitivity1.ts @@ -28,7 +28,7 @@ //// d/*1*/ goTo.marker("0"); -verify.importFixAtPosition([`import { A, B, C, a } from "./exports1";\na`]); +verify.importFixAtPosition([`import { a, A, B, C } from "./exports1";\na`]); verify.importFixAtPosition([`import { a, A, B, C } from "./exports1";\na`], /*errorCode*/ undefined, { organizeImportsIgnoreCase: true }); diff --git a/tests/cases/fourslash/autoImportTypeImport4.ts b/tests/cases/fourslash/autoImportTypeImport4.ts index 82dff821438f7..8c62d9057f9d9 100644 --- a/tests/cases/fourslash/autoImportTypeImport4.ts +++ b/tests/cases/fourslash/autoImportTypeImport4.ts @@ -39,8 +39,8 @@ // addition of correctly sorted type imports should not affect behavior as shown in autoImportSortCaseSensitivity1.ts goTo.marker("0"); verify.importFixAtPosition([ - `import { A, B, C, a } from "./exports1";\na;\nb;`, - `import { A, B, C, b } from "./exports1";\na;\nb;`, + `import { a, A, B, C } from "./exports1";\na;\nb;`, + `import { A, b, B, C } from "./exports1";\na;\nb;`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "last" }); @@ -53,8 +53,8 @@ verify.importFixAtPosition([ goTo.marker("1"); verify.importFixAtPosition([ - `import { A, B, C, a, type Y, type Z } from "./exports1";\na;\nb;`, - `import { A, B, C, b, type Y, type Z } from "./exports1";\na;\nb;`, + `import { a, A, B, C, type Y, type Z } from "./exports1";\na;\nb;`, + `import { A, b, B, C, type Y, type Z } from "./exports1";\na;\nb;`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "last" }); @@ -67,8 +67,8 @@ verify.importFixAtPosition([ // if we sort inline and sensitive, then all upper case imports should be sorted before any lower case imports verify.importFixAtPosition([ - `import { A, B, C, type Y, type Z, a } from "./exports1";\na;\nb;`, - `import { A, B, C, type Y, type Z, b } from "./exports1";\na;\nb;`, + `import { a, A, B, C, type Y, type Z } from "./exports1";\na;\nb;`, + `import { A, b, B, C, type Y, type Z } from "./exports1";\na;\nb;`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "inline" }); diff --git a/tests/cases/fourslash/autoImportTypeImport5.ts b/tests/cases/fourslash/autoImportTypeImport5.ts index a0800d3f236f0..753f2a3a7f8ec 100644 --- a/tests/cases/fourslash/autoImportTypeImport5.ts +++ b/tests/cases/fourslash/autoImportTypeImport5.ts @@ -35,8 +35,8 @@ // addition of correctly sorted regular imports should not affect correctly sorted type imports goTo.marker("0"); verify.importFixAtPosition([ - `import { type X, type Y, type Z, type x } from "./exports1";\nconst foo: x;\nconst bar: y;`, - `import { type X, type Y, type Z, type y } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type x, type X, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type X, type y, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "last" }); @@ -47,8 +47,8 @@ verify.importFixAtPosition([ /*errorCode*/ undefined, { organizeImportsIgnoreCase: true, organizeImportsTypeOrder : "last" }); verify.importFixAtPosition([ - `import { type X, type Y, type Z, type x } from "./exports1";\nconst foo: x;\nconst bar: y;`, - `import { type X, type Y, type Z, type y } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type x, type X, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type X, type y, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "inline" }); @@ -59,8 +59,8 @@ verify.importFixAtPosition([ /*errorCode*/ undefined, { organizeImportsIgnoreCase: true, organizeImportsTypeOrder : "inline" }); verify.importFixAtPosition([ - `import { type X, type Y, type Z, type x } from "./exports1";\nconst foo: x;\nconst bar: y;`, - `import { type X, type Y, type Z, type y } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type x, type X, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { type X, type y, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "first" }); @@ -73,8 +73,8 @@ verify.importFixAtPosition([ goTo.marker("1"); verify.importFixAtPosition([ - `import { A, B, type X, type Y, type Z, type x } from "./exports1";\nconst foo: x;\nconst bar: y;`, - `import { A, B, type X, type Y, type Z, type y } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { A, B, type x, type X, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, + `import { A, B, type X, type y, type Y, type Z } from "./exports1";\nconst foo: x;\nconst bar: y;`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "last" }); diff --git a/tests/cases/fourslash/codeFixInferFromUsageContextualImport1.ts b/tests/cases/fourslash/codeFixInferFromUsageContextualImport1.ts index f41399a478ac2..5e4a4ca3542ff 100644 --- a/tests/cases/fourslash/codeFixInferFromUsageContextualImport1.ts +++ b/tests/cases/fourslash/codeFixInferFromUsageContextualImport1.ts @@ -20,7 +20,7 @@ goTo.file("/b.ts"); verify.codeFix({ description: "Infer parameter types from usage", newFileContent: -`import { User, getEmail } from "./a"; +`import { getEmail, User } from "./a"; export function f(user: User) { getEmail(user); diff --git a/tests/cases/fourslash/importNameCodeFixConvertTypeOnly1.ts b/tests/cases/fourslash/importNameCodeFixConvertTypeOnly1.ts index ce877da6c34f8..24d688ea96d4a 100644 --- a/tests/cases/fourslash/importNameCodeFixConvertTypeOnly1.ts +++ b/tests/cases/fourslash/importNameCodeFixConvertTypeOnly1.ts @@ -9,5 +9,5 @@ //// new B goTo.file('/b.ts'); -verify.importFixAtPosition([`import { B, type A } from './a'; +verify.importFixAtPosition([`import { type A, B } from './a'; new B`]); diff --git a/tests/cases/fourslash/importNameCodeFix_importType4.ts b/tests/cases/fourslash/importNameCodeFix_importType4.ts index b14a2f8439445..dccab993f707f 100644 --- a/tests/cases/fourslash/importNameCodeFix_importType4.ts +++ b/tests/cases/fourslash/importNameCodeFix_importType4.ts @@ -14,5 +14,5 @@ goTo.marker(""); verify.importFixAtPosition([ -`import { SomePig, type SomeInterface } from "./exports.js"; +`import { type SomeInterface, SomePig } from "./exports.js"; new SomePig`]); diff --git a/tests/cases/fourslash/importNameCodeFix_importType7.ts b/tests/cases/fourslash/importNameCodeFix_importType7.ts index c74a3c2c22837..9e32630420e42 100644 --- a/tests/cases/fourslash/importNameCodeFix_importType7.ts +++ b/tests/cases/fourslash/importNameCodeFix_importType7.ts @@ -16,6 +16,8 @@ //// new SomePig/**/ goTo.marker(""); + +// since we cannot detect a type order from the original file, type order defaults to last verify.importFixAtPosition([ `import { SomePig, @@ -32,21 +34,22 @@ new SomePig`], /*errorCode*/ undefined, { organizeImportsTypeOrder: "last" }); -verify.importFixAtPosition([ -`import { - type SomeInterface, - SomePig, -} from "./exports.js"; -new SomePig`], - /*errorCode*/ undefined, - { organizeImportsTypeOrder: "inline" } -); - -verify.importFixAtPosition([ -`import { - type SomeInterface, - SomePig, -} from "./exports.js"; -new SomePig`], -/*errorCode*/ undefined, -{ organizeImportsTypeOrder: "first" }); +//TODO: bugged cases +// verify.importFixAtPosition([ +// `import { +// type SomeInterface, +// SomePig, +// } from "./exports.js"; +// new SomePig`], +// /*errorCode*/ undefined, +// { organizeImportsTypeOrder: "inline" } +// ); + +// verify.importFixAtPosition([ +// `import { +// type SomeInterface, +// SomePig, +// } from "./exports.js"; +// new SomePig`], +// /*errorCode*/ undefined, +// { organizeImportsTypeOrder: "first" }); From dadfe54ffc734764ea620026097952ee0b98be30 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Fri, 2 Feb 2024 20:38:24 -0800 Subject: [PATCH 13/33] move diff unit tests to unittests/utilities.ts --- .../unittests/services/organizeImports.ts | 34 ------------------ .../unittests/services/utilities.ts | 36 +++++++++++++++++++ 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index 1c3c469afe497..65d46e23a49de 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -1043,40 +1043,6 @@ export * from "lib"; } }); - describe("helper functions", () => { - it("Detection-1", () => { - const originalImports = ["abc", "Abc", "abC"]; - const test2 = ["abc", "Abc", "abC", "b"]; - const test3 = ["abc", "Abc", "abC", "c"]; - const test4 = ["abc", "Abc", "abC", "b", "d", "e"]; - const test5 = ["abc", "b", "Abc", "abC", "d", "e"]; - - assert.equal(ts.getDiffNum(originalImports, originalImports), 0); - assert.equal(ts.getDiffNum(originalImports, test2), 1); - assert.equal(ts.getDiffNum(test2, test3), 1); - assert.equal(ts.getDiffNum(originalImports, test4), 3); - assert.equal(ts.getDiffNum(originalImports, test5), 3); - assert.equal(ts.getDiffNum(test4, test5), 2); - }); - - it("mergededuplicates", () => { - const test = [1, 2, 3, 4]; - const test2 = [1, 2, 3, 4, 5]; - const test3 = [6, 7, 8, 9, 10]; - const test4 = [1, 3, 4, 5, 8, 9]; - const test5 = [1, 2, 4, 6, 7, 8, 9]; - function comparer(x: number, y: number) { - if (x > y) return ts.Comparison.GreaterThan; - if (x < y) return ts.Comparison.LessThan; - return ts.Comparison.EqualTo; - } - assert.sameOrderedMembers(ts.mergeAndDeduplicateSorted(test, test2, comparer) as any as number[], test2); - assert.sameOrderedMembers(ts.mergeAndDeduplicateSorted(test2, test2, comparer) as any as number[], test2); - assert.sameOrderedMembers(ts.mergeAndDeduplicateSorted(test2, test3, comparer) as any as number[], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - assert.sameOrderedMembers(ts.mergeAndDeduplicateSorted(test4, test5, comparer) as any as number[], [1, 2, 3, 4, 5, 6, 7, 8, 9]); - }); - }); - function parseImports(...importStrings: string[]): readonly ts.ImportDeclaration[] { const sourceFile = ts.createSourceFile("a.ts", importStrings.join("\n"), ts.ScriptTarget.ES2015, /*setParentNodes*/ true, ts.ScriptKind.TS); const imports = ts.filter(sourceFile.statements, ts.isImportDeclaration); diff --git a/src/testRunner/unittests/services/utilities.ts b/src/testRunner/unittests/services/utilities.ts index 91b82462b479e..dfdef5ffa1ca6 100644 --- a/src/testRunner/unittests/services/utilities.ts +++ b/src/testRunner/unittests/services/utilities.ts @@ -22,4 +22,40 @@ describe("unittests:: services:: utilities", () => { assert.isDefined(result); }); }); + + describe("getDiffNum", () => { + it("getDiffNum", () => { + const originalImports = ["abc", "Abc", "abC"]; + const test2 = ["abc", "Abc", "abC", "b"]; + const test3 = ["abc", "Abc", "abC", "c"]; + const test4 = ["abc", "Abc", "abC", "b", "d", "e"]; + const test5 = ["abc", "b", "Abc", "abC", "d", "e"]; + + assert.equal(ts.getDiffNum(originalImports, originalImports), 0); + assert.equal(ts.getDiffNum(originalImports, test2), 1); + assert.equal(ts.getDiffNum(test2, test3), 1); + assert.equal(ts.getDiffNum(originalImports, test4), 3); + assert.equal(ts.getDiffNum(originalImports, test5), 3); + assert.equal(ts.getDiffNum(test4, test5), 2); + }); + }); + + describe("mergeAndDeDuplicate", () => { + it("mergeAndDeDuplicate", () => { + const test = [1, 2, 3, 4]; + const test2 = [1, 2, 3, 4, 5]; + const test3 = [6, 7, 8, 9, 10]; + const test4 = [1, 3, 4, 5, 8, 9]; + const test5 = [1, 2, 4, 6, 7, 8, 9]; + function comparer(x: number, y: number) { + if (x > y) return ts.Comparison.GreaterThan; + if (x < y) return ts.Comparison.LessThan; + return ts.Comparison.EqualTo; + } + assert.sameOrderedMembers(ts.mergeAndDeduplicateSorted(test, test2, comparer) as any as number[], test2); + assert.sameOrderedMembers(ts.mergeAndDeduplicateSorted(test2, test2, comparer) as any as number[], test2); + assert.sameOrderedMembers(ts.mergeAndDeduplicateSorted(test2, test3, comparer) as any as number[], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + assert.sameOrderedMembers(ts.mergeAndDeduplicateSorted(test4, test5, comparer) as any as number[], [1, 2, 3, 4, 5, 6, 7, 8, 9]); + }); + }); }); From 8067f5f573a0d254773e859d1a646bdb436bdc3d Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Wed, 14 Feb 2024 18:32:39 -0800 Subject: [PATCH 14/33] in progress --- src/services/organizeImports.ts | 196 ++++++++++-------- .../fourslash/organizeImportsAmbientModule.ts | 24 +++ tests/cases/fourslash/organizeImportsType2.ts | 3 +- tests/cases/fourslash/organizeImportsType5.ts | 28 +-- 4 files changed, 154 insertions(+), 97 deletions(-) create mode 100644 tests/cases/fourslash/organizeImportsAmbientModule.ts diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index a3b9b1e436939..f41a223ae853f 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -50,7 +50,6 @@ import { map, MemoizeCache, memoizeCached, - mergeAndDeduplicateSorted, NamedImportBindings, NamedImports, NamespaceImport, @@ -70,9 +69,6 @@ import { tryCast, UserPreferences, } from "./_namespaces/ts"; -import { - getDiffNum, -} from "./utilities"; /** * Organize imports by: @@ -100,46 +96,36 @@ export function organizeImports( // All of the old ImportDeclarations in the file, in syntactic order. const topLevelImportGroupDecls = groupByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration)); - let { moduleSpecifierComparer, namedImportComparer, typeOrder } = getDetectionByDiff(topLevelImportGroupDecls, preferences); - const DefaultComparer = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); - const exportComparer = namedImportComparer ?? moduleSpecifierComparer ?? DefaultComparer; - - moduleSpecifierComparer = moduleSpecifierComparer ?? DefaultComparer; - namedImportComparer = namedImportComparer ?? DefaultComparer; - typeOrder = typeOrder ?? preferences.organizeImportsTypeOrder ?? "last"; - const processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => { - if (shouldRemove) importGroup = removeUnusedImports(importGroup, sourceFile, program); - if (shouldCombine) importGroup = coalesceImportsWorker(importGroup, namedImportComparer ?? DefaultComparer, sourceFile, { organizeImportsTypeOrder: typeOrder }); - if (shouldSort) importGroup = stableSort(importGroup, (s1, s2) => compareImportsOrRequireStatements(s1, s2, moduleSpecifierComparer ?? DefaultComparer)); - return importGroup; - }; + let detectedModuleCaseComparer = DefaultComparer; + let detectedNamedImportCaseComparer = (s1, s2) => compareImportOrExportSpecifiers(s1, s2, DefaultComparer, {organizeImportsTypeOrder: "last"}); + let detectedTypeOrder : typeof preferences.organizeImportsTypeOrder = "last"; - topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, processImportsOfSameModuleSpecifier)); + topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl)); // Exports are always used if (mode !== OrganizeImportsMode.RemoveUnused) { // All of the old ExportDeclarations in the file, in syntactic order. - getTopLevelExportGroups(sourceFile).forEach(exportGroupDecl => organizeImportsWorker(exportGroupDecl, group => coalesceExportsWorker(group, exportComparer, preferences))); + getTopLevelExportGroups(sourceFile).forEach(exportGroupDecl => organizeExportsWorker(exportGroupDecl)); } for (const ambientModule of sourceFile.statements.filter(isAmbientModule)) { if (!ambientModule.body) continue; const ambientModuleImportGroupDecls = groupByNewlineContiguous(sourceFile, ambientModule.body.statements.filter(isImportDeclaration)); - ambientModuleImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, processImportsOfSameModuleSpecifier)); + ambientModuleImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl)); // Exports are always used if (mode !== OrganizeImportsMode.RemoveUnused) { const ambientModuleExportDecls = ambientModule.body.statements.filter(isExportDeclaration); - organizeImportsWorker(ambientModuleExportDecls, group => coalesceExportsWorker(group, exportComparer, { organizeImportsTypeOrder: typeOrder })); + organizeExportsWorker(ambientModuleExportDecls); } } return changeTracker.getChanges(); - function organizeImportsWorker( + function organizeDeclsWorker( oldImportDecls: readonly T[], coalesce: (group: readonly T[]) => readonly T[], ) { @@ -158,7 +144,7 @@ export function organizeImports( ? group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier)!) : [oldImportDecls]; const sortedImportGroups = shouldSort - ? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiersWorker(group1[0].moduleSpecifier, group2[0].moduleSpecifier, moduleSpecifierComparer ?? DefaultComparer)) + ? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiersWorker(group1[0].moduleSpecifier, group2[0].moduleSpecifier, detectedModuleCaseComparer)) : oldImportGroups; const newImportDecls = flatMap(sortedImportGroups, importGroup => getExternalModuleName(importGroup[0].moduleSpecifier) || importGroup[0].moduleSpecifier === undefined @@ -188,6 +174,32 @@ export function organizeImports( }, hasTrailingComment); } } + + function organizeImportsWorker(oldImportDecls: readonly ImportDeclaration[], comparer?: Comparer) { + if (!comparer) { + const { moduleSpecifierComparer, namedImportComparer, typeOrder } = getDetectionByDiff(topLevelImportGroupDecls, preferences) + + detectedModuleCaseComparer = moduleSpecifierComparer ?? detectedModuleCaseComparer; + detectedNamedImportCaseComparer = namedImportComparer ?? detectedNamedImportCaseComparer; + detectedTypeOrder = typeOrder ?? detectedTypeOrder; + // TODO return unset comparer + } + + const processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => { + if (shouldRemove) importGroup = removeUnusedImports(importGroup, sourceFile, program); + if (shouldCombine) importGroup = coalesceImportsWorker(importGroup, detectedNamedImportCaseComparer, sourceFile, { organizeImportsTypeOrder: detectedTypeOrder }); + if (shouldSort) importGroup = stableSort(importGroup, (s1, s2) => compareImportsOrRequireStatements(s1, s2, detectedModuleCaseComparer)); + return importGroup; + }; + + organizeDeclsWorker(oldImportDecls, processImportsOfSameModuleSpecifier); + // return { moduleSpecifierComparer, namedImportComparer, typeOrder }; + } + + function organizeExportsWorker(oldExportDecls: readonly ExportDeclaration[], comparer?: Comparer) { + const useComparer = comparer ?? DefaultComparer; + organizeDeclsWorker(oldExportDecls, group => coalesceExportsWorker(group, useComparer, { organizeImportsTypeOrder: detectedTypeOrder ?? preferences.organizeImportsTypeOrder })) + } } function groupByNewlineContiguous(sourceFile: SourceFile, decls: T[]): T[][] { @@ -967,19 +979,20 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr const CASE_SENSITIVE_COMPARER = getOrganizeImportsComparer(preferences, /*ignoreCase*/ false); comparers = [CASE_INSENSITIVE_COMPARER, CASE_SENSITIVE_COMPARER]; - const moduleSpecifiersByGroup: string[][] = []; - importDeclsByGroup.forEach(importGroup => { - // turns importdeclbygroup into string[][] of module specifiers by group to detect sorting on module specifiers - moduleSpecifiersByGroup.push(importGroup.map(i => getExternalModuleName(i.moduleSpecifier)!)); - }); - comparer.moduleSpecifierComparer = detectCaseSensitivityByDiff(moduleSpecifiersByGroup); + getModuleSpecifierNames(importDeclsByGroup, comparer, detectCaseSensitivityBySort); } // filter for import declarations with named imports. Will be a flat array of import declarations without separations by group + let bothNamedImports = false; const importDeclsWithNamed = importDeclsByGroup.map(importGroup => importGroup.filter(i => { - const namedImports = tryCast(i.importClause?.namedBindings, isNamedImports)?.elements.length! > 1; - return namedImports; + const namedImports = tryCast(i.importClause?.namedBindings, isNamedImports)?.elements; + if (!namedImports?.length) return false; + if (!bothNamedImports && namedImports.some(n => n.isTypeOnly) && namedImports.some(n => !n.isTypeOnly)) { + //todo:improve check + bothNamedImports = true; + } + return true; }) ).flat(); @@ -988,27 +1001,37 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr // TODO combine imports with the same module specifier - let bothNamedImports = false; // formats the code in order to detect type order const namedImportsByDecl = importDeclsWithNamed.map(importDecl => { - const originalNamedImports = tryCast(importDecl.importClause?.namedBindings, isNamedImports)?.elements.map(n => n.name.text); - if (!originalNamedImports) return { originalNamedImports: [] }; - const { regular, type } = groupBy((importDecl.importClause?.namedBindings as NamedImports).elements, s => s.isTypeOnly ? "type" : "regular"); - const regularImportNames = regular?.map(n => n.name.text); - const typeImportNames = type?.map(n => n.name.text); - if (regularImportNames && typeImportNames) { - bothNamedImports = true; - } - return { regularImportNames, typeImportNames, originalNamedImports }; - }); - - const { namedImportComparer, typeOrder } = detectNamedImportOrganizationByDiff(namedImportsByDecl, bothNamedImports); + return tryCast(importDecl.importClause?.namedBindings, isNamedImports)?.elements; + }).filter(elements => elements !== undefined) as any as ImportSpecifier[][]; + // const originalNamedImports = tryCast(importDecl.importClause?.namedBindings, isNamedImports)?.elements.map(n => n.name.text); + // if (!originalNamedImports) return { originalNamedImports: [] }; + // const { regular, type } = groupBy((importDecl.importClause?.namedBindings as NamedImports).elements, s => s.isTypeOnly ? "type" : "regular"); + // const regularImportNames = regular?.map(n => n.name.text); + // const typeImportNames = type?.map(n => n.name.text); + // if (regularImportNames && typeImportNames) { + // bothNamedImports = true; + // } + // return { regularImportNames, typeImportNames, originalNamedImports }; + // }); + + const { namedImportComparer, typeOrder } = detectNamedImportOrganizationBySort(namedImportsByDecl); comparer.namedImportComparer = namedImportComparer; comparer.typeOrder = typeOrder; return comparer; - function detectCaseSensitivityByDiff(originalGroups: string[][]): Comparer { + function getSortedMeasure(arr: readonly T[], comparer: Comparer) { + let i = 0; + for (let j = 0; j < arr.length-1; j++) { + if (comparer(arr[j], arr[j+1]) > 0) { + i++; + } + } + return i; + } + function detectCaseSensitivityBySort(originalGroups: string[][]): Comparer { // each entry in originalGroups will be sorted and compared against the original entry. // the total diff of each comparison is the sum of the diffs of all groups let bestComparer; @@ -1020,8 +1043,8 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr for (const listToSort of originalGroups) { if (listToSort.length <= 1) continue; - const sortedList = sort(listToSort, curComparer) as any as string[]; - const diff = getDiffNum(listToSort, sortedList); + // const sortedList = sort(listToSort, curComparer) as any as string[]; + const diff = getSortedMeasure(listToSort, curComparer); diffOfCurrentComparer += diff; } @@ -1033,26 +1056,26 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr return bestComparer ?? comparers[0]; } - interface NamedImportByDecl { - originalNamedImports: string[]; - regularImportNames?: string[]; - typeImportNames?: string[]; - } - function detectNamedImportOrganizationByDiff(originalGroups: NamedImportByDecl[], bothNamedImports: boolean): { namedImportComparer: Comparer; typeOrder?: "first" | "last" | "inline"; } { + // interface NamedImportByDecl { + // originalNamedImports: string[]; + // regularImportNames?: string[]; + // typeImportNames?: string[]; + // } + function detectNamedImportOrganizationBySort(originalGroups: ImportSpecifier[][]): { namedImportComparer: Comparer; typeOrder?: "first" | "last" | "inline"; } { // if we don't have any import statements with both named regular and type imports, we do not need to detect a type ordering if (!bothNamedImports) { - return { namedImportComparer: detectCaseSensitivityByDiff(originalGroups.map(i => i.originalNamedImports)) }; - } - if (preferences.organizeImportsTypeOrder !== undefined) { - switch (preferences.organizeImportsTypeOrder) { - case "first": - return { namedImportComparer: detectCaseSensitivityByDiff(originalGroups.map(i => [i.typeImportNames ?? [], i.regularImportNames ?? []]).flat()), typeOrder: "first" }; - case "last": - return { namedImportComparer: detectCaseSensitivityByDiff(originalGroups.map(i => [i.typeImportNames ?? [], i.regularImportNames ?? []]).flat()), typeOrder: "last" }; - case "inline": - return { namedImportComparer: detectCaseSensitivityByDiff(originalGroups.map(i => i.originalNamedImports)), typeOrder: "inline" }; - } + return { namedImportComparer: detectCaseSensitivityBySort(originalGroups.map(i => i.map(n =>n.name.text))) }; } + // if (preferences.organizeImportsTypeOrder !== undefined) { + // switch (preferences.organizeImportsTypeOrder) { + // case "first": + // return { namedImportComparer: detectCaseSensitivityBySort(originalGroups.map(i => [i.typeImportNames ?? [], i.regularImportNames ?? []]).flat()), typeOrder: "first" }; + // case "last": + // return { namedImportComparer: detectCaseSensitivityBySort(originalGroups.map(i => [i.typeImportNames ?? [], i.regularImportNames ?? []]).flat()), typeOrder: "last" }; + // case "inline": + // return { namedImportComparer: detectCaseSensitivityBySort(originalGroups.map(i => i.originalNamedImports)), typeOrder: "inline" }; + // } + // } type TypeOrder = "first" | "last" | "inline"; @@ -1062,24 +1085,24 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr for (const curComparer of comparers) { const currDiff = { first: 0, last: 0, inline: 0 }; - for (const { regularImportNames, typeImportNames, originalNamedImports } of originalGroups) { - if (!regularImportNames || !typeImportNames) { - const sortedList = sort(originalNamedImports, curComparer) as any as string[]; - const diff = getDiffNum(originalNamedImports, sortedList); - for (const typeOrder in currDiff) { - currDiff[typeOrder as TypeOrder] += diff; - } - continue; - } + for (const importDecl of originalGroups) { + // if (!regularImportNames || !typeImportNames) { + // const sortedList = sort(originalNamedImports, curComparer) as any as string[]; + // const diff = getSortedMeasure(originalNamedImports, comparer); + // for (const typeOrder in currDiff) { + // currDiff[typeOrder as TypeOrder] += diff; + // } + // continue; + // } // ordering - const sortedRegular = sort(regularImportNames, curComparer); - const sortedType = sort(typeImportNames, curComparer); - const sortedInline = mergeAndDeduplicateSorted(sortedRegular, sortedType, curComparer); + // const sortedRegular = sort(regularImportNames, curComparer); + // const sortedType = sort(typeImportNames, curComparer); + // const sortedInline = mergeAndDeduplicateSorted(sortedRegular, sortedType, curComparer); - currDiff.inline += getDiffNum(originalNamedImports, sortedInline as any as string[]); - currDiff.first += getDiffNum(originalNamedImports, sortedType.concat(sortedRegular)); - currDiff.last += getDiffNum(originalNamedImports, sortedRegular.concat(sortedType)); + currDiff.inline += getSortedMeasure(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, {organizeImportsTypeOrder: "inline"})); + currDiff.first += getSortedMeasure(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, {organizeImportsTypeOrder: "first"})); + currDiff.last += getSortedMeasure(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, {organizeImportsTypeOrder: "last"})); } for (const key in currDiff) { const typeOrder = key as TypeOrder; @@ -1090,12 +1113,12 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr } } - if (bestDiff.last <= bestDiff.first && bestDiff.last <= bestDiff.inline) { - return { namedImportComparer: bestComparer.last, typeOrder: "last" }; - } if (bestDiff.inline <= bestDiff.first && bestDiff.inline <= bestDiff.last) { return { namedImportComparer: bestComparer.inline, typeOrder: "inline" }; } + if (bestDiff.last <= bestDiff.first && bestDiff.last <= bestDiff.inline) { + return { namedImportComparer: bestComparer.last, typeOrder: "last" }; + } if (bestDiff.first <= bestDiff.inline && bestDiff.first <= bestDiff.last) { return { namedImportComparer: bestComparer.first, typeOrder: "first" }; } @@ -1103,3 +1126,12 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr return { namedImportComparer: bestComparer.last, typeOrder: "last" }; } } +function getModuleSpecifierNames(importDeclsByGroup: ImportDeclaration[][], comparer: { moduleSpecifierComparer: Comparer; namedImportComparer?: Comparer | undefined; typeOrder?: "first" | "last" | "inline" | undefined; }, detectCaseSensitivityByDiff: (originalGroups: string[][]) => Comparer) { + const moduleSpecifiersByGroup: string[][] = []; + importDeclsByGroup.forEach(importGroup => { + // turns importdeclbygroup into string[][] of module specifiers by group to detect sorting on module specifiers + moduleSpecifiersByGroup.push(importGroup.map(i => getExternalModuleName(i.moduleSpecifier)!)); + }); + comparer.moduleSpecifierComparer = detectCaseSensitivityByDiff(moduleSpecifiersByGroup); +} + diff --git a/tests/cases/fourslash/organizeImportsAmbientModule.ts b/tests/cases/fourslash/organizeImportsAmbientModule.ts new file mode 100644 index 0000000000000..7ddb43c0ed4fe --- /dev/null +++ b/tests/cases/fourslash/organizeImportsAmbientModule.ts @@ -0,0 +1,24 @@ +/// + +// @allowSyntheticDefaultImports: true +// @moduleResolution: node +// @noUnusedLocals: true +// @target: es2018 + +//// declare module "mod" { +//// import { F1 } from "lib"; +//// import * as NS from "lib"; +//// import { F2 } from "lib"; +//// +//// function F(f1: {} = F1, f2: {} = F2) {} +//// } + +verify.organizeImports( +`declare module "mod" { + import { F1, F2 } from "lib"; + + function F(f1: {} = F1, f2: {} = F2) {} +}`, +/*mode*/ undefined +); + diff --git a/tests/cases/fourslash/organizeImportsType2.ts b/tests/cases/fourslash/organizeImportsType2.ts index 57b2b4db26950..44d34caadba42 100644 --- a/tests/cases/fourslash/organizeImportsType2.ts +++ b/tests/cases/fourslash/organizeImportsType2.ts @@ -47,4 +47,5 @@ export { A, C, type B }; `, undefined, { organizeImportsTypeOrder : "last" } -); \ No newline at end of file +); + diff --git a/tests/cases/fourslash/organizeImportsType5.ts b/tests/cases/fourslash/organizeImportsType5.ts index 698577ac2cd4a..a7cbec7b9ab13 100644 --- a/tests/cases/fourslash/organizeImportsType5.ts +++ b/tests/cases/fourslash/organizeImportsType5.ts @@ -12,20 +12,20 @@ //// } from './foo'; //// console.log(A, a, B, b, c, C, d, D); -verify.organizeImports(`import { - type A, - a, - b, - b as B, - type c, - c as C, - d, - type d as D -} from './foo'; -console.log(A, a, B, b, c, C, d, D);`, - /*mode*/ undefined, - { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "inline" } -); +// verify.organizeImports(`import { +// type A, +// a, +// b, +// b as B, +// type c, +// c as C, +// d, +// type d as D +// } from './foo'; +// console.log(A, a, B, b, c, C, d, D);`, +// /*mode*/ undefined, +// { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "inline" } +// ); verify.organizeImports(`import { type A, From e3ccaf166adfcffc5fe55a1815343b442377cdd7 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Tue, 20 Feb 2024 16:15:59 -0800 Subject: [PATCH 15/33] change detection method to "isSorted" check --- src/services/organizeImports.ts | 93 +++++++++++-------- tests/cases/fourslash/organizeImportsType9.ts | 3 +- 2 files changed, 54 insertions(+), 42 deletions(-) diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index f41a223ae853f..c759734b2a670 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -96,18 +96,29 @@ export function organizeImports( // All of the old ImportDeclarations in the file, in syntactic order. const topLevelImportGroupDecls = groupByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration)); - const DefaultComparer = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); + const DefaultComparer = getOrganizeImportsComparer(preferences, /*ignoreCase*/ typeof preferences.organizeImportsIgnoreCase === "boolean" ? preferences.organizeImportsIgnoreCase : true); - let detectedModuleCaseComparer = DefaultComparer; - let detectedNamedImportCaseComparer = (s1, s2) => compareImportOrExportSpecifiers(s1, s2, DefaultComparer, {organizeImportsTypeOrder: "last"}); - let detectedTypeOrder : typeof preferences.organizeImportsTypeOrder = "last"; + // if case sensitivity is specified (true/false), then use the same setting for both. + let detectedModuleCaseComparer: Comparer | undefined = typeof preferences.organizeImportsIgnoreCase === "boolean" ? DefaultComparer : undefined; + let detectedNamedImportCaseComparer: Comparer | undefined = detectedModuleCaseComparer; + + let detectedTypeOrder : typeof preferences.organizeImportsTypeOrder = preferences.organizeImportsTypeOrder; + + if (!detectedModuleCaseComparer || !detectedNamedImportCaseComparer || !detectedTypeOrder) { + const { moduleSpecifierComparer, namedImportComparer, typeOrder } = getDetectionByDiff(topLevelImportGroupDecls, preferences) + + detectedModuleCaseComparer = detectedModuleCaseComparer ?? moduleSpecifierComparer ?? DefaultComparer; + detectedNamedImportCaseComparer = detectedNamedImportCaseComparer ?? namedImportComparer ?? DefaultComparer; + detectedTypeOrder = detectedTypeOrder ?? typeOrder; + // TODO return unset comparer + } topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl)); // Exports are always used if (mode !== OrganizeImportsMode.RemoveUnused) { // All of the old ExportDeclarations in the file, in syntactic order. - getTopLevelExportGroups(sourceFile).forEach(exportGroupDecl => organizeExportsWorker(exportGroupDecl)); + getTopLevelExportGroups(sourceFile).forEach(exportGroupDecl => organizeExportsWorker(exportGroupDecl, detectedNamedImportCaseComparer)); } for (const ambientModule of sourceFile.statements.filter(isAmbientModule)) { @@ -119,7 +130,7 @@ export function organizeImports( // Exports are always used if (mode !== OrganizeImportsMode.RemoveUnused) { const ambientModuleExportDecls = ambientModule.body.statements.filter(isExportDeclaration); - organizeExportsWorker(ambientModuleExportDecls); + organizeExportsWorker(ambientModuleExportDecls, detectedNamedImportCaseComparer); } } @@ -144,7 +155,7 @@ export function organizeImports( ? group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier)!) : [oldImportDecls]; const sortedImportGroups = shouldSort - ? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiersWorker(group1[0].moduleSpecifier, group2[0].moduleSpecifier, detectedModuleCaseComparer)) + ? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiersWorker(group1[0].moduleSpecifier, group2[0].moduleSpecifier, detectedModuleCaseComparer ?? DefaultComparer)) : oldImportGroups; const newImportDecls = flatMap(sortedImportGroups, importGroup => getExternalModuleName(importGroup[0].moduleSpecifier) || importGroup[0].moduleSpecifier === undefined @@ -175,20 +186,12 @@ export function organizeImports( } } - function organizeImportsWorker(oldImportDecls: readonly ImportDeclaration[], comparer?: Comparer) { - if (!comparer) { - const { moduleSpecifierComparer, namedImportComparer, typeOrder } = getDetectionByDiff(topLevelImportGroupDecls, preferences) - - detectedModuleCaseComparer = moduleSpecifierComparer ?? detectedModuleCaseComparer; - detectedNamedImportCaseComparer = namedImportComparer ?? detectedNamedImportCaseComparer; - detectedTypeOrder = typeOrder ?? detectedTypeOrder; - // TODO return unset comparer - } - + function organizeImportsWorker(oldImportDecls: readonly ImportDeclaration[]) { + const specifierComparer = getOrganizeImportsSpecifierComparer({organizeImportsTypeOrder: detectedTypeOrder ?? preferences.organizeImportsTypeOrder}, detectedNamedImportCaseComparer); const processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => { if (shouldRemove) importGroup = removeUnusedImports(importGroup, sourceFile, program); - if (shouldCombine) importGroup = coalesceImportsWorker(importGroup, detectedNamedImportCaseComparer, sourceFile, { organizeImportsTypeOrder: detectedTypeOrder }); - if (shouldSort) importGroup = stableSort(importGroup, (s1, s2) => compareImportsOrRequireStatements(s1, s2, detectedModuleCaseComparer)); + if (shouldCombine) importGroup = coalesceImportsWorker(importGroup, detectedModuleCaseComparer ?? DefaultComparer, specifierComparer, sourceFile); + if (shouldSort) importGroup = stableSort(importGroup, (s1, s2) => compareImportsOrRequireStatements(s1, s2, detectedModuleCaseComparer ?? DefaultComparer)); return importGroup; }; @@ -196,9 +199,9 @@ export function organizeImports( // return { moduleSpecifierComparer, namedImportComparer, typeOrder }; } - function organizeExportsWorker(oldExportDecls: readonly ExportDeclaration[], comparer?: Comparer) { - const useComparer = comparer ?? DefaultComparer; - organizeDeclsWorker(oldExportDecls, group => coalesceExportsWorker(group, useComparer, { organizeImportsTypeOrder: detectedTypeOrder ?? preferences.organizeImportsTypeOrder })) + function organizeExportsWorker(oldExportDecls: readonly ExportDeclaration[], specifierCaseComparer?: Comparer) { + const useComparer = getOrganizeImportsSpecifierComparer(preferences, specifierCaseComparer); + organizeDeclsWorker(oldExportDecls, group => coalesceExportsWorker(group, useComparer)) } } @@ -341,10 +344,11 @@ function getExternalModuleName(specifier: Expression | undefined) { */ export function coalesceImports(importGroup: readonly ImportDeclaration[], ignoreCase: boolean, sourceFile?: SourceFile, preferences?: UserPreferences): readonly ImportDeclaration[] { const comparer = getOrganizeImportsOrdinalStringComparer(ignoreCase); - return coalesceImportsWorker(importGroup, comparer, sourceFile, preferences); + const specifierComparer = getOrganizeImportsSpecifierComparer({organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder}, comparer); + return coalesceImportsWorker(importGroup, comparer, specifierComparer, sourceFile); } -function coalesceImportsWorker(importGroup: readonly ImportDeclaration[], comparer: Comparer, sourceFile?: SourceFile, preferences?: UserPreferences): readonly ImportDeclaration[] { +function coalesceImportsWorker(importGroup: readonly ImportDeclaration[], comparer: Comparer, specifierComparer: Comparer, sourceFile?: SourceFile): readonly ImportDeclaration[] { if (importGroup.length === 0) { return importGroup; } @@ -417,7 +421,7 @@ function coalesceImportsWorker(importGroup: readonly ImportDeclaration[], compar newImportSpecifiers.push(...getNewImportSpecifiers(namedImports)); const sortedImportSpecifiers = factory.createNodeArray( - sortSpecifiers(newImportSpecifiers, comparer, preferences), + sortSpecifiers(newImportSpecifiers, specifierComparer), firstNamedImport?.importClause.namedBindings.elements.hasTrailingComma, ); @@ -536,11 +540,12 @@ function getCategorizedImports(importGroup: readonly ImportDeclaration[]) { * @internal */ export function coalesceExports(exportGroup: readonly ExportDeclaration[], ignoreCase: boolean, preferences?: UserPreferences) { - const comparer = getOrganizeImportsOrdinalStringComparer(ignoreCase); - return coalesceExportsWorker(exportGroup, comparer, preferences); + const comparer = (s1: ExportSpecifier, s2: ExportSpecifier) => + compareImportOrExportSpecifiers(s1, s2, getOrganizeImportsOrdinalStringComparer(ignoreCase), {organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder ?? "last"}); + return coalesceExportsWorker(exportGroup, comparer); } -function coalesceExportsWorker(exportGroup: readonly ExportDeclaration[], comparer: Comparer, preferences?: UserPreferences) { +function coalesceExportsWorker(exportGroup: readonly ExportDeclaration[], specifierComparer: Comparer) { if (exportGroup.length === 0) { return exportGroup; } @@ -559,7 +564,7 @@ function coalesceExportsWorker(exportGroup: readonly ExportDeclaration[], compar const newExportSpecifiers: ExportSpecifier[] = []; newExportSpecifiers.push(...flatMap(exportGroup, i => i.exportClause && isNamedExports(i.exportClause) ? i.exportClause.elements : emptyArray)); - const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers, comparer, preferences); + const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers, specifierComparer); const exportDecl = exportGroup[0]; coalescedExports.push( @@ -626,8 +631,8 @@ function updateImportDeclarationAndClause( ); } -function sortSpecifiers(specifiers: readonly T[], comparer: Comparer, preferences?: UserPreferences): readonly T[] { - return stableSort(specifiers, (s1, s2) => compareImportOrExportSpecifiers(s1, s2, comparer, preferences)); +function sortSpecifiers(specifiers: readonly T[], specifierComparer: Comparer): readonly T[] { + return stableSort(specifiers, specifierComparer); } /** @internal */ @@ -642,6 +647,11 @@ export function compareImportOrExportSpecifiers(preferences: UserPreferences, comparer?: Comparer): Comparer { + const stringComparer = comparer ?? getOrganizeImportsOrdinalStringComparer(!!preferences.organizeImportsIgnoreCase); + return (s1, s2) => compareImportOrExportSpecifiers(s1, s2, stringComparer, preferences); +} + /** * Exported for testing * @@ -963,21 +973,24 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr moduleSpecifierComparer: Comparer; namedImportComparer?: Comparer; typeOrder?: "first" | "last" | "inline"; - } = { moduleSpecifierComparer: getOrganizeImportsComparer(preferences, typeof preferences.organizeImportsIgnoreCase === "boolean" ? preferences.organizeImportsIgnoreCase : true) }; + } = { + moduleSpecifierComparer: getOrganizeImportsComparer(preferences, typeof preferences.organizeImportsIgnoreCase === "boolean" ? preferences.organizeImportsIgnoreCase : true), + typeOrder: preferences.organizeImportsTypeOrder, + }; - let comparers: Comparer[]; + let comparersToTest: Comparer[]; if (typeof preferences.organizeImportsIgnoreCase === "boolean") { // both moduleSpecifier and namedImport comparer to the correct case-sensitivity. // does not yet exit because we still need to detect for type order comparer.moduleSpecifierComparer = getOrganizeImportsComparer(preferences, preferences.organizeImportsIgnoreCase); comparer.namedImportComparer = comparer.moduleSpecifierComparer; - comparers = [comparer.moduleSpecifierComparer]; + comparersToTest = [comparer.moduleSpecifierComparer]; } else { // otherwise, we must test for both case-sensitivity and later, type order const CASE_INSENSITIVE_COMPARER = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); const CASE_SENSITIVE_COMPARER = getOrganizeImportsComparer(preferences, /*ignoreCase*/ false); - comparers = [CASE_INSENSITIVE_COMPARER, CASE_SENSITIVE_COMPARER]; + comparersToTest = [CASE_INSENSITIVE_COMPARER, CASE_SENSITIVE_COMPARER]; getModuleSpecifierNames(importDeclsByGroup, comparer, detectCaseSensitivityBySort); } @@ -1018,7 +1031,7 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr const { namedImportComparer, typeOrder } = detectNamedImportOrganizationBySort(namedImportsByDecl); comparer.namedImportComparer = namedImportComparer; - comparer.typeOrder = typeOrder; + comparer.typeOrder = comparer.typeOrder ?? typeOrder; return comparer; @@ -1037,7 +1050,7 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr let bestComparer; let bestDiff = Infinity; - for (const curComparer of comparers) { + for (const curComparer of comparersToTest) { let diffOfCurrentComparer = 0; for (const listToSort of originalGroups) { @@ -1053,7 +1066,7 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr bestComparer = curComparer; } } - return bestComparer ?? comparers[0]; + return bestComparer ?? comparersToTest[0]; } // interface NamedImportByDecl { @@ -1080,9 +1093,9 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr type TypeOrder = "first" | "last" | "inline"; const bestDiff = { first: Infinity, last: Infinity, inline: Infinity }; - const bestComparer = { first: comparers[0], last: comparers[0], inline: comparers[0] }; + const bestComparer = { first: comparersToTest[0], last: comparersToTest[0], inline: comparersToTest[0] }; - for (const curComparer of comparers) { + for (const curComparer of comparersToTest) { const currDiff = { first: 0, last: 0, inline: 0 }; for (const importDecl of originalGroups) { diff --git a/tests/cases/fourslash/organizeImportsType9.ts b/tests/cases/fourslash/organizeImportsType9.ts index 8bb21b6eaa6b7..1bbc26d8c31e3 100644 --- a/tests/cases/fourslash/organizeImportsType9.ts +++ b/tests/cases/fourslash/organizeImportsType9.ts @@ -40,10 +40,9 @@ console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: true }); -// the imports are correctly sorted when ignoreCase=false and typeOrder=first edit.replaceLine(0, 'import { type a, type A, b, B } from "foo5";'); verify.organizeImports( -`import { type A, type a, B, b } from "foo5"; +`import { type A, B, type a, b } from "foo5"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: false }); \ No newline at end of file From 094c60724788f6db793381a7a7d9d1ff31baa441 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Sun, 25 Feb 2024 23:16:56 -0800 Subject: [PATCH 16/33] rename functions and update services/utilities.ts --- src/services/codefixes/importFixes.ts | 5 +- src/services/organizeImports.ts | 56 +++---------------- src/services/utilities.ts | 18 +++--- .../unittests/services/organizeImports.ts | 54 +++++++++--------- 4 files changed, 48 insertions(+), 85 deletions(-) diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 110df9fe4669f..78166241f8f2d 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -1409,8 +1409,7 @@ function promoteFromTypeOnly( if (aliasDeclaration.parent.elements.length > 1) { const newSpecifier = factory.updateImportSpecifier(aliasDeclaration, /*isTypeOnly*/ false, aliasDeclaration.propertyName, aliasDeclaration.name); const moduleSpecifiersByGroup: ImportDeclaration[][] = [[aliasDeclaration.parent.parent.parent]]; - const comparers = OrganizeImports.getDetectionByDiff(moduleSpecifiersByGroup, preferences); - // const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); + const comparers = OrganizeImports.getDetectionBySort(moduleSpecifiersByGroup, preferences); const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, comparers.namedImportComparer ?? comparers.moduleSpecifierComparer, { organizeImportsTypeOrder: comparers.typeOrder }); if (insertionIndex !== aliasDeclaration.parent.elements.indexOf(aliasDeclaration)) { changes.delete(sourceFile, aliasDeclaration); @@ -1523,7 +1522,7 @@ function doAddExistingFix( // ignoreCaseForSorting = targetImportSorting === SortKind.CaseInsensitive; // } const moduleSpecifiersByGroup: ImportDeclaration[][] = [[clause.parent]]; - comparers = OrganizeImports.getDetectionByDiff(moduleSpecifiersByGroup, preferences); + comparers = OrganizeImports.getDetectionBySort(moduleSpecifiersByGroup, preferences); } // if (ignoreCaseForSorting === undefined) { // ignoreCaseForSorting = OrganizeImports.detectSorting(sourceFile, preferences) === SortKind.CaseInsensitive; diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index c759734b2a670..ffa8f891a9fc3 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -105,7 +105,7 @@ export function organizeImports( let detectedTypeOrder : typeof preferences.organizeImportsTypeOrder = preferences.organizeImportsTypeOrder; if (!detectedModuleCaseComparer || !detectedNamedImportCaseComparer || !detectedTypeOrder) { - const { moduleSpecifierComparer, namedImportComparer, typeOrder } = getDetectionByDiff(topLevelImportGroupDecls, preferences) + const { moduleSpecifierComparer, namedImportComparer, typeOrder } = getDetectionBySort(topLevelImportGroupDecls, preferences) detectedModuleCaseComparer = detectedModuleCaseComparer ?? moduleSpecifierComparer ?? DefaultComparer; detectedNamedImportCaseComparer = detectedNamedImportCaseComparer ?? namedImportComparer ?? DefaultComparer; @@ -342,7 +342,7 @@ function getExternalModuleName(specifier: Expression | undefined) { * @deprecated Only used for testing * @internal */ -export function coalesceImports(importGroup: readonly ImportDeclaration[], ignoreCase: boolean, sourceFile?: SourceFile, preferences?: UserPreferences): readonly ImportDeclaration[] { +export function testCoalesceImports(importGroup: readonly ImportDeclaration[], ignoreCase: boolean, sourceFile?: SourceFile, preferences?: UserPreferences): readonly ImportDeclaration[] { const comparer = getOrganizeImportsOrdinalStringComparer(ignoreCase); const specifierComparer = getOrganizeImportsSpecifierComparer({organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder}, comparer); return coalesceImportsWorker(importGroup, comparer, specifierComparer, sourceFile); @@ -539,7 +539,7 @@ function getCategorizedImports(importGroup: readonly ImportDeclaration[]) { * @deprecated Only used for testing * @internal */ -export function coalesceExports(exportGroup: readonly ExportDeclaration[], ignoreCase: boolean, preferences?: UserPreferences) { +export function testCoalesceExports(exportGroup: readonly ExportDeclaration[], ignoreCase: boolean, preferences?: UserPreferences) { const comparer = (s1: ExportSpecifier, s2: ExportSpecifier) => compareImportOrExportSpecifiers(s1, s2, getOrganizeImportsOrdinalStringComparer(ignoreCase), {organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder ?? "last"}); return coalesceExportsWorker(exportGroup, comparer); @@ -966,7 +966,7 @@ function getTopLevelExportGroups(sourceFile: SourceFile) { } /** @internal */ -export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], preferences: UserPreferences) { +export function getDetectionBySort(importDeclsByGroup: ImportDeclaration[][], preferences: UserPreferences) { // attempts to detect an independent sortkind for each of module specifiers, named imports, and named type imports // comparers should be ordered by default priority (case-insensitive first) const comparer: { @@ -992,7 +992,8 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr const CASE_SENSITIVE_COMPARER = getOrganizeImportsComparer(preferences, /*ignoreCase*/ false); comparersToTest = [CASE_INSENSITIVE_COMPARER, CASE_SENSITIVE_COMPARER]; - getModuleSpecifierNames(importDeclsByGroup, comparer, detectCaseSensitivityBySort); + const moduleSpecifiersByGroup = getModuleSpecifierNames(importDeclsByGroup); + comparer.moduleSpecifierComparer = detectCaseSensitivityBySort(moduleSpecifiersByGroup); } // filter for import declarations with named imports. Will be a flat array of import declarations without separations by group @@ -1012,22 +1013,10 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr // no need for more detection if no named imports if (importDeclsWithNamed.length === 0) return comparer; - // TODO combine imports with the same module specifier - // formats the code in order to detect type order const namedImportsByDecl = importDeclsWithNamed.map(importDecl => { return tryCast(importDecl.importClause?.namedBindings, isNamedImports)?.elements; }).filter(elements => elements !== undefined) as any as ImportSpecifier[][]; - // const originalNamedImports = tryCast(importDecl.importClause?.namedBindings, isNamedImports)?.elements.map(n => n.name.text); - // if (!originalNamedImports) return { originalNamedImports: [] }; - // const { regular, type } = groupBy((importDecl.importClause?.namedBindings as NamedImports).elements, s => s.isTypeOnly ? "type" : "regular"); - // const regularImportNames = regular?.map(n => n.name.text); - // const typeImportNames = type?.map(n => n.name.text); - // if (regularImportNames && typeImportNames) { - // bothNamedImports = true; - // } - // return { regularImportNames, typeImportNames, originalNamedImports }; - // }); const { namedImportComparer, typeOrder } = detectNamedImportOrganizationBySort(namedImportsByDecl); comparer.namedImportComparer = namedImportComparer; @@ -1069,26 +1058,11 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr return bestComparer ?? comparersToTest[0]; } - // interface NamedImportByDecl { - // originalNamedImports: string[]; - // regularImportNames?: string[]; - // typeImportNames?: string[]; - // } function detectNamedImportOrganizationBySort(originalGroups: ImportSpecifier[][]): { namedImportComparer: Comparer; typeOrder?: "first" | "last" | "inline"; } { // if we don't have any import statements with both named regular and type imports, we do not need to detect a type ordering if (!bothNamedImports) { return { namedImportComparer: detectCaseSensitivityBySort(originalGroups.map(i => i.map(n =>n.name.text))) }; } - // if (preferences.organizeImportsTypeOrder !== undefined) { - // switch (preferences.organizeImportsTypeOrder) { - // case "first": - // return { namedImportComparer: detectCaseSensitivityBySort(originalGroups.map(i => [i.typeImportNames ?? [], i.regularImportNames ?? []]).flat()), typeOrder: "first" }; - // case "last": - // return { namedImportComparer: detectCaseSensitivityBySort(originalGroups.map(i => [i.typeImportNames ?? [], i.regularImportNames ?? []]).flat()), typeOrder: "last" }; - // case "inline": - // return { namedImportComparer: detectCaseSensitivityBySort(originalGroups.map(i => i.originalNamedImports)), typeOrder: "inline" }; - // } - // } type TypeOrder = "first" | "last" | "inline"; @@ -1099,20 +1073,6 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr const currDiff = { first: 0, last: 0, inline: 0 }; for (const importDecl of originalGroups) { - // if (!regularImportNames || !typeImportNames) { - // const sortedList = sort(originalNamedImports, curComparer) as any as string[]; - // const diff = getSortedMeasure(originalNamedImports, comparer); - // for (const typeOrder in currDiff) { - // currDiff[typeOrder as TypeOrder] += diff; - // } - // continue; - // } - - // ordering - // const sortedRegular = sort(regularImportNames, curComparer); - // const sortedType = sort(typeImportNames, curComparer); - // const sortedInline = mergeAndDeduplicateSorted(sortedRegular, sortedType, curComparer); - currDiff.inline += getSortedMeasure(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, {organizeImportsTypeOrder: "inline"})); currDiff.first += getSortedMeasure(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, {organizeImportsTypeOrder: "first"})); currDiff.last += getSortedMeasure(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, {organizeImportsTypeOrder: "last"})); @@ -1139,12 +1099,12 @@ export function getDetectionByDiff(importDeclsByGroup: ImportDeclaration[][], pr return { namedImportComparer: bestComparer.last, typeOrder: "last" }; } } -function getModuleSpecifierNames(importDeclsByGroup: ImportDeclaration[][], comparer: { moduleSpecifierComparer: Comparer; namedImportComparer?: Comparer | undefined; typeOrder?: "first" | "last" | "inline" | undefined; }, detectCaseSensitivityByDiff: (originalGroups: string[][]) => Comparer) { +function getModuleSpecifierNames(importDeclsByGroup: ImportDeclaration[][]) { const moduleSpecifiersByGroup: string[][] = []; importDeclsByGroup.forEach(importGroup => { // turns importdeclbygroup into string[][] of module specifiers by group to detect sorting on module specifiers moduleSpecifiersByGroup.push(importGroup.map(i => getExternalModuleName(i.moduleSpecifier)!)); }); - comparer.moduleSpecifierComparer = detectCaseSensitivityByDiff(moduleSpecifiersByGroup); + return moduleSpecifiersByGroup; } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index abcf4c264de63..c53dee8a92259 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -4,6 +4,7 @@ import { addSyntheticLeadingComment, addSyntheticTrailingComment, AnyImportOrRequireStatement, + arrayIsSorted, assertType, AssignmentDeclarationKind, BinaryExpression, @@ -326,7 +327,6 @@ import { skipOuterExpressions, skipParentheses, some, - SortKind, SourceFile, SourceFileLike, SourceMapper, @@ -2623,16 +2623,20 @@ export function insertImports(changes: textChanges.ChangeTracker, sourceFile: So const decl = isArray(imports) ? imports[0] : imports; const importKindPredicate: (node: Node) => node is AnyImportOrRequireStatement = decl.kind === SyntaxKind.VariableStatement ? isRequireVariableStatement : isAnyImportSyntax; const existingImportStatements = filter(sourceFile.statements, importKindPredicate); - let sortKind = isArray(imports) ? OrganizeImports.detectImportDeclarationSorting(imports, preferences) : SortKind.Both; - const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); - const sortedNewImports = isArray(imports) ? stableSort(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports]; + //todo remove typecasts + const moduleSpecifiersToDetect: ImportDeclaration[][] = existingImportStatements.length > 1 + ? [existingImportStatements as ImportDeclaration[]] + : isArray(imports) + ? [imports as ImportDeclaration[]] + : [[imports as ImportDeclaration]]; + const comparers = OrganizeImports.getDetectionBySort(moduleSpecifiersToDetect, preferences); + const sortedNewImports = isArray(imports) ? stableSort(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparers.moduleSpecifierComparer)) : [imports]; if (!existingImportStatements.length) { changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); } - else if (existingImportStatements && (sortKind = OrganizeImports.detectImportDeclarationSorting(existingImportStatements, preferences))) { - const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); + else if (existingImportStatements && arrayIsSorted(existingImportStatements, (s1, s2) => OrganizeImports.compareImportsOrRequireStatements(s1, s2, comparers.moduleSpecifierComparer))) { for (const newImport of sortedNewImports) { - const insertionIndex = OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport, comparer); + const insertionIndex = OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport, comparers.moduleSpecifierComparer); if (insertionIndex === 0) { // If the first import is top-of-file, insert after the leading comment which is likely the header. const options = existingImportStatements[0] === sourceFile.statements[0] ? diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index 65d46e23a49de..5ba7acd913ca1 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -52,12 +52,12 @@ describe("unittests:: services:: organizeImports", () => { describe("Coalesce imports", () => { it("No imports", () => { - assert.isEmpty(ts.OrganizeImports.coalesceImports([], /*ignoreCase*/ true)); + assert.isEmpty(ts.OrganizeImports.testCoalesceImports([], /*ignoreCase*/ true)); }); it("Sort specifiers - case-insensitive", () => { const sortedImports = parseImports(`import { default as M, a as n, B, y, Z as O } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports(`import { B, default as M, a as n, Z as O, y } from "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -67,7 +67,7 @@ describe("unittests:: services:: organizeImports", () => { `import "lib";`, `import "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports(`import "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -77,7 +77,7 @@ describe("unittests:: services:: organizeImports", () => { `import * as x from "lib";`, `import * as y from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -87,7 +87,7 @@ describe("unittests:: services:: organizeImports", () => { `import x from "lib";`, `import y from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports(`import { default as x, default as y } from "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -97,7 +97,7 @@ describe("unittests:: services:: organizeImports", () => { `import { x } from "lib";`, `import { y as z } from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports(`import { x, y as z } from "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -107,7 +107,7 @@ describe("unittests:: services:: organizeImports", () => { `import "lib";`, `import * as x from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -117,7 +117,7 @@ describe("unittests:: services:: organizeImports", () => { `import "lib";`, `import x from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -127,7 +127,7 @@ describe("unittests:: services:: organizeImports", () => { `import "lib";`, `import { x } from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -137,7 +137,7 @@ describe("unittests:: services:: organizeImports", () => { `import * as x from "lib";`, `import y from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports( `import y, * as x from "lib";`, ); @@ -149,7 +149,7 @@ describe("unittests:: services:: organizeImports", () => { `import * as x from "lib";`, `import { y } from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -159,7 +159,7 @@ describe("unittests:: services:: organizeImports", () => { `import x from "lib";`, `import { y } from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports( `import x, { y } from "lib";`, ); @@ -177,7 +177,7 @@ describe("unittests:: services:: organizeImports", () => { `import z from "lib";`, `import { a } from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports( `import "lib";`, `import * as x from "lib";`, @@ -194,7 +194,7 @@ describe("unittests:: services:: organizeImports", () => { `import * as y from "lib";`, `import z from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = sortedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -205,7 +205,7 @@ describe("unittests:: services:: organizeImports", () => { `import type { y } from "lib";`, `import { z } from "lib";`, ); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = parseImports( `import { z } from "lib";`, `import type { x, y } from "lib";`, @@ -221,7 +221,7 @@ describe("unittests:: services:: organizeImports", () => { ); // Default import could be rewritten as a named import to combine with `x`, // but seems of debatable merit. - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true); const expectedCoalescedImports = actualCoalescedImports; assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -229,19 +229,19 @@ describe("unittests:: services:: organizeImports", () => { describe("Coalesce exports", () => { it("No exports", () => { - assert.isEmpty(ts.OrganizeImports.coalesceExports([], /*ignoreCase*/ true)); + assert.isEmpty(ts.OrganizeImports.testCoalesceExports([], /*ignoreCase*/ true)); }); it("Sort specifiers - case-insensitive", () => { const sortedExports = parseExports(`export { default as M, a as n, B, y, Z as O } from "lib";`); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports(`export { B, default as M, a as n, Z as O, y } from "lib";`); assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); it("Sort specifiers - type-only-inline", () => { const sortedImports = parseImports(`import { type z, y, type x, c, type b, a } from "lib";`); - const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true, ts.getSourceFileOfNode(sortedImports[0]), { organizeImportsTypeOrder: "inline" }); + const actualCoalescedImports = ts.OrganizeImports.testCoalesceImports(sortedImports, /*ignoreCase*/ true, ts.getSourceFileOfNode(sortedImports[0]), { organizeImportsTypeOrder: "inline" }); const expectedCoalescedImports = parseImports(`import { a, type b, c, type x, y, type z } from "lib";`); assertListEqual(actualCoalescedImports, expectedCoalescedImports); }); @@ -251,7 +251,7 @@ describe("unittests:: services:: organizeImports", () => { `export * from "lib";`, `export * from "lib";`, ); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports(`export * from "lib";`); assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -261,7 +261,7 @@ describe("unittests:: services:: organizeImports", () => { `export { x };`, `export { y as z };`, ); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports(`export { x, y as z };`); assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -271,7 +271,7 @@ describe("unittests:: services:: organizeImports", () => { `export { x } from "lib";`, `export { y as z } from "lib";`, ); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports(`export { x, y as z } from "lib";`); assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -281,7 +281,7 @@ describe("unittests:: services:: organizeImports", () => { `export * from "lib";`, `export { y } from "lib";`, ); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = sortedExports; assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -292,7 +292,7 @@ describe("unittests:: services:: organizeImports", () => { `export { y as w, z as default };`, `export { w as q };`, ); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports( `export { z as default, w as q, y as w, x };`, ); @@ -305,7 +305,7 @@ describe("unittests:: services:: organizeImports", () => { `export * from "lib";`, `export { z as b } from "lib";`, ); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports( `export * from "lib";`, `export { x as a, z as b, y } from "lib";`, @@ -318,7 +318,7 @@ describe("unittests:: services:: organizeImports", () => { `export { x };`, `export type { y };`, ); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = sortedExports; assertListEqual(actualCoalescedExports, expectedCoalescedExports); }); @@ -328,7 +328,7 @@ describe("unittests:: services:: organizeImports", () => { `export type { x };`, `export type { y };`, ); - const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true); + const actualCoalescedExports = ts.OrganizeImports.testCoalesceExports(sortedExports, /*ignoreCase*/ true); const expectedCoalescedExports = parseExports( `export type { x, y };`, ); From 7f114216f4c5ea5ba2692b7b284f31f59fff0848 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Mon, 26 Feb 2024 01:44:26 -0800 Subject: [PATCH 17/33] separate detection into module and named --- src/services/organizeImports.ts | 191 +++++++++--------- src/services/utilities.ts | 2 +- tests/cases/fourslash/organizeImportsType5.ts | 28 +-- tests/cases/fourslash/organizeImportsType8.ts | 2 +- tests/cases/fourslash/organizeImportsType9.ts | 2 +- 5 files changed, 113 insertions(+), 112 deletions(-) diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index ffa8f891a9fc3..4675a9bdf1bbb 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -96,16 +96,16 @@ export function organizeImports( // All of the old ImportDeclarations in the file, in syntactic order. const topLevelImportGroupDecls = groupByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration)); - const DefaultComparer = getOrganizeImportsComparer(preferences, /*ignoreCase*/ typeof preferences.organizeImportsIgnoreCase === "boolean" ? preferences.organizeImportsIgnoreCase : true); + const DefaultComparer = getOrganizeImportsComparer(preferences, /*ignoreCase*/ typeof preferences.organizeImportsIgnoreCase === "boolean" ? preferences.organizeImportsIgnoreCase : true); // if case sensitivity is specified (true/false), then use the same setting for both. let detectedModuleCaseComparer: Comparer | undefined = typeof preferences.organizeImportsIgnoreCase === "boolean" ? DefaultComparer : undefined; let detectedNamedImportCaseComparer: Comparer | undefined = detectedModuleCaseComparer; - let detectedTypeOrder : typeof preferences.organizeImportsTypeOrder = preferences.organizeImportsTypeOrder; + let detectedTypeOrder: typeof preferences.organizeImportsTypeOrder = preferences.organizeImportsTypeOrder; if (!detectedModuleCaseComparer || !detectedNamedImportCaseComparer || !detectedTypeOrder) { - const { moduleSpecifierComparer, namedImportComparer, typeOrder } = getDetectionBySort(topLevelImportGroupDecls, preferences) + const { moduleSpecifierComparer, namedImportComparer, typeOrder } = getDetectionBySort(topLevelImportGroupDecls, preferences); detectedModuleCaseComparer = detectedModuleCaseComparer ?? moduleSpecifierComparer ?? DefaultComparer; detectedNamedImportCaseComparer = detectedNamedImportCaseComparer ?? namedImportComparer ?? DefaultComparer; @@ -187,7 +187,7 @@ export function organizeImports( } function organizeImportsWorker(oldImportDecls: readonly ImportDeclaration[]) { - const specifierComparer = getOrganizeImportsSpecifierComparer({organizeImportsTypeOrder: detectedTypeOrder ?? preferences.organizeImportsTypeOrder}, detectedNamedImportCaseComparer); + const specifierComparer = getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: detectedTypeOrder ?? preferences.organizeImportsTypeOrder }, detectedNamedImportCaseComparer); const processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => { if (shouldRemove) importGroup = removeUnusedImports(importGroup, sourceFile, program); if (shouldCombine) importGroup = coalesceImportsWorker(importGroup, detectedModuleCaseComparer ?? DefaultComparer, specifierComparer, sourceFile); @@ -201,7 +201,7 @@ export function organizeImports( function organizeExportsWorker(oldExportDecls: readonly ExportDeclaration[], specifierCaseComparer?: Comparer) { const useComparer = getOrganizeImportsSpecifierComparer(preferences, specifierCaseComparer); - organizeDeclsWorker(oldExportDecls, group => coalesceExportsWorker(group, useComparer)) + organizeDeclsWorker(oldExportDecls, group => coalesceExportsWorker(group, useComparer)); } } @@ -344,7 +344,7 @@ function getExternalModuleName(specifier: Expression | undefined) { */ export function testCoalesceImports(importGroup: readonly ImportDeclaration[], ignoreCase: boolean, sourceFile?: SourceFile, preferences?: UserPreferences): readonly ImportDeclaration[] { const comparer = getOrganizeImportsOrdinalStringComparer(ignoreCase); - const specifierComparer = getOrganizeImportsSpecifierComparer({organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder}, comparer); + const specifierComparer = getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder }, comparer); return coalesceImportsWorker(importGroup, comparer, specifierComparer, sourceFile); } @@ -540,8 +540,7 @@ function getCategorizedImports(importGroup: readonly ImportDeclaration[]) { * @internal */ export function testCoalesceExports(exportGroup: readonly ExportDeclaration[], ignoreCase: boolean, preferences?: UserPreferences) { - const comparer = (s1: ExportSpecifier, s2: ExportSpecifier) => - compareImportOrExportSpecifiers(s1, s2, getOrganizeImportsOrdinalStringComparer(ignoreCase), {organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder ?? "last"}); + const comparer = (s1: ExportSpecifier, s2: ExportSpecifier) => compareImportOrExportSpecifiers(s1, s2, getOrganizeImportsOrdinalStringComparer(ignoreCase), { organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder ?? "last" }); return coalesceExportsWorker(exportGroup, comparer); } @@ -991,120 +990,122 @@ export function getDetectionBySort(importDeclsByGroup: ImportDeclaration[][], pr const CASE_INSENSITIVE_COMPARER = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); const CASE_SENSITIVE_COMPARER = getOrganizeImportsComparer(preferences, /*ignoreCase*/ false); comparersToTest = [CASE_INSENSITIVE_COMPARER, CASE_SENSITIVE_COMPARER]; + comparer.moduleSpecifierComparer = detectModuleSpecifierCaseBySort(importDeclsByGroup, comparersToTest); + } - const moduleSpecifiersByGroup = getModuleSpecifierNames(importDeclsByGroup); - comparer.moduleSpecifierComparer = detectCaseSensitivityBySort(moduleSpecifiersByGroup); + const typesToTest: TypeOrder[] = preferences.organizeImportsTypeOrder ? [preferences.organizeImportsTypeOrder] : ["first", "last", "inline"]; + const namedImportSort = detectNamedImportOrganizationBySort(importDeclsByGroup.flat(), comparersToTest, typesToTest); + if (!namedImportSort) { + return comparer; } + const { namedImportComparer, typeOrder } = namedImportSort; + comparer.namedImportComparer = namedImportComparer; + comparer.typeOrder = comparer.typeOrder ?? typeOrder; + + return comparer; +} + +function detectModuleSpecifierCaseBySort(importDeclsByGroup: ImportDeclaration[][], comparersToTest: Comparer[]) { + const moduleSpecifiersByGroup: string[][] = []; + importDeclsByGroup.forEach(importGroup => { + // turns importdeclbygroup into string[][] of module specifiers by group to detect sorting on module specifiers + moduleSpecifiersByGroup.push(importGroup.map(i => getExternalModuleName(i.moduleSpecifier)!)); + }); + return detectCaseSensitivityBySort(moduleSpecifiersByGroup, comparersToTest); +} +type TypeOrder = "first" | "last" | "inline"; +function detectNamedImportOrganizationBySort(originalGroups: ImportDeclaration[], comparersToTest: Comparer[], typesToTest: TypeOrder[]): { namedImportComparer: Comparer; typeOrder?: "first" | "last" | "inline"; } | undefined { // filter for import declarations with named imports. Will be a flat array of import declarations without separations by group let bothNamedImports = false; - const importDeclsWithNamed = importDeclsByGroup.map(importGroup => - importGroup.filter(i => { - const namedImports = tryCast(i.importClause?.namedBindings, isNamedImports)?.elements; - if (!namedImports?.length) return false; - if (!bothNamedImports && namedImports.some(n => n.isTypeOnly) && namedImports.some(n => !n.isTypeOnly)) { - //todo:improve check - bothNamedImports = true; - } - return true; - }) - ).flat(); + const importDeclsWithNamed = originalGroups.filter(i => { + const namedImports = tryCast(i.importClause?.namedBindings, isNamedImports)?.elements; + if (!namedImports?.length) return false; + if (!bothNamedImports && namedImports.some(n => n.isTypeOnly) && namedImports.some(n => !n.isTypeOnly)) { + // todo:improve check + bothNamedImports = true; + } + return true; + }); // no need for more detection if no named imports - if (importDeclsWithNamed.length === 0) return comparer; + if (importDeclsWithNamed.length === 0) return; - // formats the code in order to detect type order + // formats the code into lists of named imports, grouped by declaration const namedImportsByDecl = importDeclsWithNamed.map(importDecl => { return tryCast(importDecl.importClause?.namedBindings, isNamedImports)?.elements; }).filter(elements => elements !== undefined) as any as ImportSpecifier[][]; - const { namedImportComparer, typeOrder } = detectNamedImportOrganizationBySort(namedImportsByDecl); - comparer.namedImportComparer = namedImportComparer; - comparer.typeOrder = comparer.typeOrder ?? typeOrder; - - return comparer; - - function getSortedMeasure(arr: readonly T[], comparer: Comparer) { - let i = 0; - for (let j = 0; j < arr.length-1; j++) { - if (comparer(arr[j], arr[j+1]) > 0) { - i++; - } - } - return i; + // if we don't have any import statements with both named regular and type imports, we do not need to detect a type ordering + if (!bothNamedImports || typesToTest.length === 0) { + return { namedImportComparer: detectCaseSensitivityBySort(namedImportsByDecl.map(i => i.map(n => n.name.text)), comparersToTest) }; } - function detectCaseSensitivityBySort(originalGroups: string[][]): Comparer { - // each entry in originalGroups will be sorted and compared against the original entry. - // the total diff of each comparison is the sum of the diffs of all groups - let bestComparer; - let bestDiff = Infinity; - for (const curComparer of comparersToTest) { - let diffOfCurrentComparer = 0; + const bestDiff = { first: Infinity, last: Infinity, inline: Infinity }; + const bestComparer = { first: comparersToTest[0], last: comparersToTest[0], inline: comparersToTest[0] }; - for (const listToSort of originalGroups) { - if (listToSort.length <= 1) continue; - - // const sortedList = sort(listToSort, curComparer) as any as string[]; - const diff = getSortedMeasure(listToSort, curComparer); - diffOfCurrentComparer += diff; + for (const curComparer of comparersToTest) { + const currDiff = { first: 0, last: 0, inline: 0 }; + for (const importDecl of namedImportsByDecl) { + for (const typeOrder of typesToTest) { + currDiff[typeOrder] = (currDiff[typeOrder] || 0) + getSortedMeasure(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, { organizeImportsTypeOrder: typeOrder })); } - - if (diffOfCurrentComparer < bestDiff) { - bestDiff = diffOfCurrentComparer; - bestComparer = curComparer; + } + for (const key of typesToTest) { + const typeOrder = key as TypeOrder; + if (currDiff[typeOrder] < bestDiff[typeOrder]) { + bestDiff[typeOrder] = currDiff[typeOrder]; + bestComparer[typeOrder] = curComparer; } } - return bestComparer ?? comparersToTest[0]; } - function detectNamedImportOrganizationBySort(originalGroups: ImportSpecifier[][]): { namedImportComparer: Comparer; typeOrder?: "first" | "last" | "inline"; } { - // if we don't have any import statements with both named regular and type imports, we do not need to detect a type ordering - if (!bothNamedImports) { - return { namedImportComparer: detectCaseSensitivityBySort(originalGroups.map(i => i.map(n =>n.name.text))) }; + outer: for (const bestKey of typesToTest) { + const bestTypeOrder = bestKey as TypeOrder; + for (const testKey of typesToTest) { + const testTypeOrder = testKey as TypeOrder; + if (bestDiff[testTypeOrder] < bestDiff[bestTypeOrder]) continue outer; } + return { namedImportComparer: bestComparer[bestTypeOrder], typeOrder: bestTypeOrder }; + } - type TypeOrder = "first" | "last" | "inline"; + // default; hopefully never hit..... + return { namedImportComparer: bestComparer.last, typeOrder: "last" }; +} - const bestDiff = { first: Infinity, last: Infinity, inline: Infinity }; - const bestComparer = { first: comparersToTest[0], last: comparersToTest[0], inline: comparersToTest[0] }; +function getSortedMeasure(arr: readonly T[], comparer: Comparer) { + let i = 0; + for (let j = 0; j < arr.length - 1; j++) { + if (comparer(arr[j], arr[j + 1]) > 0) { + i++; + } + } + return i; +} +function detectCaseSensitivityBySort(originalGroups: string[][], comparersToTest: Comparer[]): Comparer { + // each entry in originalGroups will be sorted and compared against the original entry. + // the total diff of each comparison is the sum of the diffs of all groups + let bestComparer; + let bestDiff = Infinity; + + if (comparersToTest.length === 1) { + return comparersToTest[0]; + } + for (const curComparer of comparersToTest) { + let diffOfCurrentComparer = 0; - for (const curComparer of comparersToTest) { - const currDiff = { first: 0, last: 0, inline: 0 }; + for (const listToSort of originalGroups) { + if (listToSort.length <= 1) continue; - for (const importDecl of originalGroups) { - currDiff.inline += getSortedMeasure(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, {organizeImportsTypeOrder: "inline"})); - currDiff.first += getSortedMeasure(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, {organizeImportsTypeOrder: "first"})); - currDiff.last += getSortedMeasure(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, {organizeImportsTypeOrder: "last"})); - } - for (const key in currDiff) { - const typeOrder = key as TypeOrder; - if (currDiff[typeOrder] < bestDiff[typeOrder]) { - bestDiff[typeOrder] = currDiff[typeOrder]; - bestComparer[typeOrder] = curComparer; - } - } + // const sortedList = sort(listToSort, curComparer) as any as string[]; + const diff = getSortedMeasure(listToSort, curComparer); + diffOfCurrentComparer += diff; } - if (bestDiff.inline <= bestDiff.first && bestDiff.inline <= bestDiff.last) { - return { namedImportComparer: bestComparer.inline, typeOrder: "inline" }; - } - if (bestDiff.last <= bestDiff.first && bestDiff.last <= bestDiff.inline) { - return { namedImportComparer: bestComparer.last, typeOrder: "last" }; + if (diffOfCurrentComparer < bestDiff) { + bestDiff = diffOfCurrentComparer; + bestComparer = curComparer; } - if (bestDiff.first <= bestDiff.inline && bestDiff.first <= bestDiff.last) { - return { namedImportComparer: bestComparer.first, typeOrder: "first" }; - } - // hopefully never hit..... - return { namedImportComparer: bestComparer.last, typeOrder: "last" }; } + return bestComparer ?? comparersToTest[0]; } -function getModuleSpecifierNames(importDeclsByGroup: ImportDeclaration[][]) { - const moduleSpecifiersByGroup: string[][] = []; - importDeclsByGroup.forEach(importGroup => { - // turns importdeclbygroup into string[][] of module specifiers by group to detect sorting on module specifiers - moduleSpecifiersByGroup.push(importGroup.map(i => getExternalModuleName(i.moduleSpecifier)!)); - }); - return moduleSpecifiersByGroup; -} - diff --git a/src/services/utilities.ts b/src/services/utilities.ts index c53dee8a92259..01e57450655bb 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -2623,7 +2623,7 @@ export function insertImports(changes: textChanges.ChangeTracker, sourceFile: So const decl = isArray(imports) ? imports[0] : imports; const importKindPredicate: (node: Node) => node is AnyImportOrRequireStatement = decl.kind === SyntaxKind.VariableStatement ? isRequireVariableStatement : isAnyImportSyntax; const existingImportStatements = filter(sourceFile.statements, importKindPredicate); - //todo remove typecasts + // todo remove typecasts const moduleSpecifiersToDetect: ImportDeclaration[][] = existingImportStatements.length > 1 ? [existingImportStatements as ImportDeclaration[]] : isArray(imports) diff --git a/tests/cases/fourslash/organizeImportsType5.ts b/tests/cases/fourslash/organizeImportsType5.ts index a7cbec7b9ab13..698577ac2cd4a 100644 --- a/tests/cases/fourslash/organizeImportsType5.ts +++ b/tests/cases/fourslash/organizeImportsType5.ts @@ -12,20 +12,20 @@ //// } from './foo'; //// console.log(A, a, B, b, c, C, d, D); -// verify.organizeImports(`import { -// type A, -// a, -// b, -// b as B, -// type c, -// c as C, -// d, -// type d as D -// } from './foo'; -// console.log(A, a, B, b, c, C, d, D);`, -// /*mode*/ undefined, -// { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "inline" } -// ); +verify.organizeImports(`import { + type A, + a, + b, + b as B, + type c, + c as C, + d, + type d as D +} from './foo'; +console.log(A, a, B, b, c, C, d, D);`, + /*mode*/ undefined, + { organizeImportsIgnoreCase: "auto", organizeImportsTypeOrder: "inline" } +); verify.organizeImports(`import { type A, diff --git a/tests/cases/fourslash/organizeImportsType8.ts b/tests/cases/fourslash/organizeImportsType8.ts index 2c63d54cdb38e..50b02580e94fd 100644 --- a/tests/cases/fourslash/organizeImportsType8.ts +++ b/tests/cases/fourslash/organizeImportsType8.ts @@ -44,7 +44,7 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type A, type a, b, B } from "foo5";'); verify.organizeImports( -`import { type A, B, type a, b } from "foo5"; +`import { type A, type a, B, b } from "foo5"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: false } diff --git a/tests/cases/fourslash/organizeImportsType9.ts b/tests/cases/fourslash/organizeImportsType9.ts index 1bbc26d8c31e3..5db33ccc2a444 100644 --- a/tests/cases/fourslash/organizeImportsType9.ts +++ b/tests/cases/fourslash/organizeImportsType9.ts @@ -42,7 +42,7 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type a, type A, b, B } from "foo5";'); verify.organizeImports( -`import { type A, B, type a, b } from "foo5"; +`import { type A, type a, B, b } from "foo5"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: false }); \ No newline at end of file From ab4c78ca08d0d05604a5fb1b39fd4ef50ea7b1e9 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Mon, 26 Feb 2024 03:44:08 -0800 Subject: [PATCH 18/33] removed old detection from importFixes and services/utilities --- src/services/codefixes/importFixes.ts | 64 +++++---- src/services/organizeImports.ts | 122 ++++++++---------- src/services/utilities.ts | 8 +- .../cases/fourslash/autoImportTypeImport4.ts | 14 +- 4 files changed, 99 insertions(+), 109 deletions(-) diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 78166241f8f2d..c256e2f9a4ca6 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -66,6 +66,7 @@ import { ImportKind, importNameElisionDisabled, ImportsNotUsedAsValues, + ImportSpecifier, insertImports, InternalSymbolName, isExternalModule, @@ -1408,9 +1409,19 @@ function promoteFromTypeOnly( if (aliasDeclaration.isTypeOnly) { if (aliasDeclaration.parent.elements.length > 1) { const newSpecifier = factory.updateImportSpecifier(aliasDeclaration, /*isTypeOnly*/ false, aliasDeclaration.propertyName, aliasDeclaration.name); - const moduleSpecifiersByGroup: ImportDeclaration[][] = [[aliasDeclaration.parent.parent.parent]]; - const comparers = OrganizeImports.getDetectionBySort(moduleSpecifiersByGroup, preferences); - const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, comparers.namedImportComparer ?? comparers.moduleSpecifierComparer, { organizeImportsTypeOrder: comparers.typeOrder }); + const { comparersToTest, typeOrdersToTest } = OrganizeImports.getOrdersToDetect(preferences); + let namedImportComparer = comparersToTest[0]; + let typeOrder: OrganizeImports.TypeOrder | undefined = typeOrdersToTest[0]; + + if (typeof preferences.organizeImportsIgnoreCase !== "boolean" || !preferences.organizeImportsTypeOrder) { + const importDecl: ImportDeclaration[] = [aliasDeclaration.parent.parent.parent]; + const namedImportSort = OrganizeImports.detectNamedImportOrganizationBySort(importDecl, comparersToTest, typeOrdersToTest); + if (namedImportSort) { + ({ namedImportComparer, typeOrder } = namedImportSort); + } + } + + const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, namedImportComparer, { organizeImportsTypeOrder: typeOrder }); if (insertionIndex !== aliasDeclaration.parent.elements.indexOf(aliasDeclaration)) { changes.delete(sourceFile, aliasDeclaration); changes.insertImportSpecifierAtIndex(sourceFile, newSpecifier, aliasDeclaration.parent, insertionIndex); @@ -1504,37 +1515,20 @@ function doAddExistingFix( // - otherwise, if there are enough existing import specifiers in this import to detect unambiguously, use that // - otherwise, detect from other imports in the file // let ignoreCaseForSorting: boolean | undefined; - let comparers: { - moduleSpecifierComparer: Comparer; - namedImportComparer?: Comparer; - typeOrder?: "first" | "last" | "inline"; - } = { moduleSpecifierComparer: OrganizeImports.getOrganizeImportsComparer(preferences, typeof preferences.organizeImportsIgnoreCase === "boolean" ? preferences.organizeImportsIgnoreCase : true) }; - - if (typeof preferences.organizeImportsIgnoreCase === "boolean") { - comparers.namedImportComparer = comparers.moduleSpecifierComparer; - } - if (preferences.organizeImportsTypeOrder) { - comparers.typeOrder = preferences.organizeImportsTypeOrder; - } - else if (existingSpecifiers && typeof preferences.organizeImportsIgnoreCase !== "boolean" && !preferences.organizeImportsTypeOrder) { - // const targetImportSorting = OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences); - // if (targetImportSorting !== SortKind.Both) { - // ignoreCaseForSorting = targetImportSorting === SortKind.CaseInsensitive; - // } - const moduleSpecifiersByGroup: ImportDeclaration[][] = [[clause.parent]]; - comparers = OrganizeImports.getDetectionBySort(moduleSpecifiersByGroup, preferences); + let specifierComparer: Comparer = OrganizeImports.getOrganizeImportsSpecifierComparer(preferences); + const { comparersToTest, typeOrdersToTest } = OrganizeImports.getOrdersToDetect(preferences); + let namedImportComparer = comparersToTest[0]; + let typeOrder: OrganizeImports.TypeOrder | undefined = typeOrdersToTest[0]; + + if (existingSpecifiers && (typeof preferences.organizeImportsIgnoreCase !== "boolean" || !preferences.organizeImportsTypeOrder)) { + const moduleSpecifiersDecl: ImportDeclaration[] = [clause.parent]; + const namedImportSort = OrganizeImports.detectNamedImportOrganizationBySort(moduleSpecifiersDecl, comparersToTest, typeOrdersToTest); + if (namedImportSort) { + ({ namedImportComparer, typeOrder } = namedImportSort); + specifierComparer = OrganizeImports.getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); + } } - // if (ignoreCaseForSorting === undefined) { - // ignoreCaseForSorting = OrganizeImports.detectSorting(sourceFile, preferences) === SortKind.CaseInsensitive; - // } - - // const newSpecifier = factory.updateImportSpecifier(aliasDeclaration, /*isTypeOnly*/ false, aliasDeclaration.propertyName, aliasDeclaration.name); - // const moduleSpecifiersByGroup: ImportDeclaration[][] = [[aliasDeclaration.parent.parent.parent]]; - // const comparers = OrganizeImports.getDetectionByDiff(moduleSpecifiersByGroup, preferences); - // // const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive); - // const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, comparers.namedImportComparer ?? comparers.moduleSpecifierComparer, { organizeImportsTypeOrder: comparers.typeOrder }); - // const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, ignoreCaseForSorting); const newSpecifiers = stableSort( namedImports.map(namedImport => factory.createImportSpecifier( @@ -1543,7 +1537,7 @@ function doAddExistingFix( factory.createIdentifier(namedImport.name), ) ), - (s1, s2) => OrganizeImports.compareImportOrExportSpecifiers(s1, s2, comparers.namedImportComparer ?? comparers.moduleSpecifierComparer, { organizeImportsTypeOrder: comparers.typeOrder }), + specifierComparer, ); // The sorting preference computed earlier may or may not have validated that these particular @@ -1554,9 +1548,9 @@ function doAddExistingFix( // const specifierSort = existingSpecifiers?.length && OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences); // changed to check if existing specifiers are sorted - if (existingSpecifiers?.length && arrayIsSorted(existingSpecifiers, (s1, s2) => OrganizeImports.compareImportOrExportSpecifiers(s1, s2, comparers.namedImportComparer ?? comparers.moduleSpecifierComparer, { organizeImportsTypeOrder: comparers.typeOrder }))) { + if (existingSpecifiers?.length && arrayIsSorted(existingSpecifiers, specifierComparer)) { for (const spec of newSpecifiers) { - const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, comparers.namedImportComparer ?? comparers.moduleSpecifierComparer, preferences); + const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, namedImportComparer, { organizeImportsTypeOrder: typeOrder }); changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex); } } diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index 4675a9bdf1bbb..8049bb2bd7680 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -93,44 +93,57 @@ export function organizeImports( // shouldCombine vs shouldSort is not supported in diff-detection, diff-detection always assumes sorting will combine/deduplicate const shouldCombine = shouldSort; const shouldRemove = mode === OrganizeImportsMode.RemoveUnused || mode === OrganizeImportsMode.All; + // All of the old ImportDeclarations in the file, in syntactic order. - const topLevelImportGroupDecls = groupByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration)); + const topLevelImportDecls = sourceFile.statements.filter(isImportDeclaration); + const topLevelImportGroupDecls = groupByNewlineContiguous(sourceFile, topLevelImportDecls); const DefaultComparer = getOrganizeImportsComparer(preferences, /*ignoreCase*/ typeof preferences.organizeImportsIgnoreCase === "boolean" ? preferences.organizeImportsIgnoreCase : true); - // if case sensitivity is specified (true/false), then use the same setting for both. - let detectedModuleCaseComparer: Comparer | undefined = typeof preferences.organizeImportsIgnoreCase === "boolean" ? DefaultComparer : undefined; - let detectedNamedImportCaseComparer: Comparer | undefined = detectedModuleCaseComparer; - - let detectedTypeOrder: typeof preferences.organizeImportsTypeOrder = preferences.organizeImportsTypeOrder; + const comparer = { + moduleSpecifierComparer: typeof preferences.organizeImportsIgnoreCase === "boolean" ? DefaultComparer : undefined, + namedImportComparer: typeof preferences.organizeImportsIgnoreCase === "boolean" ? DefaultComparer : undefined, + typeOrder: preferences.organizeImportsTypeOrder, + }; - if (!detectedModuleCaseComparer || !detectedNamedImportCaseComparer || !detectedTypeOrder) { - const { moduleSpecifierComparer, namedImportComparer, typeOrder } = getDetectionBySort(topLevelImportGroupDecls, preferences); + const {comparersToTest, typeOrdersToTest} = getOrdersToDetect(preferences); + if (typeof preferences.organizeImportsIgnoreCase === "boolean") { + // if case sensitivity is specified (true/false), then use the same setting for both. - detectedModuleCaseComparer = detectedModuleCaseComparer ?? moduleSpecifierComparer ?? DefaultComparer; - detectedNamedImportCaseComparer = detectedNamedImportCaseComparer ?? namedImportComparer ?? DefaultComparer; - detectedTypeOrder = detectedTypeOrder ?? typeOrder; - // TODO return unset comparer + // both moduleSpecifier and namedImport comparer to the correct case-sensitivity. + // does not yet exit because we still need to detect for type order + comparer.moduleSpecifierComparer = getOrganizeImportsComparer(preferences, preferences.organizeImportsIgnoreCase); + comparer.namedImportComparer = comparer.moduleSpecifierComparer; + } + else { + // otherwise, we must test for both case-sensitivity and later, type order + comparer.moduleSpecifierComparer = detectModuleSpecifierCaseBySort(topLevelImportGroupDecls, comparersToTest); } - topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl)); + const namedImportSort = detectNamedImportOrganizationBySort(topLevelImportDecls, comparersToTest, typeOrdersToTest); + if (namedImportSort) { + const { namedImportComparer, typeOrder } = namedImportSort; + comparer.namedImportComparer = namedImportComparer; + comparer.typeOrder = comparer.typeOrder ?? typeOrder; + } + topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, comparer)); // Exports are always used if (mode !== OrganizeImportsMode.RemoveUnused) { // All of the old ExportDeclarations in the file, in syntactic order. - getTopLevelExportGroups(sourceFile).forEach(exportGroupDecl => organizeExportsWorker(exportGroupDecl, detectedNamedImportCaseComparer)); + getTopLevelExportGroups(sourceFile).forEach(exportGroupDecl => organizeExportsWorker(exportGroupDecl, comparer.namedImportComparer)); } for (const ambientModule of sourceFile.statements.filter(isAmbientModule)) { if (!ambientModule.body) continue; const ambientModuleImportGroupDecls = groupByNewlineContiguous(sourceFile, ambientModule.body.statements.filter(isImportDeclaration)); - ambientModuleImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl)); + ambientModuleImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, comparer)); // Exports are always used if (mode !== OrganizeImportsMode.RemoveUnused) { const ambientModuleExportDecls = ambientModule.body.statements.filter(isExportDeclaration); - organizeExportsWorker(ambientModuleExportDecls, detectedNamedImportCaseComparer); + organizeExportsWorker(ambientModuleExportDecls, comparer.namedImportComparer); } } @@ -155,7 +168,7 @@ export function organizeImports( ? group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier)!) : [oldImportDecls]; const sortedImportGroups = shouldSort - ? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiersWorker(group1[0].moduleSpecifier, group2[0].moduleSpecifier, detectedModuleCaseComparer ?? DefaultComparer)) + ? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiersWorker(group1[0].moduleSpecifier, group2[0].moduleSpecifier, comparer.moduleSpecifierComparer ?? DefaultComparer)) : oldImportGroups; const newImportDecls = flatMap(sortedImportGroups, importGroup => getExternalModuleName(importGroup[0].moduleSpecifier) || importGroup[0].moduleSpecifier === undefined @@ -186,17 +199,20 @@ export function organizeImports( } } - function organizeImportsWorker(oldImportDecls: readonly ImportDeclaration[]) { - const specifierComparer = getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: detectedTypeOrder ?? preferences.organizeImportsTypeOrder }, detectedNamedImportCaseComparer); + function organizeImportsWorker(oldImportDecls: readonly ImportDeclaration[], comparer: { moduleSpecifierComparer?: Comparer; namedImportComparer?: Comparer; typeOrder?: TypeOrder; }) { + const detectedModuleCaseComparer = comparer.moduleSpecifierComparer ?? DefaultComparer; + const detectedNamedImportCaseComparer = comparer.namedImportComparer ?? DefaultComparer; + const detectedTypeOrder = comparer.typeOrder ?? "last"; + + const specifierComparer = getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: detectedTypeOrder }, detectedNamedImportCaseComparer); const processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => { if (shouldRemove) importGroup = removeUnusedImports(importGroup, sourceFile, program); - if (shouldCombine) importGroup = coalesceImportsWorker(importGroup, detectedModuleCaseComparer ?? DefaultComparer, specifierComparer, sourceFile); - if (shouldSort) importGroup = stableSort(importGroup, (s1, s2) => compareImportsOrRequireStatements(s1, s2, detectedModuleCaseComparer ?? DefaultComparer)); + if (shouldCombine) importGroup = coalesceImportsWorker(importGroup, detectedModuleCaseComparer, specifierComparer, sourceFile); + if (shouldSort) importGroup = stableSort(importGroup, (s1, s2) => compareImportsOrRequireStatements(s1, s2, detectedModuleCaseComparer)); return importGroup; }; organizeDeclsWorker(oldImportDecls, processImportsOfSameModuleSpecifier); - // return { moduleSpecifierComparer, namedImportComparer, typeOrder }; } function organizeExportsWorker(oldExportDecls: readonly ExportDeclaration[], specifierCaseComparer?: Comparer) { @@ -646,7 +662,8 @@ export function compareImportOrExportSpecifiers(preferences: UserPreferences, comparer?: Comparer): Comparer { +/** @internal */ +export function getOrganizeImportsSpecifierComparer(preferences: UserPreferences, comparer?: Comparer): Comparer { const stringComparer = comparer ?? getOrganizeImportsOrdinalStringComparer(!!preferences.organizeImportsIgnoreCase); return (s1, s2) => compareImportOrExportSpecifiers(s1, s2, stringComparer, preferences); } @@ -965,47 +982,7 @@ function getTopLevelExportGroups(sourceFile: SourceFile) { } /** @internal */ -export function getDetectionBySort(importDeclsByGroup: ImportDeclaration[][], preferences: UserPreferences) { - // attempts to detect an independent sortkind for each of module specifiers, named imports, and named type imports - // comparers should be ordered by default priority (case-insensitive first) - const comparer: { - moduleSpecifierComparer: Comparer; - namedImportComparer?: Comparer; - typeOrder?: "first" | "last" | "inline"; - } = { - moduleSpecifierComparer: getOrganizeImportsComparer(preferences, typeof preferences.organizeImportsIgnoreCase === "boolean" ? preferences.organizeImportsIgnoreCase : true), - typeOrder: preferences.organizeImportsTypeOrder, - }; - - let comparersToTest: Comparer[]; - if (typeof preferences.organizeImportsIgnoreCase === "boolean") { - // both moduleSpecifier and namedImport comparer to the correct case-sensitivity. - // does not yet exit because we still need to detect for type order - comparer.moduleSpecifierComparer = getOrganizeImportsComparer(preferences, preferences.organizeImportsIgnoreCase); - comparer.namedImportComparer = comparer.moduleSpecifierComparer; - comparersToTest = [comparer.moduleSpecifierComparer]; - } - else { - // otherwise, we must test for both case-sensitivity and later, type order - const CASE_INSENSITIVE_COMPARER = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); - const CASE_SENSITIVE_COMPARER = getOrganizeImportsComparer(preferences, /*ignoreCase*/ false); - comparersToTest = [CASE_INSENSITIVE_COMPARER, CASE_SENSITIVE_COMPARER]; - comparer.moduleSpecifierComparer = detectModuleSpecifierCaseBySort(importDeclsByGroup, comparersToTest); - } - - const typesToTest: TypeOrder[] = preferences.organizeImportsTypeOrder ? [preferences.organizeImportsTypeOrder] : ["first", "last", "inline"]; - const namedImportSort = detectNamedImportOrganizationBySort(importDeclsByGroup.flat(), comparersToTest, typesToTest); - if (!namedImportSort) { - return comparer; - } - const { namedImportComparer, typeOrder } = namedImportSort; - comparer.namedImportComparer = namedImportComparer; - comparer.typeOrder = comparer.typeOrder ?? typeOrder; - - return comparer; -} - -function detectModuleSpecifierCaseBySort(importDeclsByGroup: ImportDeclaration[][], comparersToTest: Comparer[]) { +export function detectModuleSpecifierCaseBySort(importDeclsByGroup: ImportDeclaration[][], comparersToTest: Comparer[]) { const moduleSpecifiersByGroup: string[][] = []; importDeclsByGroup.forEach(importGroup => { // turns importdeclbygroup into string[][] of module specifiers by group to detect sorting on module specifiers @@ -1014,8 +991,11 @@ function detectModuleSpecifierCaseBySort(importDeclsByGroup: ImportDeclaration[] return detectCaseSensitivityBySort(moduleSpecifiersByGroup, comparersToTest); } -type TypeOrder = "first" | "last" | "inline"; -function detectNamedImportOrganizationBySort(originalGroups: ImportDeclaration[], comparersToTest: Comparer[], typesToTest: TypeOrder[]): { namedImportComparer: Comparer; typeOrder?: "first" | "last" | "inline"; } | undefined { +/** @internal */ +export type TypeOrder = "first" | "last" | "inline"; + +/** @internal */ +export function detectNamedImportOrganizationBySort(originalGroups: ImportDeclaration[], comparersToTest: Comparer[], typesToTest: TypeOrder[]): { namedImportComparer: Comparer; typeOrder?: "first" | "last" | "inline"; } | undefined { // filter for import declarations with named imports. Will be a flat array of import declarations without separations by group let bothNamedImports = false; const importDeclsWithNamed = originalGroups.filter(i => { @@ -1073,6 +1053,16 @@ function detectNamedImportOrganizationBySort(originalGroups: ImportDeclaration[] return { namedImportComparer: bestComparer.last, typeOrder: "last" }; } +/** @internal */ +export function getOrdersToDetect(preferences: UserPreferences): { comparersToTest: Comparer[]; typeOrdersToTest: TypeOrder[]; } { + return { + comparersToTest: typeof preferences.organizeImportsIgnoreCase === "boolean" + ? [getOrganizeImportsComparer(preferences, preferences.organizeImportsIgnoreCase)] + : [getOrganizeImportsComparer(preferences, /*ignoreCase*/ true), getOrganizeImportsComparer(preferences, /*ignoreCase*/ false)], + typeOrdersToTest: preferences.organizeImportsTypeOrder ? [preferences.organizeImportsTypeOrder] : ["last", "inline", "first"], + } +} + function getSortedMeasure(arr: readonly T[], comparer: Comparer) { let i = 0; for (let j = 0; j < arr.length - 1; j++) { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 01e57450655bb..e4db67d746c45 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -2629,14 +2629,14 @@ export function insertImports(changes: textChanges.ChangeTracker, sourceFile: So : isArray(imports) ? [imports as ImportDeclaration[]] : [[imports as ImportDeclaration]]; - const comparers = OrganizeImports.getDetectionBySort(moduleSpecifiersToDetect, preferences); - const sortedNewImports = isArray(imports) ? stableSort(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparers.moduleSpecifierComparer)) : [imports]; + const comparer = OrganizeImports.detectModuleSpecifierCaseBySort(moduleSpecifiersToDetect, OrganizeImports.getOrdersToDetect(preferences).comparersToTest); + const sortedNewImports = isArray(imports) ? stableSort(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports]; if (!existingImportStatements.length) { changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); } - else if (existingImportStatements && arrayIsSorted(existingImportStatements, (s1, s2) => OrganizeImports.compareImportsOrRequireStatements(s1, s2, comparers.moduleSpecifierComparer))) { + else if (existingImportStatements && arrayIsSorted(existingImportStatements, (s1, s2) => OrganizeImports.compareImportsOrRequireStatements(s1, s2, comparer))) { for (const newImport of sortedNewImports) { - const insertionIndex = OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport, comparers.moduleSpecifierComparer); + const insertionIndex = OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport, comparer); if (insertionIndex === 0) { // If the first import is top-of-file, insert after the leading comment which is likely the header. const options = existingImportStatements[0] === sourceFile.statements[0] ? diff --git a/tests/cases/fourslash/autoImportTypeImport4.ts b/tests/cases/fourslash/autoImportTypeImport4.ts index 8c62d9057f9d9..a8f987b659597 100644 --- a/tests/cases/fourslash/autoImportTypeImport4.ts +++ b/tests/cases/fourslash/autoImportTypeImport4.ts @@ -23,18 +23,18 @@ // @Filename: /index0.ts //// import { A, B, C } from "./exports1"; -//// a/*0*/; +//// a/*0*//*0a*/; //// b; // @Filename: /index1.ts //// import { A, B, C, type Y, type Z } from "./exports1"; -//// a/*1*/; +//// a/*1*//*1a*//*1b*//*1c*/; //// b; // @Filename: /index2.ts //// import { A, a, B, b, type Y, type Z } from "./exports1"; //// import { E } from "./exports2"; -//// d/*2*/ +//// d/*2*//*2a*//*2b*//*2c*/ // addition of correctly sorted type imports should not affect behavior as shown in autoImportSortCaseSensitivity1.ts goTo.marker("0"); @@ -44,6 +44,7 @@ verify.importFixAtPosition([ ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "last" }); +goTo.marker("0a"); verify.importFixAtPosition([ `import { a, A, B, C } from "./exports1";\na;\nb;`, `import { A, b, B, C } from "./exports1";\na;\nb;` @@ -58,6 +59,7 @@ verify.importFixAtPosition([ ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "last" }); +goTo.marker("1a"); verify.importFixAtPosition([ `import { a, A, B, C, type Y, type Z } from "./exports1";\na;\nb;`, `import { A, b, B, C, type Y, type Z } from "./exports1";\na;\nb;` @@ -65,6 +67,7 @@ verify.importFixAtPosition([ /*errorCode*/ undefined, { organizeImportsIgnoreCase: true, organizeImportsTypeOrder : "last" }); +goTo.marker("1b"); // if we sort inline and sensitive, then all upper case imports should be sorted before any lower case imports verify.importFixAtPosition([ `import { a, A, B, C, type Y, type Z } from "./exports1";\na;\nb;`, @@ -72,6 +75,7 @@ verify.importFixAtPosition([ ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "inline" }); +goTo.marker("1c"); verify.importFixAtPosition([ `import { a, A, B, C, type Y, type Z } from "./exports1";\na;\nb;`, `import { A, b, B, C, type Y, type Z } from "./exports1";\na;\nb;` @@ -87,6 +91,7 @@ d`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "last" }); +goTo.marker("2a"); verify.importFixAtPosition([ `import { A, a, B, b, type Y, type Z } from "./exports1"; import { E, d } from "./exports2"; @@ -94,7 +99,7 @@ d` ], /*errorCode*/ undefined, { organizeImportsIgnoreCase: false, organizeImportsTypeOrder : "last" }); - +goTo.marker("2b"); verify.importFixAtPosition([ `import { A, a, B, b, type Y, type Z } from "./exports1"; import { d, E } from "./exports2"; @@ -102,6 +107,7 @@ d`, ], /*errorCode*/ undefined, { organizeImportsTypeOrder : "last" }); +goTo.marker("2c"); verify.importFixAtPosition([ `import { A, a, B, b, type Y, type Z } from "./exports1"; import { E, d } from "./exports2"; From eb54e77bcd01171bd0fb36fa658aa2febf9bfaf9 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Mon, 26 Feb 2024 21:10:19 -0800 Subject: [PATCH 19/33] remove old detection --- src/services/codefixes/importFixes.ts | 18 +- src/services/organizeImports.ts | 210 ++---------------- src/services/utilities.ts | 4 +- tests/cases/fourslash/organizeImportsType8.ts | 2 +- tests/cases/fourslash/organizeImportsType9.ts | 2 +- 5 files changed, 37 insertions(+), 199 deletions(-) diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index c256e2f9a4ca6..ecd6ccca03be8 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -1409,19 +1409,19 @@ function promoteFromTypeOnly( if (aliasDeclaration.isTypeOnly) { if (aliasDeclaration.parent.elements.length > 1) { const newSpecifier = factory.updateImportSpecifier(aliasDeclaration, /*isTypeOnly*/ false, aliasDeclaration.propertyName, aliasDeclaration.name); - const { comparersToTest, typeOrdersToTest } = OrganizeImports.getOrdersToDetect(preferences); - let namedImportComparer = comparersToTest[0]; - let typeOrder: OrganizeImports.TypeOrder | undefined = typeOrdersToTest[0]; + const { comparersToTest, typeOrdersToTest } = OrganizeImports.getDetectionLists(preferences); + let specifierComparer = OrganizeImports.getOrganizeImportsSpecifierComparer(preferences, comparersToTest[0]); if (typeof preferences.organizeImportsIgnoreCase !== "boolean" || !preferences.organizeImportsTypeOrder) { const importDecl: ImportDeclaration[] = [aliasDeclaration.parent.parent.parent]; const namedImportSort = OrganizeImports.detectNamedImportOrganizationBySort(importDecl, comparersToTest, typeOrdersToTest); if (namedImportSort) { - ({ namedImportComparer, typeOrder } = namedImportSort); + const { namedImportComparer, typeOrder } = namedImportSort; + specifierComparer = OrganizeImports.getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); } } - const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, namedImportComparer, { organizeImportsTypeOrder: typeOrder }); + const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, specifierComparer); if (insertionIndex !== aliasDeclaration.parent.elements.indexOf(aliasDeclaration)) { changes.delete(sourceFile, aliasDeclaration); changes.insertImportSpecifierAtIndex(sourceFile, newSpecifier, aliasDeclaration.parent, insertionIndex); @@ -1463,8 +1463,10 @@ function promoteFromTypeOnly( if (convertExistingToTypeOnly) { const namedImports = tryCast(importClause.namedBindings, isNamedImports); if (namedImports && namedImports.elements.length > 1) { + const { comparersToTest, typeOrdersToTest } = OrganizeImports.getDetectionLists(preferences); + const sortState = OrganizeImports.detectNamedImportOrganizationBySort([importClause.parent], comparersToTest, typeOrdersToTest); if ( - OrganizeImports.detectImportSpecifierSorting(namedImports.elements, preferences) && + (!sortState || sortState.isSorted) && aliasDeclaration.kind === SyntaxKind.ImportSpecifier && namedImports.elements.indexOf(aliasDeclaration) !== 0 ) { @@ -1516,7 +1518,7 @@ function doAddExistingFix( // - otherwise, detect from other imports in the file // let ignoreCaseForSorting: boolean | undefined; let specifierComparer: Comparer = OrganizeImports.getOrganizeImportsSpecifierComparer(preferences); - const { comparersToTest, typeOrdersToTest } = OrganizeImports.getOrdersToDetect(preferences); + const { comparersToTest, typeOrdersToTest } = OrganizeImports.getDetectionLists(preferences); let namedImportComparer = comparersToTest[0]; let typeOrder: OrganizeImports.TypeOrder | undefined = typeOrdersToTest[0]; @@ -1550,7 +1552,7 @@ function doAddExistingFix( // changed to check if existing specifiers are sorted if (existingSpecifiers?.length && arrayIsSorted(existingSpecifiers, specifierComparer)) { for (const spec of newSpecifiers) { - const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, namedImportComparer, { organizeImportsTypeOrder: typeOrder }); + const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, specifierComparer); changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex); } } diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index 8049bb2bd7680..13b34bd24818b 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -1,6 +1,5 @@ import { AnyImportOrRequireStatement, - arrayIsSorted, binarySearch, compareBooleans, Comparer, @@ -9,7 +8,6 @@ import { compareValues, Comparison, createScanner, - detectSortCaseSensitivity, EmitFlags, emptyArray, ExportDeclaration, @@ -17,13 +15,11 @@ import { Expression, factory, FileTextChanges, - find, FindAllReferences, firstOrUndefined, flatMap, formatting, getNewLineOrDefaultFromHost, - getStringComparer, getUILocale, group, groupBy, @@ -48,8 +44,6 @@ import { LanguageServiceHost, length, map, - MemoizeCache, - memoizeCached, NamedImportBindings, NamedImports, NamespaceImport, @@ -60,7 +54,6 @@ import { setEmitFlags, some, sort, - SortKind, SourceFile, stableSort, SyntaxKind, @@ -106,24 +99,21 @@ export function organizeImports( typeOrder: preferences.organizeImportsTypeOrder, }; - const {comparersToTest, typeOrdersToTest} = getOrdersToDetect(preferences); + const { comparersToTest, typeOrdersToTest } = getDetectionLists(preferences); if (typeof preferences.organizeImportsIgnoreCase === "boolean") { // if case sensitivity is specified (true/false), then use the same setting for both. - - // both moduleSpecifier and namedImport comparer to the correct case-sensitivity. - // does not yet exit because we still need to detect for type order comparer.moduleSpecifierComparer = getOrganizeImportsComparer(preferences, preferences.organizeImportsIgnoreCase); comparer.namedImportComparer = comparer.moduleSpecifierComparer; } else { // otherwise, we must test for both case-sensitivity and later, type order - comparer.moduleSpecifierComparer = detectModuleSpecifierCaseBySort(topLevelImportGroupDecls, comparersToTest); + ({ comparer: comparer.moduleSpecifierComparer } = detectModuleSpecifierCaseBySort(topLevelImportGroupDecls, comparersToTest)); } const namedImportSort = detectNamedImportOrganizationBySort(topLevelImportDecls, comparersToTest, typeOrdersToTest); if (namedImportSort) { const { namedImportComparer, typeOrder } = namedImportSort; - comparer.namedImportComparer = namedImportComparer; + comparer.namedImportComparer = comparer.namedImportComparer ?? namedImportComparer; comparer.typeOrder = comparer.typeOrder ?? typeOrder; } topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, comparer)); @@ -437,7 +427,7 @@ function coalesceImportsWorker(importGroup: readonly ImportDeclaration[], compar newImportSpecifiers.push(...getNewImportSpecifiers(namedImports)); const sortedImportSpecifiers = factory.createNodeArray( - sortSpecifiers(newImportSpecifiers, specifierComparer), + stableSort(newImportSpecifiers, specifierComparer), firstNamedImport?.importClause.namedBindings.elements.hasTrailingComma, ); @@ -579,7 +569,7 @@ function coalesceExportsWorker(exportGroup: readonly ExportDeclaration[], specif const newExportSpecifiers: ExportSpecifier[] = []; newExportSpecifiers.push(...flatMap(exportGroup, i => i.exportClause && isNamedExports(i.exportClause) ? i.exportClause.elements : emptyArray)); - const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers, specifierComparer); + const sortedExportSpecifiers = stableSort(newExportSpecifiers, specifierComparer); const exportDecl = exportGroup[0]; coalescedExports.push( @@ -646,12 +636,7 @@ function updateImportDeclarationAndClause( ); } -function sortSpecifiers(specifiers: readonly T[], specifierComparer: Comparer): readonly T[] { - return stableSort(specifiers, specifierComparer); -} - -/** @internal */ -export function compareImportOrExportSpecifiers(s1: T, s2: T, comparer: Comparer, preferences?: UserPreferences): Comparison { +function compareImportOrExportSpecifiers(s1: T, s2: T, comparer: Comparer, preferences?: UserPreferences): Comparison { switch (preferences?.organizeImportsTypeOrder) { case "first": return compareBooleans(s2.isTypeOnly, s1.isTypeOnly) || comparer(s1.name.text, s2.name.text); @@ -698,153 +683,6 @@ function getModuleSpecifierExpression(declaration: AnyImportOrRequireStatement): } } -/** @internal */ -export function detectSorting(sourceFile: SourceFile, preferences: UserPreferences): SortKind { - return detectSortingWorker( - groupByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration)), - preferences, - ); -} - -function detectSortingWorker(importGroups: ImportDeclaration[][], preferences: UserPreferences): SortKind { - const collateCaseSensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ false); - const collateCaseInsensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); - let sortState = SortKind.Both; - let seenUnsortedGroup = false; - for (const importGroup of importGroups) { - // Check module specifiers - if (importGroup.length > 1) { - const moduleSpecifierSort = detectSortCaseSensitivity( - importGroup, - i => tryCast(i.moduleSpecifier, isStringLiteral)?.text ?? "", - collateCaseSensitive, - collateCaseInsensitive, - ); - if (moduleSpecifierSort) { - // Don't let a single unsorted group of module specifiers make the whole algorithm detect unsorted. - // If other things are sorted consistently, that's a stronger indicator than unsorted module specifiers. - sortState &= moduleSpecifierSort; - seenUnsortedGroup = true; - } - if (!sortState) { - return sortState; - } - } - - // Check import specifiers - const declarationWithNamedImports = find( - importGroup, - i => tryCast(i.importClause?.namedBindings, isNamedImports)?.elements.length! > 1, - ); - if (declarationWithNamedImports) { - const namedImportSort = detectImportSpecifierSorting((declarationWithNamedImports.importClause!.namedBindings as NamedImports).elements, preferences); - if (namedImportSort) { - // Don't let a single unsorted group of named imports make the whole algorithm detect unsorted. - // If other things are sorted consistently, that's a stronger indicator than unsorted named imports. - sortState &= namedImportSort; - seenUnsortedGroup = true; - } - if (!sortState) { - return sortState; - } - } - - // Quit as soon as we've disambiguated. There's a chance that something later will disagree with what we've - // found so far, but this function is only intended to infer a preference, not validate the whole file for - // consistent and correct sorting. - if (sortState !== SortKind.Both) { - return sortState; - } - } - return seenUnsortedGroup ? SortKind.None : sortState; -} - -/** @internal */ -export function detectImportDeclarationSorting(imports: readonly AnyImportOrRequireStatement[], preferences: UserPreferences): SortKind { - const collateCaseSensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ false); - const collateCaseInsensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); - return detectSortCaseSensitivity( - imports, - s => getExternalModuleName(getModuleSpecifierExpression(s)) || "", - collateCaseSensitive, - collateCaseInsensitive, - ); -} - -class ImportSpecifierSortingCache implements MemoizeCache<[readonly ImportSpecifier[], UserPreferences], SortKind> { - private _lastPreferences: UserPreferences | undefined; - private _cache: WeakMap | undefined; - - has([specifiers, preferences]: [readonly ImportSpecifier[], UserPreferences]) { - if (this._lastPreferences !== preferences || !this._cache) return false; - return this._cache.has(specifiers); - } - - get([specifiers, preferences]: [readonly ImportSpecifier[], UserPreferences]) { - if (this._lastPreferences !== preferences || !this._cache) return undefined; - return this._cache.get(specifiers); - } - - set([specifiers, preferences]: [readonly ImportSpecifier[], UserPreferences], value: SortKind) { - if (this._lastPreferences !== preferences) { - this._lastPreferences = preferences; - this._cache = undefined; - } - this._cache ??= new WeakMap(); - this._cache.set(specifiers, value); - } -} - -/** @internal */ -export const detectImportSpecifierSorting = memoizeCached((specifiers: readonly ImportSpecifier[], preferences: UserPreferences): SortKind => { - // If types are not sorted as specified, then imports are assumed to be unsorted. - // If there is no type sorting specification, we default to "last" and move on to case sensitivity detection. - switch (preferences.organizeImportsTypeOrder) { - case "first": - if (!arrayIsSorted(specifiers, (s1, s2) => compareBooleans(s2.isTypeOnly, s1.isTypeOnly))) return SortKind.None; - break; - case "inline": - if ( - !arrayIsSorted(specifiers, (s1, s2) => { - const comparer = getStringComparer(/*ignoreCase*/ true); - return comparer(s1.name.text, s2.name.text); - }) - ) { - return SortKind.None; - } - break; - default: - if (!arrayIsSorted(specifiers, (s1, s2) => compareBooleans(s1.isTypeOnly, s2.isTypeOnly))) return SortKind.None; - break; - } - - const collateCaseSensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ false); - const collateCaseInsensitive = getOrganizeImportsComparer(preferences, /*ignoreCase*/ true); - - if (preferences.organizeImportsTypeOrder !== "inline") { - const { regular: regularImports, type: typeImports } = groupBy(specifiers, s => s.isTypeOnly ? "type" : "regular"); - const regularCaseSensitivity = regularImports?.length - ? detectSortCaseSensitivity(regularImports, specifier => specifier.name.text, collateCaseSensitive, collateCaseInsensitive) - : undefined; - const typeCaseSensitivity = typeImports?.length - ? detectSortCaseSensitivity(typeImports, specifier => specifier.name.text ?? "", collateCaseSensitive, collateCaseInsensitive) - : undefined; - if (regularCaseSensitivity === undefined) { - return typeCaseSensitivity ?? SortKind.None; - } - if (typeCaseSensitivity === undefined) { - return regularCaseSensitivity; - } - if (regularCaseSensitivity === SortKind.None || typeCaseSensitivity === SortKind.None) { - return SortKind.None; - } - return typeCaseSensitivity & regularCaseSensitivity; - } - - // else inline - return detectSortCaseSensitivity(specifiers, specifier => specifier.name.text, collateCaseSensitive, collateCaseInsensitive); -}, new ImportSpecifierSortingCache()); - /** @internal */ export function getImportDeclarationInsertionIndex(sortedImports: readonly AnyImportOrRequireStatement[], newImport: AnyImportOrRequireStatement, comparer: Comparer) { const index = binarySearch(sortedImports, newImport, identity, (a, b) => compareImportsOrRequireStatements(a, b, comparer)); @@ -852,8 +690,8 @@ export function getImportDeclarationInsertionIndex(sortedImports: readonly AnyIm } /** @internal */ -export function getImportSpecifierInsertionIndex(sortedImports: readonly ImportSpecifier[], newImport: ImportSpecifier, comparer: Comparer, preferences: UserPreferences) { - const index = binarySearch(sortedImports, newImport, identity, (s1, s2) => compareImportOrExportSpecifiers(s1, s2, comparer, preferences)); +export function getImportSpecifierInsertionIndex(sortedImports: readonly ImportSpecifier[], newImport: ImportSpecifier, comparer: Comparer) { + const index = binarySearch(sortedImports, newImport, identity, comparer); return index < 0 ? ~index : index; } @@ -944,12 +782,6 @@ export function getOrganizeImportsComparer(preferences: UserPreferences, ignoreC getOrganizeImportsOrdinalStringComparer(ignoreCase); } -/** @internal */ -export function getOrganizeImportsComparerWithDetection(preferences: UserPreferences, detectIgnoreCase?: () => boolean): Comparer { - const ignoreCase = typeof preferences.organizeImportsIgnoreCase === "boolean" ? preferences.organizeImportsIgnoreCase : detectIgnoreCase?.() ?? false; - return getOrganizeImportsComparer(preferences, ignoreCase); -} - function getTopLevelExportGroups(sourceFile: SourceFile) { const topLevelExportGroups: ExportDeclaration[][] = []; const statements = sourceFile.statements; @@ -995,7 +827,7 @@ export function detectModuleSpecifierCaseBySort(importDeclsByGroup: ImportDeclar export type TypeOrder = "first" | "last" | "inline"; /** @internal */ -export function detectNamedImportOrganizationBySort(originalGroups: ImportDeclaration[], comparersToTest: Comparer[], typesToTest: TypeOrder[]): { namedImportComparer: Comparer; typeOrder?: "first" | "last" | "inline"; } | undefined { +export function detectNamedImportOrganizationBySort(originalGroups: readonly ImportDeclaration[], comparersToTest: Comparer[], typesToTest: TypeOrder[]): { namedImportComparer: Comparer; typeOrder?: "first" | "last" | "inline"; isSorted: boolean; } | undefined { // filter for import declarations with named imports. Will be a flat array of import declarations without separations by group let bothNamedImports = false; const importDeclsWithNamed = originalGroups.filter(i => { @@ -1018,7 +850,8 @@ export function detectNamedImportOrganizationBySort(originalGroups: ImportDeclar // if we don't have any import statements with both named regular and type imports, we do not need to detect a type ordering if (!bothNamedImports || typesToTest.length === 0) { - return { namedImportComparer: detectCaseSensitivityBySort(namedImportsByDecl.map(i => i.map(n => n.name.text)), comparersToTest) }; + const sortState = detectCaseSensitivityBySort(namedImportsByDecl.map(i => i.map(n => n.name.text)), comparersToTest); + return { namedImportComparer: sortState.comparer, isSorted: sortState.isSorted }; } const bestDiff = { first: Infinity, last: Infinity, inline: Infinity }; @@ -1028,7 +861,7 @@ export function detectNamedImportOrganizationBySort(originalGroups: ImportDeclar const currDiff = { first: 0, last: 0, inline: 0 }; for (const importDecl of namedImportsByDecl) { for (const typeOrder of typesToTest) { - currDiff[typeOrder] = (currDiff[typeOrder] || 0) + getSortedMeasure(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, { organizeImportsTypeOrder: typeOrder })); + currDiff[typeOrder] = (currDiff[typeOrder] ?? 0) + getSortedMeasure(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, { organizeImportsTypeOrder: typeOrder })); } } for (const key of typesToTest) { @@ -1046,21 +879,21 @@ export function detectNamedImportOrganizationBySort(originalGroups: ImportDeclar const testTypeOrder = testKey as TypeOrder; if (bestDiff[testTypeOrder] < bestDiff[bestTypeOrder]) continue outer; } - return { namedImportComparer: bestComparer[bestTypeOrder], typeOrder: bestTypeOrder }; + return { namedImportComparer: bestComparer[bestTypeOrder], typeOrder: bestTypeOrder, isSorted: bestDiff[bestTypeOrder] === 0 }; } // default; hopefully never hit..... - return { namedImportComparer: bestComparer.last, typeOrder: "last" }; + return { namedImportComparer: bestComparer.last, typeOrder: "last", isSorted: bestDiff.last === 0 }; } /** @internal */ -export function getOrdersToDetect(preferences: UserPreferences): { comparersToTest: Comparer[]; typeOrdersToTest: TypeOrder[]; } { +export function getDetectionLists(preferences: UserPreferences): { comparersToTest: Comparer[]; typeOrdersToTest: TypeOrder[]; } { return { comparersToTest: typeof preferences.organizeImportsIgnoreCase === "boolean" ? [getOrganizeImportsComparer(preferences, preferences.organizeImportsIgnoreCase)] : [getOrganizeImportsComparer(preferences, /*ignoreCase*/ true), getOrganizeImportsComparer(preferences, /*ignoreCase*/ false)], - typeOrdersToTest: preferences.organizeImportsTypeOrder ? [preferences.organizeImportsTypeOrder] : ["last", "inline", "first"], - } + typeOrdersToTest: preferences.organizeImportsTypeOrder ? [preferences.organizeImportsTypeOrder] : ["last", "inline", "first"], + }; } function getSortedMeasure(arr: readonly T[], comparer: Comparer) { @@ -1072,14 +905,14 @@ function getSortedMeasure(arr: readonly T[], comparer: Comparer) { } return i; } -function detectCaseSensitivityBySort(originalGroups: string[][], comparersToTest: Comparer[]): Comparer { +function detectCaseSensitivityBySort(originalGroups: string[][], comparersToTest: Comparer[]): { comparer: Comparer; isSorted: boolean; } { // each entry in originalGroups will be sorted and compared against the original entry. // the total diff of each comparison is the sum of the diffs of all groups let bestComparer; let bestDiff = Infinity; if (comparersToTest.length === 1) { - return comparersToTest[0]; + return { comparer: comparersToTest[0], isSorted: true }; } for (const curComparer of comparersToTest) { let diffOfCurrentComparer = 0; @@ -1097,5 +930,8 @@ function detectCaseSensitivityBySort(originalGroups: string[][], comparersToTest bestComparer = curComparer; } } - return bestComparer ?? comparersToTest[0]; + return { + comparer: bestComparer ?? comparersToTest[0], + isSorted: bestDiff === 0, + }; } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index e4db67d746c45..803453a5bf808 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -2628,8 +2628,8 @@ export function insertImports(changes: textChanges.ChangeTracker, sourceFile: So ? [existingImportStatements as ImportDeclaration[]] : isArray(imports) ? [imports as ImportDeclaration[]] - : [[imports as ImportDeclaration]]; - const comparer = OrganizeImports.detectModuleSpecifierCaseBySort(moduleSpecifiersToDetect, OrganizeImports.getOrdersToDetect(preferences).comparersToTest); + : [existingImportStatements as ImportDeclaration[]]; + const { comparer } = OrganizeImports.detectModuleSpecifierCaseBySort(moduleSpecifiersToDetect, OrganizeImports.getDetectionLists(preferences).comparersToTest); const sortedNewImports = isArray(imports) ? stableSort(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports]; if (!existingImportStatements.length) { changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); diff --git a/tests/cases/fourslash/organizeImportsType8.ts b/tests/cases/fourslash/organizeImportsType8.ts index 50b02580e94fd..2c63d54cdb38e 100644 --- a/tests/cases/fourslash/organizeImportsType8.ts +++ b/tests/cases/fourslash/organizeImportsType8.ts @@ -44,7 +44,7 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type A, type a, b, B } from "foo5";'); verify.organizeImports( -`import { type A, type a, B, b } from "foo5"; +`import { type A, B, type a, b } from "foo5"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: false } diff --git a/tests/cases/fourslash/organizeImportsType9.ts b/tests/cases/fourslash/organizeImportsType9.ts index 5db33ccc2a444..1bbc26d8c31e3 100644 --- a/tests/cases/fourslash/organizeImportsType9.ts +++ b/tests/cases/fourslash/organizeImportsType9.ts @@ -42,7 +42,7 @@ console.log(a, b, A, B);`, edit.replaceLine(0, 'import { type a, type A, b, B } from "foo5";'); verify.organizeImports( -`import { type A, type a, B, b } from "foo5"; +`import { type A, B, type a, b } from "foo5"; console.log(a, b, A, B);`, /*mode*/ undefined, { organizeImportsIgnoreCase: false }); \ No newline at end of file From c7c57f7b443cf15d6ce509889be9b6d67cf94144 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Mon, 26 Feb 2024 23:19:10 -0800 Subject: [PATCH 20/33] remove casts --- src/services/organizeImports.ts | 10 +++++++--- src/services/utilities.ts | 7 +------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index 13b34bd24818b..d5940f5444ef6 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -813,12 +813,16 @@ function getTopLevelExportGroups(sourceFile: SourceFile) { return flatMap(topLevelExportGroups, exportGroupDecls => groupByNewlineContiguous(sourceFile, exportGroupDecls)); } +function getModuleNamesFromDecls(decls: readonly AnyImportOrRequireStatement[]): string[] { + return decls.map(s => getExternalModuleName(getModuleSpecifierExpression(s)) || ""); +} + /** @internal */ -export function detectModuleSpecifierCaseBySort(importDeclsByGroup: ImportDeclaration[][], comparersToTest: Comparer[]) { +export function detectModuleSpecifierCaseBySort(importDeclsByGroup: (readonly AnyImportOrRequireStatement[])[], comparersToTest: Comparer[]) { const moduleSpecifiersByGroup: string[][] = []; importDeclsByGroup.forEach(importGroup => { - // turns importdeclbygroup into string[][] of module specifiers by group to detect sorting on module specifiers - moduleSpecifiersByGroup.push(importGroup.map(i => getExternalModuleName(i.moduleSpecifier)!)); + // turns importDeclsByGroup into string[][] of module specifiers by group to detect sorting on module specifiers + moduleSpecifiersByGroup.push(getModuleNamesFromDecls(importGroup)); }); return detectCaseSensitivityBySort(moduleSpecifiersByGroup, comparersToTest); } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 803453a5bf808..9f909edbfc09d 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -2623,12 +2623,7 @@ export function insertImports(changes: textChanges.ChangeTracker, sourceFile: So const decl = isArray(imports) ? imports[0] : imports; const importKindPredicate: (node: Node) => node is AnyImportOrRequireStatement = decl.kind === SyntaxKind.VariableStatement ? isRequireVariableStatement : isAnyImportSyntax; const existingImportStatements = filter(sourceFile.statements, importKindPredicate); - // todo remove typecasts - const moduleSpecifiersToDetect: ImportDeclaration[][] = existingImportStatements.length > 1 - ? [existingImportStatements as ImportDeclaration[]] - : isArray(imports) - ? [imports as ImportDeclaration[]] - : [existingImportStatements as ImportDeclaration[]]; + const moduleSpecifiersToDetect = existingImportStatements.length > 1 ? [existingImportStatements] : isArray(imports) ? [imports] : [existingImportStatements]; const { comparer } = OrganizeImports.detectModuleSpecifierCaseBySort(moduleSpecifiersToDetect, OrganizeImports.getDetectionLists(preferences).comparersToTest); const sortedNewImports = isArray(imports) ? stableSort(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports]; if (!existingImportStatements.length) { From 64f1468b33d9af999e60fff7d1ed14b9d7f905d2 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Mon, 26 Feb 2024 23:42:46 -0800 Subject: [PATCH 21/33] clean up --- src/services/organizeImports.ts | 66 +++++++++++++++++---------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index d5940f5444ef6..fc742f0e1a5ce 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -101,21 +101,24 @@ export function organizeImports( const { comparersToTest, typeOrdersToTest } = getDetectionLists(preferences); if (typeof preferences.organizeImportsIgnoreCase === "boolean") { - // if case sensitivity is specified (true/false), then use the same setting for both. + // If case sensitivity is specified (true/false), then use the same setting for both. comparer.moduleSpecifierComparer = getOrganizeImportsComparer(preferences, preferences.organizeImportsIgnoreCase); comparer.namedImportComparer = comparer.moduleSpecifierComparer; } else { - // otherwise, we must test for both case-sensitivity and later, type order + // Otherwise, we must test for case-sensitivity. Named import case sensitivity will be tested with type order ({ comparer: comparer.moduleSpecifierComparer } = detectModuleSpecifierCaseBySort(topLevelImportGroupDecls, comparersToTest)); } - const namedImportSort = detectNamedImportOrganizationBySort(topLevelImportDecls, comparersToTest, typeOrdersToTest); - if (namedImportSort) { - const { namedImportComparer, typeOrder } = namedImportSort; - comparer.namedImportComparer = comparer.namedImportComparer ?? namedImportComparer; - comparer.typeOrder = comparer.typeOrder ?? typeOrder; + if (!comparer.typeOrder) { + const namedImportSort = detectNamedImportOrganizationBySort(topLevelImportDecls, comparersToTest, typeOrdersToTest); + if (namedImportSort) { + const { namedImportComparer, typeOrder } = namedImportSort; + comparer.namedImportComparer = comparer.namedImportComparer ?? namedImportComparer; + comparer.typeOrder = comparer.typeOrder ?? typeOrder; + } } + topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, comparer)); // Exports are always used @@ -211,6 +214,17 @@ export function organizeImports( } } +/** @internal */ +export function getDetectionLists(preferences: UserPreferences): { comparersToTest: Comparer[]; typeOrdersToTest: TypeOrder[]; } { + // Returns the possible detection outcomes, given the user's preferences. The earlier in the list, the higher the priority. + return { + comparersToTest: typeof preferences.organizeImportsIgnoreCase === "boolean" + ? [getOrganizeImportsComparer(preferences, preferences.organizeImportsIgnoreCase)] + : [getOrganizeImportsComparer(preferences, /*ignoreCase*/ true), getOrganizeImportsComparer(preferences, /*ignoreCase*/ false)], + typeOrdersToTest: preferences.organizeImportsTypeOrder ? [preferences.organizeImportsTypeOrder] : ["last", "inline", "first"], + }; +} + function groupByNewlineContiguous(sourceFile: SourceFile, decls: T[]): T[][] { const scanner = createScanner(sourceFile.languageVersion, /*skipTrivia*/ false, sourceFile.languageVariant); const group: T[][] = []; @@ -672,6 +686,10 @@ function compareModuleSpecifiersWorker(m1: Expression | undefined, m2: Expressio comparer(name1!, name2!); } +function getModuleNamesFromDecls(decls: readonly AnyImportOrRequireStatement[]): string[] { + return decls.map(s => getExternalModuleName(getModuleSpecifierExpression(s)) || ""); +} + function getModuleSpecifierExpression(declaration: AnyImportOrRequireStatement): Expression | undefined { switch (declaration.kind) { case SyntaxKind.ImportEqualsDeclaration: @@ -813,15 +831,11 @@ function getTopLevelExportGroups(sourceFile: SourceFile) { return flatMap(topLevelExportGroups, exportGroupDecls => groupByNewlineContiguous(sourceFile, exportGroupDecls)); } -function getModuleNamesFromDecls(decls: readonly AnyImportOrRequireStatement[]): string[] { - return decls.map(s => getExternalModuleName(getModuleSpecifierExpression(s)) || ""); -} - /** @internal */ export function detectModuleSpecifierCaseBySort(importDeclsByGroup: (readonly AnyImportOrRequireStatement[])[], comparersToTest: Comparer[]) { const moduleSpecifiersByGroup: string[][] = []; importDeclsByGroup.forEach(importGroup => { - // turns importDeclsByGroup into string[][] of module specifiers by group to detect sorting on module specifiers + // Turns importDeclsByGroup into string[][] of module specifiers by group to detect sorting on module specifiers moduleSpecifiersByGroup.push(getModuleNamesFromDecls(importGroup)); }); return detectCaseSensitivityBySort(moduleSpecifiersByGroup, comparersToTest); @@ -832,27 +846,26 @@ export type TypeOrder = "first" | "last" | "inline"; /** @internal */ export function detectNamedImportOrganizationBySort(originalGroups: readonly ImportDeclaration[], comparersToTest: Comparer[], typesToTest: TypeOrder[]): { namedImportComparer: Comparer; typeOrder?: "first" | "last" | "inline"; isSorted: boolean; } | undefined { - // filter for import declarations with named imports. Will be a flat array of import declarations without separations by group + // Filter for import declarations with named imports. Will be a flat array of import declarations without separations by group let bothNamedImports = false; const importDeclsWithNamed = originalGroups.filter(i => { const namedImports = tryCast(i.importClause?.namedBindings, isNamedImports)?.elements; if (!namedImports?.length) return false; if (!bothNamedImports && namedImports.some(n => n.isTypeOnly) && namedImports.some(n => !n.isTypeOnly)) { - // todo:improve check bothNamedImports = true; } return true; }); - // no need for more detection if no named imports + // No need for more detection, if no named imports if (importDeclsWithNamed.length === 0) return; - // formats the code into lists of named imports, grouped by declaration + // Formats into lists of named imports, grouped by declaration const namedImportsByDecl = importDeclsWithNamed.map(importDecl => { return tryCast(importDecl.importClause?.namedBindings, isNamedImports)?.elements; }).filter(elements => elements !== undefined) as any as ImportSpecifier[][]; - // if we don't have any import statements with both named regular and type imports, we do not need to detect a type ordering + // If we don't have any import statements with both named regular and type imports, we do not need to detect a type ordering if (!bothNamedImports || typesToTest.length === 0) { const sortState = detectCaseSensitivityBySort(namedImportsByDecl.map(i => i.map(n => n.name.text)), comparersToTest); return { namedImportComparer: sortState.comparer, isSorted: sortState.isSorted }; @@ -886,20 +899,10 @@ export function detectNamedImportOrganizationBySort(originalGroups: readonly Imp return { namedImportComparer: bestComparer[bestTypeOrder], typeOrder: bestTypeOrder, isSorted: bestDiff[bestTypeOrder] === 0 }; } - // default; hopefully never hit..... + // Default behavior. It shouldn't be hit if typesToTest.length > 0 return { namedImportComparer: bestComparer.last, typeOrder: "last", isSorted: bestDiff.last === 0 }; } -/** @internal */ -export function getDetectionLists(preferences: UserPreferences): { comparersToTest: Comparer[]; typeOrdersToTest: TypeOrder[]; } { - return { - comparersToTest: typeof preferences.organizeImportsIgnoreCase === "boolean" - ? [getOrganizeImportsComparer(preferences, preferences.organizeImportsIgnoreCase)] - : [getOrganizeImportsComparer(preferences, /*ignoreCase*/ true), getOrganizeImportsComparer(preferences, /*ignoreCase*/ false)], - typeOrdersToTest: preferences.organizeImportsTypeOrder ? [preferences.organizeImportsTypeOrder] : ["last", "inline", "first"], - }; -} - function getSortedMeasure(arr: readonly T[], comparer: Comparer) { let i = 0; for (let j = 0; j < arr.length - 1; j++) { @@ -909,9 +912,10 @@ function getSortedMeasure(arr: readonly T[], comparer: Comparer) { } return i; } + function detectCaseSensitivityBySort(originalGroups: string[][], comparersToTest: Comparer[]): { comparer: Comparer; isSorted: boolean; } { - // each entry in originalGroups will be sorted and compared against the original entry. - // the total diff of each comparison is the sum of the diffs of all groups + // Each entry in originalGroups will be sorted and compared against the original entry. + // The total diff of each comparison is the sum of the diffs over all groups let bestComparer; let bestDiff = Infinity; @@ -923,8 +927,6 @@ function detectCaseSensitivityBySort(originalGroups: string[][], comparersToTest for (const listToSort of originalGroups) { if (listToSort.length <= 1) continue; - - // const sortedList = sort(listToSort, curComparer) as any as string[]; const diff = getSortedMeasure(listToSort, curComparer); diffOfCurrentComparer += diff; } From bd5c01a21f7c4dfac5a3e3aad03a0206b13765f0 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Tue, 27 Feb 2024 10:41:35 -0800 Subject: [PATCH 22/33] fix comments --- src/services/organizeImports.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index fc742f0e1a5ce..83a62171cf6fa 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -83,7 +83,6 @@ export function organizeImports( const shouldSort = mode === OrganizeImportsMode.SortAndCombine || mode === OrganizeImportsMode.All; // These are currently inseparable, but I draw a distinction for clarity and in case we add modes in the future. - // shouldCombine vs shouldSort is not supported in diff-detection, diff-detection always assumes sorting will combine/deduplicate const shouldCombine = shouldSort; const shouldRemove = mode === OrganizeImportsMode.RemoveUnused || mode === OrganizeImportsMode.All; From 34e3502cdcca3ca905feb70372ef90d845fdad2b Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Tue, 27 Feb 2024 12:54:57 -0800 Subject: [PATCH 23/33] remove unused functions --- src/compiler/core.ts | 40 ------------------- src/services/utilities.ts | 33 --------------- .../unittests/services/utilities.ts | 36 ----------------- 3 files changed, 109 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index cba0dbb94ec6a..1ee2fefe16048 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -837,46 +837,6 @@ export function sortAndDeduplicate(array: readonly T[], comparer?: Comparer); } -/** - * Merges two sorted arrays so that the resulting array is sorted and has no duplicates. - * @internal - */ -export function mergeAndDeduplicateSorted(array1: readonly T[], array2: readonly T[], comparer: Comparer): SortedReadonlyArray { - if (array1.length === 0) return array2 as any as SortedReadonlyArray; - if (array2.length === 0) return array1 as any as SortedReadonlyArray; - - const finalArray: T[] = []; - let i = 0, j = 0; - while (i < array1.length && j < array2.length) { - switch (comparer(array1[i], array2[j])) { - case Comparison.LessThan: - finalArray.push(array1[i]); - i++; - break; - case Comparison.EqualTo: - finalArray.push(array1[i]); - i++, j++; - break; - case Comparison.GreaterThan: - finalArray.push(array2[j]); - j++; - break; - default: - Debug.fail("not a valid Comparison"); - } - } - if (i === array1.length && j === array2.length) { - return finalArray as any as SortedReadonlyArray; - } - if (i === array1.length) { - return finalArray.concat(array2.slice(j)) as any as SortedReadonlyArray; - } - if (j === array2.length) { - return finalArray.concat(array1.slice(i)) as any as SortedReadonlyArray; - } - return Debug.fail("unreachable"); -} - /** @internal */ export function arrayIsSorted(array: readonly T[], comparer: Comparer) { if (array.length < 2) return true; diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 5b05e4487d3d3..02b013394702f 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -4275,39 +4275,6 @@ export function fileShouldUseJavaScriptRequire(file: SourceFile | string, progra return preferRequire; } -/** - * Does not perform change matching, only returns a the number of the minimum edit - * @internal - */ -export function getDiffNum(s1: T[], s2: T[]): number { - return getDiffOpt(s1, s2); - - function getDiffOpt(s1: T[], s2: T[]): number { - const diffTable = new Array(s1.length + 1); - diffTable[s1.length] = Array.from({ length: s2.length + 1 }, (_, i) => s2.length - i); - - for (let i = s1.length - 1; i >= 0; i--) { - diffTable[i] = new Array(s2.length + 1); - diffTable[i][s2.length] = s2.length - i; - - for (let j = s2.length - 1; j >= 0; j--) { - if (s1[i] === s2[j]) { - diffTable[i][j] = diffTable[i + 1][j + 1]; - } - else { - // all change types are currently weighted the same - diffTable[i][j] = 1 + Math.min( - diffTable[i + 1][j], - diffTable[i + 1][j + 1], - diffTable[i][j + 1], - ); - } - } - } - return diffTable[0][0]; - } -} - /** @internal */ export function isBlockLike(node: Node): node is BlockLike { switch (node.kind) { diff --git a/src/testRunner/unittests/services/utilities.ts b/src/testRunner/unittests/services/utilities.ts index dfdef5ffa1ca6..91b82462b479e 100644 --- a/src/testRunner/unittests/services/utilities.ts +++ b/src/testRunner/unittests/services/utilities.ts @@ -22,40 +22,4 @@ describe("unittests:: services:: utilities", () => { assert.isDefined(result); }); }); - - describe("getDiffNum", () => { - it("getDiffNum", () => { - const originalImports = ["abc", "Abc", "abC"]; - const test2 = ["abc", "Abc", "abC", "b"]; - const test3 = ["abc", "Abc", "abC", "c"]; - const test4 = ["abc", "Abc", "abC", "b", "d", "e"]; - const test5 = ["abc", "b", "Abc", "abC", "d", "e"]; - - assert.equal(ts.getDiffNum(originalImports, originalImports), 0); - assert.equal(ts.getDiffNum(originalImports, test2), 1); - assert.equal(ts.getDiffNum(test2, test3), 1); - assert.equal(ts.getDiffNum(originalImports, test4), 3); - assert.equal(ts.getDiffNum(originalImports, test5), 3); - assert.equal(ts.getDiffNum(test4, test5), 2); - }); - }); - - describe("mergeAndDeDuplicate", () => { - it("mergeAndDeDuplicate", () => { - const test = [1, 2, 3, 4]; - const test2 = [1, 2, 3, 4, 5]; - const test3 = [6, 7, 8, 9, 10]; - const test4 = [1, 3, 4, 5, 8, 9]; - const test5 = [1, 2, 4, 6, 7, 8, 9]; - function comparer(x: number, y: number) { - if (x > y) return ts.Comparison.GreaterThan; - if (x < y) return ts.Comparison.LessThan; - return ts.Comparison.EqualTo; - } - assert.sameOrderedMembers(ts.mergeAndDeduplicateSorted(test, test2, comparer) as any as number[], test2); - assert.sameOrderedMembers(ts.mergeAndDeduplicateSorted(test2, test2, comparer) as any as number[], test2); - assert.sameOrderedMembers(ts.mergeAndDeduplicateSorted(test2, test3, comparer) as any as number[], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - assert.sameOrderedMembers(ts.mergeAndDeduplicateSorted(test4, test5, comparer) as any as number[], [1, 2, 3, 4, 5, 6, 7, 8, 9]); - }); - }); }); From 2593f8ca700f0297e72cf47ca62e933c8e4ee54e Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Tue, 27 Feb 2024 12:59:28 -0800 Subject: [PATCH 24/33] add type OrganizeImportsTypeOrder --- src/compiler/types.ts | 4 ++- src/server/protocol.ts | 2 +- src/services/codefixes/importFixes.ts | 13 +++++--- src/services/organizeImports.ts | 31 +++++++++---------- tests/baselines/reference/api/typescript.d.ts | 5 +-- 5 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 28061c88e94bb..3c2a5dd87009c 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -10094,10 +10094,12 @@ export interface UserPreferences { readonly organizeImportsNumericCollation?: boolean; readonly organizeImportsAccentCollation?: boolean; readonly organizeImportsCaseFirst?: "upper" | "lower" | false; - readonly organizeImportsTypeOrder?: "first" | "last" | "inline"; + readonly organizeImportsTypeOrder?: OrganizeImportsTypeOrder; readonly excludeLibrarySymbolsInNavTo?: boolean; } +export type OrganizeImportsTypeOrder = "first" | "last" | "inline"; + /** Represents a bigint literal value without requiring bigint support */ export interface PseudoBigInt { negative: boolean; diff --git a/src/server/protocol.ts b/src/server/protocol.ts index e61c2393f1d14..2f739c23e8389 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -3649,7 +3649,7 @@ export interface UserPreferences { * * Default: `last` */ - readonly organizeImportsTypeOrder?: "last" | "first" | "inline"; + readonly organizeImportsTypeOrder?: ts.OrganizeImportsTypeOrder; /** * Indicates whether {@link ReferencesResponseItem.lineText} is supported. diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index a815cb06772b7..14043e64b48bb 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -111,6 +111,7 @@ import { nodeIsMissing, ObjectBindingPattern, OrganizeImports, + OrganizeImportsTypeOrder, PackageJsonImportFilter, Path, pathContainsNodeModules, @@ -127,7 +128,6 @@ import { skipAlias, some, sort, - // SortKind, SourceFile, stableSort, startsWith, @@ -1517,16 +1517,20 @@ function doAddExistingFix( let specifierComparer: Comparer = OrganizeImports.getOrganizeImportsSpecifierComparer(preferences); const { comparersToTest, typeOrdersToTest } = OrganizeImports.getDetectionLists(preferences); let namedImportComparer = comparersToTest[0]; - let typeOrder: OrganizeImports.TypeOrder | undefined = typeOrdersToTest[0]; + let typeOrder: OrganizeImportsTypeOrder | undefined = typeOrdersToTest[0]; + let isSorted: boolean | undefined; if (existingSpecifiers && (typeof preferences.organizeImportsIgnoreCase !== "boolean" || !preferences.organizeImportsTypeOrder)) { const moduleSpecifiersDecl: ImportDeclaration[] = [clause.parent]; const namedImportSort = OrganizeImports.detectNamedImportOrganizationBySort(moduleSpecifiersDecl, comparersToTest, typeOrdersToTest); if (namedImportSort) { - ({ namedImportComparer, typeOrder } = namedImportSort); + ({ namedImportComparer, typeOrder, isSorted } = namedImportSort); specifierComparer = OrganizeImports.getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); } } + else { + isSorted = existingSpecifiers ? arrayIsSorted(existingSpecifiers, specifierComparer) : true; + } const newSpecifiers = stableSort( namedImports.map(namedImport => @@ -1544,10 +1548,9 @@ function doAddExistingFix( // nonsense. So if there are existing specifiers, even if we know the sorting preference, we // need to ensure that the existing specifiers are sorted according to the preference in order // to do a sorted insertion. - // const specifierSort = existingSpecifiers?.length && OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences); // changed to check if existing specifiers are sorted - if (existingSpecifiers?.length && arrayIsSorted(existingSpecifiers, specifierComparer)) { + if (existingSpecifiers?.length && isSorted) { for (const spec of newSpecifiers) { const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, specifierComparer); changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex); diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index 83a62171cf6fa..4e8695cd530e8 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -48,6 +48,7 @@ import { NamedImports, NamespaceImport, OrganizeImportsMode, + OrganizeImportsTypeOrder, Program, rangeIsOnSingleLine, Scanner, @@ -90,15 +91,14 @@ export function organizeImports( const topLevelImportDecls = sourceFile.statements.filter(isImportDeclaration); const topLevelImportGroupDecls = groupByNewlineContiguous(sourceFile, topLevelImportDecls); - const DefaultComparer = getOrganizeImportsComparer(preferences, /*ignoreCase*/ typeof preferences.organizeImportsIgnoreCase === "boolean" ? preferences.organizeImportsIgnoreCase : true); - + const { comparersToTest, typeOrdersToTest } = getDetectionLists(preferences); + const defaultComparer = comparersToTest[0]; const comparer = { - moduleSpecifierComparer: typeof preferences.organizeImportsIgnoreCase === "boolean" ? DefaultComparer : undefined, - namedImportComparer: typeof preferences.organizeImportsIgnoreCase === "boolean" ? DefaultComparer : undefined, + moduleSpecifierComparer: typeof preferences.organizeImportsIgnoreCase === "boolean" ? defaultComparer : undefined, + namedImportComparer: typeof preferences.organizeImportsIgnoreCase === "boolean" ? defaultComparer : undefined, typeOrder: preferences.organizeImportsTypeOrder, }; - const { comparersToTest, typeOrdersToTest } = getDetectionLists(preferences); if (typeof preferences.organizeImportsIgnoreCase === "boolean") { // If case sensitivity is specified (true/false), then use the same setting for both. comparer.moduleSpecifierComparer = getOrganizeImportsComparer(preferences, preferences.organizeImportsIgnoreCase); @@ -160,7 +160,7 @@ export function organizeImports( ? group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier)!) : [oldImportDecls]; const sortedImportGroups = shouldSort - ? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiersWorker(group1[0].moduleSpecifier, group2[0].moduleSpecifier, comparer.moduleSpecifierComparer ?? DefaultComparer)) + ? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiersWorker(group1[0].moduleSpecifier, group2[0].moduleSpecifier, comparer.moduleSpecifierComparer ?? defaultComparer)) : oldImportGroups; const newImportDecls = flatMap(sortedImportGroups, importGroup => getExternalModuleName(importGroup[0].moduleSpecifier) || importGroup[0].moduleSpecifier === undefined @@ -191,9 +191,9 @@ export function organizeImports( } } - function organizeImportsWorker(oldImportDecls: readonly ImportDeclaration[], comparer: { moduleSpecifierComparer?: Comparer; namedImportComparer?: Comparer; typeOrder?: TypeOrder; }) { - const detectedModuleCaseComparer = comparer.moduleSpecifierComparer ?? DefaultComparer; - const detectedNamedImportCaseComparer = comparer.namedImportComparer ?? DefaultComparer; + function organizeImportsWorker(oldImportDecls: readonly ImportDeclaration[], comparer: { moduleSpecifierComparer?: Comparer; namedImportComparer?: Comparer; typeOrder?: OrganizeImportsTypeOrder; }) { + const detectedModuleCaseComparer = comparer.moduleSpecifierComparer ?? defaultComparer; + const detectedNamedImportCaseComparer = comparer.namedImportComparer ?? defaultComparer; const detectedTypeOrder = comparer.typeOrder ?? "last"; const specifierComparer = getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: detectedTypeOrder }, detectedNamedImportCaseComparer); @@ -214,7 +214,7 @@ export function organizeImports( } /** @internal */ -export function getDetectionLists(preferences: UserPreferences): { comparersToTest: Comparer[]; typeOrdersToTest: TypeOrder[]; } { +export function getDetectionLists(preferences: UserPreferences): { comparersToTest: Comparer[]; typeOrdersToTest: OrganizeImportsTypeOrder[]; } { // Returns the possible detection outcomes, given the user's preferences. The earlier in the list, the higher the priority. return { comparersToTest: typeof preferences.organizeImportsIgnoreCase === "boolean" @@ -841,10 +841,7 @@ export function detectModuleSpecifierCaseBySort(importDeclsByGroup: (readonly An } /** @internal */ -export type TypeOrder = "first" | "last" | "inline"; - -/** @internal */ -export function detectNamedImportOrganizationBySort(originalGroups: readonly ImportDeclaration[], comparersToTest: Comparer[], typesToTest: TypeOrder[]): { namedImportComparer: Comparer; typeOrder?: "first" | "last" | "inline"; isSorted: boolean; } | undefined { +export function detectNamedImportOrganizationBySort(originalGroups: readonly ImportDeclaration[], comparersToTest: Comparer[], typesToTest: OrganizeImportsTypeOrder[]): { namedImportComparer: Comparer; typeOrder?: OrganizeImportsTypeOrder; isSorted: boolean; } | undefined { // Filter for import declarations with named imports. Will be a flat array of import declarations without separations by group let bothNamedImports = false; const importDeclsWithNamed = originalGroups.filter(i => { @@ -881,7 +878,7 @@ export function detectNamedImportOrganizationBySort(originalGroups: readonly Imp } } for (const key of typesToTest) { - const typeOrder = key as TypeOrder; + const typeOrder = key as OrganizeImportsTypeOrder; if (currDiff[typeOrder] < bestDiff[typeOrder]) { bestDiff[typeOrder] = currDiff[typeOrder]; bestComparer[typeOrder] = curComparer; @@ -890,9 +887,9 @@ export function detectNamedImportOrganizationBySort(originalGroups: readonly Imp } outer: for (const bestKey of typesToTest) { - const bestTypeOrder = bestKey as TypeOrder; + const bestTypeOrder = bestKey as OrganizeImportsTypeOrder; for (const testKey of typesToTest) { - const testTypeOrder = testKey as TypeOrder; + const testTypeOrder = testKey as OrganizeImportsTypeOrder; if (bestDiff[testTypeOrder] < bestDiff[bestTypeOrder]) continue outer; } return { namedImportComparer: bestComparer[bestTypeOrder], typeOrder: bestTypeOrder, isSorted: bestDiff[bestTypeOrder] === 0 }; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index bf2b10738827f..c4b4ad5957c92 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2939,7 +2939,7 @@ declare namespace ts { * * Default: `last` */ - readonly organizeImportsTypeOrder?: "last" | "first" | "inline"; + readonly organizeImportsTypeOrder?: ts.OrganizeImportsTypeOrder; /** * Indicates whether {@link ReferencesResponseItem.lineText} is supported. */ @@ -8808,9 +8808,10 @@ declare namespace ts { readonly organizeImportsNumericCollation?: boolean; readonly organizeImportsAccentCollation?: boolean; readonly organizeImportsCaseFirst?: "upper" | "lower" | false; - readonly organizeImportsTypeOrder?: "first" | "last" | "inline"; + readonly organizeImportsTypeOrder?: OrganizeImportsTypeOrder; readonly excludeLibrarySymbolsInNavTo?: boolean; } + type OrganizeImportsTypeOrder = "first" | "last" | "inline"; /** Represents a bigint literal value without requiring bigint support */ interface PseudoBigInt { negative: boolean; From 97e3e7547efb15c242357f5a03f668e0da34b9f5 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Tue, 27 Feb 2024 16:18:03 -0800 Subject: [PATCH 25/33] fix autoimport type order bug --- src/server/protocol.ts | 3 +- src/services/organizeImports.ts | 8 +++- tests/baselines/reference/api/typescript.d.ts | 2 +- .../importNameCodeFix_importType7.ts | 37 +++++++++---------- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 2f739c23e8389..6190f91c0adfb 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -6,6 +6,7 @@ import type { HighlightSpanKind, InteractiveRefactorArguments, MapLike, + OrganizeImportsTypeOrder, OutliningSpanKind, OutputFile, PluginImport, @@ -3649,7 +3650,7 @@ export interface UserPreferences { * * Default: `last` */ - readonly organizeImportsTypeOrder?: ts.OrganizeImportsTypeOrder; + readonly organizeImportsTypeOrder?: OrganizeImportsTypeOrder; /** * Indicates whether {@link ReferencesResponseItem.lineText} is supported. diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index 4e8695cd530e8..ed7a031dd17b9 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -841,7 +841,7 @@ export function detectModuleSpecifierCaseBySort(importDeclsByGroup: (readonly An } /** @internal */ -export function detectNamedImportOrganizationBySort(originalGroups: readonly ImportDeclaration[], comparersToTest: Comparer[], typesToTest: OrganizeImportsTypeOrder[]): { namedImportComparer: Comparer; typeOrder?: OrganizeImportsTypeOrder; isSorted: boolean; } | undefined { +export function detectNamedImportOrganizationBySort(originalGroups: readonly ImportDeclaration[], comparersToTest: Comparer[], typesToTest: OrganizeImportsTypeOrder[]): { namedImportComparer: Comparer; typeOrder: OrganizeImportsTypeOrder | undefined; isSorted: boolean; } | undefined { // Filter for import declarations with named imports. Will be a flat array of import declarations without separations by group let bothNamedImports = false; const importDeclsWithNamed = originalGroups.filter(i => { @@ -864,7 +864,11 @@ export function detectNamedImportOrganizationBySort(originalGroups: readonly Imp // If we don't have any import statements with both named regular and type imports, we do not need to detect a type ordering if (!bothNamedImports || typesToTest.length === 0) { const sortState = detectCaseSensitivityBySort(namedImportsByDecl.map(i => i.map(n => n.name.text)), comparersToTest); - return { namedImportComparer: sortState.comparer, isSorted: sortState.isSorted }; + return { + namedImportComparer: sortState.comparer, + typeOrder: typesToTest.length === 1 ? typesToTest[0] : undefined, + isSorted: sortState.isSorted, + }; } const bestDiff = { first: Infinity, last: Infinity, inline: Infinity }; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index c4b4ad5957c92..4f048ad8ceffd 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2939,7 +2939,7 @@ declare namespace ts { * * Default: `last` */ - readonly organizeImportsTypeOrder?: ts.OrganizeImportsTypeOrder; + readonly organizeImportsTypeOrder?: OrganizeImportsTypeOrder; /** * Indicates whether {@link ReferencesResponseItem.lineText} is supported. */ diff --git a/tests/cases/fourslash/importNameCodeFix_importType7.ts b/tests/cases/fourslash/importNameCodeFix_importType7.ts index 9e32630420e42..b218c6623f5c8 100644 --- a/tests/cases/fourslash/importNameCodeFix_importType7.ts +++ b/tests/cases/fourslash/importNameCodeFix_importType7.ts @@ -34,22 +34,21 @@ new SomePig`], /*errorCode*/ undefined, { organizeImportsTypeOrder: "last" }); -//TODO: bugged cases -// verify.importFixAtPosition([ -// `import { -// type SomeInterface, -// SomePig, -// } from "./exports.js"; -// new SomePig`], -// /*errorCode*/ undefined, -// { organizeImportsTypeOrder: "inline" } -// ); - -// verify.importFixAtPosition([ -// `import { -// type SomeInterface, -// SomePig, -// } from "./exports.js"; -// new SomePig`], -// /*errorCode*/ undefined, -// { organizeImportsTypeOrder: "first" }); +verify.importFixAtPosition([ +`import { + type SomeInterface, + SomePig, +} from "./exports.js"; +new SomePig`], + /*errorCode*/ undefined, + { organizeImportsTypeOrder: "inline" } +); + +verify.importFixAtPosition([ +`import { + type SomeInterface, + SomePig, +} from "./exports.js"; +new SomePig`], +/*errorCode*/ undefined, +{ organizeImportsTypeOrder: "first" }); From 5d0517ff834df38b19ae12de6cd914e010241664 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Tue, 5 Mar 2024 13:56:16 -0800 Subject: [PATCH 26/33] remove unused --- src/compiler/core.ts | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 1ee2fefe16048..9abe57598be8b 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -848,38 +848,6 @@ export function arrayIsSorted(array: readonly T[], comparer: Comparer) { return true; } -/** @internal */ -export const enum SortKind { - None = 0, - CaseSensitive = 1 << 0, - CaseInsensitive = 1 << 1, - Both = CaseSensitive | CaseInsensitive, -} - -/** @internal */ -export function detectSortCaseSensitivity( - array: readonly T[], - getString: (element: T) => string, - compareStringsCaseSensitive: Comparer, - compareStringsCaseInsensitive: Comparer, -): SortKind { - let kind = SortKind.Both; - if (array.length < 2) return kind; - - let prevElement = getString(array[0]); - for (let i = 1, len = array.length; i < len && kind !== SortKind.None; i++) { - const element = getString(array[i]); - if (kind & SortKind.CaseSensitive && compareStringsCaseSensitive(prevElement, element) > 0) { - kind &= ~SortKind.CaseSensitive; - } - if (kind & SortKind.CaseInsensitive && compareStringsCaseInsensitive(prevElement, element) > 0) { - kind &= ~SortKind.CaseInsensitive; - } - prevElement = element; - } - return kind; -} - /** @internal */ export function arrayIsEqualTo(array1: readonly T[] | undefined, array2: readonly T[] | undefined, equalityComparer: (a: T, b: T, index: number) => boolean = equateValues): boolean { if (!array1 || !array2) { From 10f64a2bcbb94662695ffc2a0e3bb58c18a17264 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Thu, 7 Mar 2024 23:36:47 -0800 Subject: [PATCH 27/33] in progress--moving to codespace --- src/compiler/types.ts | 2 +- src/services/codefixes/importFixes.ts | 22 +++-------- src/services/organizeImports.ts | 37 +++++++++++++++++-- src/services/utilities.ts | 6 +-- .../unittests/services/organizeImports.ts | 35 ++++++++++++++++++ tests/baselines/reference/api/typescript.d.ts | 2 +- .../reference/organizeImports/detection9.ts | 6 +++ 7 files changed, 83 insertions(+), 27 deletions(-) create mode 100644 tests/baselines/reference/organizeImports/detection9.ts diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 3c2a5dd87009c..1fe089c8ba1d4 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -10098,7 +10098,7 @@ export interface UserPreferences { readonly excludeLibrarySymbolsInNavTo?: boolean; } -export type OrganizeImportsTypeOrder = "first" | "last" | "inline"; +export type OrganizeImportsTypeOrder = "last" | "inline" | "first"; /** Represents a bigint literal value without requiring bigint support */ export interface PseudoBigInt { diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 14043e64b48bb..0047fc7834d8a 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -1406,18 +1406,7 @@ function promoteFromTypeOnly( if (aliasDeclaration.isTypeOnly) { if (aliasDeclaration.parent.elements.length > 1) { const newSpecifier = factory.updateImportSpecifier(aliasDeclaration, /*isTypeOnly*/ false, aliasDeclaration.propertyName, aliasDeclaration.name); - const { comparersToTest, typeOrdersToTest } = OrganizeImports.getDetectionLists(preferences); - let specifierComparer = OrganizeImports.getOrganizeImportsSpecifierComparer(preferences, comparersToTest[0]); - - if (typeof preferences.organizeImportsIgnoreCase !== "boolean" || !preferences.organizeImportsTypeOrder) { - const importDecl: ImportDeclaration[] = [aliasDeclaration.parent.parent.parent]; - const namedImportSort = OrganizeImports.detectNamedImportOrganizationBySort(importDecl, comparersToTest, typeOrdersToTest); - if (namedImportSort) { - const { namedImportComparer, typeOrder } = namedImportSort; - specifierComparer = OrganizeImports.getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); - } - } - + const { specifierComparer } = OrganizeImports.getOrganizeImportsSpecifierComparerWithDetection(aliasDeclaration.parent.parent.parent, preferences, sourceFile); const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, specifierComparer); if (insertionIndex !== aliasDeclaration.parent.elements.indexOf(aliasDeclaration)) { changes.delete(sourceFile, aliasDeclaration); @@ -1460,10 +1449,9 @@ function promoteFromTypeOnly( if (convertExistingToTypeOnly) { const namedImports = tryCast(importClause.namedBindings, isNamedImports); if (namedImports && namedImports.elements.length > 1) { - const { comparersToTest, typeOrdersToTest } = OrganizeImports.getDetectionLists(preferences); - const sortState = OrganizeImports.detectNamedImportOrganizationBySort([importClause.parent], comparersToTest, typeOrdersToTest); + const sortState = OrganizeImports.getOrganizeImportsSpecifierComparerWithDetection(importClause.parent, preferences, sourceFile); if ( - (!sortState || sortState.isSorted) && + (sortState.isSorted !== false) && aliasDeclaration.kind === SyntaxKind.ImportSpecifier && namedImports.elements.indexOf(aliasDeclaration) !== 0 ) { @@ -1513,10 +1501,10 @@ function doAddExistingFix( // - if the user preference is explicit, use that // - otherwise, if there are enough existing import specifiers in this import to detect unambiguously, use that // - otherwise, detect from other imports in the file - // let ignoreCaseForSorting: boolean | undefined; - let specifierComparer: Comparer = OrganizeImports.getOrganizeImportsSpecifierComparer(preferences); const { comparersToTest, typeOrdersToTest } = OrganizeImports.getDetectionLists(preferences); let namedImportComparer = comparersToTest[0]; + // TODO update the detection code (it's been refactored) + let specifierComparer: Comparer = OrganizeImports.getOrganizeImportsSpecifierComparer(preferences, namedImportComparer); let typeOrder: OrganizeImportsTypeOrder | undefined = typeOrdersToTest[0]; let isSorted: boolean | undefined; diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index ed7a031dd17b9..bbd51db204e0b 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -666,6 +666,33 @@ export function getOrganizeImportsSpecifierComparer compareImportOrExportSpecifiers(s1, s2, stringComparer, preferences); } +/** @internal */ +export function getOrganizeImportsSpecifierComparerWithDetection(importDecl: ImportDeclaration, preferences: UserPreferences, sourceFile?: SourceFile): { specifierComparer: Comparer, isSorted: boolean | undefined } { + const { comparersToTest, typeOrdersToTest } = getDetectionLists(preferences); + const detectFromDecl = detectNamedImportOrganizationBySort([importDecl], comparersToTest, typeOrdersToTest); + let specifierComparer = getOrganizeImportsSpecifierComparer(preferences, comparersToTest[0]); + let isSorted; + + if (typeof preferences.organizeImportsIgnoreCase !== "boolean" || !preferences.organizeImportsTypeOrder) { + if (detectFromDecl) { + const { namedImportComparer, typeOrder, isSorted: isDetectedSorted } = detectFromDecl; + isSorted = isDetectedSorted; + specifierComparer = getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); + } + else if (sourceFile) { + // If a sourceFile is specified, we can also try detecting using the other import statements + const detectFromFile = detectNamedImportOrganizationBySort(sourceFile.statements.filter(isImportDeclaration), comparersToTest, typeOrdersToTest); + if (detectFromFile) { + const { namedImportComparer, typeOrder, isSorted: isDetectedSorted } = detectFromFile; + isSorted = isDetectedSorted; + specifierComparer = getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); + } + } + } + + return { specifierComparer, isSorted }; +} + /** * Exported for testing * @@ -791,6 +818,11 @@ function getOrganizeImportsLocale(preferences: UserPreferences): string { return resolvedLocale; } +/** @internal */ +export function getOrganizeImportsComparerWithDetection(originalImportDecls: readonly AnyImportOrRequireStatement[], preferences: UserPreferences): { comparer: Comparer, isSorted: boolean } { + return detectModuleSpecifierCaseBySort([originalImportDecls], getDetectionLists(preferences).comparersToTest); +} + /** @internal */ export function getOrganizeImportsComparer(preferences: UserPreferences, ignoreCase: boolean): Comparer { const collation = preferences.organizeImportsCollation ?? "ordinal"; @@ -841,7 +873,7 @@ export function detectModuleSpecifierCaseBySort(importDeclsByGroup: (readonly An } /** @internal */ -export function detectNamedImportOrganizationBySort(originalGroups: readonly ImportDeclaration[], comparersToTest: Comparer[], typesToTest: OrganizeImportsTypeOrder[]): { namedImportComparer: Comparer; typeOrder: OrganizeImportsTypeOrder | undefined; isSorted: boolean; } | undefined { +export function detectNamedImportOrganizationBySort(originalGroups: readonly ImportDeclaration[], comparersToTest: Comparer[], typesToTest: OrganizeImportsTypeOrder[]): { namedImportComparer: Comparer; typeOrder: OrganizeImportsTypeOrder | undefined; isSorted: boolean; } | undefined { // Filter for import declarations with named imports. Will be a flat array of import declarations without separations by group let bothNamedImports = false; const importDeclsWithNamed = originalGroups.filter(i => { @@ -919,9 +951,6 @@ function detectCaseSensitivityBySort(originalGroups: string[][], comparersToTest let bestComparer; let bestDiff = Infinity; - if (comparersToTest.length === 1) { - return { comparer: comparersToTest[0], isSorted: true }; - } for (const curComparer of comparersToTest) { let diffOfCurrentComparer = 0; diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 02b013394702f..6cac36786b358 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -4,7 +4,6 @@ import { addSyntheticLeadingComment, addSyntheticTrailingComment, AnyImportOrRequireStatement, - arrayIsSorted, assertType, AssignmentDeclarationKind, BinaryExpression, @@ -2624,13 +2623,12 @@ export function insertImports(changes: textChanges.ChangeTracker, sourceFile: So const decl = isArray(imports) ? imports[0] : imports; const importKindPredicate: (node: Node) => node is AnyImportOrRequireStatement = decl.kind === SyntaxKind.VariableStatement ? isRequireVariableStatement : isAnyImportSyntax; const existingImportStatements = filter(sourceFile.statements, importKindPredicate); - const moduleSpecifiersToDetect = existingImportStatements.length > 1 ? [existingImportStatements] : isArray(imports) ? [imports] : [existingImportStatements]; - const { comparer } = OrganizeImports.detectModuleSpecifierCaseBySort(moduleSpecifiersToDetect, OrganizeImports.getDetectionLists(preferences).comparersToTest); + const { comparer, isSorted } = OrganizeImports.getOrganizeImportsComparerWithDetection(existingImportStatements, preferences); const sortedNewImports = isArray(imports) ? stableSort(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports]; if (!existingImportStatements.length) { changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); } - else if (existingImportStatements && arrayIsSorted(existingImportStatements, (s1, s2) => OrganizeImports.compareImportsOrRequireStatements(s1, s2, comparer))) { + else if (existingImportStatements && isSorted) { for (const newImport of sortedNewImports) { const insertionIndex = OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport, comparer); if (insertionIndex === 0) { diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index 5ba7acd913ca1..48eb23895099a 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -440,6 +440,41 @@ const x = abc + Abc + I + M + R;`, console.log(Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9, func1, func2, func3, func4, func5, func6, func7, func8, func9);`, }); + testOrganizeImports("detection6", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: +`import { A, B, a, b } from 'foo'; +console.log(A, B, a, b);` + }); + + testOrganizeImports("detection7", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: +`import { A, a, B, b } from 'foo'; +console.log(A, B, a, b);` + }); + + testOrganizeImports("detection8", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: +`import { A, a, b, B } from 'foo'; +console.log(A, B, a, b);` + }); + + testOrganizeImports("detection9", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: +`import { a, b, A, B } from 'foo'; +console.log(A, B, a, b);` + }); + + testOrganizeImports("detection10", /*skipDestructiveCodeActions*/ false, { + path: "/test.ts", + content: +`import { a, A, b, B } from 'foo'; +console.log(A, B, a, b);` + }); + testOrganizeImports("Renamed_used", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", content: ` diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 4f048ad8ceffd..0b85a66fbb853 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -8811,7 +8811,7 @@ declare namespace ts { readonly organizeImportsTypeOrder?: OrganizeImportsTypeOrder; readonly excludeLibrarySymbolsInNavTo?: boolean; } - type OrganizeImportsTypeOrder = "first" | "last" | "inline"; + type OrganizeImportsTypeOrder = "last" | "inline" | "first"; /** Represents a bigint literal value without requiring bigint support */ interface PseudoBigInt { negative: boolean; diff --git a/tests/baselines/reference/organizeImports/detection9.ts b/tests/baselines/reference/organizeImports/detection9.ts new file mode 100644 index 0000000000000..1079617d5220e --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection9.ts @@ -0,0 +1,6 @@ +// ==ORIGINAL== +import { a, b, A, B } from 'foo'; +console.log(A, B, a, b); +// ==ORGANIZED== +import { a, A, b, B } from 'foo'; +console.log(A, B, a, b); \ No newline at end of file From 0364735afb51fc49f436dba4222a3c017649c46c Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Fri, 8 Mar 2024 11:37:29 -0800 Subject: [PATCH 28/33] add detection baselines --- .../unittests/services/organizeImports.ts | 51 +++++++++++++------ .../reference/organizeImports/detection10.ts | 6 +++ .../reference/organizeImports/detection6.ts | 6 +++ .../reference/organizeImports/detection7.ts | 6 +++ .../reference/organizeImports/detection8.ts | 6 +++ 5 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 tests/baselines/reference/organizeImports/detection10.ts create mode 100644 tests/baselines/reference/organizeImports/detection6.ts create mode 100644 tests/baselines/reference/organizeImports/detection7.ts create mode 100644 tests/baselines/reference/organizeImports/detection8.ts diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index 48eb23895099a..e4a1938903322 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -387,35 +387,35 @@ export const Other = 1; assert.isEmpty(changes); }); - testOrganizeImports("detection1", /*skipDestructiveCodeActions*/ false, { + testDetectionBaseline("detection1", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", content: `import { abc, Abc } from 'b'; import { I, M, R } from 'a'; const x = abc + Abc + I + M + R;`, }); - testOrganizeImports("detection2", /*skipDestructiveCodeActions*/ false, { + testDetectionBaseline("detection2", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", content: `import { abc, Abc } from 'a'; import { I, M, R } from 'b'; const x = abc + Abc + I + M + R;`, }); - testOrganizeImports("detection3", /*skipDestructiveCodeActions*/ false, { + testDetectionBaseline("detection3", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", content: `import { I, M, R } from 'a'; import { Abc, abc } from 'b'; const x = abc + Abc + I + M + R;`, }); - testOrganizeImports("detection4", /*skipDestructiveCodeActions*/ false, { + testDetectionBaseline("detection4", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", content: `import { I, M, R } from 'a'; import { abc, Abc } from 'b'; const x = abc + Abc + I + M + R;`, }); - testOrganizeImports("detection5", /*skipDestructiveCodeActions*/ false, { + testDetectionBaseline("detection5", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", content: `import { Type9, @@ -440,37 +440,37 @@ const x = abc + Abc + I + M + R;`, console.log(Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9, func1, func2, func3, func4, func5, func6, func7, func8, func9);`, }); - testOrganizeImports("detection6", /*skipDestructiveCodeActions*/ false, { + testDetectionBaseline("detection6", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", - content: + content: `import { A, B, a, b } from 'foo'; console.log(A, B, a, b);` }); - testOrganizeImports("detection7", /*skipDestructiveCodeActions*/ false, { + testDetectionBaseline("detection7", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", - content: + content: `import { A, a, B, b } from 'foo'; console.log(A, B, a, b);` }); - testOrganizeImports("detection8", /*skipDestructiveCodeActions*/ false, { + testDetectionBaseline("detection8", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", - content: + content: `import { A, a, b, B } from 'foo'; console.log(A, B, a, b);` }); - testOrganizeImports("detection9", /*skipDestructiveCodeActions*/ false, { + testDetectionBaseline("detection9", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", - content: + content: `import { a, b, A, B } from 'foo'; console.log(A, B, a, b);` }); - testOrganizeImports("detection10", /*skipDestructiveCodeActions*/ false, { + testDetectionBaseline("detection10", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", - content: + content: `import { a, A, b, B } from 'foo'; console.log(A, B, a, b);` }); @@ -1069,6 +1069,27 @@ export * from "lib"; ); } + function testDetectionBaseline(testName: string, skipDestructiveCodeActions: boolean, testFile: File, ... otherFiles: File[]) { + it(testName, () => { + // this differs from the test above, in that it doesn't assert that there are changes + const baselinePath = `organizeImports/${testName}.ts`; + const { path: testPath, content: testContent } = testFile; + const languageService = makeLanguageService(testFile, ...otherFiles); + const changes = languageService.organizeImports({ skipDestructiveCodeActions, type: "file", fileName: testPath }, ts.testFormatSettings, ts.emptyOptions); + + const newText = changes.length ? ts.textChanges.applyChanges(testContent, changes[0].textChanges) : testContent; + Harness.Baseline.runBaseline( + baselinePath, + [ + "// ==ORIGINAL==", + testContent, + "// ==ORGANIZED==", + newText, + ].join(newLineCharacter), + ); + }); + } + function makeLanguageService(...files: File[]) { const host = createServerHost(files); const projectService = new TestProjectService({ host, useSingleInferredProject: true }); diff --git a/tests/baselines/reference/organizeImports/detection10.ts b/tests/baselines/reference/organizeImports/detection10.ts new file mode 100644 index 0000000000000..2ffeb72c48871 --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection10.ts @@ -0,0 +1,6 @@ +// ==ORIGINAL== +import { a, A, b, B } from 'foo'; +console.log(A, B, a, b); +// ==ORGANIZED== +import { a, A, b, B } from 'foo'; +console.log(A, B, a, b); \ No newline at end of file diff --git a/tests/baselines/reference/organizeImports/detection6.ts b/tests/baselines/reference/organizeImports/detection6.ts new file mode 100644 index 0000000000000..75cab8092d135 --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection6.ts @@ -0,0 +1,6 @@ +// ==ORIGINAL== +import { A, B, a, b } from 'foo'; +console.log(A, B, a, b); +// ==ORGANIZED== +import { A, B, a, b } from 'foo'; +console.log(A, B, a, b); \ No newline at end of file diff --git a/tests/baselines/reference/organizeImports/detection7.ts b/tests/baselines/reference/organizeImports/detection7.ts new file mode 100644 index 0000000000000..183961ef15dd7 --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection7.ts @@ -0,0 +1,6 @@ +// ==ORIGINAL== +import { A, a, B, b } from 'foo'; +console.log(A, B, a, b); +// ==ORGANIZED== +import { A, a, B, b } from 'foo'; +console.log(A, B, a, b); \ No newline at end of file diff --git a/tests/baselines/reference/organizeImports/detection8.ts b/tests/baselines/reference/organizeImports/detection8.ts new file mode 100644 index 0000000000000..791ff2994e2de --- /dev/null +++ b/tests/baselines/reference/organizeImports/detection8.ts @@ -0,0 +1,6 @@ +// ==ORIGINAL== +import { A, a, b, B } from 'foo'; +console.log(A, B, a, b); +// ==ORGANIZED== +import { A, a, b, B } from 'foo'; +console.log(A, B, a, b); \ No newline at end of file From 567260418bc53ebf4f14dfd6962ce177b37e887e Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Fri, 8 Mar 2024 14:46:40 -0800 Subject: [PATCH 29/33] finish updating autoImports --- src/services/codefixes/importFixes.ts | 35 +++------------------------ src/services/utilities.ts | 2 +- 2 files changed, 5 insertions(+), 32 deletions(-) diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 0047fc7834d8a..cb427157854bf 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -3,7 +3,6 @@ import { AnyImportOrRequireStatement, AnyImportSyntax, arrayFrom, - arrayIsSorted, CancellationToken, cast, changeAnyExtension, @@ -13,7 +12,6 @@ import { combine, compareBooleans, compareNumberOfDirectorySeparators, - Comparer, compareValues, Comparison, CompilerOptions, @@ -60,13 +58,11 @@ import { hostGetCanonicalFileName, Identifier, ImportClause, - ImportDeclaration, ImportEqualsDeclaration, importFromModuleSpecifier, ImportKind, importNameElisionDisabled, ImportsNotUsedAsValues, - ImportSpecifier, insertImports, InternalSymbolName, isExternalModule, @@ -111,7 +107,6 @@ import { nodeIsMissing, ObjectBindingPattern, OrganizeImports, - OrganizeImportsTypeOrder, PackageJsonImportFilter, Path, pathContainsNodeModules, @@ -1406,7 +1401,7 @@ function promoteFromTypeOnly( if (aliasDeclaration.isTypeOnly) { if (aliasDeclaration.parent.elements.length > 1) { const newSpecifier = factory.updateImportSpecifier(aliasDeclaration, /*isTypeOnly*/ false, aliasDeclaration.propertyName, aliasDeclaration.name); - const { specifierComparer } = OrganizeImports.getOrganizeImportsSpecifierComparerWithDetection(aliasDeclaration.parent.parent.parent, preferences, sourceFile); + const { specifierComparer } = OrganizeImports.getNamedImportSpecifierComparerWithDetection(aliasDeclaration.parent.parent.parent, preferences, sourceFile); const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, specifierComparer); if (insertionIndex !== aliasDeclaration.parent.elements.indexOf(aliasDeclaration)) { changes.delete(sourceFile, aliasDeclaration); @@ -1449,7 +1444,7 @@ function promoteFromTypeOnly( if (convertExistingToTypeOnly) { const namedImports = tryCast(importClause.namedBindings, isNamedImports); if (namedImports && namedImports.elements.length > 1) { - const sortState = OrganizeImports.getOrganizeImportsSpecifierComparerWithDetection(importClause.parent, preferences, sourceFile); + const sortState = OrganizeImports.getNamedImportSpecifierComparerWithDetection(importClause.parent, preferences, sourceFile); if ( (sortState.isSorted !== false) && aliasDeclaration.kind === SyntaxKind.ImportSpecifier && @@ -1497,29 +1492,7 @@ function doAddExistingFix( } if (namedImports.length) { - // sort case sensitivity: - // - if the user preference is explicit, use that - // - otherwise, if there are enough existing import specifiers in this import to detect unambiguously, use that - // - otherwise, detect from other imports in the file - const { comparersToTest, typeOrdersToTest } = OrganizeImports.getDetectionLists(preferences); - let namedImportComparer = comparersToTest[0]; - // TODO update the detection code (it's been refactored) - let specifierComparer: Comparer = OrganizeImports.getOrganizeImportsSpecifierComparer(preferences, namedImportComparer); - let typeOrder: OrganizeImportsTypeOrder | undefined = typeOrdersToTest[0]; - let isSorted: boolean | undefined; - - if (existingSpecifiers && (typeof preferences.organizeImportsIgnoreCase !== "boolean" || !preferences.organizeImportsTypeOrder)) { - const moduleSpecifiersDecl: ImportDeclaration[] = [clause.parent]; - const namedImportSort = OrganizeImports.detectNamedImportOrganizationBySort(moduleSpecifiersDecl, comparersToTest, typeOrdersToTest); - if (namedImportSort) { - ({ namedImportComparer, typeOrder, isSorted } = namedImportSort); - specifierComparer = OrganizeImports.getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); - } - } - else { - isSorted = existingSpecifiers ? arrayIsSorted(existingSpecifiers, specifierComparer) : true; - } - + const { specifierComparer, isSorted } = OrganizeImports.getNamedImportSpecifierComparerWithDetection(clause.parent, preferences, sourceFile); const newSpecifiers = stableSort( namedImports.map(namedImport => factory.createImportSpecifier( @@ -1538,7 +1511,7 @@ function doAddExistingFix( // to do a sorted insertion. // changed to check if existing specifiers are sorted - if (existingSpecifiers?.length && isSorted) { + if (existingSpecifiers?.length && isSorted !== false) { for (const spec of newSpecifiers) { const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, specifierComparer); changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex); diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 6cac36786b358..62443ac2049b6 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -2623,7 +2623,7 @@ export function insertImports(changes: textChanges.ChangeTracker, sourceFile: So const decl = isArray(imports) ? imports[0] : imports; const importKindPredicate: (node: Node) => node is AnyImportOrRequireStatement = decl.kind === SyntaxKind.VariableStatement ? isRequireVariableStatement : isAnyImportSyntax; const existingImportStatements = filter(sourceFile.statements, importKindPredicate); - const { comparer, isSorted } = OrganizeImports.getOrganizeImportsComparerWithDetection(existingImportStatements, preferences); + const { comparer, isSorted } = OrganizeImports.getOrganizeImportsStringComparerWithDetection(existingImportStatements, preferences); const sortedNewImports = isArray(imports) ? stableSort(imports, (a, b) => OrganizeImports.compareImportsOrRequireStatements(a, b, comparer)) : [imports]; if (!existingImportStatements.length) { changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween); From fa5bbdbc2437b2b5e5af8a3d9dd1df0f8e5e9ccc Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Fri, 8 Mar 2024 17:01:09 -0800 Subject: [PATCH 30/33] clean up organizeImports file --- src/compiler/types.ts | 6 +- src/services/organizeImports.ts | 503 +++++++++--------- .../unittests/services/organizeImports.ts | 27 +- 3 files changed, 262 insertions(+), 274 deletions(-) diff --git a/src/compiler/types.ts b/src/compiler/types.ts index d631ec3e9a14d..30feee6d61e52 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -9966,9 +9966,9 @@ export interface UserPreferences { /** * Indicates where named type-only imports should sort. "inline" sorts named imports without regard to if the import is * type-only. - * - * Default: `last` - */ + * + * Default: `last` + */ readonly organizeImportsTypeOrder?: OrganizeImportsTypeOrder; /** * Indicates whether to exclude standard library and node_modules file symbols from navTo results. diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index bbd51db204e0b..a6a300ed82df4 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -93,23 +93,20 @@ export function organizeImports( const { comparersToTest, typeOrdersToTest } = getDetectionLists(preferences); const defaultComparer = comparersToTest[0]; + + // If case sensitivity is specified (true/false), then use the same setting for both. const comparer = { moduleSpecifierComparer: typeof preferences.organizeImportsIgnoreCase === "boolean" ? defaultComparer : undefined, namedImportComparer: typeof preferences.organizeImportsIgnoreCase === "boolean" ? defaultComparer : undefined, typeOrder: preferences.organizeImportsTypeOrder, }; - - if (typeof preferences.organizeImportsIgnoreCase === "boolean") { - // If case sensitivity is specified (true/false), then use the same setting for both. - comparer.moduleSpecifierComparer = getOrganizeImportsComparer(preferences, preferences.organizeImportsIgnoreCase); - comparer.namedImportComparer = comparer.moduleSpecifierComparer; - } - else { + if (typeof preferences.organizeImportsIgnoreCase !== "boolean") { // Otherwise, we must test for case-sensitivity. Named import case sensitivity will be tested with type order ({ comparer: comparer.moduleSpecifierComparer } = detectModuleSpecifierCaseBySort(topLevelImportGroupDecls, comparersToTest)); } - if (!comparer.typeOrder) { + if (!comparer.typeOrder || typeof preferences.organizeImportsIgnoreCase !== "boolean") { + // Through getDetectionLists, the set of orders returned will be compatible const namedImportSort = detectNamedImportOrganizationBySort(topLevelImportDecls, comparersToTest, typeOrdersToTest); if (namedImportSort) { const { namedImportComparer, typeOrder } = namedImportSort; @@ -196,7 +193,7 @@ export function organizeImports( const detectedNamedImportCaseComparer = comparer.namedImportComparer ?? defaultComparer; const detectedTypeOrder = comparer.typeOrder ?? "last"; - const specifierComparer = getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: detectedTypeOrder }, detectedNamedImportCaseComparer); + const specifierComparer = getNamedImportSpecifierComparer({ organizeImportsTypeOrder: detectedTypeOrder }, detectedNamedImportCaseComparer); const processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => { if (shouldRemove) importGroup = removeUnusedImports(importGroup, sourceFile, program); if (shouldCombine) importGroup = coalesceImportsWorker(importGroup, detectedModuleCaseComparer, specifierComparer, sourceFile); @@ -208,7 +205,7 @@ export function organizeImports( } function organizeExportsWorker(oldExportDecls: readonly ExportDeclaration[], specifierCaseComparer?: Comparer) { - const useComparer = getOrganizeImportsSpecifierComparer(preferences, specifierCaseComparer); + const useComparer = getNamedImportSpecifierComparer(preferences, specifierCaseComparer); organizeDeclsWorker(oldExportDecls, group => coalesceExportsWorker(group, useComparer)); } } @@ -218,8 +215,8 @@ export function getDetectionLists(preferences: UserPreferences): { comparersToTe // Returns the possible detection outcomes, given the user's preferences. The earlier in the list, the higher the priority. return { comparersToTest: typeof preferences.organizeImportsIgnoreCase === "boolean" - ? [getOrganizeImportsComparer(preferences, preferences.organizeImportsIgnoreCase)] - : [getOrganizeImportsComparer(preferences, /*ignoreCase*/ true), getOrganizeImportsComparer(preferences, /*ignoreCase*/ false)], + ? [getOrganizeImportsStringComparer(preferences, preferences.organizeImportsIgnoreCase)] + : [getOrganizeImportsStringComparer(preferences, /*ignoreCase*/ true), getOrganizeImportsStringComparer(preferences, /*ignoreCase*/ false)], typeOrdersToTest: preferences.organizeImportsTypeOrder ? [preferences.organizeImportsTypeOrder] : ["last", "inline", "first"], }; } @@ -266,6 +263,37 @@ function isNewGroup(sourceFile: SourceFile, decl: ImportDeclaration | ExportDecl return false; } +function getTopLevelExportGroups(sourceFile: SourceFile) { + const topLevelExportGroups: ExportDeclaration[][] = []; + const statements = sourceFile.statements; + const len = length(statements); + + let i = 0; + let groupIndex = 0; + while (i < len) { + if (isExportDeclaration(statements[i])) { + if (topLevelExportGroups[groupIndex] === undefined) { + topLevelExportGroups[groupIndex] = []; + } + const exportDecl = statements[i] as ExportDeclaration; + if (exportDecl.moduleSpecifier) { + topLevelExportGroups[groupIndex].push(exportDecl); + i++; + } + else { + while (i < len && isExportDeclaration(statements[i])) { + topLevelExportGroups[groupIndex].push(statements[i++] as ExportDeclaration); + } + groupIndex++; + } + } + else { + i++; + } + } + return flatMap(topLevelExportGroups, exportGroupDecls => groupByNewlineContiguous(sourceFile, exportGroupDecls)); +} + function removeUnusedImports(oldImports: readonly ImportDeclaration[], sourceFile: SourceFile, program: Program) { const typeChecker = program.getTypeChecker(); const compilerOptions = program.getCompilerOptions(); @@ -341,30 +369,54 @@ function removeUnusedImports(oldImports: readonly ImportDeclaration[], sourceFil } } -function hasModuleDeclarationMatchingSpecifier(sourceFile: SourceFile, moduleSpecifier: Expression) { - const moduleSpecifierText = isStringLiteral(moduleSpecifier) && moduleSpecifier.text; - return isString(moduleSpecifierText) && some(sourceFile.moduleAugmentations, moduleName => - isStringLiteral(moduleName) - && moduleName.text === moduleSpecifierText); -} - function getExternalModuleName(specifier: Expression | undefined) { return specifier !== undefined && isStringLiteralLike(specifier) ? specifier.text : undefined; } -// Internal for testing -/** - * @param importGroup a list of ImportDeclarations, all with the same module name. +/* + * Returns entire import declarations because they may already have been rewritten and + * may lack parent pointers. The desired parts can easily be recovered based on the + * categorization. * - * @deprecated Only used for testing - * @internal + * NB: There may be overlap between `defaultImports` and `namespaceImports`/`namedImports`. */ -export function testCoalesceImports(importGroup: readonly ImportDeclaration[], ignoreCase: boolean, sourceFile?: SourceFile, preferences?: UserPreferences): readonly ImportDeclaration[] { - const comparer = getOrganizeImportsOrdinalStringComparer(ignoreCase); - const specifierComparer = getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder }, comparer); - return coalesceImportsWorker(importGroup, comparer, specifierComparer, sourceFile); +function getCategorizedImports(importGroup: readonly ImportDeclaration[]) { + let importWithoutClause: ImportDeclaration | undefined; + const typeOnlyImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; + const regularImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; + + for (const importDeclaration of importGroup) { + if (importDeclaration.importClause === undefined) { + // Only the first such import is interesting - the others are redundant. + // Note: Unfortunately, we will lose trivia that was on this node. + importWithoutClause = importWithoutClause || importDeclaration; + continue; + } + + const group = importDeclaration.importClause.isTypeOnly ? typeOnlyImports : regularImports; + const { name, namedBindings } = importDeclaration.importClause; + + if (name) { + group.defaultImports.push(importDeclaration as ImportDeclarationWithDefaultImport); + } + + if (namedBindings) { + if (isNamespaceImport(namedBindings)) { + group.namespaceImports.push(importDeclaration as ImportDeclarationWithNamespaceImport); + } + else { + group.namedImports.push(importDeclaration as ImportDeclarationWithNamedImports); + } + } + } + + return { + importWithoutClause, + typeOnlyImports, + regularImports, + }; } function coalesceImportsWorker(importGroup: readonly ImportDeclaration[], comparer: Comparer, specifierComparer: Comparer, sourceFile?: SourceFile): readonly ImportDeclaration[] { @@ -507,62 +559,6 @@ interface ImportGroup { namedImports: ImportDeclarationWithNamedImports[]; } -/* - * Returns entire import declarations because they may already have been rewritten and - * may lack parent pointers. The desired parts can easily be recovered based on the - * categorization. - * - * NB: There may be overlap between `defaultImports` and `namespaceImports`/`namedImports`. - */ -function getCategorizedImports(importGroup: readonly ImportDeclaration[]) { - let importWithoutClause: ImportDeclaration | undefined; - const typeOnlyImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; - const regularImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; - - for (const importDeclaration of importGroup) { - if (importDeclaration.importClause === undefined) { - // Only the first such import is interesting - the others are redundant. - // Note: Unfortunately, we will lose trivia that was on this node. - importWithoutClause = importWithoutClause || importDeclaration; - continue; - } - - const group = importDeclaration.importClause.isTypeOnly ? typeOnlyImports : regularImports; - const { name, namedBindings } = importDeclaration.importClause; - - if (name) { - group.defaultImports.push(importDeclaration as ImportDeclarationWithDefaultImport); - } - - if (namedBindings) { - if (isNamespaceImport(namedBindings)) { - group.namespaceImports.push(importDeclaration as ImportDeclarationWithNamespaceImport); - } - else { - group.namedImports.push(importDeclaration as ImportDeclarationWithNamedImports); - } - } - } - - return { - importWithoutClause, - typeOnlyImports, - regularImports, - }; -} - -// Internal for testing -/** - * @param exportGroup a list of ExportDeclarations, all with the same module name. - * - * @deprecated Only used for testing - * @internal - */ -export function testCoalesceExports(exportGroup: readonly ExportDeclaration[], ignoreCase: boolean, preferences?: UserPreferences) { - const comparer = (s1: ExportSpecifier, s2: ExportSpecifier) => compareImportOrExportSpecifiers(s1, s2, getOrganizeImportsOrdinalStringComparer(ignoreCase), { organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder ?? "last" }); - return coalesceExportsWorker(exportGroup, comparer); -} - function coalesceExportsWorker(exportGroup: readonly ExportDeclaration[], specifierComparer: Comparer) { if (exportGroup.length === 0) { return exportGroup; @@ -660,50 +656,6 @@ function compareImportOrExportSpecifiers(s1: } } -/** @internal */ -export function getOrganizeImportsSpecifierComparer(preferences: UserPreferences, comparer?: Comparer): Comparer { - const stringComparer = comparer ?? getOrganizeImportsOrdinalStringComparer(!!preferences.organizeImportsIgnoreCase); - return (s1, s2) => compareImportOrExportSpecifiers(s1, s2, stringComparer, preferences); -} - -/** @internal */ -export function getOrganizeImportsSpecifierComparerWithDetection(importDecl: ImportDeclaration, preferences: UserPreferences, sourceFile?: SourceFile): { specifierComparer: Comparer, isSorted: boolean | undefined } { - const { comparersToTest, typeOrdersToTest } = getDetectionLists(preferences); - const detectFromDecl = detectNamedImportOrganizationBySort([importDecl], comparersToTest, typeOrdersToTest); - let specifierComparer = getOrganizeImportsSpecifierComparer(preferences, comparersToTest[0]); - let isSorted; - - if (typeof preferences.organizeImportsIgnoreCase !== "boolean" || !preferences.organizeImportsTypeOrder) { - if (detectFromDecl) { - const { namedImportComparer, typeOrder, isSorted: isDetectedSorted } = detectFromDecl; - isSorted = isDetectedSorted; - specifierComparer = getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); - } - else if (sourceFile) { - // If a sourceFile is specified, we can also try detecting using the other import statements - const detectFromFile = detectNamedImportOrganizationBySort(sourceFile.statements.filter(isImportDeclaration), comparersToTest, typeOrdersToTest); - if (detectFromFile) { - const { namedImportComparer, typeOrder, isSorted: isDetectedSorted } = detectFromFile; - isSorted = isDetectedSorted; - specifierComparer = getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); - } - } - } - - return { specifierComparer, isSorted }; -} - -/** - * Exported for testing - * - * @deprecated Only used for testing - * @internal - */ -export function compareModuleSpecifiers(m1: Expression | undefined, m2: Expression | undefined, ignoreCase?: boolean) { - const comparer = getOrganizeImportsOrdinalStringComparer(!!ignoreCase); - return compareModuleSpecifiersWorker(m1, m2, comparer); -} - function compareModuleSpecifiersWorker(m1: Expression | undefined, m2: Expression | undefined, comparer: Comparer) { const name1 = m1 === undefined ? undefined : getExternalModuleName(m1); const name2 = m2 === undefined ? undefined : getExternalModuleName(m2); @@ -727,47 +679,11 @@ function getModuleSpecifierExpression(declaration: AnyImportOrRequireStatement): } } -/** @internal */ -export function getImportDeclarationInsertionIndex(sortedImports: readonly AnyImportOrRequireStatement[], newImport: AnyImportOrRequireStatement, comparer: Comparer) { - const index = binarySearch(sortedImports, newImport, identity, (a, b) => compareImportsOrRequireStatements(a, b, comparer)); - return index < 0 ? ~index : index; -} - -/** @internal */ -export function getImportSpecifierInsertionIndex(sortedImports: readonly ImportSpecifier[], newImport: ImportSpecifier, comparer: Comparer) { - const index = binarySearch(sortedImports, newImport, identity, comparer); - return index < 0 ? ~index : index; -} - -/** @internal */ -export function compareImportsOrRequireStatements(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement, comparer: Comparer) { - return compareModuleSpecifiersWorker(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2), comparer) || compareImportKind(s1, s2); -} - -function compareImportKind(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement) { - return compareValues(getImportKindOrder(s1), getImportKindOrder(s2)); -} - -// 1. Side-effect imports -// 2. Type-only imports -// 3. Namespace imports -// 4. Default imports -// 5. Named imports -// 6. ImportEqualsDeclarations -// 7. Require variable statements -function getImportKindOrder(s1: AnyImportOrRequireStatement) { - switch (s1.kind) { - case SyntaxKind.ImportDeclaration: - if (!s1.importClause) return 0; - if (s1.importClause.isTypeOnly) return 1; - if (s1.importClause.namedBindings?.kind === SyntaxKind.NamespaceImport) return 2; - if (s1.importClause.name) return 3; - return 4; - case SyntaxKind.ImportEqualsDeclaration: - return 5; - case SyntaxKind.VariableStatement: - return 6; - } +function hasModuleDeclarationMatchingSpecifier(sourceFile: SourceFile, moduleSpecifier: Expression) { + const moduleSpecifierText = isStringLiteral(moduleSpecifier) && moduleSpecifier.text; + return isString(moduleSpecifierText) && some(sourceFile.moduleAugmentations, moduleName => + isStringLiteral(moduleName) + && moduleName.text === moduleSpecifierText); } function getNewImportSpecifiers(namedImports: ImportDeclaration[]) { @@ -784,96 +700,16 @@ function tryGetNamedBindingElements(namedImport: ImportDeclaration) { : undefined; } -function getOrganizeImportsOrdinalStringComparer(ignoreCase: boolean) { - return ignoreCase ? compareStringsCaseInsensitiveEslintCompatible : compareStringsCaseSensitive; -} - -function getOrganizeImportsUnicodeStringComparer(ignoreCase: boolean, preferences: UserPreferences): Comparer { - const resolvedLocale = getOrganizeImportsLocale(preferences); - const caseFirst = preferences.organizeImportsCaseFirst ?? false; - const numeric = preferences.organizeImportsNumericCollation ?? false; - const accents = preferences.organizeImportsAccentCollation ?? true; - const sensitivity = ignoreCase ? - accents ? "accent" : "base" : - accents ? "variant" : "case"; - - const collator = new Intl.Collator(resolvedLocale, { - usage: "sort", - caseFirst: caseFirst || "false", - sensitivity, - numeric, - }); - - // `compare` is a bound method, so we do not need to close over `collator`. - return collator.compare; -} - -function getOrganizeImportsLocale(preferences: UserPreferences): string { - let locale = preferences.organizeImportsLocale; - if (locale === "auto") locale = getUILocale(); - if (locale === undefined) locale = "en"; - - const supportedLocales = Intl.Collator.supportedLocalesOf(locale); - const resolvedLocale = supportedLocales.length ? supportedLocales[0] : "en"; - return resolvedLocale; -} - -/** @internal */ -export function getOrganizeImportsComparerWithDetection(originalImportDecls: readonly AnyImportOrRequireStatement[], preferences: UserPreferences): { comparer: Comparer, isSorted: boolean } { - return detectModuleSpecifierCaseBySort([originalImportDecls], getDetectionLists(preferences).comparersToTest); -} - -/** @internal */ -export function getOrganizeImportsComparer(preferences: UserPreferences, ignoreCase: boolean): Comparer { - const collation = preferences.organizeImportsCollation ?? "ordinal"; - return collation === "unicode" ? - getOrganizeImportsUnicodeStringComparer(ignoreCase, preferences) : - getOrganizeImportsOrdinalStringComparer(ignoreCase); -} - -function getTopLevelExportGroups(sourceFile: SourceFile) { - const topLevelExportGroups: ExportDeclaration[][] = []; - const statements = sourceFile.statements; - const len = length(statements); - - let i = 0; - let groupIndex = 0; - while (i < len) { - if (isExportDeclaration(statements[i])) { - if (topLevelExportGroups[groupIndex] === undefined) { - topLevelExportGroups[groupIndex] = []; - } - const exportDecl = statements[i] as ExportDeclaration; - if (exportDecl.moduleSpecifier) { - topLevelExportGroups[groupIndex].push(exportDecl); - i++; - } - else { - while (i < len && isExportDeclaration(statements[i])) { - topLevelExportGroups[groupIndex].push(statements[i++] as ExportDeclaration); - } - groupIndex++; - } - } - else { - i++; - } - } - return flatMap(topLevelExportGroups, exportGroupDecls => groupByNewlineContiguous(sourceFile, exportGroupDecls)); -} - -/** @internal */ -export function detectModuleSpecifierCaseBySort(importDeclsByGroup: (readonly AnyImportOrRequireStatement[])[], comparersToTest: Comparer[]) { +function detectModuleSpecifierCaseBySort(importDeclsByGroup: (readonly AnyImportOrRequireStatement[])[], comparersToTest: Comparer[]) { const moduleSpecifiersByGroup: string[][] = []; importDeclsByGroup.forEach(importGroup => { - // Turns importDeclsByGroup into string[][] of module specifiers by group to detect sorting on module specifiers + // Turns importDeclsByGroup into string[][] of module specifiers grouped by declGroup moduleSpecifiersByGroup.push(getModuleNamesFromDecls(importGroup)); }); return detectCaseSensitivityBySort(moduleSpecifiersByGroup, comparersToTest); } -/** @internal */ -export function detectNamedImportOrganizationBySort(originalGroups: readonly ImportDeclaration[], comparersToTest: Comparer[], typesToTest: OrganizeImportsTypeOrder[]): { namedImportComparer: Comparer; typeOrder: OrganizeImportsTypeOrder | undefined; isSorted: boolean; } | undefined { +function detectNamedImportOrganizationBySort(originalGroups: readonly ImportDeclaration[], comparersToTest: Comparer[], typesToTest: OrganizeImportsTypeOrder[]): { namedImportComparer: Comparer; typeOrder: OrganizeImportsTypeOrder | undefined; isSorted: boolean; } | undefined { // Filter for import declarations with named imports. Will be a flat array of import declarations without separations by group let bothNamedImports = false; const importDeclsWithNamed = originalGroups.filter(i => { @@ -910,7 +746,7 @@ export function detectNamedImportOrganizationBySort(originalGroups: readonly Imp const currDiff = { first: 0, last: 0, inline: 0 }; for (const importDecl of namedImportsByDecl) { for (const typeOrder of typesToTest) { - currDiff[typeOrder] = (currDiff[typeOrder] ?? 0) + getSortedMeasure(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, { organizeImportsTypeOrder: typeOrder })); + currDiff[typeOrder] = (currDiff[typeOrder] ?? 0) + measureSortedness(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, { organizeImportsTypeOrder: typeOrder })); } } for (const key of typesToTest) { @@ -935,7 +771,7 @@ export function detectNamedImportOrganizationBySort(originalGroups: readonly Imp return { namedImportComparer: bestComparer.last, typeOrder: "last", isSorted: bestDiff.last === 0 }; } -function getSortedMeasure(arr: readonly T[], comparer: Comparer) { +function measureSortedness(arr: readonly T[], comparer: Comparer) { let i = 0; for (let j = 0; j < arr.length - 1; j++) { if (comparer(arr[j], arr[j + 1]) > 0) { @@ -956,7 +792,7 @@ function detectCaseSensitivityBySort(originalGroups: string[][], comparersToTest for (const listToSort of originalGroups) { if (listToSort.length <= 1) continue; - const diff = getSortedMeasure(listToSort, curComparer); + const diff = measureSortedness(listToSort, curComparer); diffOfCurrentComparer += diff; } @@ -970,3 +806,160 @@ function detectCaseSensitivityBySort(originalGroups: string[][], comparersToTest isSorted: bestDiff === 0, }; } + +function compareImportKind(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement) { + return compareValues(getImportKindOrder(s1), getImportKindOrder(s2)); +} + +// 1. Side-effect imports +// 2. Type-only imports +// 3. Namespace imports +// 4. Default imports +// 5. Named imports +// 6. ImportEqualsDeclarations +// 7. Require variable statements +function getImportKindOrder(s1: AnyImportOrRequireStatement) { + switch (s1.kind) { + case SyntaxKind.ImportDeclaration: + if (!s1.importClause) return 0; + if (s1.importClause.isTypeOnly) return 1; + if (s1.importClause.namedBindings?.kind === SyntaxKind.NamespaceImport) return 2; + if (s1.importClause.name) return 3; + return 4; + case SyntaxKind.ImportEqualsDeclaration: + return 5; + case SyntaxKind.VariableStatement: + return 6; + } +} + +function getOrganizeImportsOrdinalStringComparer(ignoreCase: boolean) { + return ignoreCase ? compareStringsCaseInsensitiveEslintCompatible : compareStringsCaseSensitive; +} + +function getOrganizeImportsUnicodeStringComparer(ignoreCase: boolean, preferences: UserPreferences): Comparer { + const resolvedLocale = getOrganizeImportsLocale(preferences); + const caseFirst = preferences.organizeImportsCaseFirst ?? false; + const numeric = preferences.organizeImportsNumericCollation ?? false; + const accents = preferences.organizeImportsAccentCollation ?? true; + const sensitivity = ignoreCase ? + accents ? "accent" : "base" : + accents ? "variant" : "case"; + + const collator = new Intl.Collator(resolvedLocale, { + usage: "sort", + caseFirst: caseFirst || "false", + sensitivity, + numeric, + }); + + // `compare` is a bound method, so we do not need to close over `collator`. + return collator.compare; +} + +function getOrganizeImportsLocale(preferences: UserPreferences): string { + let locale = preferences.organizeImportsLocale; + if (locale === "auto") locale = getUILocale(); + if (locale === undefined) locale = "en"; + + const supportedLocales = Intl.Collator.supportedLocalesOf(locale); + const resolvedLocale = supportedLocales.length ? supportedLocales[0] : "en"; + return resolvedLocale; +} + +function getOrganizeImportsStringComparer(preferences: UserPreferences, ignoreCase: boolean): Comparer { + const collation = preferences.organizeImportsCollation ?? "ordinal"; + return collation === "unicode" ? + getOrganizeImportsUnicodeStringComparer(ignoreCase, preferences) : + getOrganizeImportsOrdinalStringComparer(ignoreCase); +} + +/** @internal */ +export function getOrganizeImportsStringComparerWithDetection(originalImportDecls: readonly AnyImportOrRequireStatement[], preferences: UserPreferences): { comparer: Comparer; isSorted: boolean; } { + return detectModuleSpecifierCaseBySort([originalImportDecls], getDetectionLists(preferences).comparersToTest); +} +function getNamedImportSpecifierComparer(preferences: UserPreferences, comparer?: Comparer): Comparer { + const stringComparer = comparer ?? getOrganizeImportsOrdinalStringComparer(!!preferences.organizeImportsIgnoreCase); + return (s1, s2) => compareImportOrExportSpecifiers(s1, s2, stringComparer, preferences); +} + +/** @internal */ +export function getNamedImportSpecifierComparerWithDetection(importDecl: ImportDeclaration, preferences: UserPreferences, sourceFile?: SourceFile): { specifierComparer: Comparer; isSorted: boolean | undefined; } { + // sort case sensitivity: + // - if the user preference is explicit, use that + // - otherwise, if there are enough existing import specifiers in this import to detect unambiguously, use that + // - otherwise, detect from other imports in the file + const { comparersToTest, typeOrdersToTest } = getDetectionLists(preferences); + const detectFromDecl = detectNamedImportOrganizationBySort([importDecl], comparersToTest, typeOrdersToTest); + let specifierComparer = getNamedImportSpecifierComparer(preferences, comparersToTest[0]); + let isSorted; + if (typeof preferences.organizeImportsIgnoreCase !== "boolean" || !preferences.organizeImportsTypeOrder) { + if (detectFromDecl) { + const { namedImportComparer, typeOrder, isSorted: isDetectedSorted } = detectFromDecl; + isSorted = isDetectedSorted; + specifierComparer = getNamedImportSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); + } + else if (sourceFile) { + // If a sourceFile is specified, we can also try detecting using the other import statements + const detectFromFile = detectNamedImportOrganizationBySort(sourceFile.statements.filter(isImportDeclaration), comparersToTest, typeOrdersToTest); + if (detectFromFile) { + const { namedImportComparer, typeOrder, isSorted: isDetectedSorted } = detectFromFile; + isSorted = isDetectedSorted; + specifierComparer = getNamedImportSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); + } + } + } + + return { specifierComparer, isSorted }; +} + +/** @internal */ +export function getImportDeclarationInsertionIndex(sortedImports: readonly AnyImportOrRequireStatement[], newImport: AnyImportOrRequireStatement, comparer: Comparer) { + const index = binarySearch(sortedImports, newImport, identity, (a, b) => compareImportsOrRequireStatements(a, b, comparer)); + return index < 0 ? ~index : index; +} + +/** @internal */ +export function getImportSpecifierInsertionIndex(sortedImports: readonly ImportSpecifier[], newImport: ImportSpecifier, comparer: Comparer) { + const index = binarySearch(sortedImports, newImport, identity, comparer); + return index < 0 ? ~index : index; +} + +/** @internal */ +export function compareImportsOrRequireStatements(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement, comparer: Comparer) { + return compareModuleSpecifiersWorker(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2), comparer) || compareImportKind(s1, s2); +} + +/* ======== Functions that are internal for testing ======== */ + +/** + * @param importGroup a list of ImportDeclarations, all with the same module name. + * + * @deprecated Only used for testing + * @internal + */ +export function testCoalesceImports(importGroup: readonly ImportDeclaration[], ignoreCase: boolean, sourceFile?: SourceFile, preferences?: UserPreferences): readonly ImportDeclaration[] { + const comparer = getOrganizeImportsOrdinalStringComparer(ignoreCase); + const specifierComparer = getNamedImportSpecifierComparer({ organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder }, comparer); + return coalesceImportsWorker(importGroup, comparer, specifierComparer, sourceFile); +} + +/** + * @param exportGroup a list of ExportDeclarations, all with the same module name. + * + * @deprecated Only used for testing + * @internal + */ +export function testCoalesceExports(exportGroup: readonly ExportDeclaration[], ignoreCase: boolean, preferences?: UserPreferences) { + const comparer = (s1: ExportSpecifier, s2: ExportSpecifier) => compareImportOrExportSpecifiers(s1, s2, getOrganizeImportsOrdinalStringComparer(ignoreCase), { organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder ?? "last" }); + return coalesceExportsWorker(exportGroup, comparer); +} + +/** + * @deprecated Only used for testing + * @internal + */ +export function compareModuleSpecifiers(m1: Expression | undefined, m2: Expression | undefined, ignoreCase?: boolean) { + const comparer = getOrganizeImportsOrdinalStringComparer(!!ignoreCase); + return compareModuleSpecifiersWorker(m1, m2, comparer); +} diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index e4a1938903322..30c5abcfc0f58 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -442,37 +442,32 @@ console.log(Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9, func1 testDetectionBaseline("detection6", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", - content: -`import { A, B, a, b } from 'foo'; -console.log(A, B, a, b);` + content: `import { A, B, a, b } from 'foo'; +console.log(A, B, a, b);`, }); testDetectionBaseline("detection7", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", - content: -`import { A, a, B, b } from 'foo'; -console.log(A, B, a, b);` + content: `import { A, a, B, b } from 'foo'; +console.log(A, B, a, b);`, }); testDetectionBaseline("detection8", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", - content: -`import { A, a, b, B } from 'foo'; -console.log(A, B, a, b);` + content: `import { A, a, b, B } from 'foo'; +console.log(A, B, a, b);`, }); testDetectionBaseline("detection9", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", - content: -`import { a, b, A, B } from 'foo'; -console.log(A, B, a, b);` + content: `import { a, b, A, B } from 'foo'; +console.log(A, B, a, b);`, }); testDetectionBaseline("detection10", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", - content: -`import { a, A, b, B } from 'foo'; -console.log(A, B, a, b);` + content: `import { a, A, b, B } from 'foo'; +console.log(A, B, a, b);`, }); testOrganizeImports("Renamed_used", /*skipDestructiveCodeActions*/ false, { @@ -1069,7 +1064,7 @@ export * from "lib"; ); } - function testDetectionBaseline(testName: string, skipDestructiveCodeActions: boolean, testFile: File, ... otherFiles: File[]) { + function testDetectionBaseline(testName: string, skipDestructiveCodeActions: boolean, testFile: File, ...otherFiles: File[]) { it(testName, () => { // this differs from the test above, in that it doesn't assert that there are changes const baselinePath = `organizeImports/${testName}.ts`; From 74422365b9142ab9f8c570bbf3621008f34d142a Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Fri, 8 Mar 2024 17:01:09 -0800 Subject: [PATCH 31/33] clean up organizeImports file --- src/compiler/types.ts | 6 +- src/services/organizeImports.ts | 503 +++++++++--------- .../unittests/services/organizeImports.ts | 27 +- .../fourslash/organizeImportsAmbientModule.ts | 24 - tests/cases/fourslash/organizeImportsType2.ts | 3 +- 5 files changed, 263 insertions(+), 300 deletions(-) delete mode 100644 tests/cases/fourslash/organizeImportsAmbientModule.ts diff --git a/src/compiler/types.ts b/src/compiler/types.ts index d631ec3e9a14d..30feee6d61e52 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -9966,9 +9966,9 @@ export interface UserPreferences { /** * Indicates where named type-only imports should sort. "inline" sorts named imports without regard to if the import is * type-only. - * - * Default: `last` - */ + * + * Default: `last` + */ readonly organizeImportsTypeOrder?: OrganizeImportsTypeOrder; /** * Indicates whether to exclude standard library and node_modules file symbols from navTo results. diff --git a/src/services/organizeImports.ts b/src/services/organizeImports.ts index bbd51db204e0b..a6a300ed82df4 100644 --- a/src/services/organizeImports.ts +++ b/src/services/organizeImports.ts @@ -93,23 +93,20 @@ export function organizeImports( const { comparersToTest, typeOrdersToTest } = getDetectionLists(preferences); const defaultComparer = comparersToTest[0]; + + // If case sensitivity is specified (true/false), then use the same setting for both. const comparer = { moduleSpecifierComparer: typeof preferences.organizeImportsIgnoreCase === "boolean" ? defaultComparer : undefined, namedImportComparer: typeof preferences.organizeImportsIgnoreCase === "boolean" ? defaultComparer : undefined, typeOrder: preferences.organizeImportsTypeOrder, }; - - if (typeof preferences.organizeImportsIgnoreCase === "boolean") { - // If case sensitivity is specified (true/false), then use the same setting for both. - comparer.moduleSpecifierComparer = getOrganizeImportsComparer(preferences, preferences.organizeImportsIgnoreCase); - comparer.namedImportComparer = comparer.moduleSpecifierComparer; - } - else { + if (typeof preferences.organizeImportsIgnoreCase !== "boolean") { // Otherwise, we must test for case-sensitivity. Named import case sensitivity will be tested with type order ({ comparer: comparer.moduleSpecifierComparer } = detectModuleSpecifierCaseBySort(topLevelImportGroupDecls, comparersToTest)); } - if (!comparer.typeOrder) { + if (!comparer.typeOrder || typeof preferences.organizeImportsIgnoreCase !== "boolean") { + // Through getDetectionLists, the set of orders returned will be compatible const namedImportSort = detectNamedImportOrganizationBySort(topLevelImportDecls, comparersToTest, typeOrdersToTest); if (namedImportSort) { const { namedImportComparer, typeOrder } = namedImportSort; @@ -196,7 +193,7 @@ export function organizeImports( const detectedNamedImportCaseComparer = comparer.namedImportComparer ?? defaultComparer; const detectedTypeOrder = comparer.typeOrder ?? "last"; - const specifierComparer = getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: detectedTypeOrder }, detectedNamedImportCaseComparer); + const specifierComparer = getNamedImportSpecifierComparer({ organizeImportsTypeOrder: detectedTypeOrder }, detectedNamedImportCaseComparer); const processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => { if (shouldRemove) importGroup = removeUnusedImports(importGroup, sourceFile, program); if (shouldCombine) importGroup = coalesceImportsWorker(importGroup, detectedModuleCaseComparer, specifierComparer, sourceFile); @@ -208,7 +205,7 @@ export function organizeImports( } function organizeExportsWorker(oldExportDecls: readonly ExportDeclaration[], specifierCaseComparer?: Comparer) { - const useComparer = getOrganizeImportsSpecifierComparer(preferences, specifierCaseComparer); + const useComparer = getNamedImportSpecifierComparer(preferences, specifierCaseComparer); organizeDeclsWorker(oldExportDecls, group => coalesceExportsWorker(group, useComparer)); } } @@ -218,8 +215,8 @@ export function getDetectionLists(preferences: UserPreferences): { comparersToTe // Returns the possible detection outcomes, given the user's preferences. The earlier in the list, the higher the priority. return { comparersToTest: typeof preferences.organizeImportsIgnoreCase === "boolean" - ? [getOrganizeImportsComparer(preferences, preferences.organizeImportsIgnoreCase)] - : [getOrganizeImportsComparer(preferences, /*ignoreCase*/ true), getOrganizeImportsComparer(preferences, /*ignoreCase*/ false)], + ? [getOrganizeImportsStringComparer(preferences, preferences.organizeImportsIgnoreCase)] + : [getOrganizeImportsStringComparer(preferences, /*ignoreCase*/ true), getOrganizeImportsStringComparer(preferences, /*ignoreCase*/ false)], typeOrdersToTest: preferences.organizeImportsTypeOrder ? [preferences.organizeImportsTypeOrder] : ["last", "inline", "first"], }; } @@ -266,6 +263,37 @@ function isNewGroup(sourceFile: SourceFile, decl: ImportDeclaration | ExportDecl return false; } +function getTopLevelExportGroups(sourceFile: SourceFile) { + const topLevelExportGroups: ExportDeclaration[][] = []; + const statements = sourceFile.statements; + const len = length(statements); + + let i = 0; + let groupIndex = 0; + while (i < len) { + if (isExportDeclaration(statements[i])) { + if (topLevelExportGroups[groupIndex] === undefined) { + topLevelExportGroups[groupIndex] = []; + } + const exportDecl = statements[i] as ExportDeclaration; + if (exportDecl.moduleSpecifier) { + topLevelExportGroups[groupIndex].push(exportDecl); + i++; + } + else { + while (i < len && isExportDeclaration(statements[i])) { + topLevelExportGroups[groupIndex].push(statements[i++] as ExportDeclaration); + } + groupIndex++; + } + } + else { + i++; + } + } + return flatMap(topLevelExportGroups, exportGroupDecls => groupByNewlineContiguous(sourceFile, exportGroupDecls)); +} + function removeUnusedImports(oldImports: readonly ImportDeclaration[], sourceFile: SourceFile, program: Program) { const typeChecker = program.getTypeChecker(); const compilerOptions = program.getCompilerOptions(); @@ -341,30 +369,54 @@ function removeUnusedImports(oldImports: readonly ImportDeclaration[], sourceFil } } -function hasModuleDeclarationMatchingSpecifier(sourceFile: SourceFile, moduleSpecifier: Expression) { - const moduleSpecifierText = isStringLiteral(moduleSpecifier) && moduleSpecifier.text; - return isString(moduleSpecifierText) && some(sourceFile.moduleAugmentations, moduleName => - isStringLiteral(moduleName) - && moduleName.text === moduleSpecifierText); -} - function getExternalModuleName(specifier: Expression | undefined) { return specifier !== undefined && isStringLiteralLike(specifier) ? specifier.text : undefined; } -// Internal for testing -/** - * @param importGroup a list of ImportDeclarations, all with the same module name. +/* + * Returns entire import declarations because they may already have been rewritten and + * may lack parent pointers. The desired parts can easily be recovered based on the + * categorization. * - * @deprecated Only used for testing - * @internal + * NB: There may be overlap between `defaultImports` and `namespaceImports`/`namedImports`. */ -export function testCoalesceImports(importGroup: readonly ImportDeclaration[], ignoreCase: boolean, sourceFile?: SourceFile, preferences?: UserPreferences): readonly ImportDeclaration[] { - const comparer = getOrganizeImportsOrdinalStringComparer(ignoreCase); - const specifierComparer = getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder }, comparer); - return coalesceImportsWorker(importGroup, comparer, specifierComparer, sourceFile); +function getCategorizedImports(importGroup: readonly ImportDeclaration[]) { + let importWithoutClause: ImportDeclaration | undefined; + const typeOnlyImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; + const regularImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; + + for (const importDeclaration of importGroup) { + if (importDeclaration.importClause === undefined) { + // Only the first such import is interesting - the others are redundant. + // Note: Unfortunately, we will lose trivia that was on this node. + importWithoutClause = importWithoutClause || importDeclaration; + continue; + } + + const group = importDeclaration.importClause.isTypeOnly ? typeOnlyImports : regularImports; + const { name, namedBindings } = importDeclaration.importClause; + + if (name) { + group.defaultImports.push(importDeclaration as ImportDeclarationWithDefaultImport); + } + + if (namedBindings) { + if (isNamespaceImport(namedBindings)) { + group.namespaceImports.push(importDeclaration as ImportDeclarationWithNamespaceImport); + } + else { + group.namedImports.push(importDeclaration as ImportDeclarationWithNamedImports); + } + } + } + + return { + importWithoutClause, + typeOnlyImports, + regularImports, + }; } function coalesceImportsWorker(importGroup: readonly ImportDeclaration[], comparer: Comparer, specifierComparer: Comparer, sourceFile?: SourceFile): readonly ImportDeclaration[] { @@ -507,62 +559,6 @@ interface ImportGroup { namedImports: ImportDeclarationWithNamedImports[]; } -/* - * Returns entire import declarations because they may already have been rewritten and - * may lack parent pointers. The desired parts can easily be recovered based on the - * categorization. - * - * NB: There may be overlap between `defaultImports` and `namespaceImports`/`namedImports`. - */ -function getCategorizedImports(importGroup: readonly ImportDeclaration[]) { - let importWithoutClause: ImportDeclaration | undefined; - const typeOnlyImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; - const regularImports: ImportGroup = { defaultImports: [], namespaceImports: [], namedImports: [] }; - - for (const importDeclaration of importGroup) { - if (importDeclaration.importClause === undefined) { - // Only the first such import is interesting - the others are redundant. - // Note: Unfortunately, we will lose trivia that was on this node. - importWithoutClause = importWithoutClause || importDeclaration; - continue; - } - - const group = importDeclaration.importClause.isTypeOnly ? typeOnlyImports : regularImports; - const { name, namedBindings } = importDeclaration.importClause; - - if (name) { - group.defaultImports.push(importDeclaration as ImportDeclarationWithDefaultImport); - } - - if (namedBindings) { - if (isNamespaceImport(namedBindings)) { - group.namespaceImports.push(importDeclaration as ImportDeclarationWithNamespaceImport); - } - else { - group.namedImports.push(importDeclaration as ImportDeclarationWithNamedImports); - } - } - } - - return { - importWithoutClause, - typeOnlyImports, - regularImports, - }; -} - -// Internal for testing -/** - * @param exportGroup a list of ExportDeclarations, all with the same module name. - * - * @deprecated Only used for testing - * @internal - */ -export function testCoalesceExports(exportGroup: readonly ExportDeclaration[], ignoreCase: boolean, preferences?: UserPreferences) { - const comparer = (s1: ExportSpecifier, s2: ExportSpecifier) => compareImportOrExportSpecifiers(s1, s2, getOrganizeImportsOrdinalStringComparer(ignoreCase), { organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder ?? "last" }); - return coalesceExportsWorker(exportGroup, comparer); -} - function coalesceExportsWorker(exportGroup: readonly ExportDeclaration[], specifierComparer: Comparer) { if (exportGroup.length === 0) { return exportGroup; @@ -660,50 +656,6 @@ function compareImportOrExportSpecifiers(s1: } } -/** @internal */ -export function getOrganizeImportsSpecifierComparer(preferences: UserPreferences, comparer?: Comparer): Comparer { - const stringComparer = comparer ?? getOrganizeImportsOrdinalStringComparer(!!preferences.organizeImportsIgnoreCase); - return (s1, s2) => compareImportOrExportSpecifiers(s1, s2, stringComparer, preferences); -} - -/** @internal */ -export function getOrganizeImportsSpecifierComparerWithDetection(importDecl: ImportDeclaration, preferences: UserPreferences, sourceFile?: SourceFile): { specifierComparer: Comparer, isSorted: boolean | undefined } { - const { comparersToTest, typeOrdersToTest } = getDetectionLists(preferences); - const detectFromDecl = detectNamedImportOrganizationBySort([importDecl], comparersToTest, typeOrdersToTest); - let specifierComparer = getOrganizeImportsSpecifierComparer(preferences, comparersToTest[0]); - let isSorted; - - if (typeof preferences.organizeImportsIgnoreCase !== "boolean" || !preferences.organizeImportsTypeOrder) { - if (detectFromDecl) { - const { namedImportComparer, typeOrder, isSorted: isDetectedSorted } = detectFromDecl; - isSorted = isDetectedSorted; - specifierComparer = getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); - } - else if (sourceFile) { - // If a sourceFile is specified, we can also try detecting using the other import statements - const detectFromFile = detectNamedImportOrganizationBySort(sourceFile.statements.filter(isImportDeclaration), comparersToTest, typeOrdersToTest); - if (detectFromFile) { - const { namedImportComparer, typeOrder, isSorted: isDetectedSorted } = detectFromFile; - isSorted = isDetectedSorted; - specifierComparer = getOrganizeImportsSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); - } - } - } - - return { specifierComparer, isSorted }; -} - -/** - * Exported for testing - * - * @deprecated Only used for testing - * @internal - */ -export function compareModuleSpecifiers(m1: Expression | undefined, m2: Expression | undefined, ignoreCase?: boolean) { - const comparer = getOrganizeImportsOrdinalStringComparer(!!ignoreCase); - return compareModuleSpecifiersWorker(m1, m2, comparer); -} - function compareModuleSpecifiersWorker(m1: Expression | undefined, m2: Expression | undefined, comparer: Comparer) { const name1 = m1 === undefined ? undefined : getExternalModuleName(m1); const name2 = m2 === undefined ? undefined : getExternalModuleName(m2); @@ -727,47 +679,11 @@ function getModuleSpecifierExpression(declaration: AnyImportOrRequireStatement): } } -/** @internal */ -export function getImportDeclarationInsertionIndex(sortedImports: readonly AnyImportOrRequireStatement[], newImport: AnyImportOrRequireStatement, comparer: Comparer) { - const index = binarySearch(sortedImports, newImport, identity, (a, b) => compareImportsOrRequireStatements(a, b, comparer)); - return index < 0 ? ~index : index; -} - -/** @internal */ -export function getImportSpecifierInsertionIndex(sortedImports: readonly ImportSpecifier[], newImport: ImportSpecifier, comparer: Comparer) { - const index = binarySearch(sortedImports, newImport, identity, comparer); - return index < 0 ? ~index : index; -} - -/** @internal */ -export function compareImportsOrRequireStatements(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement, comparer: Comparer) { - return compareModuleSpecifiersWorker(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2), comparer) || compareImportKind(s1, s2); -} - -function compareImportKind(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement) { - return compareValues(getImportKindOrder(s1), getImportKindOrder(s2)); -} - -// 1. Side-effect imports -// 2. Type-only imports -// 3. Namespace imports -// 4. Default imports -// 5. Named imports -// 6. ImportEqualsDeclarations -// 7. Require variable statements -function getImportKindOrder(s1: AnyImportOrRequireStatement) { - switch (s1.kind) { - case SyntaxKind.ImportDeclaration: - if (!s1.importClause) return 0; - if (s1.importClause.isTypeOnly) return 1; - if (s1.importClause.namedBindings?.kind === SyntaxKind.NamespaceImport) return 2; - if (s1.importClause.name) return 3; - return 4; - case SyntaxKind.ImportEqualsDeclaration: - return 5; - case SyntaxKind.VariableStatement: - return 6; - } +function hasModuleDeclarationMatchingSpecifier(sourceFile: SourceFile, moduleSpecifier: Expression) { + const moduleSpecifierText = isStringLiteral(moduleSpecifier) && moduleSpecifier.text; + return isString(moduleSpecifierText) && some(sourceFile.moduleAugmentations, moduleName => + isStringLiteral(moduleName) + && moduleName.text === moduleSpecifierText); } function getNewImportSpecifiers(namedImports: ImportDeclaration[]) { @@ -784,96 +700,16 @@ function tryGetNamedBindingElements(namedImport: ImportDeclaration) { : undefined; } -function getOrganizeImportsOrdinalStringComparer(ignoreCase: boolean) { - return ignoreCase ? compareStringsCaseInsensitiveEslintCompatible : compareStringsCaseSensitive; -} - -function getOrganizeImportsUnicodeStringComparer(ignoreCase: boolean, preferences: UserPreferences): Comparer { - const resolvedLocale = getOrganizeImportsLocale(preferences); - const caseFirst = preferences.organizeImportsCaseFirst ?? false; - const numeric = preferences.organizeImportsNumericCollation ?? false; - const accents = preferences.organizeImportsAccentCollation ?? true; - const sensitivity = ignoreCase ? - accents ? "accent" : "base" : - accents ? "variant" : "case"; - - const collator = new Intl.Collator(resolvedLocale, { - usage: "sort", - caseFirst: caseFirst || "false", - sensitivity, - numeric, - }); - - // `compare` is a bound method, so we do not need to close over `collator`. - return collator.compare; -} - -function getOrganizeImportsLocale(preferences: UserPreferences): string { - let locale = preferences.organizeImportsLocale; - if (locale === "auto") locale = getUILocale(); - if (locale === undefined) locale = "en"; - - const supportedLocales = Intl.Collator.supportedLocalesOf(locale); - const resolvedLocale = supportedLocales.length ? supportedLocales[0] : "en"; - return resolvedLocale; -} - -/** @internal */ -export function getOrganizeImportsComparerWithDetection(originalImportDecls: readonly AnyImportOrRequireStatement[], preferences: UserPreferences): { comparer: Comparer, isSorted: boolean } { - return detectModuleSpecifierCaseBySort([originalImportDecls], getDetectionLists(preferences).comparersToTest); -} - -/** @internal */ -export function getOrganizeImportsComparer(preferences: UserPreferences, ignoreCase: boolean): Comparer { - const collation = preferences.organizeImportsCollation ?? "ordinal"; - return collation === "unicode" ? - getOrganizeImportsUnicodeStringComparer(ignoreCase, preferences) : - getOrganizeImportsOrdinalStringComparer(ignoreCase); -} - -function getTopLevelExportGroups(sourceFile: SourceFile) { - const topLevelExportGroups: ExportDeclaration[][] = []; - const statements = sourceFile.statements; - const len = length(statements); - - let i = 0; - let groupIndex = 0; - while (i < len) { - if (isExportDeclaration(statements[i])) { - if (topLevelExportGroups[groupIndex] === undefined) { - topLevelExportGroups[groupIndex] = []; - } - const exportDecl = statements[i] as ExportDeclaration; - if (exportDecl.moduleSpecifier) { - topLevelExportGroups[groupIndex].push(exportDecl); - i++; - } - else { - while (i < len && isExportDeclaration(statements[i])) { - topLevelExportGroups[groupIndex].push(statements[i++] as ExportDeclaration); - } - groupIndex++; - } - } - else { - i++; - } - } - return flatMap(topLevelExportGroups, exportGroupDecls => groupByNewlineContiguous(sourceFile, exportGroupDecls)); -} - -/** @internal */ -export function detectModuleSpecifierCaseBySort(importDeclsByGroup: (readonly AnyImportOrRequireStatement[])[], comparersToTest: Comparer[]) { +function detectModuleSpecifierCaseBySort(importDeclsByGroup: (readonly AnyImportOrRequireStatement[])[], comparersToTest: Comparer[]) { const moduleSpecifiersByGroup: string[][] = []; importDeclsByGroup.forEach(importGroup => { - // Turns importDeclsByGroup into string[][] of module specifiers by group to detect sorting on module specifiers + // Turns importDeclsByGroup into string[][] of module specifiers grouped by declGroup moduleSpecifiersByGroup.push(getModuleNamesFromDecls(importGroup)); }); return detectCaseSensitivityBySort(moduleSpecifiersByGroup, comparersToTest); } -/** @internal */ -export function detectNamedImportOrganizationBySort(originalGroups: readonly ImportDeclaration[], comparersToTest: Comparer[], typesToTest: OrganizeImportsTypeOrder[]): { namedImportComparer: Comparer; typeOrder: OrganizeImportsTypeOrder | undefined; isSorted: boolean; } | undefined { +function detectNamedImportOrganizationBySort(originalGroups: readonly ImportDeclaration[], comparersToTest: Comparer[], typesToTest: OrganizeImportsTypeOrder[]): { namedImportComparer: Comparer; typeOrder: OrganizeImportsTypeOrder | undefined; isSorted: boolean; } | undefined { // Filter for import declarations with named imports. Will be a flat array of import declarations without separations by group let bothNamedImports = false; const importDeclsWithNamed = originalGroups.filter(i => { @@ -910,7 +746,7 @@ export function detectNamedImportOrganizationBySort(originalGroups: readonly Imp const currDiff = { first: 0, last: 0, inline: 0 }; for (const importDecl of namedImportsByDecl) { for (const typeOrder of typesToTest) { - currDiff[typeOrder] = (currDiff[typeOrder] ?? 0) + getSortedMeasure(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, { organizeImportsTypeOrder: typeOrder })); + currDiff[typeOrder] = (currDiff[typeOrder] ?? 0) + measureSortedness(importDecl, (n1, n2) => compareImportOrExportSpecifiers(n1, n2, curComparer, { organizeImportsTypeOrder: typeOrder })); } } for (const key of typesToTest) { @@ -935,7 +771,7 @@ export function detectNamedImportOrganizationBySort(originalGroups: readonly Imp return { namedImportComparer: bestComparer.last, typeOrder: "last", isSorted: bestDiff.last === 0 }; } -function getSortedMeasure(arr: readonly T[], comparer: Comparer) { +function measureSortedness(arr: readonly T[], comparer: Comparer) { let i = 0; for (let j = 0; j < arr.length - 1; j++) { if (comparer(arr[j], arr[j + 1]) > 0) { @@ -956,7 +792,7 @@ function detectCaseSensitivityBySort(originalGroups: string[][], comparersToTest for (const listToSort of originalGroups) { if (listToSort.length <= 1) continue; - const diff = getSortedMeasure(listToSort, curComparer); + const diff = measureSortedness(listToSort, curComparer); diffOfCurrentComparer += diff; } @@ -970,3 +806,160 @@ function detectCaseSensitivityBySort(originalGroups: string[][], comparersToTest isSorted: bestDiff === 0, }; } + +function compareImportKind(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement) { + return compareValues(getImportKindOrder(s1), getImportKindOrder(s2)); +} + +// 1. Side-effect imports +// 2. Type-only imports +// 3. Namespace imports +// 4. Default imports +// 5. Named imports +// 6. ImportEqualsDeclarations +// 7. Require variable statements +function getImportKindOrder(s1: AnyImportOrRequireStatement) { + switch (s1.kind) { + case SyntaxKind.ImportDeclaration: + if (!s1.importClause) return 0; + if (s1.importClause.isTypeOnly) return 1; + if (s1.importClause.namedBindings?.kind === SyntaxKind.NamespaceImport) return 2; + if (s1.importClause.name) return 3; + return 4; + case SyntaxKind.ImportEqualsDeclaration: + return 5; + case SyntaxKind.VariableStatement: + return 6; + } +} + +function getOrganizeImportsOrdinalStringComparer(ignoreCase: boolean) { + return ignoreCase ? compareStringsCaseInsensitiveEslintCompatible : compareStringsCaseSensitive; +} + +function getOrganizeImportsUnicodeStringComparer(ignoreCase: boolean, preferences: UserPreferences): Comparer { + const resolvedLocale = getOrganizeImportsLocale(preferences); + const caseFirst = preferences.organizeImportsCaseFirst ?? false; + const numeric = preferences.organizeImportsNumericCollation ?? false; + const accents = preferences.organizeImportsAccentCollation ?? true; + const sensitivity = ignoreCase ? + accents ? "accent" : "base" : + accents ? "variant" : "case"; + + const collator = new Intl.Collator(resolvedLocale, { + usage: "sort", + caseFirst: caseFirst || "false", + sensitivity, + numeric, + }); + + // `compare` is a bound method, so we do not need to close over `collator`. + return collator.compare; +} + +function getOrganizeImportsLocale(preferences: UserPreferences): string { + let locale = preferences.organizeImportsLocale; + if (locale === "auto") locale = getUILocale(); + if (locale === undefined) locale = "en"; + + const supportedLocales = Intl.Collator.supportedLocalesOf(locale); + const resolvedLocale = supportedLocales.length ? supportedLocales[0] : "en"; + return resolvedLocale; +} + +function getOrganizeImportsStringComparer(preferences: UserPreferences, ignoreCase: boolean): Comparer { + const collation = preferences.organizeImportsCollation ?? "ordinal"; + return collation === "unicode" ? + getOrganizeImportsUnicodeStringComparer(ignoreCase, preferences) : + getOrganizeImportsOrdinalStringComparer(ignoreCase); +} + +/** @internal */ +export function getOrganizeImportsStringComparerWithDetection(originalImportDecls: readonly AnyImportOrRequireStatement[], preferences: UserPreferences): { comparer: Comparer; isSorted: boolean; } { + return detectModuleSpecifierCaseBySort([originalImportDecls], getDetectionLists(preferences).comparersToTest); +} +function getNamedImportSpecifierComparer(preferences: UserPreferences, comparer?: Comparer): Comparer { + const stringComparer = comparer ?? getOrganizeImportsOrdinalStringComparer(!!preferences.organizeImportsIgnoreCase); + return (s1, s2) => compareImportOrExportSpecifiers(s1, s2, stringComparer, preferences); +} + +/** @internal */ +export function getNamedImportSpecifierComparerWithDetection(importDecl: ImportDeclaration, preferences: UserPreferences, sourceFile?: SourceFile): { specifierComparer: Comparer; isSorted: boolean | undefined; } { + // sort case sensitivity: + // - if the user preference is explicit, use that + // - otherwise, if there are enough existing import specifiers in this import to detect unambiguously, use that + // - otherwise, detect from other imports in the file + const { comparersToTest, typeOrdersToTest } = getDetectionLists(preferences); + const detectFromDecl = detectNamedImportOrganizationBySort([importDecl], comparersToTest, typeOrdersToTest); + let specifierComparer = getNamedImportSpecifierComparer(preferences, comparersToTest[0]); + let isSorted; + if (typeof preferences.organizeImportsIgnoreCase !== "boolean" || !preferences.organizeImportsTypeOrder) { + if (detectFromDecl) { + const { namedImportComparer, typeOrder, isSorted: isDetectedSorted } = detectFromDecl; + isSorted = isDetectedSorted; + specifierComparer = getNamedImportSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); + } + else if (sourceFile) { + // If a sourceFile is specified, we can also try detecting using the other import statements + const detectFromFile = detectNamedImportOrganizationBySort(sourceFile.statements.filter(isImportDeclaration), comparersToTest, typeOrdersToTest); + if (detectFromFile) { + const { namedImportComparer, typeOrder, isSorted: isDetectedSorted } = detectFromFile; + isSorted = isDetectedSorted; + specifierComparer = getNamedImportSpecifierComparer({ organizeImportsTypeOrder: typeOrder }, namedImportComparer); + } + } + } + + return { specifierComparer, isSorted }; +} + +/** @internal */ +export function getImportDeclarationInsertionIndex(sortedImports: readonly AnyImportOrRequireStatement[], newImport: AnyImportOrRequireStatement, comparer: Comparer) { + const index = binarySearch(sortedImports, newImport, identity, (a, b) => compareImportsOrRequireStatements(a, b, comparer)); + return index < 0 ? ~index : index; +} + +/** @internal */ +export function getImportSpecifierInsertionIndex(sortedImports: readonly ImportSpecifier[], newImport: ImportSpecifier, comparer: Comparer) { + const index = binarySearch(sortedImports, newImport, identity, comparer); + return index < 0 ? ~index : index; +} + +/** @internal */ +export function compareImportsOrRequireStatements(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement, comparer: Comparer) { + return compareModuleSpecifiersWorker(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2), comparer) || compareImportKind(s1, s2); +} + +/* ======== Functions that are internal for testing ======== */ + +/** + * @param importGroup a list of ImportDeclarations, all with the same module name. + * + * @deprecated Only used for testing + * @internal + */ +export function testCoalesceImports(importGroup: readonly ImportDeclaration[], ignoreCase: boolean, sourceFile?: SourceFile, preferences?: UserPreferences): readonly ImportDeclaration[] { + const comparer = getOrganizeImportsOrdinalStringComparer(ignoreCase); + const specifierComparer = getNamedImportSpecifierComparer({ organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder }, comparer); + return coalesceImportsWorker(importGroup, comparer, specifierComparer, sourceFile); +} + +/** + * @param exportGroup a list of ExportDeclarations, all with the same module name. + * + * @deprecated Only used for testing + * @internal + */ +export function testCoalesceExports(exportGroup: readonly ExportDeclaration[], ignoreCase: boolean, preferences?: UserPreferences) { + const comparer = (s1: ExportSpecifier, s2: ExportSpecifier) => compareImportOrExportSpecifiers(s1, s2, getOrganizeImportsOrdinalStringComparer(ignoreCase), { organizeImportsTypeOrder: preferences?.organizeImportsTypeOrder ?? "last" }); + return coalesceExportsWorker(exportGroup, comparer); +} + +/** + * @deprecated Only used for testing + * @internal + */ +export function compareModuleSpecifiers(m1: Expression | undefined, m2: Expression | undefined, ignoreCase?: boolean) { + const comparer = getOrganizeImportsOrdinalStringComparer(!!ignoreCase); + return compareModuleSpecifiersWorker(m1, m2, comparer); +} diff --git a/src/testRunner/unittests/services/organizeImports.ts b/src/testRunner/unittests/services/organizeImports.ts index e4a1938903322..30c5abcfc0f58 100644 --- a/src/testRunner/unittests/services/organizeImports.ts +++ b/src/testRunner/unittests/services/organizeImports.ts @@ -442,37 +442,32 @@ console.log(Type1, Type2, Type3, Type4, Type5, Type6, Type7, Type8, Type9, func1 testDetectionBaseline("detection6", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", - content: -`import { A, B, a, b } from 'foo'; -console.log(A, B, a, b);` + content: `import { A, B, a, b } from 'foo'; +console.log(A, B, a, b);`, }); testDetectionBaseline("detection7", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", - content: -`import { A, a, B, b } from 'foo'; -console.log(A, B, a, b);` + content: `import { A, a, B, b } from 'foo'; +console.log(A, B, a, b);`, }); testDetectionBaseline("detection8", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", - content: -`import { A, a, b, B } from 'foo'; -console.log(A, B, a, b);` + content: `import { A, a, b, B } from 'foo'; +console.log(A, B, a, b);`, }); testDetectionBaseline("detection9", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", - content: -`import { a, b, A, B } from 'foo'; -console.log(A, B, a, b);` + content: `import { a, b, A, B } from 'foo'; +console.log(A, B, a, b);`, }); testDetectionBaseline("detection10", /*skipDestructiveCodeActions*/ false, { path: "/test.ts", - content: -`import { a, A, b, B } from 'foo'; -console.log(A, B, a, b);` + content: `import { a, A, b, B } from 'foo'; +console.log(A, B, a, b);`, }); testOrganizeImports("Renamed_used", /*skipDestructiveCodeActions*/ false, { @@ -1069,7 +1064,7 @@ export * from "lib"; ); } - function testDetectionBaseline(testName: string, skipDestructiveCodeActions: boolean, testFile: File, ... otherFiles: File[]) { + function testDetectionBaseline(testName: string, skipDestructiveCodeActions: boolean, testFile: File, ...otherFiles: File[]) { it(testName, () => { // this differs from the test above, in that it doesn't assert that there are changes const baselinePath = `organizeImports/${testName}.ts`; diff --git a/tests/cases/fourslash/organizeImportsAmbientModule.ts b/tests/cases/fourslash/organizeImportsAmbientModule.ts deleted file mode 100644 index 7ddb43c0ed4fe..0000000000000 --- a/tests/cases/fourslash/organizeImportsAmbientModule.ts +++ /dev/null @@ -1,24 +0,0 @@ -/// - -// @allowSyntheticDefaultImports: true -// @moduleResolution: node -// @noUnusedLocals: true -// @target: es2018 - -//// declare module "mod" { -//// import { F1 } from "lib"; -//// import * as NS from "lib"; -//// import { F2 } from "lib"; -//// -//// function F(f1: {} = F1, f2: {} = F2) {} -//// } - -verify.organizeImports( -`declare module "mod" { - import { F1, F2 } from "lib"; - - function F(f1: {} = F1, f2: {} = F2) {} -}`, -/*mode*/ undefined -); - diff --git a/tests/cases/fourslash/organizeImportsType2.ts b/tests/cases/fourslash/organizeImportsType2.ts index 44d34caadba42..57b2b4db26950 100644 --- a/tests/cases/fourslash/organizeImportsType2.ts +++ b/tests/cases/fourslash/organizeImportsType2.ts @@ -47,5 +47,4 @@ export { A, C, type B }; `, undefined, { organizeImportsTypeOrder : "last" } -); - +); \ No newline at end of file From 2127d55a6b68ee016148e0a65c0e633b089cb4fc Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Thu, 14 Mar 2024 16:11:53 -0700 Subject: [PATCH 32/33] correct auto imports tests for auto type promotion --- tests/cases/fourslash/importNameCodeFixConvertTypeOnly1.ts | 2 +- tests/cases/fourslash/importNameCodeFix_importType4.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cases/fourslash/importNameCodeFixConvertTypeOnly1.ts b/tests/cases/fourslash/importNameCodeFixConvertTypeOnly1.ts index 24d688ea96d4a..ce877da6c34f8 100644 --- a/tests/cases/fourslash/importNameCodeFixConvertTypeOnly1.ts +++ b/tests/cases/fourslash/importNameCodeFixConvertTypeOnly1.ts @@ -9,5 +9,5 @@ //// new B goTo.file('/b.ts'); -verify.importFixAtPosition([`import { type A, B } from './a'; +verify.importFixAtPosition([`import { B, type A } from './a'; new B`]); diff --git a/tests/cases/fourslash/importNameCodeFix_importType4.ts b/tests/cases/fourslash/importNameCodeFix_importType4.ts index dccab993f707f..b14a2f8439445 100644 --- a/tests/cases/fourslash/importNameCodeFix_importType4.ts +++ b/tests/cases/fourslash/importNameCodeFix_importType4.ts @@ -14,5 +14,5 @@ goTo.marker(""); verify.importFixAtPosition([ -`import { type SomeInterface, SomePig } from "./exports.js"; +`import { SomePig, type SomeInterface } from "./exports.js"; new SomePig`]); From 520d4cad5e590c3e53750a39d7b43f73f1cfd725 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Thu, 14 Mar 2024 17:18:16 -0700 Subject: [PATCH 33/33] fix importFixes to compare the new imports --- src/services/codefixes/importFixes.ts | 9 +++- .../importNameCodeFix_all_promoteType.ts | 42 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tests/cases/fourslash/importNameCodeFix_all_promoteType.ts diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index e9c3405affef6..80400b140438e 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -114,6 +114,7 @@ import { removeFileExtension, removeSuffix, RequireVariableStatement, + sameMap, ScriptTarget, SemanticMeaning, shouldUseUriStyleNodeCoreModules, @@ -1477,6 +1478,7 @@ function doAddExistingFix( return; } + // promoteFromTypeOnly = true if we need to promote the entire original clause from type only const promoteFromTypeOnly = clause.isTypeOnly && some([defaultImport, ...namedImports], i => i?.addAsTypeOnly === AddAsTypeOnly.NotAllowed); const existingSpecifiers = clause.namedBindings && tryCast(clause.namedBindings, isNamedImports)?.elements; @@ -1506,8 +1508,13 @@ function doAddExistingFix( // changed to check if existing specifiers are sorted if (existingSpecifiers?.length && isSorted !== false) { + // if we're promoting the clause from type-only, we need to transform the existing imports before attempting to insert the new named imports + const transformedExistingSpecifiers = (promoteFromTypeOnly && existingSpecifiers) ? factory.updateNamedImports( + clause.namedBindings as NamedImports, + sameMap(existingSpecifiers, e => factory.updateImportSpecifier(e, /*isTypeOnly*/ true, e.propertyName, e.name)), + ).elements : existingSpecifiers; for (const spec of newSpecifiers) { - const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, specifierComparer); + const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(transformedExistingSpecifiers, spec, specifierComparer); changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex); } } diff --git a/tests/cases/fourslash/importNameCodeFix_all_promoteType.ts b/tests/cases/fourslash/importNameCodeFix_all_promoteType.ts new file mode 100644 index 0000000000000..4a86151ac5c55 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_all_promoteType.ts @@ -0,0 +1,42 @@ +/// + +// @Filename: /a.ts +//// export class A {} +//// export class B {} +//// export class C {} +//// export class D {} +//// export class E {} +//// export class F {} +//// export class G {} + +// @Filename: /b.ts +//// import type { A, C, D, E, G } from './a'; +//// type Z = B | A; +//// new F; + +// @Filename: /c.ts +//// import type { A, C, D, E, G } from './a'; +//// type Z = B | A; +//// type Y = F; + +goTo.file('/b.ts'); +verify.codeFixAll({ + fixId: "fixMissingImport", + fixAllDescription: "Add all missing imports", + newFileContent: +`import { B, F, type A, type C, type D, type E, type G } from './a'; +type Z = B | A; +new F;` +}); + +goTo.file('/c.ts'); +verify.codeFixAll({ + fixId: "fixMissingImport", + fixAllDescription: "Add all missing imports", + newFileContent: +`import type { A, B, C, D, E, F, G } from './a'; +type Z = B | A; +type Y = F;` +}); + +