Skip to content

Commit fa188b1

Browse files
committed
Revamp string completions ending preferences
1 parent 6e1989e commit fa188b1

File tree

3 files changed

+179
-114
lines changed

3 files changed

+179
-114
lines changed

src/compiler/moduleSpecifiers.ts

Lines changed: 37 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,35 @@ import {
44
containsPath, createGetCanonicalFileName, Debug, directorySeparator, emptyArray, endsWith,
55
ensurePathIsNonModuleName, ensureTrailingDirectorySeparator, every, ExportAssignment, Extension, extensionFromPath,
66
fileExtensionIsOneOf, FileIncludeKind, firstDefined, flatMap, flatten, forEach, forEachAncestorDirectory,
7-
GetCanonicalFileName, getDirectoryPath, getEmitModuleResolutionKind, getImpliedNodeFormatForFile,
7+
GetCanonicalFileName, getDirectoryPath, getEmitModuleResolutionKind,
88
getModeForResolutionAtIndex, getModuleNameStringLiteralAt, getNodeModulePathParts, getNormalizedAbsolutePath,
99
getOwnKeys, getPackageJsonTypesVersionsPaths, getPackageNameFromTypesPackageName, getPathsBasePath,
1010
getRelativePathFromDirectory, getRelativePathToDirectoryOrUrl, getSourceFileOfModule, getSupportedExtensions,
1111
getTextOfIdentifierOrLiteral, hasJSFileExtension, hasTSFileExtension, hostGetCanonicalFileName, Identifier,
1212
isAmbientModule, isApplicableVersionedTypesKey, isExternalModuleAugmentation, isExternalModuleNameRelative,
1313
isModuleBlock, isModuleDeclaration, isNonGlobalAmbientModule, isRootedDiskPath, isSourceFile, isString, JsxEmit,
1414
map, mapDefined, MapLike, matchPatternOrExact, min, ModuleDeclaration, ModuleKind, ModulePath,
15-
ModuleResolutionHost, ModuleResolutionKind, ModuleSpecifierCache, ModuleSpecifierOptions,
15+
ModuleResolutionKind, ModuleSpecifierCache, ModuleSpecifierOptions,
1616
ModuleSpecifierResolutionHost, NodeFlags, NodeModulePathParts, normalizePath, Path, pathContainsNodeModules,
1717
pathIsBareSpecifier, pathIsRelative, PropertyAccessExpression, removeFileExtension, removeSuffix, resolvePath,
1818
ScriptKind, some, SourceFile, startsWith, startsWithDirectory, stringContains, StringLiteral, Symbol, SymbolFlags,
19-
toPath, tryGetExtensionFromPath, tryParsePatterns, TypeChecker, UserPreferences, shouldAllowImportingTsExtension, ResolutionMode,
19+
toPath, tryGetExtensionFromPath, tryParsePatterns, TypeChecker, UserPreferences, shouldAllowImportingTsExtension, ResolutionMode, ModuleSpecifierEnding, getModuleSpecifierEndingPreference,
2020
} from "./_namespaces/ts";
2121

2222
// Used by importFixes, getEditsForFileRename, and declaration emit to synthesize import module specifiers.
2323

2424
const enum RelativePreference { Relative, NonRelative, Shortest, ExternalNonRelative }
25-
// See UserPreferences#importPathEnding
26-
const enum Ending { Minimal, Index, JsExtension, TsExtension }
2725

2826
// Processed preferences
2927
interface Preferences {
3028
readonly relativePreference: RelativePreference;
3129
/**
3230
* @param syntaxImpliedNodeFormat Used when the import syntax implies ESM or CJS irrespective of the mode of the file.
3331
*/
34-
getAllowedEndingsInPrefererredOrder(syntaxImpliedNodeFormat?: SourceFile["impliedNodeFormat"]): Ending[];
32+
getAllowedEndingsInPrefererredOrder(syntaxImpliedNodeFormat?: SourceFile["impliedNodeFormat"]): ModuleSpecifierEnding[];
3533
}
3634

3735
function getPreferences(
38-
host: ModuleSpecifierResolutionHost,
3936
{ importModuleSpecifierPreference, importModuleSpecifierEnding }: UserPreferences,
4037
compilerOptions: CompilerOptions,
4138
importingSourceFile: SourceFile,
@@ -52,66 +49,38 @@ function getPreferences(
5249
importModuleSpecifierPreference === "project-relative" ? RelativePreference.ExternalNonRelative :
5350
RelativePreference.Shortest,
5451
getAllowedEndingsInPrefererredOrder: syntaxImpliedNodeFormat => {
55-
if (syntaxImpliedNodeFormat === ModuleKind.ESNext || isFormatRequiringExtensions()) {
52+
if (syntaxImpliedNodeFormat === ModuleKind.ESNext || (syntaxImpliedNodeFormat ?? importingSourceFile.impliedNodeFormat) === ModuleKind.ESNext) {
5653
if (shouldAllowImportingTsExtension(compilerOptions, importingSourceFile.fileName)) {
57-
return [Ending.TsExtension, Ending.JsExtension];
54+
return [ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.JsExtension];
5855
}
59-
return [Ending.JsExtension];
56+
return [ModuleSpecifierEnding.JsExtension];
6057
}
6158
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Classic) {
62-
return [Ending.Index, Ending.JsExtension];
59+
return [ModuleSpecifierEnding.Index, ModuleSpecifierEnding.JsExtension];
6360
}
6461
switch (preferredEnding) {
65-
case Ending.JsExtension: return [Ending.JsExtension, Ending.Minimal, Ending.Index];
66-
case Ending.TsExtension: return [Ending.TsExtension, Ending.TsExtension, Ending.Minimal, Ending.Index];
67-
case Ending.Index: return [Ending.Index, Ending.Minimal, Ending.JsExtension];
68-
case Ending.Minimal: return [Ending.Minimal, Ending.Index, Ending.JsExtension];
62+
case ModuleSpecifierEnding.JsExtension: return [ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index];
63+
case ModuleSpecifierEnding.TsExtension: return [ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index];
64+
case ModuleSpecifierEnding.Index: return [ModuleSpecifierEnding.Index, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.JsExtension];
65+
case ModuleSpecifierEnding.Minimal: return [ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index, ModuleSpecifierEnding.JsExtension];
6966
default: Debug.assertNever(preferredEnding);
7067
}
7168
},
7269
};
7370

74-
function getPreferredEnding(): Ending {
71+
function getPreferredEnding(): ModuleSpecifierEnding {
7572
if (oldImportSpecifier !== undefined) {
76-
if (hasJSFileExtension(oldImportSpecifier)) return Ending.JsExtension;
77-
if (endsWith(oldImportSpecifier, "/index")) return Ending.Index;
78-
}
79-
switch (importModuleSpecifierEnding) {
80-
case "minimal": return Ending.Minimal;
81-
case "index": return Ending.Index;
82-
case "js": return shouldAllowImportingTsExtension(compilerOptions) ? Ending.TsExtension : Ending.JsExtension;
83-
default: return usesExtensionsOnImports(importingSourceFile)
84-
? shouldAllowImportingTsExtension(compilerOptions) ? Ending.TsExtension : Ending.JsExtension
85-
: Ending.Minimal;
86-
}
87-
}
88-
89-
function isFormatRequiringExtensions() {
90-
switch (getEmitModuleResolutionKind(compilerOptions)) {
91-
case ModuleResolutionKind.Node16:
92-
case ModuleResolutionKind.NodeNext:
93-
return getImpliedNodeFormatForFile(
94-
importingSourceFile.path,
95-
host.getPackageJsonInfoCache?.(),
96-
getModuleResolutionHost(host), compilerOptions,
97-
) !== ModuleKind.CommonJS;
98-
default:
99-
return false;
73+
if (hasJSFileExtension(oldImportSpecifier)) return ModuleSpecifierEnding.JsExtension;
74+
if (endsWith(oldImportSpecifier, "/index")) return ModuleSpecifierEnding.Index;
10075
}
76+
return getModuleSpecifierEndingPreference(
77+
importModuleSpecifierEnding,
78+
importingSourceFile.impliedNodeFormat,
79+
compilerOptions,
80+
importingSourceFile);
10181
}
10282
}
10383

104-
function getModuleResolutionHost(host: ModuleSpecifierResolutionHost): ModuleResolutionHost {
105-
return {
106-
fileExists: host.fileExists,
107-
readFile: Debug.checkDefined(host.readFile),
108-
directoryExists: host.directoryExists,
109-
getCurrentDirectory: host.getCurrentDirectory,
110-
realpath: host.realpath,
111-
useCaseSensitiveFileNames: host.useCaseSensitiveFileNames?.(),
112-
};
113-
}
114-
11584
// `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`?
11685
// Because when this is called by the file renamer, `importingSourceFile` is the file being renamed,
11786
// while `importingSourceFileName` its *new* name. We need a source file just to get its
@@ -126,7 +95,7 @@ export function updateModuleSpecifier(
12695
oldImportSpecifier: string,
12796
options: ModuleSpecifierOptions = {},
12897
): string | undefined {
129-
const res = getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferences(host, {}, compilerOptions, importingSourceFile, oldImportSpecifier), {}, options);
98+
const res = getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferences({}, compilerOptions, importingSourceFile, oldImportSpecifier), {}, options);
13099
if (res === oldImportSpecifier) return undefined;
131100
return res;
132101
}
@@ -146,7 +115,7 @@ export function getModuleSpecifier(
146115
host: ModuleSpecifierResolutionHost,
147116
options: ModuleSpecifierOptions = {},
148117
): string {
149-
return getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferences(host, {}, compilerOptions, importingSourceFile), {}, options);
118+
return getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferences({}, compilerOptions, importingSourceFile), {}, options);
150119
}
151120

152121
/** @internal */
@@ -279,7 +248,7 @@ function computeModuleSpecifiers(
279248
options: ModuleSpecifierOptions = {},
280249
): readonly string[] {
281250
const info = getInfo(importingSourceFile.path, host);
282-
const preferences = getPreferences(host, userPreferences, compilerOptions, importingSourceFile);
251+
const preferences = getPreferences(userPreferences, compilerOptions, importingSourceFile);
283252
const existingSpecifier = forEach(modulePaths, modulePath => forEach(
284253
host.getFileIncludeReasons().get(toPath(modulePath.path, host.getCurrentDirectory(), info.getCanonicalFileName)),
285254
reason => {
@@ -458,10 +427,6 @@ export function countPathComponents(path: string): number {
458427
return count;
459428
}
460429

461-
function usesExtensionsOnImports({ imports }: SourceFile): boolean {
462-
return firstDefined(imports, ({ text }) => pathIsRelative(text) ? (hasJSFileExtension(text) || hasTSFileExtension(text)) : undefined) || false;
463-
}
464-
465430
function comparePathsByRedirectAndNumberOfDirectorySeparators(a: ModulePath, b: ModulePath) {
466431
return compareBooleans(b.isRedirect, a.isRedirect) || compareNumberOfDirectorySeparators(a.path, b.path);
467432
}
@@ -648,7 +613,7 @@ function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol, checker: TypeCh
648613
}
649614
}
650615

651-
function tryGetModuleNameFromPaths(relativeToBaseUrl: string, paths: MapLike<readonly string[]>, allowedEndings: Ending[], host: ModuleSpecifierResolutionHost, compilerOptions: CompilerOptions): string | undefined {
616+
function tryGetModuleNameFromPaths(relativeToBaseUrl: string, paths: MapLike<readonly string[]>, allowedEndings: ModuleSpecifierEnding[], host: ModuleSpecifierResolutionHost, compilerOptions: CompilerOptions): string | undefined {
652617
for (const key in paths) {
653618
for (const patternText of paths[key]) {
654619
const pattern = normalizePath(patternText);
@@ -689,7 +654,7 @@ function tryGetModuleNameFromPaths(relativeToBaseUrl: string, paths: MapLike<rea
689654
// resolution, but as long criteria (b) above is met, I don't think its result needs to be the highest priority result
690655
// in module specifier generation. I have included it last, as it's difficult to tell exactly where it should be
691656
// sorted among the others for a particular value of `importModuleSpecifierEnding`.
692-
const candidates: { ending: Ending | undefined, value: string }[] = allowedEndings.map(ending => ({
657+
const candidates: { ending: ModuleSpecifierEnding | undefined, value: string }[] = allowedEndings.map(ending => ({
693658
ending,
694659
value: processEnding(relativeToBaseUrl, ending, compilerOptions)
695660
}));
@@ -712,23 +677,23 @@ function tryGetModuleNameFromPaths(relativeToBaseUrl: string, paths: MapLike<rea
712677
}
713678
}
714679
else if (
715-
some(candidates, c => c.ending !== Ending.Minimal && pattern === c.value) ||
716-
some(candidates, c => c.ending === Ending.Minimal && pattern === c.value && validateEnding(c))
680+
some(candidates, c => c.ending !== ModuleSpecifierEnding.Minimal && pattern === c.value) ||
681+
some(candidates, c => c.ending === ModuleSpecifierEnding.Minimal && pattern === c.value && validateEnding(c))
717682
) {
718683
return key;
719684
}
720685
}
721686
}
722687

723-
function validateEnding({ ending, value }: { ending: Ending | undefined, value: string }) {
688+
function validateEnding({ ending, value }: { ending: ModuleSpecifierEnding | undefined, value: string }) {
724689
// Optimization: `removeExtensionAndIndexPostFix` can query the file system (a good bit) if `ending` is `Minimal`, the basename
725690
// is 'index', and a `host` is provided. To avoid that until it's unavoidable, we ran the function with no `host` above. Only
726691
// here, after we've checked that the minimal ending is indeed a match (via the length and prefix/suffix checks / `some` calls),
727692
// do we check that the host-validated result is consistent with the answer we got before. If it's not, it falls back to the
728-
// `Ending.Index` result, which should already be in the list of candidates if `Minimal` was. (Note: the assumption here is
693+
// `ModuleSpecifierEnding.Index` result, which should already be in the list of candidates if `Minimal` was. (Note: the assumption here is
729694
// that every module resolution mode that supports dropping extensions also supports dropping `/index`. Like literally
730695
// everything else in this file, this logic needs to be updated if that's not true in some future module resolution mode.)
731-
return ending !== Ending.Minimal || value === processEnding(relativeToBaseUrl, ending, compilerOptions, host);
696+
return ending !== ModuleSpecifierEnding.Minimal || value === processEnding(relativeToBaseUrl, ending, compilerOptions, host);
732697
}
733698
}
734699

@@ -803,7 +768,7 @@ function tryGetModuleNameFromExports(options: CompilerOptions, targetFilePath: s
803768
return undefined;
804769
}
805770

806-
function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string, ending: Ending, compilerOptions: CompilerOptions): string | undefined {
771+
function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string, ending: ModuleSpecifierEnding, compilerOptions: CompilerOptions): string | undefined {
807772
const normalizedTargetPaths = getPathsRelativeToRootDirs(moduleFileName, rootDirs, getCanonicalFileName);
808773
if (normalizedTargetPaths === undefined) {
809774
return undefined;
@@ -831,7 +796,7 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
831796

832797
// Simplify the full file path to something that can be resolved by Node.
833798

834-
const preferences = getPreferences(host, userPreferences, options, importingSourceFile);
799+
const preferences = getPreferences(userPreferences, options, importingSourceFile);
835800
const allowedEndings = preferences.getAllowedEndingsInPrefererredOrder();
836801
let moduleSpecifier = path;
837802
let isPackageRootPath = false;
@@ -972,25 +937,25 @@ function getPathsRelativeToRootDirs(path: string, rootDirs: readonly string[], g
972937
});
973938
}
974939

975-
function processEnding(fileName: string, ending: Ending, options: CompilerOptions, host?: ModuleSpecifierResolutionHost): string {
940+
function processEnding(fileName: string, ending: ModuleSpecifierEnding, options: CompilerOptions, host?: ModuleSpecifierResolutionHost): string {
976941
if (fileExtensionIsOneOf(fileName, [Extension.Json, Extension.Mjs, Extension.Cjs])) return fileName;
977942
const noExtension = removeFileExtension(fileName);
978943
if (fileName === noExtension) return fileName;
979944
if (fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Dcts, Extension.Cts])) return noExtension + getJSExtensionForFile(fileName, options);
980945
switch (ending) {
981-
case Ending.Minimal:
946+
case ModuleSpecifierEnding.Minimal:
982947
const withoutIndex = removeSuffix(noExtension, "/index");
983948
if (host && withoutIndex !== noExtension && tryGetAnyFileFromPath(host, withoutIndex)) {
984949
// Can't remove index if there's a file by the same name as the directory.
985950
// Probably more callers should pass `host` so we can determine this?
986951
return noExtension;
987952
}
988953
return withoutIndex;
989-
case Ending.Index:
954+
case ModuleSpecifierEnding.Index:
990955
return noExtension;
991-
case Ending.JsExtension:
956+
case ModuleSpecifierEnding.JsExtension:
992957
return noExtension + getJSExtensionForFile(fileName, options);
993-
case Ending.TsExtension:
958+
case ModuleSpecifierEnding.TsExtension:
994959
return fileName;
995960
default:
996961
return Debug.assertNever(ending);

0 commit comments

Comments
 (0)