Skip to content

Commit e33e229

Browse files
author
Andy
authored
Extract 'moduleSpecifiers' namespace out of importFixes (microsoft#24010)
1 parent e296301 commit e33e229

File tree

6 files changed

+329
-310
lines changed

6 files changed

+329
-310
lines changed

src/harness/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
"../services/codefixes/inferFromUsage.ts",
113113
"../services/codefixes/fixInvalidImportSyntax.ts",
114114
"../services/codefixes/fixStrictClassInitialization.ts",
115+
"../services/codefixes/moduleSpecifiers.ts",
115116
"../services/codefixes/requireInTs.ts",
116117
"../services/codefixes/useDefaultImport.ts",
117118
"../services/refactors/extractSymbol.ts",

src/server/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
"../services/codefixes/inferFromUsage.ts",
109109
"../services/codefixes/fixInvalidImportSyntax.ts",
110110
"../services/codefixes/fixStrictClassInitialization.ts",
111+
"../services/codefixes/moduleSpecifiers.ts",
111112
"../services/codefixes/requireInTs.ts",
112113
"../services/codefixes/useDefaultImport.ts",
113114
"../services/refactors/extractSymbol.ts",

src/server/tsconfig.library.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"../services/codefixes/inferFromUsage.ts",
115115
"../services/codefixes/fixInvalidImportSyntax.ts",
116116
"../services/codefixes/fixStrictClassInitialization.ts",
117+
"../services/codefixes/moduleSpecifiers.ts",
117118
"../services/codefixes/requireInTs.ts",
118119
"../services/codefixes/useDefaultImport.ts",
119120
"../services/refactors/extractSymbol.ts",

src/services/codefixes/importFixes.ts

Lines changed: 3 additions & 310 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ namespace ts.codefix {
101101
const exportInfos = getAllReExportingModules(exportedSymbol, moduleSymbol, symbolName, sourceFile, checker, allSourceFiles);
102102
Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol));
103103
// We sort the best codefixes first, so taking `first` is best for completions.
104-
const moduleSpecifier = first(getNewImportInfos(program, sourceFile, exportInfos, compilerOptions, getCanonicalFileName, host, preferences)).moduleSpecifier;
104+
const moduleSpecifier = first(getNewImportInfos(program, sourceFile, exportInfos, host, preferences)).moduleSpecifier;
105105
const ctx: ImportCodeFixContext = { host, program, checker, compilerOptions, sourceFile, formatContext, symbolName, getCanonicalFileName, symbolToken, preferences };
106106
return { moduleSpecifier, codeAction: first(getCodeActionsForImport(exportInfos, ctx)) };
107107
}
@@ -225,10 +225,6 @@ namespace ts.codefix {
225225
}
226226
}
227227

228-
function usesJsExtensionOnImports(sourceFile: SourceFile): boolean {
229-
return firstDefined(sourceFile.imports, ({ text }) => pathIsRelative(text) ? fileExtensionIs(text, Extension.Js) : undefined) || false;
230-
}
231-
232228
function createImportClauseOfKind(kind: ImportKind.Default | ImportKind.Named | ImportKind.Namespace, symbolName: string) {
233229
const id = createIdentifier(symbolName);
234230
switch (kind) {
@@ -247,320 +243,17 @@ namespace ts.codefix {
247243
program: Program,
248244
sourceFile: SourceFile,
249245
moduleSymbols: ReadonlyArray<SymbolExportInfo>,
250-
compilerOptions: CompilerOptions,
251-
getCanonicalFileName: (file: string) => string,
252246
host: LanguageServiceHost,
253247
preferences: UserPreferences,
254248
): ReadonlyArray<NewImportInfo> {
255-
const { baseUrl, paths, rootDirs } = compilerOptions;
256-
const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions);
257-
const addJsExtension = usesJsExtensionOnImports(sourceFile);
258249
const choicesForEachExportingModule = flatMap<SymbolExportInfo, NewImportInfo[]>(moduleSymbols, ({ moduleSymbol, importKind }) => {
259-
const modulePathsGroups = getAllModulePaths(program, moduleSymbol.valueDeclaration.getSourceFile()).map(moduleFileName => {
260-
const sourceDirectory = getDirectoryPath(sourceFile.fileName);
261-
const global = tryGetModuleNameFromAmbientModule(moduleSymbol)
262-
|| tryGetModuleNameFromTypeRoots(compilerOptions, host, getCanonicalFileName, moduleFileName, addJsExtension)
263-
|| tryGetModuleNameAsNodeModule(compilerOptions, moduleFileName, host, getCanonicalFileName, sourceDirectory)
264-
|| rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName);
265-
if (global) {
266-
return [global];
267-
}
268-
269-
const relativePath = removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), moduleResolutionKind, addJsExtension);
270-
if (!baseUrl || preferences.importModuleSpecifierPreference === "relative") {
271-
return [relativePath];
272-
}
273-
274-
const relativeToBaseUrl = getRelativePathIfInDirectory(moduleFileName, baseUrl, getCanonicalFileName);
275-
if (!relativeToBaseUrl) {
276-
return [relativePath];
277-
}
278-
279-
const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, moduleResolutionKind, addJsExtension);
280-
if (paths) {
281-
const fromPaths = tryGetModuleNameFromPaths(removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths);
282-
if (fromPaths) {
283-
return [fromPaths];
284-
}
285-
}
286-
287-
if (preferences.importModuleSpecifierPreference === "non-relative") {
288-
return [importRelativeToBaseUrl];
289-
}
290-
291-
if (preferences.importModuleSpecifierPreference !== undefined) Debug.assertNever(preferences.importModuleSpecifierPreference);
292-
293-
if (isPathRelativeToParent(relativeToBaseUrl)) {
294-
return [relativePath];
295-
}
296-
297-
/*
298-
Prefer a relative import over a baseUrl import if it doesn't traverse up to baseUrl.
299-
300-
Suppose we have:
301-
baseUrl = /base
302-
sourceDirectory = /base/a/b
303-
moduleFileName = /base/foo/bar
304-
Then:
305-
relativePath = ../../foo/bar
306-
getRelativePathNParents(relativePath) = 2
307-
pathFromSourceToBaseUrl = ../../
308-
getRelativePathNParents(pathFromSourceToBaseUrl) = 2
309-
2 < 2 = false
310-
In this case we should prefer using the baseUrl path "/a/b" instead of the relative path "../../foo/bar".
311-
312-
Suppose we have:
313-
baseUrl = /base
314-
sourceDirectory = /base/foo/a
315-
moduleFileName = /base/foo/bar
316-
Then:
317-
relativePath = ../a
318-
getRelativePathNParents(relativePath) = 1
319-
pathFromSourceToBaseUrl = ../../
320-
getRelativePathNParents(pathFromSourceToBaseUrl) = 2
321-
1 < 2 = true
322-
In this case we should prefer using the relative path "../a" instead of the baseUrl path "foo/a".
323-
*/
324-
const pathFromSourceToBaseUrl = ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, baseUrl, getCanonicalFileName));
325-
const relativeFirst = getRelativePathNParents(relativePath) < getRelativePathNParents(pathFromSourceToBaseUrl);
326-
return relativeFirst ? [relativePath, importRelativeToBaseUrl] : [importRelativeToBaseUrl, relativePath];
327-
});
250+
const modulePathsGroups = moduleSpecifiers.getModuleSpecifiers(moduleSymbol, program, sourceFile, host, preferences);
328251
return modulePathsGroups.map(group => group.map(moduleSpecifier => ({ moduleSpecifier, importKind })));
329252
});
330253
// Sort to keep the shortest paths first, but keep [relativePath, importRelativeToBaseUrl] groups together
331254
return flatten<NewImportInfo>(choicesForEachExportingModule.sort((a, b) => first(a).moduleSpecifier.length - first(b).moduleSpecifier.length));
332255
}
333256

334-
/**
335-
* Looks for a existing imports that use symlinks to this module.
336-
* Only if no symlink is available, the real path will be used.
337-
*/
338-
function getAllModulePaths(program: Program, { fileName }: SourceFile): ReadonlyArray<string> {
339-
const symlinks = mapDefined(program.getSourceFiles(), sf =>
340-
sf.resolvedModules && firstDefinedIterator(sf.resolvedModules.values(), res =>
341-
res && res.resolvedFileName === fileName ? res.originalPath : undefined));
342-
return symlinks.length === 0 ? [fileName] : symlinks;
343-
}
344-
345-
function getRelativePathNParents(relativePath: string): number {
346-
const components = getPathComponents(relativePath);
347-
if (components[0] || components.length === 1) return 0;
348-
for (let i = 1; i < components.length; i++) {
349-
if (components[i] !== "..") return i - 1;
350-
}
351-
return components.length - 1;
352-
}
353-
354-
function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol): string | undefined {
355-
const decl = moduleSymbol.valueDeclaration;
356-
if (isModuleDeclaration(decl) && isStringLiteral(decl.name)) {
357-
return decl.name.text;
358-
}
359-
}
360-
361-
function tryGetModuleNameFromPaths(relativeToBaseUrlWithIndex: string, relativeToBaseUrl: string, paths: MapLike<ReadonlyArray<string>>): string | undefined {
362-
for (const key in paths) {
363-
for (const patternText of paths[key]) {
364-
const pattern = removeFileExtension(normalizePath(patternText));
365-
const indexOfStar = pattern.indexOf("*");
366-
if (indexOfStar === 0 && pattern.length === 1) {
367-
continue;
368-
}
369-
else if (indexOfStar !== -1) {
370-
const prefix = pattern.substr(0, indexOfStar);
371-
const suffix = pattern.substr(indexOfStar + 1);
372-
if (relativeToBaseUrl.length >= prefix.length + suffix.length &&
373-
startsWith(relativeToBaseUrl, prefix) &&
374-
endsWith(relativeToBaseUrl, suffix)) {
375-
const matchedStar = relativeToBaseUrl.substr(prefix.length, relativeToBaseUrl.length - suffix.length);
376-
return key.replace("*", matchedStar);
377-
}
378-
}
379-
else if (pattern === relativeToBaseUrl || pattern === relativeToBaseUrlWithIndex) {
380-
return key;
381-
}
382-
}
383-
}
384-
}
385-
386-
function tryGetModuleNameFromRootDirs(rootDirs: ReadonlyArray<string>, moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string): string | undefined {
387-
const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, rootDirs, getCanonicalFileName);
388-
if (normalizedTargetPath === undefined) {
389-
return undefined;
390-
}
391-
392-
const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName);
393-
const relativePath = normalizedSourcePath !== undefined ? ensurePathIsNonModuleName(getRelativePathFromDirectory(normalizedSourcePath, normalizedTargetPath, getCanonicalFileName)) : normalizedTargetPath;
394-
return removeFileExtension(relativePath);
395-
}
396-
397-
function tryGetModuleNameFromTypeRoots(
398-
options: CompilerOptions,
399-
host: GetEffectiveTypeRootsHost,
400-
getCanonicalFileName: (file: string) => string,
401-
moduleFileName: string,
402-
addJsExtension: boolean,
403-
): string | undefined {
404-
const roots = getEffectiveTypeRoots(options, host);
405-
return firstDefined(roots, unNormalizedTypeRoot => {
406-
const typeRoot = toPath(unNormalizedTypeRoot, /*basePath*/ undefined, getCanonicalFileName);
407-
if (startsWith(moduleFileName, typeRoot)) {
408-
// For a type definition, we can strip `/index` even with classic resolution.
409-
return removeExtensionAndIndexPostFix(moduleFileName.substring(typeRoot.length + 1), ModuleResolutionKind.NodeJs, addJsExtension);
410-
}
411-
});
412-
}
413-
414-
function tryGetModuleNameAsNodeModule(
415-
options: CompilerOptions,
416-
moduleFileName: string,
417-
host: LanguageServiceHost,
418-
getCanonicalFileName: (file: string) => string,
419-
sourceDirectory: string,
420-
): string | undefined {
421-
if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs) {
422-
// nothing to do here
423-
return undefined;
424-
}
425-
426-
const parts = getNodeModulePathParts(moduleFileName);
427-
428-
if (!parts) {
429-
return undefined;
430-
}
431-
432-
// Simplify the full file path to something that can be resolved by Node.
433-
434-
// If the module could be imported by a directory name, use that directory's name
435-
let moduleSpecifier = getDirectoryOrExtensionlessFileName(moduleFileName);
436-
// Get a path that's relative to node_modules or the importing file's path
437-
moduleSpecifier = getNodeResolvablePath(moduleSpecifier);
438-
// If the module was found in @types, get the actual Node package name
439-
return getPackageNameFromAtTypesDirectory(moduleSpecifier);
440-
441-
function getDirectoryOrExtensionlessFileName(path: string): string {
442-
// If the file is the main module, it can be imported by the package name
443-
const packageRootPath = path.substring(0, parts.packageRootIndex);
444-
const packageJsonPath = combinePaths(packageRootPath, "package.json");
445-
if (host.fileExists(packageJsonPath)) {
446-
const packageJsonContent = JSON.parse(host.readFile(packageJsonPath));
447-
if (packageJsonContent) {
448-
const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main;
449-
if (mainFileRelative) {
450-
const mainExportFile = toPath(mainFileRelative, packageRootPath, getCanonicalFileName);
451-
if (mainExportFile === getCanonicalFileName(path)) {
452-
return packageRootPath;
453-
}
454-
}
455-
}
456-
}
457-
458-
// We still have a file name - remove the extension
459-
const fullModulePathWithoutExtension = removeFileExtension(path);
460-
461-
// If the file is /index, it can be imported by its directory name
462-
if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts.fileNameIndex)) === "/index") {
463-
return fullModulePathWithoutExtension.substring(0, parts.fileNameIndex);
464-
}
465-
466-
return fullModulePathWithoutExtension;
467-
}
468-
469-
function getNodeResolvablePath(path: string): string {
470-
const basePath = path.substring(0, parts.topLevelNodeModulesIndex);
471-
if (sourceDirectory.indexOf(basePath) === 0) {
472-
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
473-
return path.substring(parts.topLevelPackageNameIndex + 1);
474-
}
475-
else {
476-
return ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, path, getCanonicalFileName));
477-
}
478-
}
479-
}
480-
481-
function getNodeModulePathParts(fullPath: string) {
482-
// If fullPath can't be valid module file within node_modules, returns undefined.
483-
// Example of expected pattern: /base/path/node_modules/[@scope/otherpackage/@otherscope/node_modules/]package/[subdirectory/]file.js
484-
// Returns indices: ^ ^ ^ ^
485-
486-
let topLevelNodeModulesIndex = 0;
487-
let topLevelPackageNameIndex = 0;
488-
let packageRootIndex = 0;
489-
let fileNameIndex = 0;
490-
491-
const enum States {
492-
BeforeNodeModules,
493-
NodeModules,
494-
Scope,
495-
PackageContent
496-
}
497-
498-
let partStart = 0;
499-
let partEnd = 0;
500-
let state = States.BeforeNodeModules;
501-
502-
while (partEnd >= 0) {
503-
partStart = partEnd;
504-
partEnd = fullPath.indexOf("/", partStart + 1);
505-
switch (state) {
506-
case States.BeforeNodeModules:
507-
if (fullPath.indexOf("/node_modules/", partStart) === partStart) {
508-
topLevelNodeModulesIndex = partStart;
509-
topLevelPackageNameIndex = partEnd;
510-
state = States.NodeModules;
511-
}
512-
break;
513-
case States.NodeModules:
514-
case States.Scope:
515-
if (state === States.NodeModules && fullPath.charAt(partStart + 1) === "@") {
516-
state = States.Scope;
517-
}
518-
else {
519-
packageRootIndex = partEnd;
520-
state = States.PackageContent;
521-
}
522-
break;
523-
case States.PackageContent:
524-
if (fullPath.indexOf("/node_modules/", partStart) === partStart) {
525-
state = States.NodeModules;
526-
}
527-
else {
528-
state = States.PackageContent;
529-
}
530-
break;
531-
}
532-
}
533-
534-
fileNameIndex = partStart;
535-
536-
return state > States.NodeModules ? { topLevelNodeModulesIndex, topLevelPackageNameIndex, packageRootIndex, fileNameIndex } : undefined;
537-
}
538-
539-
function getPathRelativeToRootDirs(path: string, rootDirs: ReadonlyArray<string>, getCanonicalFileName: GetCanonicalFileName): string | undefined {
540-
return firstDefined(rootDirs, rootDir => {
541-
const relativePath = getRelativePathIfInDirectory(path, rootDir, getCanonicalFileName);
542-
return isPathRelativeToParent(relativePath) ? undefined : relativePath;
543-
});
544-
}
545-
546-
function removeExtensionAndIndexPostFix(fileName: string, moduleResolutionKind: ModuleResolutionKind, addJsExtension: boolean): string {
547-
const noExtension = removeFileExtension(fileName);
548-
return addJsExtension
549-
? noExtension + ".js"
550-
: moduleResolutionKind === ModuleResolutionKind.NodeJs
551-
? removeSuffix(noExtension, "/index")
552-
: noExtension;
553-
}
554-
555-
function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName): string | undefined {
556-
const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
557-
return isRootedDiskPath(relativePath) ? undefined : relativePath;
558-
}
559-
560-
function isPathRelativeToParent(path: string): boolean {
561-
return startsWith(path, "..");
562-
}
563-
564257
function getCodeActionsForAddImport(
565258
exportInfos: ReadonlyArray<SymbolExportInfo>,
566259
ctx: ImportCodeFixContext,
@@ -585,7 +278,7 @@ namespace ts.codefix {
585278
const existingDeclaration = firstDefined(existingImports, newImportInfoFromExistingSpecifier);
586279
const newImportInfos = existingDeclaration
587280
? [existingDeclaration]
588-
: getNewImportInfos(ctx.program, ctx.sourceFile, exportInfos, ctx.compilerOptions, ctx.getCanonicalFileName, ctx.host, ctx.preferences);
281+
: getNewImportInfos(ctx.program, ctx.sourceFile, exportInfos, ctx.host, ctx.preferences);
589282
for (const info of newImportInfos) {
590283
addNew.push(getCodeActionForNewImport(ctx, info));
591284
}

0 commit comments

Comments
 (0)