Skip to content

Commit 1c25b00

Browse files
authored
Sort import fixes by number of directory separators (#42614)
* Add failing test * Sort all import fixes by number of directory separators
1 parent bfc55b5 commit 1c25b00

File tree

4 files changed

+67
-28
lines changed

4 files changed

+67
-28
lines changed

src/compiler/moduleSpecifiers.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -255,16 +255,8 @@ namespace ts.moduleSpecifiers {
255255
return firstDefined(imports, ({ text }) => pathIsRelative(text) ? hasJSFileExtension(text) : undefined) || false;
256256
}
257257

258-
function numberOfDirectorySeparators(str: string) {
259-
const match = str.match(/\//g);
260-
return match ? match.length : 0;
261-
}
262-
263258
function comparePathsByRedirectAndNumberOfDirectorySeparators(a: ModulePath, b: ModulePath) {
264-
return compareBooleans(b.isRedirect, a.isRedirect) || compareValues(
265-
numberOfDirectorySeparators(a.path),
266-
numberOfDirectorySeparators(b.path)
267-
);
259+
return compareBooleans(b.isRedirect, a.isRedirect) || compareNumberOfDirectorySeparators(a.path, b.path);
268260
}
269261

270262
function getNearestAncestorDirectoryWithPackageJson(host: ModuleSpecifierResolutionHost, fileName: string) {

src/compiler/utilities.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6586,6 +6586,18 @@ namespace ts {
65866586
return false;
65876587
}
65886588

6589+
function numberOfDirectorySeparators(str: string) {
6590+
const match = str.match(/\//g);
6591+
return match ? match.length : 0;
6592+
}
6593+
6594+
export function compareNumberOfDirectorySeparators(path1: string, path2: string) {
6595+
return compareValues(
6596+
numberOfDirectorySeparators(path1),
6597+
numberOfDirectorySeparators(path2)
6598+
);
6599+
}
6600+
65896601
/**
65906602
* Extension boundaries by priority. Lower numbers indicate higher priorities, and are
65916603
* aligned to the offset of the highest priority extension in the

src/services/codefixes/importFixes.ts

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -215,15 +215,15 @@ namespace ts.codefix {
215215
: getAllReExportingModules(sourceFile, exportedSymbol, moduleSymbol, symbolName, host, program, /*useAutoImportProvider*/ true);
216216
const useRequire = shouldUseRequire(sourceFile, program);
217217
const preferTypeOnlyImport = compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error && !isSourceFileJS(sourceFile) && isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position));
218-
const moduleSpecifier = first(getNewImportInfos(program, sourceFile, position, preferTypeOnlyImport, useRequire, exportInfos, host, preferences)).moduleSpecifier;
218+
const moduleSpecifier = getBestFix(getNewImportInfos(program, sourceFile, position, preferTypeOnlyImport, useRequire, exportInfos, host, preferences), sourceFile, program, host).moduleSpecifier;
219219
const fix = getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, position, preferTypeOnlyImport, useRequire, host, preferences);
220220
return { moduleSpecifier, codeAction: codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, getQuotePreference(sourceFile, preferences))) };
221221
}
222222

223223
function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], moduleSymbol: Symbol, symbolName: string, program: Program, position: number | undefined, preferTypeOnlyImport: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) {
224224
Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol), "Some exportInfo should match the specified moduleSymbol");
225225
// We sort the best codefixes first, so taking `first` is best.
226-
return first(getFixForImport(exportInfos, symbolName, position, preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences));
226+
return getBestFix(getFixForImport(exportInfos, symbolName, position, preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences), sourceFile, program, host);
227227
}
228228

229229
function codeFixActionToCodeAction({ description, changes, commands }: CodeFixAction): CodeAction {
@@ -424,28 +424,13 @@ namespace ts.codefix {
424424
): readonly (FixAddNewImport | FixUseImportType)[] {
425425
const isJs = isSourceFileJS(sourceFile);
426426
const compilerOptions = program.getCompilerOptions();
427-
const { allowsImportingSpecifier } = createAutoImportFilter(sourceFile, program, host);
428-
429-
const choicesForEachExportingModule = flatMap(moduleSymbols, ({ moduleSymbol, importKind, exportedSymbolIsTypeOnly }) =>
427+
return flatMap(moduleSymbols, ({ moduleSymbol, importKind, exportedSymbolIsTypeOnly }) =>
430428
moduleSpecifiers.getModuleSpecifiers(moduleSymbol, program.getTypeChecker(), compilerOptions, sourceFile, createModuleSpecifierResolutionHost(program, host), preferences)
431429
.map((moduleSpecifier): FixAddNewImport | FixUseImportType =>
432430
// `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types.
433431
exportedSymbolIsTypeOnly && isJs
434432
? { kind: ImportFixKind.ImportType, moduleSpecifier, position: Debug.checkDefined(position, "position should be defined") }
435433
: { kind: ImportFixKind.AddNew, moduleSpecifier, importKind, useRequire, typeOnly: preferTypeOnlyImport }));
436-
437-
// Sort by presence in package.json, then shortest paths first
438-
return sort(choicesForEachExportingModule, (a, b) => {
439-
const allowsImportingA = allowsImportingSpecifier(a.moduleSpecifier);
440-
const allowsImportingB = allowsImportingSpecifier(b.moduleSpecifier);
441-
if (allowsImportingA && !allowsImportingB) {
442-
return -1;
443-
}
444-
if (allowsImportingB && !allowsImportingA) {
445-
return 1;
446-
}
447-
return a.moduleSpecifier.length - b.moduleSpecifier.length;
448-
});
449434
}
450435

451436
function getFixesForAddImport(
@@ -479,7 +464,31 @@ namespace ts.codefix {
479464
const info = errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code
480465
? getFixesInfoForUMDImport(context, symbolToken)
481466
: isIdentifier(symbolToken) ? getFixesInfoForNonUMDImport(context, symbolToken, useAutoImportProvider) : undefined;
482-
return info && { ...info, fixes: sort(info.fixes, (a, b) => a.kind - b.kind) };
467+
return info && { ...info, fixes: sortFixes(info.fixes, context.sourceFile, context.program, context.host) };
468+
}
469+
470+
function sortFixes(fixes: readonly ImportFix[], sourceFile: SourceFile, program: Program, host: LanguageServiceHost): readonly ImportFix[] {
471+
const { allowsImportingSpecifier } = createAutoImportFilter(sourceFile, program, host);
472+
return sort(fixes, (a, b) => compareValues(a.kind, b.kind) || compareModuleSpecifiers(a, b, allowsImportingSpecifier));
473+
}
474+
475+
function getBestFix<T extends ImportFix>(fixes: readonly T[], sourceFile: SourceFile, program: Program, host: LanguageServiceHost): T {
476+
// These will always be placed first if available, and are better than other kinds
477+
if (fixes[0].kind === ImportFixKind.UseNamespace || fixes[0].kind === ImportFixKind.AddToExisting) {
478+
return fixes[0];
479+
}
480+
const { allowsImportingSpecifier } = createAutoImportFilter(sourceFile, program, host);
481+
return fixes.reduce((best, fix) =>
482+
compareModuleSpecifiers(fix, best, allowsImportingSpecifier) === Comparison.LessThan ? fix : best
483+
);
484+
}
485+
486+
function compareModuleSpecifiers(a: ImportFix, b: ImportFix, allowsImportingSpecifier: (specifier: string) => boolean): Comparison {
487+
if (a.kind !== ImportFixKind.UseNamespace && b.kind !== ImportFixKind.UseNamespace) {
488+
return compareBooleans(allowsImportingSpecifier(a.moduleSpecifier), allowsImportingSpecifier(b.moduleSpecifier))
489+
|| compareNumberOfDirectorySeparators(a.moduleSpecifier, b.moduleSpecifier);
490+
}
491+
return Comparison.EqualTo;
483492
}
484493

485494
function getFixesInfoForUMDImport({ sourceFile, program, host, preferences }: CodeFixContextBase, token: Node): FixesInfo | undefined {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @module: commonjs
4+
5+
// @Filename: /src/admin/utils/db/db.ts
6+
//// export const db = {};
7+
8+
// @Filename: /src/admin/utils/db/index.ts
9+
//// export * from "./db";
10+
11+
// @Filename: /src/client/helpers/db.ts
12+
//// export const db = {};
13+
14+
// @Filename: /src/client/db.ts
15+
//// export const db = {};
16+
17+
// @Filename: /src/client/foo.ts
18+
//// db/**/
19+
20+
goTo.marker("");
21+
verify.importFixAtPosition([
22+
`import { db } from "./db";\n\ndb`,
23+
`import { db } from "./helpers/db";\n\ndb`,
24+
`import { db } from "../admin/utils/db";\n\ndb`,
25+
`import { db } from "../admin/utils/db/db";\n\ndb`
26+
]);

0 commit comments

Comments
 (0)