Skip to content

Commit 0a671aa

Browse files
authored
Change detection to detection by diff in organizeImports (#57267)
1 parent 06f6f35 commit 0a671aa

30 files changed

+851
-432
lines changed

src/compiler/core.ts

-32
Original file line numberDiff line numberDiff line change
@@ -848,38 +848,6 @@ export function arrayIsSorted<T>(array: readonly T[], comparer: Comparer<T>) {
848848
return true;
849849
}
850850

851-
/** @internal */
852-
export const enum SortKind {
853-
None = 0,
854-
CaseSensitive = 1 << 0,
855-
CaseInsensitive = 1 << 1,
856-
Both = CaseSensitive | CaseInsensitive,
857-
}
858-
859-
/** @internal */
860-
export function detectSortCaseSensitivity<T>(
861-
array: readonly T[],
862-
getString: (element: T) => string,
863-
compareStringsCaseSensitive: Comparer<string>,
864-
compareStringsCaseInsensitive: Comparer<string>,
865-
): SortKind {
866-
let kind = SortKind.Both;
867-
if (array.length < 2) return kind;
868-
869-
let prevElement = getString(array[0]);
870-
for (let i = 1, len = array.length; i < len && kind !== SortKind.None; i++) {
871-
const element = getString(array[i]);
872-
if (kind & SortKind.CaseSensitive && compareStringsCaseSensitive(prevElement, element) > 0) {
873-
kind &= ~SortKind.CaseSensitive;
874-
}
875-
if (kind & SortKind.CaseInsensitive && compareStringsCaseInsensitive(prevElement, element) > 0) {
876-
kind &= ~SortKind.CaseInsensitive;
877-
}
878-
prevElement = element;
879-
}
880-
return kind;
881-
}
882-
883851
/** @internal */
884852
export function arrayIsEqualTo<T>(array1: readonly T[] | undefined, array2: readonly T[] | undefined, equalityComparer: (a: T, b: T, index: number) => boolean = equateValues): boolean {
885853
if (!array1 || !array2) {

src/compiler/types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -10031,7 +10031,7 @@ export interface UserPreferences {
1003110031
*
1003210032
* Default: `last`
1003310033
*/
10034-
readonly organizeImportsTypeOrder?: "first" | "last" | "inline";
10034+
readonly organizeImportsTypeOrder?: OrganizeImportsTypeOrder;
1003510035
/**
1003610036
* Indicates whether to exclude standard library and node_modules file symbols from navTo results.
1003710037
*/
@@ -10042,6 +10042,8 @@ export interface UserPreferences {
1004210042
readonly disableLineTextInReferences?: boolean;
1004310043
}
1004410044

10045+
export type OrganizeImportsTypeOrder = "last" | "inline" | "first";
10046+
1004510047
/** Represents a bigint literal value without requiring bigint support */
1004610048
export interface PseudoBigInt {
1004710049
negative: boolean;

src/services/codefixes/importFixes.ts

+18-34
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,14 @@ import {
114114
removeFileExtension,
115115
removeSuffix,
116116
RequireVariableStatement,
117+
sameMap,
117118
ScriptTarget,
118119
SemanticMeaning,
119120
shouldUseUriStyleNodeCoreModules,
120121
single,
121122
skipAlias,
122123
some,
123124
sort,
124-
SortKind,
125125
SourceFile,
126126
stableSort,
127127
startsWith,
@@ -1394,11 +1394,10 @@ function promoteFromTypeOnly(
13941394
switch (aliasDeclaration.kind) {
13951395
case SyntaxKind.ImportSpecifier:
13961396
if (aliasDeclaration.isTypeOnly) {
1397-
const sortKind = OrganizeImports.detectImportSpecifierSorting(aliasDeclaration.parent.elements, preferences);
1398-
if (aliasDeclaration.parent.elements.length > 1 && sortKind) {
1397+
if (aliasDeclaration.parent.elements.length > 1) {
13991398
const newSpecifier = factory.updateImportSpecifier(aliasDeclaration, /*isTypeOnly*/ false, aliasDeclaration.propertyName, aliasDeclaration.name);
1400-
const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, sortKind === SortKind.CaseInsensitive);
1401-
const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, comparer, preferences);
1399+
const { specifierComparer } = OrganizeImports.getNamedImportSpecifierComparerWithDetection(aliasDeclaration.parent.parent.parent, preferences, sourceFile);
1400+
const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(aliasDeclaration.parent.elements, newSpecifier, specifierComparer);
14021401
if (insertionIndex !== aliasDeclaration.parent.elements.indexOf(aliasDeclaration)) {
14031402
changes.delete(sourceFile, aliasDeclaration);
14041403
changes.insertImportSpecifierAtIndex(sourceFile, newSpecifier, aliasDeclaration.parent, insertionIndex);
@@ -1440,8 +1439,9 @@ function promoteFromTypeOnly(
14401439
if (convertExistingToTypeOnly) {
14411440
const namedImports = tryCast(importClause.namedBindings, isNamedImports);
14421441
if (namedImports && namedImports.elements.length > 1) {
1442+
const sortState = OrganizeImports.getNamedImportSpecifierComparerWithDetection(importClause.parent, preferences, sourceFile);
14431443
if (
1444-
OrganizeImports.detectImportSpecifierSorting(namedImports.elements, preferences) &&
1444+
(sortState.isSorted !== false) &&
14451445
aliasDeclaration.kind === SyntaxKind.ImportSpecifier &&
14461446
namedImports.elements.indexOf(aliasDeclaration) !== 0
14471447
) {
@@ -1478,6 +1478,7 @@ function doAddExistingFix(
14781478
return;
14791479
}
14801480

1481+
// promoteFromTypeOnly = true if we need to promote the entire original clause from type only
14811482
const promoteFromTypeOnly = clause.isTypeOnly && some([defaultImport, ...namedImports], i => i?.addAsTypeOnly === AddAsTypeOnly.NotAllowed);
14821483
const existingSpecifiers = clause.namedBindings && tryCast(clause.namedBindings, isNamedImports)?.elements;
14831484

@@ -1487,25 +1488,7 @@ function doAddExistingFix(
14871488
}
14881489

14891490
if (namedImports.length) {
1490-
// sort case sensitivity:
1491-
// - if the user preference is explicit, use that
1492-
// - otherwise, if there are enough existing import specifiers in this import to detect unambiguously, use that
1493-
// - otherwise, detect from other imports in the file
1494-
let ignoreCaseForSorting: boolean | undefined;
1495-
if (typeof preferences.organizeImportsIgnoreCase === "boolean") {
1496-
ignoreCaseForSorting = preferences.organizeImportsIgnoreCase;
1497-
}
1498-
else if (existingSpecifiers) {
1499-
const targetImportSorting = OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences);
1500-
if (targetImportSorting !== SortKind.Both) {
1501-
ignoreCaseForSorting = targetImportSorting === SortKind.CaseInsensitive;
1502-
}
1503-
}
1504-
if (ignoreCaseForSorting === undefined) {
1505-
ignoreCaseForSorting = OrganizeImports.detectSorting(sourceFile, preferences) === SortKind.CaseInsensitive;
1506-
}
1507-
1508-
const comparer = OrganizeImports.getOrganizeImportsComparer(preferences, ignoreCaseForSorting);
1491+
const { specifierComparer, isSorted } = OrganizeImports.getNamedImportSpecifierComparerWithDetection(clause.parent, preferences, sourceFile);
15091492
const newSpecifiers = stableSort(
15101493
namedImports.map(namedImport =>
15111494
factory.createImportSpecifier(
@@ -1514,23 +1497,24 @@ function doAddExistingFix(
15141497
factory.createIdentifier(namedImport.name),
15151498
)
15161499
),
1517-
(s1, s2) => OrganizeImports.compareImportOrExportSpecifiers(s1, s2, comparer),
1500+
specifierComparer,
15181501
);
15191502

15201503
// The sorting preference computed earlier may or may not have validated that these particular
15211504
// import specifiers are sorted. If they aren't, `getImportSpecifierInsertionIndex` will return
15221505
// nonsense. So if there are existing specifiers, even if we know the sorting preference, we
15231506
// need to ensure that the existing specifiers are sorted according to the preference in order
15241507
// to do a sorted insertion.
1525-
const specifierSort = existingSpecifiers?.length && OrganizeImports.detectImportSpecifierSorting(existingSpecifiers, preferences);
1526-
if (specifierSort && !(ignoreCaseForSorting && specifierSort === SortKind.CaseSensitive)) {
1508+
1509+
// changed to check if existing specifiers are sorted
1510+
if (existingSpecifiers?.length && isSorted !== false) {
1511+
// if we're promoting the clause from type-only, we need to transform the existing imports before attempting to insert the new named imports
1512+
const transformedExistingSpecifiers = (promoteFromTypeOnly && existingSpecifiers) ? factory.updateNamedImports(
1513+
clause.namedBindings as NamedImports,
1514+
sameMap(existingSpecifiers, e => factory.updateImportSpecifier(e, /*isTypeOnly*/ true, e.propertyName, e.name)),
1515+
).elements : existingSpecifiers;
15271516
for (const spec of newSpecifiers) {
1528-
// Organize imports puts type-only import specifiers last, so if we're
1529-
// adding a non-type-only specifier and converting all the other ones to
1530-
// type-only, there's no need to ask for the insertion index - it's 0.
1531-
const insertionIndex = promoteFromTypeOnly && !spec.isTypeOnly
1532-
? 0
1533-
: OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec, comparer, preferences);
1517+
const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(transformedExistingSpecifiers, spec, specifierComparer);
15341518
changes.insertImportSpecifierAtIndex(sourceFile, spec, clause.namedBindings as NamedImports, insertionIndex);
15351519
}
15361520
}

0 commit comments

Comments
 (0)