Skip to content

Commit 20182cf

Browse files
authored
Organize imports collation (#52115)
1 parent abd6cb4 commit 20182cf

19 files changed

+721
-109
lines changed

src/compiler/core.ts

+71-20
Original file line numberDiff line numberDiff line change
@@ -865,32 +865,25 @@ export const enum SortKind {
865865
}
866866

867867
/** @internal */
868-
export function detectSortCaseSensitivity(array: readonly string[], useEslintOrdering?: boolean): SortKind;
869-
/** @internal */
870-
export function detectSortCaseSensitivity<T>(array: readonly T[], useEslintOrdering: boolean, getString: (element: T) => string): SortKind;
871-
/** @internal */
872-
export function detectSortCaseSensitivity<T>(array: readonly T[], useEslintOrdering?: boolean, getString?: (element: T) => string): SortKind {
868+
export function detectSortCaseSensitivity<T>(
869+
array: readonly T[],
870+
getString: (element: T) => string,
871+
compareStringsCaseSensitive: Comparer<string>,
872+
compareStringsCaseInsensitive: Comparer<string>,
873+
): SortKind {
873874
let kind = SortKind.Both;
874875
if (array.length < 2) return kind;
875-
const caseSensitiveComparer = getString
876-
? (a: T, b: T) => compareStringsCaseSensitive(getString(a), getString(b))
877-
: compareStringsCaseSensitive as (a: T | undefined, b: T | undefined) => Comparison;
878-
const compareCaseInsensitive = useEslintOrdering ? compareStringsCaseInsensitiveEslintCompatible : compareStringsCaseInsensitive;
879-
const caseInsensitiveComparer = getString
880-
? (a: T, b: T) => compareCaseInsensitive(getString(a), getString(b))
881-
: compareCaseInsensitive as (a: T | undefined, b: T | undefined) => Comparison;
882-
for (let i = 1, len = array.length; i < len; i++) {
883-
const prevElement = array[i - 1];
884-
const element = array[i];
885-
if (kind & SortKind.CaseSensitive && caseSensitiveComparer(prevElement, element) === Comparison.GreaterThan) {
876+
877+
let prevElement = getString(array[0]);
878+
for (let i = 1, len = array.length; i < len && kind !== SortKind.None; i++) {
879+
const element = getString(array[i]);
880+
if (kind & SortKind.CaseSensitive && compareStringsCaseSensitive(prevElement, element) > 0) {
886881
kind &= ~SortKind.CaseSensitive;
887882
}
888-
if (kind & SortKind.CaseInsensitive && caseInsensitiveComparer(prevElement, element) === Comparison.GreaterThan) {
883+
if (kind & SortKind.CaseInsensitive && compareStringsCaseInsensitive(prevElement, element) > 0) {
889884
kind &= ~SortKind.CaseInsensitive;
890885
}
891-
if (kind === SortKind.None) {
892-
return kind;
893-
}
886+
prevElement = element;
894887
}
895888
return kind;
896889
}
@@ -2048,6 +2041,64 @@ export function memoizeWeak<A extends object, T>(callback: (arg: A) => T): (arg:
20482041
};
20492042
}
20502043

2044+
/** @internal */
2045+
export interface MemoizeCache<A extends any[], T> {
2046+
has(args: A): boolean;
2047+
get(args: A): T | undefined;
2048+
set(args: A, value: T): void;
2049+
}
2050+
2051+
/**
2052+
* A version of `memoize` that supports multiple arguments, backed by a provided cache.
2053+
*
2054+
* @internal
2055+
*/
2056+
export function memoizeCached<A extends any[], T>(callback: (...args: A) => T, cache: MemoizeCache<A, T>): (...args: A) => T {
2057+
return (...args: A) => {
2058+
let value = cache.get(args);
2059+
if (value === undefined && !cache.has(args)) {
2060+
value = callback(...args);
2061+
cache.set(args, value);
2062+
}
2063+
return value!;
2064+
};
2065+
}
2066+
2067+
/**
2068+
* High-order function, composes functions. Note that functions are composed inside-out;
2069+
* for example, `compose(a, b)` is the equivalent of `x => b(a(x))`.
2070+
*
2071+
* @param args The functions to compose.
2072+
*
2073+
* @internal
2074+
*/
2075+
export function compose<T>(...args: ((t: T) => T)[]): (t: T) => T;
2076+
/** @internal */
2077+
export function compose<T>(a: (t: T) => T, b: (t: T) => T, c: (t: T) => T, d: (t: T) => T, e: (t: T) => T): (t: T) => T {
2078+
if (!!e) {
2079+
const args: ((t: T) => T)[] = [];
2080+
for (let i = 0; i < arguments.length; i++) {
2081+
args[i] = arguments[i];
2082+
}
2083+
2084+
return t => reduceLeft(args, (u, f) => f(u), t);
2085+
}
2086+
else if (d) {
2087+
return t => d(c(b(a(t))));
2088+
}
2089+
else if (c) {
2090+
return t => c(b(a(t)));
2091+
}
2092+
else if (b) {
2093+
return t => b(a(t));
2094+
}
2095+
else if (a) {
2096+
return t => a(t);
2097+
}
2098+
else {
2099+
return t => t;
2100+
}
2101+
}
20512102
/** @internal */
20522103
export const enum AssertionLevel {
20532104
None = 0,

src/compiler/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -9832,6 +9832,11 @@ export interface UserPreferences {
98329832
readonly allowRenameOfImportPath?: boolean;
98339833
readonly autoImportFileExcludePatterns?: string[];
98349834
readonly organizeImportsIgnoreCase?: "auto" | boolean;
9835+
readonly organizeImportsCollation?: "ordinal" | "unicode";
9836+
readonly organizeImportsLocale?: string;
9837+
readonly organizeImportsNumericCollation?: boolean;
9838+
readonly organizeImportsAccentCollation?: boolean;
9839+
readonly organizeImportsCaseFirst?: "upper" | "lower" | false;
98359840
}
98369841

98379842
/** Represents a bigint literal value without requiring bigint support */

src/server/protocol.ts

+53
Original file line numberDiff line numberDiff line change
@@ -3516,7 +3516,60 @@ export interface UserPreferences {
35163516
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
35173517
readonly includeInlayEnumMemberValueHints?: boolean;
35183518
readonly autoImportFileExcludePatterns?: string[];
3519+
3520+
/**
3521+
* Indicates whether imports should be organized in a case-insensitive manner.
3522+
*/
35193523
readonly organizeImportsIgnoreCase?: "auto" | boolean;
3524+
/**
3525+
* Indicates whether imports should be organized via an "ordinal" (binary) comparison using the numeric value
3526+
* of their code points, or via "unicode" collation (via the
3527+
* [Unicode Collation Algorithm](https://unicode.org/reports/tr10/#Scope)) using rules associated with the locale
3528+
* specified in {@link organizeImportsCollationLocale}.
3529+
*
3530+
* Default: `"ordinal"`.
3531+
*/
3532+
readonly organizeImportsCollation?: "ordinal" | "unicode";
3533+
/**
3534+
* Indicates the locale to use for "unicode" collation. If not specified, the locale `"en"` is used as an invariant
3535+
* for the sake of consistent sorting. Use `"auto"` to use the detected UI locale.
3536+
*
3537+
* This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`.
3538+
*
3539+
* Default: `"en"`
3540+
*/
3541+
readonly organizeImportsCollationLocale?: string;
3542+
/**
3543+
* Indicates whether numeric collation should be used for digit sequences in strings. When `true`, will collate
3544+
* strings such that `a1z < a2z < a100z`. When `false`, will collate strings such that `a1z < a100z < a2z`.
3545+
*
3546+
* This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`.
3547+
*
3548+
* Default: `false`
3549+
*/
3550+
readonly organizeImportsNumericCollation?: boolean;
3551+
/**
3552+
* Indicates whether accents and other diacritic marks are considered unequal for the purpose of collation. When
3553+
* `true`, characters with accents and other diacritics will be collated in the order defined by the locale specified
3554+
* in {@link organizeImportsCollationLocale}.
3555+
*
3556+
* This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`.
3557+
*
3558+
* Default: `true`
3559+
*/
3560+
readonly organizeImportsAccentCollation?: boolean;
3561+
/**
3562+
* Indicates whether upper case or lower case should sort first. When `false`, the default order for the locale
3563+
* specified in {@link organizeImportsCollationLocale} is used.
3564+
*
3565+
* This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`. This preference is also
3566+
* ignored if we are using case-insensitive sorting, which occurs when {@link organizeImportsIgnoreCase} is `true`,
3567+
* or if {@link organizeImportsIgnoreCase} is `"auto"` and the auto-detected case sensitivity is determined to be
3568+
* case-insensitive.
3569+
*
3570+
* Default: `false`
3571+
*/
3572+
readonly organizeImportsCaseFirst?: "upper" | "lower" | false;
35203573

35213574
/**
35223575
* Indicates whether {@link ReferencesResponseItem.lineText} is supported.

src/services/codefixes/importFixes.ts

+14-12
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAu
375375
newDeclarations = combine(newDeclarations, declarations);
376376
});
377377
if (newDeclarations) {
378-
insertImports(changeTracker, sourceFile, newDeclarations, /*blankLineBetween*/ true);
378+
insertImports(changeTracker, sourceFile, newDeclarations, /*blankLineBetween*/ true, preferences);
379379
}
380380
}
381381

@@ -1221,14 +1221,14 @@ function codeActionForFixWorker(changes: textChanges.ChangeTracker, sourceFile:
12211221
const defaultImport: Import | undefined = importKind === ImportKind.Default ? { name: symbolName, addAsTypeOnly } : undefined;
12221222
const namedImports: Import[] | undefined = importKind === ImportKind.Named ? [{ name: symbolName, addAsTypeOnly }] : undefined;
12231223
const namespaceLikeImport = importKind === ImportKind.Namespace || importKind === ImportKind.CommonJS ? { importKind, name: symbolName, addAsTypeOnly } : undefined;
1224-
insertImports(changes, sourceFile, getDeclarations(moduleSpecifier, quotePreference, defaultImport, namedImports, namespaceLikeImport), /*blankLineBetween*/ true);
1224+
insertImports(changes, sourceFile, getDeclarations(moduleSpecifier, quotePreference, defaultImport, namedImports, namespaceLikeImport), /*blankLineBetween*/ true, preferences);
12251225
return includeSymbolNameInDescription
12261226
? [Diagnostics.Import_0_from_1, symbolName, moduleSpecifier]
12271227
: [Diagnostics.Add_import_from_0, moduleSpecifier];
12281228
}
12291229
case ImportFixKind.PromoteTypeOnly: {
12301230
const { typeOnlyAliasDeclaration } = fix;
1231-
const promotedDeclaration = promoteFromTypeOnly(changes, typeOnlyAliasDeclaration, compilerOptions, sourceFile);
1231+
const promotedDeclaration = promoteFromTypeOnly(changes, typeOnlyAliasDeclaration, compilerOptions, sourceFile, preferences);
12321232
return promotedDeclaration.kind === SyntaxKind.ImportSpecifier
12331233
? [Diagnostics.Remove_type_from_import_of_0_from_1, symbolName, getModuleSpecifierText(promotedDeclaration.parent.parent)]
12341234
: [Diagnostics.Remove_type_from_import_declaration_from_0, getModuleSpecifierText(promotedDeclaration)];
@@ -1244,17 +1244,18 @@ function getModuleSpecifierText(promotedDeclaration: ImportClause | ImportEquals
12441244
: cast(promotedDeclaration.parent.moduleSpecifier, isStringLiteral).text;
12451245
}
12461246

1247-
function promoteFromTypeOnly(changes: textChanges.ChangeTracker, aliasDeclaration: TypeOnlyAliasDeclaration, compilerOptions: CompilerOptions, sourceFile: SourceFile) {
1247+
function promoteFromTypeOnly(changes: textChanges.ChangeTracker, aliasDeclaration: TypeOnlyAliasDeclaration, compilerOptions: CompilerOptions, sourceFile: SourceFile, preferences: UserPreferences) {
12481248
// See comment in `doAddExistingFix` on constant with the same name.
12491249
const convertExistingToTypeOnly = compilerOptions.preserveValueImports && compilerOptions.isolatedModules;
12501250
switch (aliasDeclaration.kind) {
12511251
case SyntaxKind.ImportSpecifier:
12521252
if (aliasDeclaration.isTypeOnly) {
1253-
const sortKind = OrganizeImports.detectImportSpecifierSorting(aliasDeclaration.parent.elements);
1253+
const sortKind = OrganizeImports.detectImportSpecifierSorting(aliasDeclaration.parent.elements, preferences);
12541254
if (aliasDeclaration.parent.elements.length > 1 && sortKind) {
12551255
changes.delete(sourceFile, aliasDeclaration);
12561256
const newSpecifier = factory.updateImportSpecifier(aliasDeclaration, /*isTypeOnly*/ false, aliasDeclaration.propertyName, aliasDeclaration.name);
1257-
const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, sortKind === SortKind.CaseInsensitive);
1257+
const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive);
1258+
const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, comparer);
12581259
changes.insertImportSpecifierAtIndex(sourceFile, newSpecifier, aliasDeclaration.parent, insertionIndex);
12591260
}
12601261
else {
@@ -1285,7 +1286,7 @@ function promoteFromTypeOnly(changes: textChanges.ChangeTracker, aliasDeclaratio
12851286
if (convertExistingToTypeOnly) {
12861287
const namedImports = tryCast(importClause.namedBindings, isNamedImports);
12871288
if (namedImports && namedImports.elements.length > 1) {
1288-
if (OrganizeImports.detectImportSpecifierSorting(namedImports.elements) &&
1289+
if (OrganizeImports.detectImportSpecifierSorting(namedImports.elements, preferences) &&
12891290
aliasDeclaration.kind === SyntaxKind.ImportSpecifier &&
12901291
namedImports.elements.indexOf(aliasDeclaration) !== 0
12911292
) {
@@ -1348,36 +1349,37 @@ function doAddExistingFix(
13481349
ignoreCaseForSorting = preferences.organizeImportsIgnoreCase;
13491350
}
13501351
else if (existingSpecifiers) {
1351-
const targetImportSorting = OrganizeImports.detectImportSpecifierSorting(existingSpecifiers);
1352+
const targetImportSorting = OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences);
13521353
if (targetImportSorting !== SortKind.Both) {
13531354
ignoreCaseForSorting = targetImportSorting === SortKind.CaseInsensitive;
13541355
}
13551356
}
13561357
if (ignoreCaseForSorting === undefined) {
1357-
ignoreCaseForSorting = OrganizeImports.detectSorting(sourceFile) === SortKind.CaseInsensitive;
1358+
ignoreCaseForSorting = OrganizeImports.detectSorting(sourceFile, preferences) === SortKind.CaseInsensitive;
13581359
}
13591360

1361+
const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, ignoreCaseForSorting);
13601362
const newSpecifiers = stableSort(
13611363
namedImports.map(namedImport => factory.createImportSpecifier(
13621364
(!clause.isTypeOnly || promoteFromTypeOnly) && needsTypeOnly(namedImport),
13631365
/*propertyName*/ undefined,
13641366
factory.createIdentifier(namedImport.name))),
1365-
(s1, s2) => OrganizeImports.compareImportOrExportSpecifiers(s1, s2, ignoreCaseForSorting));
1367+
(s1, s2) => OrganizeImports.compareImportOrExportSpecifiers(s1, s2, comparer));
13661368

13671369
// The sorting preference computed earlier may or may not have validated that these particular
13681370
// import specifiers are sorted. If they aren't, `getImportSpecifierInsertionIndex` will return
13691371
// nonsense. So if there are existing specifiers, even if we know the sorting preference, we
13701372
// need to ensure that the existing specifiers are sorted according to the preference in order
13711373
// to do a sorted insertion.
1372-
const specifierSort = existingSpecifiers?.length && OrganizeImports.detectImportSpecifierSorting(existingSpecifiers);
1374+
const specifierSort = existingSpecifiers?.length && OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences);
13731375
if (specifierSort && !(ignoreCaseForSorting && specifierSort === SortKind.CaseSensitive)) {
13741376
for (const spec of newSpecifiers) {
13751377
// Organize imports puts type-only import specifiers last, so if we're
13761378
// adding a non-type-only specifier and converting all the other ones to
13771379
// type-only, there's no need to ask for the insertion index - it's 0.
13781380
const insertionIndex = convertExistingToTypeOnly && !spec.isTypeOnly
13791381
? 0
1380-
: OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, ignoreCaseForSorting);
1382+
: OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, comparer);
13811383
changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex);
13821384
}
13831385
}

0 commit comments

Comments
 (0)