Add option for organize imports case sensitivity (#51733)

* Add ignore case option to organizeImports

* Adopt in auto-imports, use same case-insensitive comparison as eslint

* Fix build/lint

* Mark functions internal

* Update affected auto import test

* Update API baseline

* Update protocol

* Update API baseline

* Short-circuit comparisons that have already failed
This commit is contained in:
Andrew Branch 2022-12-13 14:37:29 -08:00 committed by GitHub
parent a5dde88dce
commit 4076ff8fd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 430 additions and 109 deletions

View File

@ -944,16 +944,53 @@ export function sortAndDeduplicate<T>(array: readonly T[], comparer?: Comparer<T
/** @internal */
export function arrayIsSorted<T>(array: readonly T[], comparer: Comparer<T>) {
if (array.length < 2) return true;
let prevElement = array[0];
for (const element of array.slice(1)) {
if (comparer(prevElement, element) === Comparison.GreaterThan) {
for (let i = 1, len = array.length; i < len; i++) {
if (comparer(array[i - 1], array[i]) === Comparison.GreaterThan) {
return false;
}
prevElement = element;
}
return true;
}
/** @internal */
export const enum SortKind {
None = 0,
CaseSensitive = 1 << 0,
CaseInsensitive = 1 << 1,
Both = CaseSensitive | CaseInsensitive,
}
/** @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 {
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) {
kind &= ~SortKind.CaseSensitive;
}
if (kind & SortKind.CaseInsensitive && caseInsensitiveComparer(prevElement, element) === Comparison.GreaterThan) {
kind &= ~SortKind.CaseInsensitive;
}
if (kind === SortKind.None) {
return kind;
}
}
return kind;
}
/** @internal */
export function arrayIsEqualTo<T>(array1: readonly T[] | undefined, array2: readonly T[] | undefined, equalityComparer: (a: T, b: T, index: number) => boolean = equateValues): boolean {
if (!array1 || !array2) {
@ -2144,6 +2181,23 @@ export function memoizeOne<A extends string | number | boolean | undefined, T>(c
};
}
/**
* A version of `memoize` that supports a single non-primitive argument, stored as keys of a WeakMap.
*
* @internal
*/
export function memoizeWeak<A extends object, T>(callback: (arg: A) => T): (arg: A) => T {
const map = new WeakMap<A, T>();
return (arg: A) => {
let value = map.get(arg);
if (value === undefined && !map.has(arg)) {
value = callback(arg);
map.set(arg, 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))`.
@ -2293,6 +2347,27 @@ export function compareStringsCaseInsensitive(a: string, b: string) {
return a < b ? Comparison.LessThan : a > b ? Comparison.GreaterThan : Comparison.EqualTo;
}
/**
* `compareStringsCaseInsensitive` transforms letters to uppercase for unicode reasons,
* while eslint's `sort-imports` rule transforms letters to lowercase. Which one you choose
* affects the relative order of letters and ASCII characters 91-96, of which `_` is a
* valid character in an identifier. So if we used `compareStringsCaseInsensitive` for
* import sorting, TypeScript and eslint would disagree about the correct case-insensitive
* sort order for `__String` and `Foo`. Since eslint's whole job is to create consistency
* by enforcing nitpicky details like this, it makes way more sense for us to just adopt
* their convention so users can have auto-imports without making eslint angry.
*
* @internal
*/
export function compareStringsCaseInsensitiveEslintCompatible(a: string, b: string) {
if (a === b) return Comparison.EqualTo;
if (a === undefined) return Comparison.LessThan;
if (b === undefined) return Comparison.GreaterThan;
a = a.toLowerCase();
b = b.toLowerCase();
return a < b ? Comparison.LessThan : a > b ? Comparison.GreaterThan : Comparison.EqualTo;
}
/**
* Compare two strings using a case-sensitive ordinal comparison.
*

View File

@ -9740,6 +9740,7 @@ export interface UserPreferences {
readonly includeInlayEnumMemberValueHints?: boolean;
readonly allowRenameOfImportPath?: boolean;
readonly autoImportFileExcludePatterns?: string[];
readonly organizeImportsIgnoreCase?: "auto" | boolean;
}
/** Represents a bigint literal value without requiring bigint support */

View File

@ -535,8 +535,8 @@ export class TestState {
}
}
public verifyOrganizeImports(newContent: string, mode?: ts.OrganizeImportsMode) {
const changes = this.languageService.organizeImports({ fileName: this.activeFile.fileName, type: "file", mode }, this.formatCodeSettings, ts.emptyOptions);
public verifyOrganizeImports(newContent: string, mode?: ts.OrganizeImportsMode, preferences?: ts.UserPreferences) {
const changes = this.languageService.organizeImports({ fileName: this.activeFile.fileName, type: "file", mode }, this.formatCodeSettings, preferences);
this.applyChanges(changes);
this.verifyFileContent(this.activeFile.fileName, newContent);
}

View File

@ -630,8 +630,8 @@ export class Verify extends VerifyNegatable {
this.state.noMoveToNewFile();
}
public organizeImports(newContent: string, mode?: ts.OrganizeImportsMode): void {
this.state.verifyOrganizeImports(newContent, mode);
public organizeImports(newContent: string, mode?: ts.OrganizeImportsMode, preferences?: ts.UserPreferences): void {
this.state.verifyOrganizeImports(newContent, mode, preferences);
}
}

View File

@ -3516,6 +3516,7 @@ export interface UserPreferences {
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
readonly includeInlayEnumMemberValueHints?: boolean;
readonly autoImportFileExcludePatterns?: string[];
readonly organizeImportsIgnoreCase?: "auto" | boolean;
/**
* Indicates whether {@link ReferencesResponseItem.lineText} is supported.

View File

@ -118,6 +118,7 @@ import {
skipAlias,
some,
sort,
SortKind,
SourceFile,
stableSort,
startsWith,
@ -164,15 +165,14 @@ registerCodeFix({
const { errorCode, preferences, sourceFile, span, program } = context;
const info = getFixInfos(context, errorCode, span.start, /*useAutoImportProvider*/ true);
if (!info) return undefined;
const quotePreference = getQuotePreference(sourceFile, preferences);
return info.map(({ fix, symbolName, errorIdentifierText }) => codeActionForFix(
context,
sourceFile,
symbolName,
fix,
/*includeSymbolNameInDescription*/ symbolName !== errorIdentifierText,
quotePreference,
program.getCompilerOptions()));
program.getCompilerOptions(),
preferences));
},
fixIds: [importFixId],
getAllCodeActions: context => {
@ -358,7 +358,8 @@ function createImportAdderWorker(sourceFile: SourceFile, program: Program, useAu
importClauseOrBindingPattern,
defaultImport,
arrayFrom(namedImports.entries(), ([name, addAsTypeOnly]) => ({ addAsTypeOnly, name })),
compilerOptions);
compilerOptions,
preferences);
});
let newDeclarations: AnyImportOrRequireStatement | readonly AnyImportOrRequireStatement[] | undefined;
@ -516,7 +517,8 @@ export function getImportCompletionAction(
symbolName,
fix,
/*includeSymbolNameInDescription*/ false,
getQuotePreference(sourceFile, preferences), compilerOptions))
compilerOptions,
preferences))
};
}
@ -526,7 +528,7 @@ export function getPromoteTypeOnlyCompletionAction(sourceFile: SourceFile, symbo
const symbolName = single(getSymbolNamesToImport(sourceFile, program.getTypeChecker(), symbolToken, compilerOptions));
const fix = getTypeOnlyPromotionFix(sourceFile, symbolToken, symbolName, program);
const includeSymbolNameInDescription = symbolName !== symbolToken.text;
return fix && codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, includeSymbolNameInDescription, QuotePreference.Double, compilerOptions));
return fix && codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, includeSymbolNameInDescription, compilerOptions, preferences));
}
function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], moduleSymbol: Symbol, program: Program, useNamespaceInfo: { position: number, symbolName: string } | undefined, isValidTypeOnlyUseSite: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) {
@ -1176,14 +1178,15 @@ function getExportEqualsImportKind(importingFile: SourceFile, compilerOptions: C
return allowSyntheticDefaults ? ImportKind.Default : ImportKind.CommonJS;
}
function codeActionForFix(context: textChanges.TextChangesContext, sourceFile: SourceFile, symbolName: string, fix: ImportFix, includeSymbolNameInDescription: boolean, quotePreference: QuotePreference, compilerOptions: CompilerOptions): CodeFixAction {
function codeActionForFix(context: textChanges.TextChangesContext, sourceFile: SourceFile, symbolName: string, fix: ImportFix, includeSymbolNameInDescription: boolean, compilerOptions: CompilerOptions, preferences: UserPreferences): CodeFixAction {
let diag!: DiagnosticAndArguments;
const changes = textChanges.ChangeTracker.with(context, tracker => {
diag = codeActionForFixWorker(tracker, sourceFile, symbolName, fix, includeSymbolNameInDescription, quotePreference, compilerOptions);
diag = codeActionForFixWorker(tracker, sourceFile, symbolName, fix, includeSymbolNameInDescription, compilerOptions, preferences);
});
return createCodeFixAction(importFixName, changes, diag, importFixId, Diagnostics.Add_all_missing_imports);
}
function codeActionForFixWorker(changes: textChanges.ChangeTracker, sourceFile: SourceFile, symbolName: string, fix: ImportFix, includeSymbolNameInDescription: boolean, quotePreference: QuotePreference, compilerOptions: CompilerOptions): DiagnosticAndArguments {
function codeActionForFixWorker(changes: textChanges.ChangeTracker, sourceFile: SourceFile, symbolName: string, fix: ImportFix, includeSymbolNameInDescription: boolean, compilerOptions: CompilerOptions, preferences: UserPreferences): DiagnosticAndArguments {
const quotePreference = getQuotePreference(sourceFile, preferences);
switch (fix.kind) {
case ImportFixKind.UseNamespace:
addNamespaceQualifier(changes, sourceFile, fix);
@ -1199,7 +1202,8 @@ function codeActionForFixWorker(changes: textChanges.ChangeTracker, sourceFile:
importClauseOrBindingPattern,
importKind === ImportKind.Default ? { name: symbolName, addAsTypeOnly } : undefined,
importKind === ImportKind.Named ? [{ name: symbolName, addAsTypeOnly }] : emptyArray,
compilerOptions);
compilerOptions,
preferences);
const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier);
return includeSymbolNameInDescription
? [Diagnostics.Import_0_from_1, symbolName, moduleSpecifierWithoutQuotes]
@ -1240,10 +1244,11 @@ function promoteFromTypeOnly(changes: textChanges.ChangeTracker, aliasDeclaratio
switch (aliasDeclaration.kind) {
case SyntaxKind.ImportSpecifier:
if (aliasDeclaration.isTypeOnly) {
if (aliasDeclaration.parent.elements.length > 1 && OrganizeImports.importSpecifiersAreSorted(aliasDeclaration.parent.elements)) {
const sortKind = OrganizeImports.detectImportSpecifierSorting(aliasDeclaration.parent.elements);
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);
const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, sortKind === SortKind.CaseInsensitive);
changes.insertImportSpecifierAtIndex(sourceFile, newSpecifier, aliasDeclaration.parent, insertionIndex);
}
else {
@ -1274,7 +1279,7 @@ function promoteFromTypeOnly(changes: textChanges.ChangeTracker, aliasDeclaratio
if (convertExistingToTypeOnly) {
const namedImports = tryCast(importClause.namedBindings, isNamedImports);
if (namedImports && namedImports.elements.length > 1) {
if (OrganizeImports.importSpecifiersAreSorted(namedImports.elements) &&
if (OrganizeImports.detectImportSpecifierSorting(namedImports.elements) &&
aliasDeclaration.kind === SyntaxKind.ImportSpecifier &&
namedImports.elements.indexOf(aliasDeclaration) !== 0
) {
@ -1300,6 +1305,7 @@ function doAddExistingFix(
defaultImport: Import | undefined,
namedImports: readonly Import[],
compilerOptions: CompilerOptions,
preferences: UserPreferences,
): void {
if (clause.kind === SyntaxKind.ObjectBindingPattern) {
if (defaultImport) {
@ -1327,21 +1333,45 @@ 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
let ignoreCaseForSorting: boolean | undefined;
if (typeof preferences.organizeImportsIgnoreCase === "boolean") {
ignoreCaseForSorting = preferences.organizeImportsIgnoreCase;
}
else if (existingSpecifiers) {
const targetImportSorting = OrganizeImports.detectImportSpecifierSorting(existingSpecifiers);
if (targetImportSorting !== SortKind.Both) {
ignoreCaseForSorting = targetImportSorting === SortKind.CaseInsensitive;
}
}
if (ignoreCaseForSorting === undefined) {
ignoreCaseForSorting = OrganizeImports.detectSorting(sourceFile) === SortKind.CaseInsensitive;
}
const newSpecifiers = stableSort(
namedImports.map(namedImport => factory.createImportSpecifier(
(!clause.isTypeOnly || promoteFromTypeOnly) && needsTypeOnly(namedImport),
/*propertyName*/ undefined,
factory.createIdentifier(namedImport.name))),
OrganizeImports.compareImportOrExportSpecifiers);
(s1, s2) => OrganizeImports.compareImportOrExportSpecifiers(s1, s2, ignoreCaseForSorting));
if (existingSpecifiers?.length && OrganizeImports.importSpecifiersAreSorted(existingSpecifiers)) {
// 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);
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);
: OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, ignoreCaseForSorting);
changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex);
}
}

View File

@ -3,10 +3,12 @@ import {
arrayIsSorted,
binarySearch,
compareBooleans,
compareStringsCaseInsensitive,
compareStringsCaseInsensitiveEslintCompatible,
compareStringsCaseSensitive,
compareValues,
Comparison,
createScanner,
detectSortCaseSensitivity,
EmitFlags,
emptyArray,
ExportDeclaration,
@ -14,6 +16,7 @@ import {
Expression,
factory,
FileTextChanges,
find,
FindAllReferences,
flatMap,
formatting,
@ -39,6 +42,7 @@ import {
LanguageServiceHost,
length,
map,
memoizeWeak,
NamedImportBindings,
NamedImports,
NamespaceImport,
@ -48,7 +52,7 @@ import {
Scanner,
setEmitFlags,
some,
SortedReadonlyArray,
SortKind,
SourceFile,
stableSort,
suppressLeadingTrivia,
@ -81,22 +85,26 @@ export function organizeImports(
const shouldRemove = mode === OrganizeImportsMode.RemoveUnused || mode === OrganizeImportsMode.All;
const maybeRemove = shouldRemove ? removeUnusedImports : identity;
const maybeCoalesce = shouldCombine ? coalesceImports : identity;
// All of the old ImportDeclarations in the file, in syntactic order.
const topLevelImportGroupDecls = groupImportsByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration));
const ignoreCase = typeof preferences.organizeImportsIgnoreCase === "boolean"
? preferences.organizeImportsIgnoreCase
: shouldSort && detectSortingWorker(topLevelImportGroupDecls) === SortKind.CaseInsensitive;
const processImportsOfSameModuleSpecifier = (importGroup: readonly ImportDeclaration[]) => {
const processedDeclarations = maybeCoalesce(maybeRemove(importGroup, sourceFile, program), sourceFile);
const processedDeclarations = maybeCoalesce(maybeRemove(importGroup, sourceFile, program), ignoreCase, sourceFile);
return shouldSort
? stableSort(processedDeclarations, (s1, s2) => compareImportsOrRequireStatements(s1, s2))
: processedDeclarations;
};
// All of the old ImportDeclarations in the file, in syntactic order.
const topLevelImportGroupDecls = groupImportsByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration));
topLevelImportGroupDecls.forEach(importGroupDecl => organizeImportsWorker(importGroupDecl, processImportsOfSameModuleSpecifier));
// Exports are always used
if (mode !== OrganizeImportsMode.RemoveUnused) {
// All of the old ExportDeclarations in the file, in syntactic order.
const topLevelExportDecls = sourceFile.statements.filter(isExportDeclaration);
organizeImportsWorker(topLevelExportDecls, coalesceExports);
organizeImportsWorker(topLevelExportDecls, group => coalesceExports(group, ignoreCase));
}
for (const ambientModule of sourceFile.statements.filter(isAmbientModule)) {
@ -108,7 +116,7 @@ export function organizeImports(
// Exports are always used
if (mode !== OrganizeImportsMode.RemoveUnused) {
const ambientModuleExportDecls = ambientModule.body.statements.filter(isExportDeclaration);
organizeImportsWorker(ambientModuleExportDecls, coalesceExports);
organizeImportsWorker(ambientModuleExportDecls, group => coalesceExports(group, ignoreCase));
}
}
@ -130,13 +138,13 @@ export function organizeImports(
suppressLeadingTrivia(oldImportDecls[0]);
const oldImportGroups = shouldCombine
? group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier!)!)
? group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier)!)
: [oldImportDecls];
const sortedImportGroups = shouldSort
? stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiers(group1[0].moduleSpecifier, group2[0].moduleSpecifier))
: oldImportGroups;
const newImportDecls = flatMap(sortedImportGroups, importGroup =>
getExternalModuleName(importGroup[0].moduleSpecifier!)
getExternalModuleName(importGroup[0].moduleSpecifier)
? coalesce(importGroup)
: importGroup);
@ -170,7 +178,7 @@ function groupImportsByNewlineContiguous(sourceFile: SourceFile, importDecls: Im
const groupImports: ImportDeclaration[][] = [];
let groupIndex = 0;
for (const topLevelImportDecl of importDecls) {
if (isNewGroup(sourceFile, topLevelImportDecl, scanner)) {
if (groupImports[groupIndex] && isNewGroup(sourceFile, topLevelImportDecl, scanner)) {
groupIndex++;
}
@ -288,7 +296,7 @@ function hasModuleDeclarationMatchingSpecifier(sourceFile: SourceFile, moduleSpe
&& moduleName.text === moduleSpecifierText);
}
function getExternalModuleName(specifier: Expression) {
function getExternalModuleName(specifier: Expression | undefined) {
return specifier !== undefined && isStringLiteralLike(specifier)
? specifier.text
: undefined;
@ -300,13 +308,13 @@ function getExternalModuleName(specifier: Expression) {
*
* @internal
*/
export function coalesceImports(importGroup: readonly ImportDeclaration[], sourceFile?: SourceFile) {
export function coalesceImports(importGroup: readonly ImportDeclaration[], ignoreCase?: boolean, sourceFile?: SourceFile): readonly ImportDeclaration[] {
if (importGroup.length === 0) {
return importGroup;
}
const { importWithoutClause, typeOnlyImports, regularImports } = getCategorizedImports(importGroup);
const compareIdentifiers = ignoreCase ? compareIdentifiersCaseInsensitive : compareIdentifiersCaseSensitive;
const coalescedImports: ImportDeclaration[] = [];
if (importWithoutClause) {
@ -355,7 +363,7 @@ export function coalesceImports(importGroup: readonly ImportDeclaration[], sourc
newImportSpecifiers.push(...getNewImportSpecifiers(namedImports));
const sortedImportSpecifiers = factory.createNodeArray(
sortSpecifiers(newImportSpecifiers),
sortSpecifiers(newImportSpecifiers, ignoreCase),
(namedImports[0]?.importClause!.namedBindings as NamedImports)?.elements.hasTrailingComma
);
@ -454,7 +462,7 @@ function getCategorizedImports(importGroup: readonly ImportDeclaration[]) {
*
* @internal
*/
export function coalesceExports(exportGroup: readonly ExportDeclaration[]) {
export function coalesceExports(exportGroup: readonly ExportDeclaration[], ignoreCase: boolean) {
if (exportGroup.length === 0) {
return exportGroup;
}
@ -474,7 +482,7 @@ export function coalesceExports(exportGroup: readonly ExportDeclaration[]) {
const newExportSpecifiers: ExportSpecifier[] = [];
newExportSpecifiers.push(...flatMap(exportGroup, i => i.exportClause && isNamedExports(i.exportClause) ? i.exportClause.elements : emptyArray));
const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers);
const sortedExportSpecifiers = sortSpecifiers(newExportSpecifiers, ignoreCase);
const exportDecl = exportGroup[0];
coalescedExports.push(
@ -538,15 +546,22 @@ function updateImportDeclarationAndClause(
importDeclaration.assertClause);
}
function sortSpecifiers<T extends ImportOrExportSpecifier>(specifiers: readonly T[]) {
return stableSort(specifiers, compareImportOrExportSpecifiers);
function sortSpecifiers<T extends ImportOrExportSpecifier>(specifiers: readonly T[], ignoreCase?: boolean) {
return stableSort(specifiers, (s1, s2) => compareImportOrExportSpecifiers(s1, s2, ignoreCase));
}
/** @internal */
export function compareImportOrExportSpecifiers<T extends ImportOrExportSpecifier>(s1: T, s2: T): Comparison {
return compareBooleans(s1.isTypeOnly, s2.isTypeOnly)
|| compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name)
|| compareIdentifiers(s1.name, s2.name);
export function compareImportOrExportSpecifiers<T extends ImportOrExportSpecifier>(s1: T, s2: T, ignoreCase?: boolean): Comparison {
const compareIdentifiers = ignoreCase ? compareIdentifiersCaseInsensitive : compareIdentifiersCaseSensitive;
return compareBooleans(s1.isTypeOnly, s2.isTypeOnly) || compareIdentifiers(s1.name, s2.name);
}
function compareIdentifiersCaseSensitive(s1: Identifier, s2: Identifier) {
return compareStringsCaseSensitive(s1.text, s2.text);
}
function compareIdentifiersCaseInsensitive(s1: Identifier, s2: Identifier) {
return compareStringsCaseInsensitiveEslintCompatible(s1.text, s2.text);
}
/**
@ -554,16 +569,14 @@ export function compareImportOrExportSpecifiers<T extends ImportOrExportSpecifie
*
* @internal
*/
export function compareModuleSpecifiers(m1: Expression | undefined, m2: Expression | undefined) {
export function compareModuleSpecifiers(m1: Expression | undefined, m2: Expression | undefined, ignoreCase?: boolean) {
const name1 = m1 === undefined ? undefined : getExternalModuleName(m1);
const name2 = m2 === undefined ? undefined : getExternalModuleName(m2);
const compareStrings = ignoreCase ? compareStringsCaseInsensitiveEslintCompatible : compareStringsCaseSensitive;
return compareBooleans(name1 === undefined, name2 === undefined) ||
compareBooleans(isExternalModuleNameRelative(name1!), isExternalModuleNameRelative(name2!)) ||
compareStringsCaseInsensitive(name1!, name2!);
}
function compareIdentifiers(s1: Identifier, s2: Identifier) {
return compareStringsCaseInsensitive(s1.text, s2.text);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
compareStrings(name1!, name2!); // I don't know why eslint is wrong but this one is necessary
}
function getModuleSpecifierExpression(declaration: AnyImportOrRequireStatement): Expression | undefined {
@ -578,30 +591,71 @@ function getModuleSpecifierExpression(declaration: AnyImportOrRequireStatement):
}
/** @internal */
export function importsAreSorted(imports: readonly AnyImportOrRequireStatement[]): imports is SortedReadonlyArray<AnyImportOrRequireStatement> {
return arrayIsSorted(imports, compareImportsOrRequireStatements);
export function detectSorting(sourceFile: SourceFile): SortKind {
return detectSortingWorker(
groupImportsByNewlineContiguous(sourceFile, sourceFile.statements.filter(isImportDeclaration)));
}
function detectSortingWorker(importGroups: ImportDeclaration[][]): SortKind {
let sortState = SortKind.Both;
for (const importGroup of importGroups) {
// Check module specifiers
if (importGroup.length > 1) {
sortState &= detectSortCaseSensitivity(importGroup, /*useEslintOrdering*/ true, i => tryCast(i.moduleSpecifier, isStringLiteral)?.text ?? "");
if (!sortState) {
return sortState;
}
}
// Check import specifiers
const declarationWithNamedImports = find(
importGroup,
i => tryCast(i.importClause?.namedBindings, isNamedImports)?.elements.length! > 1);
if (declarationWithNamedImports) {
sortState &= detectImportSpecifierSorting((declarationWithNamedImports.importClause!.namedBindings as NamedImports).elements);
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 sortState;
}
/** @internal */
export function importSpecifiersAreSorted(imports: readonly ImportSpecifier[]): imports is SortedReadonlyArray<ImportSpecifier> {
return arrayIsSorted(imports, compareImportOrExportSpecifiers);
export function detectImportDeclarationSorting(imports: readonly AnyImportOrRequireStatement[]): SortKind {
return detectSortCaseSensitivity(imports, /*useEslintOrdering*/ true, s => getExternalModuleName(getModuleSpecifierExpression(s)) || "");
}
/** @internal */
export function getImportDeclarationInsertionIndex(sortedImports: SortedReadonlyArray<AnyImportOrRequireStatement>, newImport: AnyImportOrRequireStatement) {
const index = binarySearch(sortedImports, newImport, identity, compareImportsOrRequireStatements);
export const detectImportSpecifierSorting = memoizeWeak((specifiers: readonly ImportSpecifier[]): SortKind => {
if (!arrayIsSorted(specifiers, (s1, s2) => compareBooleans(s1.isTypeOnly, s2.isTypeOnly))) {
return SortKind.None;
}
return detectSortCaseSensitivity(specifiers, /*useEslintOrdering*/ true, specifier => specifier.name.text);
});
/** @internal */
export function getImportDeclarationInsertionIndex(sortedImports: readonly AnyImportOrRequireStatement[], newImport: AnyImportOrRequireStatement, ignoreCase?: boolean) {
const index = binarySearch(sortedImports, newImport, identity, (a, b) => compareImportsOrRequireStatements(a, b, ignoreCase));
return index < 0 ? ~index : index;
}
/** @internal */
export function getImportSpecifierInsertionIndex(sortedImports: SortedReadonlyArray<ImportSpecifier>, newImport: ImportSpecifier) {
const index = binarySearch(sortedImports, newImport, identity, compareImportOrExportSpecifiers);
export function getImportSpecifierInsertionIndex(sortedImports: readonly ImportSpecifier[], newImport: ImportSpecifier, ignoreCase?: boolean) {
const index = binarySearch(sortedImports, newImport, identity, (s1, s2) => compareImportOrExportSpecifiers(s1, s2, ignoreCase));
return index < 0 ? ~index : index;
}
/** @internal */
export function compareImportsOrRequireStatements(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement) {
return compareModuleSpecifiers(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2)) || compareImportKind(s1, s2);
export function compareImportsOrRequireStatements(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement, ignoreCase?: boolean) {
return compareModuleSpecifiers(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2), ignoreCase) || compareImportKind(s1, s2);
}
function compareImportKind(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement) {

View File

@ -2541,7 +2541,7 @@ export function insertImports(changes: textChanges.ChangeTracker, sourceFile: So
if (!existingImportStatements.length) {
changes.insertNodesAtTopOfFile(sourceFile, sortedNewImports, blankLineBetween);
}
else if (existingImportStatements && OrganizeImports.importsAreSorted(existingImportStatements)) {
else if (existingImportStatements && OrganizeImports.detectImportDeclarationSorting(existingImportStatements)) {
for (const newImport of sortedNewImports) {
const insertionIndex = OrganizeImports.getImportDeclarationInsertionIndex(existingImportStatements, newImport);
if (insertionIndex === 0) {

View File

@ -38,20 +38,20 @@ describe("unittests:: services:: organizeImports", () => {
function assertSortsBefore(importString1: string, importString2: string) {
const [{moduleSpecifier: moduleSpecifier1}, {moduleSpecifier: moduleSpecifier2}] = parseImports(importString1, importString2);
assert.equal(ts.OrganizeImports.compareModuleSpecifiers(moduleSpecifier1, moduleSpecifier2), ts.Comparison.LessThan);
assert.equal(ts.OrganizeImports.compareModuleSpecifiers(moduleSpecifier2, moduleSpecifier1), ts.Comparison.GreaterThan);
assert.equal(ts.OrganizeImports.compareModuleSpecifiers(moduleSpecifier1, moduleSpecifier2, /*ignoreCase*/ true), ts.Comparison.LessThan);
assert.equal(ts.OrganizeImports.compareModuleSpecifiers(moduleSpecifier2, moduleSpecifier1, /*ignoreCase*/ true), ts.Comparison.GreaterThan);
}
});
describe("Coalesce imports", () => {
it("No imports", () => {
assert.isEmpty(ts.OrganizeImports.coalesceImports([]));
assert.isEmpty(ts.OrganizeImports.coalesceImports([], /*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);
const expectedCoalescedImports = parseImports(`import { a as n, B, default as M, y, Z as O } from "lib";`);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true);
const expectedCoalescedImports = parseImports(`import { B, default as M, a as n, Z as O, y } from "lib";`);
assertListEqual(actualCoalescedImports, expectedCoalescedImports);
});
@ -59,7 +59,7 @@ describe("unittests:: services:: organizeImports", () => {
const sortedImports = parseImports(
`import "lib";`,
`import "lib";`);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true);
const expectedCoalescedImports = parseImports(`import "lib";`);
assertListEqual(actualCoalescedImports, expectedCoalescedImports);
});
@ -68,7 +68,7 @@ describe("unittests:: services:: organizeImports", () => {
const sortedImports = parseImports(
`import * as x from "lib";`,
`import * as y from "lib";`);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true);
const expectedCoalescedImports = sortedImports;
assertListEqual(actualCoalescedImports, expectedCoalescedImports);
});
@ -77,7 +77,7 @@ describe("unittests:: services:: organizeImports", () => {
const sortedImports = parseImports(
`import x from "lib";`,
`import y from "lib";`);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true);
const expectedCoalescedImports = parseImports(`import { default as x, default as y } from "lib";`);
assertListEqual(actualCoalescedImports, expectedCoalescedImports);
});
@ -86,7 +86,7 @@ describe("unittests:: services:: organizeImports", () => {
const sortedImports = parseImports(
`import { x } from "lib";`,
`import { y as z } from "lib";`);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true);
const expectedCoalescedImports = parseImports(`import { x, y as z } from "lib";`);
assertListEqual(actualCoalescedImports, expectedCoalescedImports);
});
@ -95,7 +95,7 @@ describe("unittests:: services:: organizeImports", () => {
const sortedImports = parseImports(
`import "lib";`,
`import * as x from "lib";`);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true);
const expectedCoalescedImports = sortedImports;
assertListEqual(actualCoalescedImports, expectedCoalescedImports);
});
@ -104,7 +104,7 @@ describe("unittests:: services:: organizeImports", () => {
const sortedImports = parseImports(
`import "lib";`,
`import x from "lib";`);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true);
const expectedCoalescedImports = sortedImports;
assertListEqual(actualCoalescedImports, expectedCoalescedImports);
});
@ -113,7 +113,7 @@ describe("unittests:: services:: organizeImports", () => {
const sortedImports = parseImports(
`import "lib";`,
`import { x } from "lib";`);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true);
const expectedCoalescedImports = sortedImports;
assertListEqual(actualCoalescedImports, expectedCoalescedImports);
});
@ -122,7 +122,7 @@ describe("unittests:: services:: organizeImports", () => {
const sortedImports = parseImports(
`import * as x from "lib";`,
`import y from "lib";`);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true);
const expectedCoalescedImports = parseImports(
`import y, * as x from "lib";`);
assertListEqual(actualCoalescedImports, expectedCoalescedImports);
@ -132,7 +132,7 @@ describe("unittests:: services:: organizeImports", () => {
const sortedImports = parseImports(
`import * as x from "lib";`,
`import { y } from "lib";`);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true);
const expectedCoalescedImports = sortedImports;
assertListEqual(actualCoalescedImports, expectedCoalescedImports);
});
@ -141,7 +141,7 @@ describe("unittests:: services:: organizeImports", () => {
const sortedImports = parseImports(
`import x from "lib";`,
`import { y } from "lib";`);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true);
const expectedCoalescedImports = parseImports(
`import x, { y } from "lib";`);
assertListEqual(actualCoalescedImports, expectedCoalescedImports);
@ -157,7 +157,7 @@ describe("unittests:: services:: organizeImports", () => {
`import * as x from "lib";`,
`import z from "lib";`,
`import { a } from "lib";`);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true);
const expectedCoalescedImports = parseImports(
`import "lib";`,
`import * as x from "lib";`,
@ -172,7 +172,7 @@ describe("unittests:: services:: organizeImports", () => {
`import * as x from "lib";`,
`import * as y from "lib";`,
`import z from "lib";`);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true);
const expectedCoalescedImports = sortedImports;
assertListEqual(actualCoalescedImports, expectedCoalescedImports);
});
@ -182,7 +182,7 @@ describe("unittests:: services:: organizeImports", () => {
`import type { x } from "lib";`,
`import type { y } from "lib";`,
`import { z } from "lib";`);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true);
const expectedCoalescedImports = parseImports(
`import { z } from "lib";`,
`import type { x, y } from "lib";`);
@ -196,7 +196,7 @@ describe("unittests:: services:: organizeImports", () => {
`import type z from "lib";`);
// Default import could be rewritten as a named import to combine with `x`,
// but seems of debatable merit.
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true);
const expectedCoalescedImports = actualCoalescedImports;
assertListEqual(actualCoalescedImports, expectedCoalescedImports);
});
@ -204,19 +204,19 @@ describe("unittests:: services:: organizeImports", () => {
describe("Coalesce exports", () => {
it("No exports", () => {
assert.isEmpty(ts.OrganizeImports.coalesceExports([]));
assert.isEmpty(ts.OrganizeImports.coalesceExports([], /*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);
const expectedCoalescedExports = parseExports(`export { a as n, B, default as M, y, Z as O } from "lib";`);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(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", () => {
const sortedImports = parseImports(`import { type z, y, type x, c, type b, a } from "lib";`);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports);
const actualCoalescedImports = ts.OrganizeImports.coalesceImports(sortedImports, /*ignoreCase*/ true);
const expectedCoalescedImports = parseImports(`import { a, c, y, type b, type x, type z } from "lib";`);
assertListEqual(actualCoalescedImports, expectedCoalescedImports);
});
@ -225,7 +225,7 @@ describe("unittests:: services:: organizeImports", () => {
const sortedExports = parseExports(
`export * from "lib";`,
`export * from "lib";`);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true);
const expectedCoalescedExports = parseExports(`export * from "lib";`);
assertListEqual(actualCoalescedExports, expectedCoalescedExports);
});
@ -234,7 +234,7 @@ describe("unittests:: services:: organizeImports", () => {
const sortedExports = parseExports(
`export { x };`,
`export { y as z };`);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true);
const expectedCoalescedExports = parseExports(`export { x, y as z };`);
assertListEqual(actualCoalescedExports, expectedCoalescedExports);
});
@ -243,7 +243,7 @@ describe("unittests:: services:: organizeImports", () => {
const sortedExports = parseExports(
`export { x } from "lib";`,
`export { y as z } from "lib";`);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true);
const expectedCoalescedExports = parseExports(`export { x, y as z } from "lib";`);
assertListEqual(actualCoalescedExports, expectedCoalescedExports);
});
@ -252,7 +252,7 @@ describe("unittests:: services:: organizeImports", () => {
const sortedExports = parseExports(
`export * from "lib";`,
`export { y } from "lib";`);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true);
const expectedCoalescedExports = sortedExports;
assertListEqual(actualCoalescedExports, expectedCoalescedExports);
});
@ -262,9 +262,9 @@ describe("unittests:: services:: organizeImports", () => {
`export { x };`,
`export { y as w, z as default };`,
`export { w as q };`);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true);
const expectedCoalescedExports = parseExports(
`export { w as q, x, y as w, z as default };`);
`export { z as default, w as q, y as w, x };`);
assertListEqual(actualCoalescedExports, expectedCoalescedExports);
});
@ -273,10 +273,10 @@ describe("unittests:: services:: organizeImports", () => {
`export { x as a, y } from "lib";`,
`export * from "lib";`,
`export { z as b } from "lib";`);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true);
const expectedCoalescedExports = parseExports(
`export * from "lib";`,
`export { x as a, y, z as b } from "lib";`);
`export { x as a, z as b, y } from "lib";`);
assertListEqual(actualCoalescedExports, expectedCoalescedExports);
});
@ -284,7 +284,7 @@ describe("unittests:: services:: organizeImports", () => {
const sortedExports = parseExports(
`export { x };`,
`export type { y };`);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true);
const expectedCoalescedExports = sortedExports;
assertListEqual(actualCoalescedExports, expectedCoalescedExports);
});
@ -293,7 +293,7 @@ describe("unittests:: services:: organizeImports", () => {
const sortedExports = parseExports(
`export type { x };`,
`export type { y };`);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports);
const actualCoalescedExports = ts.OrganizeImports.coalesceExports(sortedExports, /*ignoreCase*/ true);
const expectedCoalescedExports = parseExports(
`export type { x, y };`);
assertListEqual(actualCoalescedExports, expectedCoalescedExports);

View File

@ -2802,6 +2802,7 @@ declare namespace ts {
readonly includeInlayFunctionLikeReturnTypeHints?: boolean;
readonly includeInlayEnumMemberValueHints?: boolean;
readonly autoImportFileExcludePatterns?: string[];
readonly organizeImportsIgnoreCase?: "auto" | boolean;
/**
* Indicates whether {@link ReferencesResponseItem.lineText} is supported.
*/
@ -8393,6 +8394,7 @@ declare namespace ts {
readonly includeInlayEnumMemberValueHints?: boolean;
readonly allowRenameOfImportPath?: boolean;
readonly autoImportFileExcludePatterns?: string[];
readonly organizeImportsIgnoreCase?: "auto" | boolean;
}
/** Represents a bigint literal value without requiring bigint support */
interface PseudoBigInt {

View File

@ -4459,6 +4459,7 @@ declare namespace ts {
readonly includeInlayEnumMemberValueHints?: boolean;
readonly allowRenameOfImportPath?: boolean;
readonly autoImportFileExcludePatterns?: string[];
readonly organizeImportsIgnoreCase?: "auto" | boolean;
}
/** Represents a bigint literal value without requiring bigint support */
interface PseudoBigInt {

View File

@ -10,7 +10,7 @@ D;
// ==ORGANIZED==
import * as NS from "lib";
import D, { class, class, class, F1, F2 } from "lib";
import D, { F1, F2, class, class, class } from "lib";
class class class;
D;

View File

@ -0,0 +1,44 @@
/// <reference path="fourslash.ts" />
// @Filename: /exports1.ts
//// export const a = 0;
//// export const A = 1;
//// export const b = 2;
//// export const B = 3;
//// export const c = 4;
//// export const C = 5;
// @Filename: /exports2.ts
//// export const d = 0;
//// export const D = 1;
//// export const e = 2;
//// export const E = 3;
// Ambiguous in whole file: use user preference, default to case-sensitive
// @Filename: /index0.ts
//// import { A, B, C } from "./exports1";
//// a/*0*/
// Ambiguous in target import: use user preference, look at other imports in file
// @Filename: /index1.ts
//// import { A, a, B, b } from "./exports1";
//// import { E } from "./exports2";
//// 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`],
/*errorCode*/ undefined,
{ organizeImportsIgnoreCase: true });
goTo.marker("1");
verify.importFixAtPosition([
`import { A, a, B, b } from "./exports1";
import { d, E } from "./exports2";
d`]);
verify.importFixAtPosition([
`import { A, a, B, b } from "./exports1";
import { E, d } from "./exports2";
d`], /*errorCode*/ undefined, { organizeImportsIgnoreCase: false });

View File

@ -0,0 +1,33 @@
/// <reference path="fourslash.ts" />
// @Filename: /a.ts
////export interface HasBar { bar: number }
////export function hasBar(x: unknown): x is HasBar { return x && typeof x.bar === "number" }
////export function foo() {}
////export type __String = string;
// @Filename: /b.ts
////import { __String, HasBar, hasBar } from "./a";
////f/**/;
verify.completions({
marker: "",
includes: {
name: "foo",
source: "/a",
sourceDisplay: "./a",
text: "function foo(): void",
kind: "function",
kindModifiers: "export",
hasAction: true,
sortText: completion.SortText.AutoImportSuggestions
},
preferences: { includeCompletionsForModuleExports: true },
});
verify.applyCodeActionFromCompletion("", {
name: "foo",
source: "/a",
description: `Update import from "./a"`,
newFileContent: `import { __String, foo, HasBar, hasBar } from "./a";
f;`,
});

View File

@ -20,7 +20,7 @@ goTo.file("/b.ts");
verify.codeFix({
description: "Infer parameter types from usage",
newFileContent:
`import { getEmail, User } from "./a";
`import { User, getEmail } from "./a";
export function f(user: User) {
getEmail(user);

View File

@ -448,7 +448,7 @@ declare namespace FourSlashInterface {
generateTypes(...options: GenerateTypesOptions[]): void;
organizeImports(newContent: string, mode?: ts.OrganizeImportsMode): void;
organizeImports(newContent: string, mode?: ts.OrganizeImportsMode, preferences?: UserPreferences): void;
toggleLineComment(newFileContent: string): void;
toggleMultilineComment(newFileContent: string): void;
@ -672,6 +672,7 @@ declare namespace FourSlashInterface {
readonly providePrefixAndSuffixTextForRename?: boolean;
readonly allowRenameOfImportPath?: boolean;
readonly autoImportFileExcludePatterns?: readonly string[];
readonly organizeImportsIgnoreCase?: "auto" | boolean;
}
interface InlayHintsOptions extends UserPreferences {
readonly includeInlayParameterNameHints?: "none" | "literals" | "all";

View File

@ -18,22 +18,44 @@
//// console.log(a, B, b, c, C, d, D);
//// console.log(e, f, F, g, G, H, h);
// verify.organizeImports(
// `import {
// a,
// b,
// b as B,
// c,
// c as C,
// d, d as D,
// e,
// f,
// f as F,
// g,
// g as G,
// h, h as H
// } from './foo';
// console.log(a, B, b, c, C, d, D);
// console.log(e, f, F, g, G, H, h);`,
// /*mode*/ undefined,
// { organizeImportsIgnoreCase: true });
verify.organizeImports(
`import {
b as B,
c as C,
d as D,
f as F,
g as G,
h as H,
a,
b,
b as B,
c,
c as C,
d, d as D,
d,
e,
f,
f as F,
g,
g as G,
h, h as H
h
} from './foo';
console.log(a, B, b, c, C, d, D);
console.log(e, f, F, g, G, H, h);`
);
console.log(e, f, F, g, G, H, h);`, /*mode*/ undefined, { organizeImportsIgnoreCase: false });

View File

@ -23,6 +23,31 @@
////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 {
Type1,
Type2,
Type3,
Type4,
Type5,
Type6,
Type7,
Type8,
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);`
);
verify.organizeImports(
`import {
func1,
@ -45,5 +70,6 @@ verify.organizeImports(
Type9,
} 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);`
);
console.log(func1, func2, func3, func4, func5, func6, func7, func8, func9);`,
/*mode*/ undefined,
{ organizeImportsIgnoreCase: true });

View File

@ -0,0 +1,31 @@
/// <reference path="fourslash.ts" />
//// import { a, A, b } from "foo";
//// interface Use extends A {}
//// console.log(a, b);
verify.organizeImports(
`import { a, A, b } from "foo";
interface Use extends A {}
console.log(a, b);`);
verify.organizeImports(
`import { a, A, b } from "foo";
interface Use extends A {}
console.log(a, b);`,
/*mode*/ undefined,
{ organizeImportsIgnoreCase: "auto" });
verify.organizeImports(
`import { a, A, b } from "foo";
interface Use extends A {}
console.log(a, b);`,
/*mode*/ undefined,
{ organizeImportsIgnoreCase: true });
verify.organizeImports(
`import { A, a, b } from "foo";
interface Use extends A {}
console.log(a, b);`,
/*mode*/ undefined,
{ organizeImportsIgnoreCase: false });