Skip to content

Commit 03c79d7

Browse files
authored
Insert auto-imports in sorted order (#39394)
* Sort auto-imports * Avoid re-checking sort all the time * Add comment
1 parent 8e5d495 commit 03c79d7

File tree

51 files changed

+261
-134
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+261
-134
lines changed

src/compiler/core.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,18 @@ namespace ts {
819819
return deduplicateSorted(sort(array, comparer), equalityComparer || comparer || compareStringsCaseSensitive as any as Comparer<T>);
820820
}
821821

822+
export function arrayIsSorted<T>(array: readonly T[], comparer: Comparer<T>) {
823+
if (array.length < 2) return true;
824+
let prevElement = array[0];
825+
for (const element of array.slice(1)) {
826+
if (comparer(prevElement, element) === Comparison.GreaterThan) {
827+
return false;
828+
}
829+
prevElement = element;
830+
}
831+
return true;
832+
}
833+
822834
export function arrayIsEqualTo<T>(array1: readonly T[] | undefined, array2: readonly T[] | undefined, equalityComparer: (a: T, b: T, index: number) => boolean = equateValues): boolean {
823835
if (!array1 || !array2) {
824836
return array1 === array2;

src/compiler/types.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4336,6 +4336,9 @@ namespace ts {
43364336
/* @internal */
43374337
export type AnyImportOrRequire = AnyImportSyntax | RequireVariableDeclaration;
43384338

4339+
/* @internal */
4340+
export type AnyImportOrRequireStatement = AnyImportSyntax | RequireVariableStatement;
4341+
43394342

43404343
/* @internal */
43414344
export type AnyImportOrReExport = AnyImportSyntax | ExportDeclaration;
@@ -4357,8 +4360,17 @@ namespace ts {
43574360

43584361
/* @internal */
43594362
export interface RequireVariableDeclaration extends VariableDeclaration {
4363+
readonly initializer: RequireOrImportCall;
4364+
}
43604365

4361-
initializer: RequireOrImportCall;
4366+
/* @internal */
4367+
export interface RequireVariableStatement extends VariableStatement {
4368+
readonly declarationList: RequireVariableDeclarationList;
4369+
}
4370+
4371+
/* @internal */
4372+
export interface RequireVariableDeclarationList extends VariableDeclarationList {
4373+
readonly declarations: NodeArray<RequireVariableDeclaration>;
43624374
}
43634375

43644376
/* @internal */

src/compiler/utilities.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1934,8 +1934,10 @@ namespace ts {
19341934
return isVariableDeclaration(node) && !!node.initializer && isRequireCall(node.initializer, requireStringLiteralLikeArgument);
19351935
}
19361936

1937-
export function isRequireVariableDeclarationStatement(node: Node, requireStringLiteralLikeArgument = true): node is VariableStatement {
1938-
return isVariableStatement(node) && every(node.declarationList.declarations, decl => isRequireVariableDeclaration(decl, requireStringLiteralLikeArgument));
1937+
export function isRequireVariableStatement(node: Node, requireStringLiteralLikeArgument = true): node is RequireVariableStatement {
1938+
return isVariableStatement(node)
1939+
&& node.declarationList.declarations.length > 0
1940+
&& every(node.declarationList.declarations, decl => isRequireVariableDeclaration(decl, requireStringLiteralLikeArgument));
19391941
}
19401942

19411943
export function isSingleOrDoubleQuote(charCode: number) {

src/services/codefixes/importFixes.ts

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ namespace ts.codefix {
138138
doAddExistingFix(changeTracker, sourceFile, importClauseOrBindingPattern, defaultImport, namedImports, canUseTypeOnlyImport);
139139
});
140140

141-
let newDeclarations: Statement | readonly Statement[] | undefined;
141+
let newDeclarations: AnyImportOrRequireStatement | readonly AnyImportOrRequireStatement[] | undefined;
142142
newImports.forEach(({ useRequire, ...imports }, moduleSpecifier) => {
143143
const getDeclarations = useRequire ? getNewRequires : getNewImports;
144144
newDeclarations = combine(newDeclarations, getDeclarations(moduleSpecifier, quotePreference, imports));
@@ -671,15 +671,35 @@ namespace ts.codefix {
671671
}
672672

673673
if (namedImports.length) {
674-
const specifiers = namedImports.map(name => factory.createImportSpecifier(/*propertyName*/ undefined, factory.createIdentifier(name)));
675-
if (clause.namedBindings && cast(clause.namedBindings, isNamedImports).elements.length) {
676-
for (const spec of specifiers) {
677-
changes.insertNodeInListAfter(sourceFile, last(cast(clause.namedBindings, isNamedImports).elements), spec);
674+
const existingSpecifiers = clause.namedBindings && cast(clause.namedBindings, isNamedImports).elements;
675+
const newSpecifiers = stableSort(
676+
namedImports.map(name => factory.createImportSpecifier(/*propertyName*/ undefined, factory.createIdentifier(name))),
677+
OrganizeImports.compareImportOrExportSpecifiers);
678+
679+
if (existingSpecifiers?.length && OrganizeImports.importSpecifiersAreSorted(existingSpecifiers)) {
680+
for (const spec of newSpecifiers) {
681+
const insertionIndex = OrganizeImports.getImportSpecifierInsertionIndex(existingSpecifiers, spec);
682+
const prevSpecifier = (clause.namedBindings as NamedImports).elements[insertionIndex - 1];
683+
if (prevSpecifier) {
684+
changes.insertNodeInListAfter(sourceFile, prevSpecifier, spec);
685+
}
686+
else {
687+
changes.insertNodeBefore(
688+
sourceFile,
689+
existingSpecifiers[0],
690+
spec,
691+
!positionsAreOnSameLine(existingSpecifiers[0].getStart(), clause.parent.getStart(), sourceFile));
692+
}
693+
}
694+
}
695+
else if (existingSpecifiers?.length) {
696+
for (const spec of newSpecifiers) {
697+
changes.insertNodeAtEndOfList(sourceFile, existingSpecifiers, spec);
678698
}
679699
}
680700
else {
681-
if (specifiers.length) {
682-
const namedImports = factory.createNamedImports(specifiers);
701+
if (newSpecifiers.length) {
702+
const namedImports = factory.createNamedImports(newSpecifiers);
683703
if (clause.namedBindings) {
684704
changes.replaceNode(sourceFile, clause.namedBindings, namedImports);
685705
}
@@ -727,9 +747,9 @@ namespace ts.codefix {
727747
readonly name: string;
728748
};
729749
}
730-
function getNewImports(moduleSpecifier: string, quotePreference: QuotePreference, imports: ImportsCollection): Statement | readonly Statement[] {
750+
function getNewImports(moduleSpecifier: string, quotePreference: QuotePreference, imports: ImportsCollection): AnyImportSyntax | readonly AnyImportSyntax[] {
731751
const quotedModuleSpecifier = makeStringLiteral(moduleSpecifier, quotePreference);
732-
let statements: Statement | readonly Statement[] | undefined;
752+
let statements: AnyImportSyntax | readonly AnyImportSyntax[] | undefined;
733753
if (imports.defaultImport !== undefined || imports.namedImports?.length) {
734754
statements = combine(statements, makeImport(
735755
imports.defaultImport === undefined ? undefined : factory.createIdentifier(imports.defaultImport),
@@ -756,9 +776,9 @@ namespace ts.codefix {
756776
return Debug.checkDefined(statements);
757777
}
758778

759-
function getNewRequires(moduleSpecifier: string, quotePreference: QuotePreference, imports: ImportsCollection): Statement | readonly Statement[] {
779+
function getNewRequires(moduleSpecifier: string, quotePreference: QuotePreference, imports: ImportsCollection): RequireVariableStatement | readonly RequireVariableStatement[] {
760780
const quotedModuleSpecifier = makeStringLiteral(moduleSpecifier, quotePreference);
761-
let statements: Statement | readonly Statement[] | undefined;
781+
let statements: RequireVariableStatement | readonly RequireVariableStatement[] | undefined;
762782
// const { default: foo, bar, etc } = require('./mod');
763783
if (imports.defaultImport || imports.namedImports?.length) {
764784
const bindingElements = imports.namedImports?.map(name => factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, name)) || [];
@@ -776,7 +796,7 @@ namespace ts.codefix {
776796
return Debug.checkDefined(statements);
777797
}
778798

779-
function createConstEqualsRequireDeclaration(name: string | ObjectBindingPattern, quotedModuleSpecifier: StringLiteral): VariableStatement {
799+
function createConstEqualsRequireDeclaration(name: string | ObjectBindingPattern, quotedModuleSpecifier: StringLiteral): RequireVariableStatement {
780800
return factory.createVariableStatement(
781801
/*modifiers*/ undefined,
782802
factory.createVariableDeclarationList([
@@ -785,7 +805,7 @@ namespace ts.codefix {
785805
/*exclamationToken*/ undefined,
786806
/*type*/ undefined,
787807
factory.createCallExpression(factory.createIdentifier("require"), /*typeArguments*/ undefined, [quotedModuleSpecifier]))],
788-
NodeFlags.Const));
808+
NodeFlags.Const)) as RequireVariableStatement;
789809
}
790810

791811
function symbolHasMeaning({ declarations }: Symbol, meaning: SemanticMeaning): boolean {

src/services/organizeImports.ts

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ namespace ts.OrganizeImports {
1717

1818
const changeTracker = textChanges.ChangeTracker.fromContext({ host, formatContext, preferences });
1919

20-
const coalesceAndOrganizeImports = (importGroup: readonly ImportDeclaration[]) => coalesceImports(removeUnusedImports(importGroup, sourceFile, program));
20+
const coalesceAndOrganizeImports = (importGroup: readonly ImportDeclaration[]) => stableSort(
21+
coalesceImports(removeUnusedImports(importGroup, sourceFile, program)),
22+
(s1, s2) => compareImportsOrRequireStatements(s1, s2));
2123

2224
// All of the old ImportDeclarations in the file, in syntactic order.
2325
const topLevelImportDecls = sourceFile.statements.filter(isImportDeclaration);
@@ -55,7 +57,7 @@ namespace ts.OrganizeImports {
5557
suppressLeadingTrivia(oldImportDecls[0]);
5658

5759
const oldImportGroups = group(oldImportDecls, importDecl => getExternalModuleName(importDecl.moduleSpecifier!)!);
58-
const sortedImportGroups = stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiers(group1[0].moduleSpecifier!, group2[0].moduleSpecifier!));
60+
const sortedImportGroups = stableSort(oldImportGroups, (group1, group2) => compareModuleSpecifiers(group1[0].moduleSpecifier, group2[0].moduleSpecifier));
5961
const newImportDecls = flatMap(sortedImportGroups, importGroup =>
6062
getExternalModuleName(importGroup[0].moduleSpecifier!)
6163
? coalesce(importGroup)
@@ -395,15 +397,18 @@ namespace ts.OrganizeImports {
395397
}
396398

397399
function sortSpecifiers<T extends ImportOrExportSpecifier>(specifiers: readonly T[]) {
398-
return stableSort(specifiers, (s1, s2) =>
399-
compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name) ||
400-
compareIdentifiers(s1.name, s2.name));
400+
return stableSort(specifiers, compareImportOrExportSpecifiers);
401+
}
402+
403+
export function compareImportOrExportSpecifiers<T extends ImportOrExportSpecifier>(s1: T, s2: T) {
404+
return compareIdentifiers(s1.propertyName || s1.name, s2.propertyName || s2.name)
405+
|| compareIdentifiers(s1.name, s2.name);
401406
}
402407

403408
/* internal */ // Exported for testing
404-
export function compareModuleSpecifiers(m1: Expression, m2: Expression) {
405-
const name1 = getExternalModuleName(m1);
406-
const name2 = getExternalModuleName(m2);
409+
export function compareModuleSpecifiers(m1: Expression | undefined, m2: Expression | undefined) {
410+
const name1 = m1 === undefined ? undefined : getExternalModuleName(m1);
411+
const name2 = m2 === undefined ? undefined : getExternalModuleName(m2);
407412
return compareBooleans(name1 === undefined, name2 === undefined) ||
408413
compareBooleans(isExternalModuleNameRelative(name1!), isExternalModuleNameRelative(name2!)) ||
409414
compareStringsCaseInsensitive(name1!, name2!);
@@ -412,4 +417,63 @@ namespace ts.OrganizeImports {
412417
function compareIdentifiers(s1: Identifier, s2: Identifier) {
413418
return compareStringsCaseInsensitive(s1.text, s2.text);
414419
}
420+
421+
function getModuleSpecifierExpression(declaration: AnyImportOrRequireStatement): Expression | undefined {
422+
switch (declaration.kind) {
423+
case SyntaxKind.ImportEqualsDeclaration:
424+
return tryCast(declaration.moduleReference, isExternalModuleReference)?.expression;
425+
case SyntaxKind.ImportDeclaration:
426+
return declaration.moduleSpecifier;
427+
case SyntaxKind.VariableStatement:
428+
return declaration.declarationList.declarations[0].initializer.arguments[0];
429+
}
430+
}
431+
432+
export function importsAreSorted(imports: readonly AnyImportOrRequireStatement[]): imports is SortedReadonlyArray<AnyImportOrRequireStatement> {
433+
return arrayIsSorted(imports, compareImportsOrRequireStatements);
434+
}
435+
436+
export function importSpecifiersAreSorted(imports: readonly ImportSpecifier[]): imports is SortedReadonlyArray<ImportSpecifier> {
437+
return arrayIsSorted(imports, compareImportOrExportSpecifiers);
438+
}
439+
440+
export function getImportDeclarationInsertionIndex(sortedImports: SortedReadonlyArray<AnyImportOrRequireStatement>, newImport: AnyImportOrRequireStatement) {
441+
const index = binarySearch(sortedImports, newImport, identity, compareImportsOrRequireStatements);
442+
return index < 0 ? ~index : index;
443+
}
444+
445+
export function getImportSpecifierInsertionIndex(sortedImports: SortedReadonlyArray<ImportSpecifier>, newImport: ImportSpecifier) {
446+
const index = binarySearch(sortedImports, newImport, identity, compareImportOrExportSpecifiers);
447+
return index < 0 ? ~index : index;
448+
}
449+
450+
export function compareImportsOrRequireStatements(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement) {
451+
return compareModuleSpecifiers(getModuleSpecifierExpression(s1), getModuleSpecifierExpression(s2)) || compareImportKind(s1, s2);
452+
}
453+
454+
function compareImportKind(s1: AnyImportOrRequireStatement, s2: AnyImportOrRequireStatement) {
455+
return compareValues(getImportKindOrder(s1), getImportKindOrder(s2));
456+
}
457+
458+
// 1. Side-effect imports
459+
// 2. Type-only imports
460+
// 3. Namespace imports
461+
// 4. Default imports
462+
// 5. Named imports
463+
// 6. ImportEqualsDeclarations
464+
// 7. Require variable statements
465+
function getImportKindOrder(s1: AnyImportOrRequireStatement) {
466+
switch (s1.kind) {
467+
case SyntaxKind.ImportDeclaration:
468+
if (!s1.importClause) return 0;
469+
if (s1.importClause.isTypeOnly) return 1;
470+
if (s1.importClause.namedBindings?.kind === SyntaxKind.NamespaceImport) return 2;
471+
if (s1.importClause.name) return 3;
472+
return 4;
473+
case SyntaxKind.ImportEqualsDeclaration:
474+
return 5;
475+
case SyntaxKind.VariableStatement:
476+
return 6;
477+
}
478+
}
415479
}

src/services/refactors/moveToNewFile.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ namespace ts.refactor {
269269
| ImportEqualsDeclaration
270270
| VariableStatement;
271271

272-
function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean, quotePreference: QuotePreference): Statement | undefined {
272+
function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean, quotePreference: QuotePreference): AnyImportOrRequireStatement | undefined {
273273
let defaultImport: Identifier | undefined;
274274
const imports: string[] = [];
275275
newFileNeedExport.forEach(symbol => {
@@ -283,7 +283,7 @@ namespace ts.refactor {
283283
return makeImportOrRequire(defaultImport, imports, newFileNameWithExtension, useEs6Imports, quotePreference);
284284
}
285285

286-
function makeImportOrRequire(defaultImport: Identifier | undefined, imports: readonly string[], path: string, useEs6Imports: boolean, quotePreference: QuotePreference): Statement | undefined {
286+
function makeImportOrRequire(defaultImport: Identifier | undefined, imports: readonly string[], path: string, useEs6Imports: boolean, quotePreference: QuotePreference): AnyImportOrRequireStatement | undefined {
287287
path = ensurePathIsNonModuleName(path);
288288
if (useEs6Imports) {
289289
const specifiers = imports.map(i => factory.createImportSpecifier(/*propertyName*/ undefined, factory.createIdentifier(i)));
@@ -293,7 +293,7 @@ namespace ts.refactor {
293293
Debug.assert(!defaultImport, "No default import should exist"); // If there's a default export, it should have been an es6 module.
294294
const bindingElements = imports.map(i => factory.createBindingElement(/*dotDotDotToken*/ undefined, /*propertyName*/ undefined, i));
295295
return bindingElements.length
296-
? makeVariableStatement(factory.createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(factory.createStringLiteral(path)))
296+
? makeVariableStatement(factory.createObjectBindingPattern(bindingElements), /*type*/ undefined, createRequireCall(factory.createStringLiteral(path))) as RequireVariableStatement
297297
: undefined;
298298
}
299299
}

src/services/textChanges.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,9 +472,9 @@ namespace ts.textChanges {
472472
this.insertNodesAt(sourceFile, start, typeParameters, { prefix: "<", suffix: ">" });
473473
}
474474

475-
private getOptionsForInsertNodeBefore(before: Node, inserted: Node, doubleNewlines: boolean): InsertNodeOptions {
475+
private getOptionsForInsertNodeBefore(before: Node, inserted: Node, blankLineBetween: boolean): InsertNodeOptions {
476476
if (isStatement(before) || isClassElement(before)) {
477-
return { suffix: doubleNewlines ? this.newLineCharacter + this.newLineCharacter : this.newLineCharacter };
477+
return { suffix: blankLineBetween ? this.newLineCharacter + this.newLineCharacter : this.newLineCharacter };
478478
}
479479
else if (isVariableDeclaration(before)) { // insert `x = 1, ` into `const x = 1, y = 2;
480480
return { suffix: ", " };
@@ -485,6 +485,9 @@ namespace ts.textChanges {
485485
else if (isStringLiteral(before) && isImportDeclaration(before.parent) || isNamedImports(before)) {
486486
return { suffix: ", " };
487487
}
488+
else if (isImportSpecifier(before)) {
489+
return { suffix: "," + (blankLineBetween ? this.newLineCharacter : " ") };
490+
}
488491
return Debug.failBadSyntaxKind(before); // We haven't handled this kind of node yet -- add it
489492
}
490493

0 commit comments

Comments
 (0)