Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Organize imports collation #52115

Merged
merged 11 commits into from
Jan 19, 2023
91 changes: 71 additions & 20 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -865,32 +865,25 @@ export const enum SortKind {
}

/** @internal */
export function detectSortCaseSensitivity(array: readonly string[], useEslintOrdering?: boolean): SortKind;
/** @internal */
export function detectSortCaseSensitivity<T>(array: readonly T[], useEslintOrdering: boolean, getString: (element: T) => string): SortKind;
/** @internal */
export function detectSortCaseSensitivity<T>(array: readonly T[], useEslintOrdering?: boolean, getString?: (element: T) => string): SortKind {
export function detectSortCaseSensitivity<T>(
array: readonly T[],
getString: (element: T) => string,
compareStringsCaseSensitive: Comparer<string>,
compareStringsCaseInsensitive: Comparer<string>,
): SortKind {
let kind = SortKind.Both;
if (array.length < 2) return kind;
const caseSensitiveComparer = getString
? (a: T, b: T) => compareStringsCaseSensitive(getString(a), getString(b))
: compareStringsCaseSensitive as (a: T | undefined, b: T | undefined) => Comparison;
const compareCaseInsensitive = useEslintOrdering ? compareStringsCaseInsensitiveEslintCompatible : compareStringsCaseInsensitive;
const caseInsensitiveComparer = getString
? (a: T, b: T) => compareCaseInsensitive(getString(a), getString(b))
: compareCaseInsensitive as (a: T | undefined, b: T | undefined) => Comparison;
for (let i = 1, len = array.length; i < len; i++) {
const prevElement = array[i - 1];
const element = array[i];
if (kind & SortKind.CaseSensitive && caseSensitiveComparer(prevElement, element) === Comparison.GreaterThan) {

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 && caseInsensitiveComparer(prevElement, element) === Comparison.GreaterThan) {
if (kind & SortKind.CaseInsensitive && compareStringsCaseInsensitive(prevElement, element) > 0) {
kind &= ~SortKind.CaseInsensitive;
}
if (kind === SortKind.None) {
return kind;
}
prevElement = element;
}
return kind;
}
Expand Down Expand Up @@ -2048,6 +2041,64 @@ export function memoizeWeak<A extends object, T>(callback: (arg: A) => T): (arg:
};
}

/** @internal */
export interface MemoizeCache<A extends any[], T> {
has(args: A): boolean;
get(args: A): T | undefined;
set(args: A, value: T): void;
}

/**
* A version of `memoize` that supports multiple arguments, backed by a provided cache.
*
* @internal
*/
export function memoizeCached<A extends any[], T>(callback: (...args: A) => T, cache: MemoizeCache<A, T>): (...args: A) => T {
return (...args: A) => {
let value = cache.get(args);
if (value === undefined && !cache.has(args)) {
value = callback(...args);
cache.set(args, value);
}
return value!;
};
}

/**
* High-order function, composes functions. Note that functions are composed inside-out;
* for example, `compose(a, b)` is the equivalent of `x => b(a(x))`.
*
* @param args The functions to compose.
*
* @internal
*/
export function compose<T>(...args: ((t: T) => T)[]): (t: T) => T;
/** @internal */
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 {
if (!!e) {
const args: ((t: T) => T)[] = [];
for (let i = 0; i < arguments.length; i++) {
args[i] = arguments[i];
}

return t => reduceLeft(args, (u, f) => f(u), t);
}
else if (d) {
return t => d(c(b(a(t))));
}
else if (c) {
return t => c(b(a(t)));
}
else if (b) {
return t => b(a(t));
}
else if (a) {
return t => a(t);
}
else {
return t => t;
}
}
/** @internal */
export const enum AssertionLevel {
None = 0,
Expand Down
5 changes: 5 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9832,6 +9832,11 @@ export interface UserPreferences {
readonly allowRenameOfImportPath?: boolean;
readonly autoImportFileExcludePatterns?: string[];
readonly organizeImportsIgnoreCase?: "auto" | boolean;
readonly organizeImportsCollation?: "ordinal" | "unicode";
readonly organizeImportsLocale?: string;
readonly organizeImportsNumericCollation?: boolean;
readonly organizeImportsAccentCollation?: boolean;
readonly organizeImportsCaseFirst?: "upper" | "lower" | false;
}

/** Represents a bigint literal value without requiring bigint support */
Expand Down
53 changes: 53 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3516,7 +3516,60 @@ export interface UserPreferences {
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
readonly includeInlayEnumMemberValueHints?: boolean;
readonly autoImportFileExcludePatterns?: string[];

/**
* Indicates whether imports should be organized in a case-insensitive manner.
*/
readonly organizeImportsIgnoreCase?: "auto" | boolean;
/**
* Indicates whether imports should be organized via an "ordinal" (binary) comparison using the numeric value
* of their code points, or via "unicode" collation (via the
* [Unicode Collation Algorithm](https://unicode.org/reports/tr10/#Scope)) using rules associated with the locale
* specified in {@link organizeImportsCollationLocale}.
*
* Default: `"ordinal"`.
*/
readonly organizeImportsCollation?: "ordinal" | "unicode";
/**
* Indicates the locale to use for "unicode" collation. If not specified, the locale `"en"` is used as an invariant
* for the sake of consistent sorting. Use `"auto"` to use the detected UI locale.
*
* This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`.
*
* Default: `"en"`
*/
readonly organizeImportsCollationLocale?: string;
/**
* Indicates whether numeric collation should be used for digit sequences in strings. When `true`, will collate
* strings such that `a1z < a2z < a100z`. When `false`, will collate strings such that `a1z < a100z < a2z`.
*
* This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`.
*
* Default: `false`
*/
readonly organizeImportsNumericCollation?: boolean;
/**
* Indicates whether accents and other diacritic marks are considered unequal for the purpose of collation. When
* `true`, characters with accents and other diacritics will be collated in the order defined by the locale specified
* in {@link organizeImportsCollationLocale}.
*
* This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`.
*
* Default: `true`
*/
readonly organizeImportsAccentCollation?: boolean;
/**
* Indicates whether upper case or lower case should sort first. When `false`, the default order for the locale
* specified in {@link organizeImportsCollationLocale} is used.
*
* This preference is ignored if {@link organizeImportsCollation} is not `"unicode"`. This preference is also
* ignored if we are using case-insensitive sorting, which occurs when {@link organizeImportsIgnoreCase} is `true`,
* or if {@link organizeImportsIgnoreCase} is `"auto"` and the auto-detected case sensitivity is determined to be
* case-insensitive.
*
* Default: `false`
*/
readonly organizeImportsCaseFirst?: "upper" | "lower" | false;

/**
* Indicates whether {@link ReferencesResponseItem.lineText} is supported.
Expand Down
26 changes: 14 additions & 12 deletions src/services/codefixes/importFixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAu
newDeclarations = combine(newDeclarations, declarations);
});
if (newDeclarations) {
insertImports(changeTracker, sourceFile, newDeclarations, /*blankLineBetween*/ true);
insertImports(changeTracker, sourceFile, newDeclarations, /*blankLineBetween*/ true, preferences);
}
}

Expand Down Expand Up @@ -1221,14 +1221,14 @@ function codeActionForFixWorker(changes: textChanges.ChangeTracker, sourceFile:
const defaultImport: Import | undefined = importKind === ImportKind.Default ? { name: symbolName, addAsTypeOnly } : undefined;
const namedImports: Import[] | undefined = importKind === ImportKind.Named ? [{ name: symbolName, addAsTypeOnly }] : undefined;
const namespaceLikeImport = importKind === ImportKind.Namespace || importKind === ImportKind.CommonJS ? { importKind, name: symbolName, addAsTypeOnly } : undefined;
insertImports(changes, sourceFile, getDeclarations(moduleSpecifier, quotePreference, defaultImport, namedImports, namespaceLikeImport), /*blankLineBetween*/ true);
insertImports(changes, sourceFile, getDeclarations(moduleSpecifier, quotePreference, defaultImport, namedImports, namespaceLikeImport), /*blankLineBetween*/ true, preferences);
return includeSymbolNameInDescription
? [Diagnostics.Import_0_from_1, symbolName, moduleSpecifier]
: [Diagnostics.Add_import_from_0, moduleSpecifier];
}
case ImportFixKind.PromoteTypeOnly: {
const { typeOnlyAliasDeclaration } = fix;
const promotedDeclaration = promoteFromTypeOnly(changes, typeOnlyAliasDeclaration, compilerOptions, sourceFile);
const promotedDeclaration = promoteFromTypeOnly(changes, typeOnlyAliasDeclaration, compilerOptions, sourceFile, preferences);
return promotedDeclaration.kind === SyntaxKind.ImportSpecifier
? [Diagnostics.Remove_type_from_import_of_0_from_1, symbolName, getModuleSpecifierText(promotedDeclaration.parent.parent)]
: [Diagnostics.Remove_type_from_import_declaration_from_0, getModuleSpecifierText(promotedDeclaration)];
Expand All @@ -1244,17 +1244,18 @@ function getModuleSpecifierText(promotedDeclaration: ImportClause | ImportEquals
: cast(promotedDeclaration.parent.moduleSpecifier, isStringLiteral).text;
}

function promoteFromTypeOnly(changes: textChanges.ChangeTracker, aliasDeclaration: TypeOnlyAliasDeclaration, compilerOptions: CompilerOptions, sourceFile: SourceFile) {
function promoteFromTypeOnly(changes: textChanges.ChangeTracker, aliasDeclaration: TypeOnlyAliasDeclaration, compilerOptions: CompilerOptions, sourceFile: SourceFile, preferences: UserPreferences) {
// See comment in `doAddExistingFix` on constant with the same name.
const convertExistingToTypeOnly = compilerOptions.preserveValueImports && compilerOptions.isolatedModules;
switch (aliasDeclaration.kind) {
case SyntaxKind.ImportSpecifier:
if (aliasDeclaration.isTypeOnly) {
const sortKind = OrganizeImports.detectImportSpecifierSorting(aliasDeclaration.parent.elements);
const sortKind = OrganizeImports.detectImportSpecifierSorting(aliasDeclaration.parent.elements, preferences);
if (aliasDeclaration.parent.elements.length > 1 && sortKind) {
changes.delete(sourceFile, aliasDeclaration);
const newSpecifier = factory.updateImportSpecifier(aliasDeclaration, /*isTypeOnly*/ false, aliasDeclaration.propertyName, aliasDeclaration.name);
const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, sortKind === SortKind.CaseInsensitive);
const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive);
const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, comparer);
changes.insertImportSpecifierAtIndex(sourceFile, newSpecifier, aliasDeclaration.parent, insertionIndex);
}
else {
Expand Down Expand Up @@ -1285,7 +1286,7 @@ function promoteFromTypeOnly(changes: textChanges.ChangeTracker, aliasDeclaratio
if (convertExistingToTypeOnly) {
const namedImports = tryCast(importClause.namedBindings, isNamedImports);
if (namedImports && namedImports.elements.length > 1) {
if (OrganizeImports.detectImportSpecifierSorting(namedImports.elements) &&
if (OrganizeImports.detectImportSpecifierSorting(namedImports.elements, preferences) &&
aliasDeclaration.kind === SyntaxKind.ImportSpecifier &&
namedImports.elements.indexOf(aliasDeclaration) !== 0
) {
Expand Down Expand Up @@ -1348,36 +1349,37 @@ function doAddExistingFix(
ignoreCaseForSorting = preferences.organizeImportsIgnoreCase;
}
else if (existingSpecifiers) {
const targetImportSorting = OrganizeImports.detectImportSpecifierSorting(existingSpecifiers);
const targetImportSorting = OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences);
if (targetImportSorting !== SortKind.Both) {
ignoreCaseForSorting = targetImportSorting === SortKind.CaseInsensitive;
}
}
if (ignoreCaseForSorting === undefined) {
ignoreCaseForSorting = OrganizeImports.detectSorting(sourceFile) === SortKind.CaseInsensitive;
ignoreCaseForSorting = OrganizeImports.detectSorting(sourceFile, preferences) === SortKind.CaseInsensitive;
}

const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, ignoreCaseForSorting);
const newSpecifiers = stableSort(
namedImports.map(namedImport => factory.createImportSpecifier(
(!clause.isTypeOnly || promoteFromTypeOnly) && needsTypeOnly(namedImport),
/*propertyName*/ undefined,
factory.createIdentifier(namedImport.name))),
(s1, s2) => OrganizeImports.compareImportOrExportSpecifiers(s1, s2, ignoreCaseForSorting));
(s1, s2) => OrganizeImports.compareImportOrExportSpecifiers(s1, s2, comparer));

// The sorting preference computed earlier may or may not have validated that these particular
// import specifiers are sorted. If they aren't, `getImportSpecifierInsertionIndex` will return
// 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);
const specifierSort = existingSpecifiers?.length && OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences);
if (specifierSort && !(ignoreCaseForSorting && specifierSort === SortKind.CaseSensitive)) {
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 = convertExistingToTypeOnly && !spec.isTypeOnly
? 0
: OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, ignoreCaseForSorting);
: OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, comparer);
changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex);
}
}
Expand Down
Loading