From 502fdbe3156ed21567b8ae4756887c306a1d60b0 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 2 Aug 2018 17:31:52 -0700 Subject: [PATCH 1/6] getEditsForFileRename: Avoid changing import specifier ending --- src/compiler/moduleSpecifiers.ts | 153 ++++++++++-------- src/services/getEditsForFileRename.ts | 2 +- ...etEditsForFileRename_preservePathEnding.ts | 29 ++++ .../importNameCodeFixNewImportTypeRoots0.ts | 22 --- .../importNameCodeFixNewImportTypeRoots1.ts | 23 --- .../importNameCodeFix_types_classic.ts | 18 ++- 6 files changed, 128 insertions(+), 119 deletions(-) create mode 100644 tests/cases/fourslash/getEditsForFileRename_preservePathEnding.ts delete mode 100644 tests/cases/fourslash/importNameCodeFixNewImportTypeRoots0.ts delete mode 100644 tests/cases/fourslash/importNameCodeFixNewImportTypeRoots1.ts diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index a82f5b854e12a..3263e5a50ea23 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -5,6 +5,47 @@ namespace ts.moduleSpecifiers { readonly importModuleSpecifierPreference?: "relative" | "non-relative"; } + const enum RelativePreference { Relative, NonRelative, Auto } + // Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" + const enum Ending { Minimal, Index, JsExtension } + + // Processed preferences + interface Preferences { + readonly relativePreference: RelativePreference; + readonly ending: Ending; + } + + function getPreferences({ importModuleSpecifierPreference }: ModuleSpecifierPreferences, compilerOptions: CompilerOptions, importingSourceFile: SourceFile): Preferences { + return { + relativePreference: importModuleSpecifierPreference === "relative" ? RelativePreference.Relative : importModuleSpecifierPreference === "non-relative" ? RelativePreference.NonRelative : RelativePreference.Auto, + ending: usesJsExtensionOnImports(importingSourceFile) ? Ending.JsExtension + : getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs ? Ending.Index : Ending.Minimal, + }; + } + + function getPreferencesForUpdate(_: ModuleSpecifierPreferences, compilerOptions: CompilerOptions, oldImportSpecifier: string): Preferences { + return { + relativePreference: isExternalModuleNameRelative(oldImportSpecifier) ? RelativePreference.Relative : RelativePreference.NonRelative, + ending: fileExtensionIs(oldImportSpecifier, Extension.Js) ? Ending.JsExtension + : getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs || endsWith(oldImportSpecifier, "index") || endsWith(oldImportSpecifier, "index.js") ? Ending.Index : Ending.Minimal, + }; + } + + export function updateModuleSpecifier( + compilerOptions: CompilerOptions, + importingSourceFileName: Path, + toFileName: string, + host: ModuleSpecifierResolutionHost, + files: ReadonlyArray, + preferences: ModuleSpecifierPreferences = {}, + redirectTargetsMap: RedirectTargetsMap, + oldImportSpecifier: string, + ): string | undefined { + const res = getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, files, redirectTargetsMap, getPreferencesForUpdate(preferences, compilerOptions, oldImportSpecifier)); + if (res === oldImportSpecifier) return undefined; + return res; + } + // Note: importingSourceFile is just for usesJsExtensionOnImports export function getModuleSpecifier( compilerOptions: CompilerOptions, @@ -16,9 +57,21 @@ namespace ts.moduleSpecifiers { preferences: ModuleSpecifierPreferences = {}, redirectTargetsMap: RedirectTargetsMap, ): string { - const info = getInfo(compilerOptions, importingSourceFile, importingSourceFileName, host); + return getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, files, redirectTargetsMap, getPreferences(preferences, compilerOptions, importingSourceFile)); + } + + function getModuleSpecifierWorker( + compilerOptions: CompilerOptions, + importingSourceFileName: Path, + toFileName: string, + host: ModuleSpecifierResolutionHost, + files: ReadonlyArray, + redirectTargetsMap: RedirectTargetsMap, + preferences: Preferences + ): string { + const info = getInfo(importingSourceFileName, host); const modulePaths = getAllModulePaths(files, importingSourceFileName, toFileName, info.getCanonicalFileName, host, redirectTargetsMap); - return firstDefined(modulePaths, moduleFileName => getGlobalModuleSpecifier(moduleFileName, info, host, compilerOptions)) || + return firstDefined(modulePaths, moduleFileName => tryGetModuleNameAsNodeModule(moduleFileName, info, host, compilerOptions)) || first(getLocalModuleSpecifiers(toFileName, info, compilerOptions, preferences)); } @@ -41,60 +94,42 @@ namespace ts.moduleSpecifiers { importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, files: ReadonlyArray, - preferences: ModuleSpecifierPreferences, + userPreferences: ModuleSpecifierPreferences, redirectTargetsMap: RedirectTargetsMap, ): ReadonlyArray> { const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol); if (ambient) return [[ambient]]; - const info = getInfo(compilerOptions, importingSourceFile, importingSourceFile.path, host); + const info = getInfo(importingSourceFile.path, host); if (!files) { return Debug.fail("Files list must be present to resolve symlinks in specifier resolution"); } const moduleSourceFile = getSourceFileOfNode(moduleSymbol.valueDeclaration); const modulePaths = getAllModulePaths(files, importingSourceFile.path, moduleSourceFile.fileName, info.getCanonicalFileName, host, redirectTargetsMap); - const global = mapDefined(modulePaths, moduleFileName => getGlobalModuleSpecifier(moduleFileName, info, host, compilerOptions)); + const preferences = getPreferences(userPreferences, compilerOptions, importingSourceFile); + const global = mapDefined(modulePaths, moduleFileName => tryGetModuleNameAsNodeModule(moduleFileName, info, host, compilerOptions)); return global.length ? global.map(g => [g]) : modulePaths.map(moduleFileName => getLocalModuleSpecifiers(moduleFileName, info, compilerOptions, preferences)); } interface Info { - readonly moduleResolutionKind: ModuleResolutionKind; - readonly addJsExtension: boolean; readonly getCanonicalFileName: GetCanonicalFileName; readonly sourceDirectory: Path; } // importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path - function getInfo(compilerOptions: CompilerOptions, importingSourceFile: SourceFile, importingSourceFileName: Path, host: ModuleSpecifierResolutionHost): Info { - const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions); - const addJsExtension = usesJsExtensionOnImports(importingSourceFile); + function getInfo(importingSourceFileName: Path, host: ModuleSpecifierResolutionHost): Info { const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : true); const sourceDirectory = getDirectoryPath(importingSourceFileName); - return { moduleResolutionKind, addJsExtension, getCanonicalFileName, sourceDirectory }; + return { getCanonicalFileName, sourceDirectory }; } - function getGlobalModuleSpecifier( - moduleFileName: string, - { addJsExtension, getCanonicalFileName, sourceDirectory }: Info, - host: ModuleSpecifierResolutionHost, - compilerOptions: CompilerOptions, - ) { - return tryGetModuleNameFromTypeRoots(compilerOptions, host, getCanonicalFileName, moduleFileName, addJsExtension) - || tryGetModuleNameAsNodeModule(compilerOptions, moduleFileName, host, getCanonicalFileName, sourceDirectory); - } - - function getLocalModuleSpecifiers( - moduleFileName: string, - { moduleResolutionKind, addJsExtension, getCanonicalFileName, sourceDirectory }: Info, - compilerOptions: CompilerOptions, - preferences: ModuleSpecifierPreferences, - ): ReadonlyArray { + function getLocalModuleSpecifiers(moduleFileName: string, { getCanonicalFileName, sourceDirectory }: Info, compilerOptions: CompilerOptions, { ending, relativePreference }: Preferences): ReadonlyArray { const { baseUrl, paths, rootDirs } = compilerOptions; const relativePath = rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName) || - removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), moduleResolutionKind, addJsExtension); - if (!baseUrl || preferences.importModuleSpecifierPreference === "relative") { + removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), ending); + if (!baseUrl || relativePreference === RelativePreference.Relative) { return [relativePath]; } @@ -103,7 +138,7 @@ namespace ts.moduleSpecifiers { return [relativePath]; } - const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, moduleResolutionKind, addJsExtension); + const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, ending); if (paths) { const fromPaths = tryGetModuleNameFromPaths(removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths); if (fromPaths) { @@ -111,11 +146,11 @@ namespace ts.moduleSpecifiers { } } - if (preferences.importModuleSpecifierPreference === "non-relative") { + if (relativePreference === RelativePreference.NonRelative) { return [importRelativeToBaseUrl]; } - if (preferences.importModuleSpecifierPreference !== undefined) Debug.assertNever(preferences.importModuleSpecifierPreference); + if (relativePreference !== RelativePreference.Auto) Debug.assertNever(relativePreference); if (isPathRelativeToParent(relativeToBaseUrl)) { return [relativePath]; @@ -271,37 +306,8 @@ namespace ts.moduleSpecifiers { return removeFileExtension(relativePath); } - function tryGetModuleNameFromTypeRoots( - options: CompilerOptions, - host: GetEffectiveTypeRootsHost, - getCanonicalFileName: (file: string) => string, - moduleFileName: string, - addJsExtension: boolean, - ): string | undefined { - const roots = getEffectiveTypeRoots(options, host); - return firstDefined(roots, unNormalizedTypeRoot => { - const typeRoot = toPath(unNormalizedTypeRoot, /*basePath*/ undefined, getCanonicalFileName); - if (startsWith(moduleFileName, typeRoot)) { - // For a type definition, we can strip `/index` even with classic resolution. - return removeExtensionAndIndexPostFix(moduleFileName.substring(typeRoot.length + 1), ModuleResolutionKind.NodeJs, addJsExtension); - } - }); - } - - function tryGetModuleNameAsNodeModule( - options: CompilerOptions, - moduleFileName: string, - host: ModuleSpecifierResolutionHost, - getCanonicalFileName: (file: string) => string, - sourceDirectory: Path, - ): string | undefined { - if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs) { - // nothing to do here - return undefined; - } - + function tryGetModuleNameAsNodeModule(moduleFileName: string, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions): string | undefined { const parts: NodeModulePathParts = getNodeModulePathParts(moduleFileName)!; - if (!parts) { return undefined; } @@ -313,8 +319,12 @@ namespace ts.moduleSpecifiers { // Get a path that's relative to node_modules or the importing file's path // if node_modules folder is in this folder or any of its parent folders, no need to keep it. if (!startsWith(sourceDirectory, getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex)))) return undefined; + // If the module was found in @types, get the actual Node package name - return getPackageNameFromAtTypesDirectory(moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1)); + const nodeModulesDirectoryName = moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1); + const packageName = getPackageNameFromAtTypesDirectory(nodeModulesDirectoryName); + // For classic resolution, only allow importing from node_modules/@types, not other node_modules + return getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs && packageName === nodeModulesDirectoryName ? undefined : packageName; function getDirectoryOrExtensionlessFileName(path: string): string { // If the file is the main module, it can be imported by the package name @@ -428,13 +438,18 @@ namespace ts.moduleSpecifiers { }); } - function removeExtensionAndIndexPostFix(fileName: string, moduleResolutionKind: ModuleResolutionKind, addJsExtension: boolean): string { + function removeExtensionAndIndexPostFix(fileName: string, ending: Ending): string { const noExtension = removeFileExtension(fileName); - return addJsExtension - ? noExtension + ".js" - : moduleResolutionKind === ModuleResolutionKind.NodeJs - ? removeSuffix(noExtension, "/index") - : noExtension; + switch (ending) { + case Ending.Minimal: + return removeSuffix(noExtension, "/index"); + case Ending.Index: + return noExtension; + case Ending.JsExtension: + return noExtension + ".js"; + default: + return Debug.assertNever(ending); + } } function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName): string | undefined { diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index bd00ce5dd9e23..45c000a6f9fc8 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -156,7 +156,7 @@ namespace ts { // Need an update if the imported file moved, or the importing file moved and was using a relative path. return toImport !== undefined && (toImport.updated || (importingSourceFileMoved && pathIsRelative(importLiteral.text))) - ? moduleSpecifiers.getModuleSpecifier(program.getCompilerOptions(), sourceFile, newImportFromPath, toImport.newFileName, host, allFiles, preferences, program.redirectTargetsMap) + ? moduleSpecifiers.updateModuleSpecifier(program.getCompilerOptions(), newImportFromPath, toImport.newFileName, host, allFiles, preferences, program.redirectTargetsMap, importLiteral.text) : undefined; }); } diff --git a/tests/cases/fourslash/getEditsForFileRename_preservePathEnding.ts b/tests/cases/fourslash/getEditsForFileRename_preservePathEnding.ts new file mode 100644 index 0000000000000..186bfc62a1f84 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_preservePathEnding.ts @@ -0,0 +1,29 @@ +/// + +// @allowJs: true + +// @Filename: /index.js +////export const x = 0; + +// @Filename: /a.js +////import { x as x0 } from "."; +////import { x as x1 } from "./index"; +////import { x as x2 } from "./index.js"; + +verify.getEditsForFileRename({ + oldPath: "/a.js", + newPath: "/b.js", + newFileContents: {}, // No change +}); + +verify.getEditsForFileRename({ + oldPath: "/b.js", + newPath: "/src/b.js", + newFileContents: { + "/b.js": +`import { x as x0 } from ".."; +import { x as x1 } from "../index"; +import { x as x2 } from "../index.js";`, + }, +}); + diff --git a/tests/cases/fourslash/importNameCodeFixNewImportTypeRoots0.ts b/tests/cases/fourslash/importNameCodeFixNewImportTypeRoots0.ts deleted file mode 100644 index cb7ef465affe8..0000000000000 --- a/tests/cases/fourslash/importNameCodeFixNewImportTypeRoots0.ts +++ /dev/null @@ -1,22 +0,0 @@ -/// - -// @Filename: a/f1.ts -//// [|foo/*0*/();|] - -// @Filename: types/random/index.ts -//// export function foo() {}; - -// @Filename: tsconfig.json -//// { -//// "compilerOptions": { -//// "typeRoots": [ -//// "./types" -//// ] -//// } -//// } - -verify.importFixAtPosition([ -`import { foo } from "random"; - -foo();` -]); diff --git a/tests/cases/fourslash/importNameCodeFixNewImportTypeRoots1.ts b/tests/cases/fourslash/importNameCodeFixNewImportTypeRoots1.ts deleted file mode 100644 index 1398a5d786614..0000000000000 --- a/tests/cases/fourslash/importNameCodeFixNewImportTypeRoots1.ts +++ /dev/null @@ -1,23 +0,0 @@ -/// - -// @Filename: a/f1.ts -//// [|foo/*0*/();|] - -// @Filename: types/random/index.ts -//// export function foo() {}; - -// @Filename: tsconfig.json -//// { -//// "compilerOptions": { -//// "baseUrl": ".", -//// "typeRoots": [ -//// "./types" -//// ] -//// } -//// } - -verify.importFixAtPosition([ -`import { foo } from "random"; - -foo();` -]); diff --git a/tests/cases/fourslash/importNameCodeFix_types_classic.ts b/tests/cases/fourslash/importNameCodeFix_types_classic.ts index b9ba463fada80..4d3bdda4d3142 100644 --- a/tests/cases/fourslash/importNameCodeFix_types_classic.ts +++ b/tests/cases/fourslash/importNameCodeFix_types_classic.ts @@ -5,12 +5,22 @@ // @Filename: /node_modules/@types/foo/index.d.ts ////export const xyz: number; +// @Filename: /node_modules/bar/index.d.ts +////export const qrs: number; + // @Filename: /a.ts -////[|xyz|] +////xyz; +////qrs; goTo.file("/a.ts"); -verify.importFixAtPosition([ +verify.codeFixAll({ + fixId: "fixMissingImport", + fixAllDescription: "Add all missing imports", + newFileContent: `import { xyz } from "foo"; -xyz` -]); +import { qrs } from "./node_modules/bar/index"; + +xyz; +qrs;`, +}); From 644568f0f17e3a27544b925be4d2f3acaa366239 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 6 Aug 2018 15:01:20 -0700 Subject: [PATCH 2/6] Support .json and .jsx extensions --- src/compiler/moduleSpecifiers.ts | 24 +++++++++++++++---- src/compiler/utilities.ts | 5 ++++ src/services/getEditsForFileRename.ts | 2 +- ...etEditsForFileRename_preservePathEnding.ts | 19 +++++++++++++-- 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index 3263e5a50ea23..bbafd08a68778 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -26,8 +26,8 @@ namespace ts.moduleSpecifiers { function getPreferencesForUpdate(_: ModuleSpecifierPreferences, compilerOptions: CompilerOptions, oldImportSpecifier: string): Preferences { return { relativePreference: isExternalModuleNameRelative(oldImportSpecifier) ? RelativePreference.Relative : RelativePreference.NonRelative, - ending: fileExtensionIs(oldImportSpecifier, Extension.Js) ? Ending.JsExtension - : getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs || endsWith(oldImportSpecifier, "index") || endsWith(oldImportSpecifier, "index.js") ? Ending.Index : Ending.Minimal, + ending: hasJavaScriptOrJsonFileExtension(oldImportSpecifier) ? Ending.JsExtension + : getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs || endsWith(oldImportSpecifier, "index") ? Ending.Index : Ending.Minimal, }; } @@ -189,7 +189,7 @@ namespace ts.moduleSpecifiers { } function usesJsExtensionOnImports({ imports }: SourceFile): boolean { - return firstDefined(imports, ({ text }) => pathIsRelative(text) ? fileExtensionIs(text, Extension.Js) : undefined) || false; + return firstDefined(imports, ({ text }) => pathIsRelative(text) ? hasJavaScriptOrJsonFileExtension(text) : undefined) || false; } function stringsEqual(a: string, b: string, getCanonicalFileName: GetCanonicalFileName): boolean { @@ -446,12 +446,28 @@ namespace ts.moduleSpecifiers { case Ending.Index: return noExtension; case Ending.JsExtension: - return noExtension + ".js"; + return noExtension + getJavaScriptExtensionForFile(fileName); default: return Debug.assertNever(ending); } } + function getJavaScriptExtensionForFile(fileName: string): Extension { + const ext = extensionFromPath(fileName); + switch (ext) { + case Extension.Ts: + case Extension.Tsx: + case Extension.Dts: + return Extension.Js; + case Extension.Js: + case Extension.Jsx: + case Extension.Json: + return ext; + default: + return Debug.assertNever(ext); + } + } + function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: GetCanonicalFileName): string | undefined { const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); return isRootedDiskPath(relativePath) ? undefined : relativePath; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 56ea3cace0c39..92b10296997eb 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -7865,6 +7865,7 @@ namespace ts { /** Must have ".d.ts" first because if ".ts" goes first, that will be detected as the extension instead of ".d.ts". */ export const supportedTypescriptExtensionsForExtractExtension: ReadonlyArray = [Extension.Dts, Extension.Ts, Extension.Tsx]; export const supportedJavascriptExtensions: ReadonlyArray = [Extension.Js, Extension.Jsx]; + export const supportedJavaScriptAndJsonExtensions: ReadonlyArray = [Extension.Js, Extension.Jsx, Extension.Json]; const allSupportedExtensions: ReadonlyArray = [...supportedTypeScriptExtensions, ...supportedJavascriptExtensions]; export function getSupportedExtensions(options?: CompilerOptions, extraFileExtensions?: ReadonlyArray): ReadonlyArray { @@ -7890,6 +7891,10 @@ namespace ts { return some(supportedJavascriptExtensions, extension => fileExtensionIs(fileName, extension)); } + export function hasJavaScriptOrJsonFileExtension(fileName: string): boolean { + return supportedJavaScriptAndJsonExtensions.some(ext => fileExtensionIs(fileName, ext)); + } + export function hasTypeScriptFileExtension(fileName: string): boolean { return some(supportedTypeScriptExtensions, extension => fileExtensionIs(fileName, extension)); } diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 45c000a6f9fc8..6690d0334433f 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -210,7 +210,7 @@ namespace ts { } function updateImportsWorker(sourceFile: SourceFile, changeTracker: textChanges.ChangeTracker, updateRef: (refText: string) => string | undefined, updateImport: (importLiteral: StringLiteralLike) => string | undefined) { - for (const ref of sourceFile.referencedFiles) { + for (const ref of sourceFile.referencedFiles || emptyArray) { // TODO: GH#26162 const updated = updateRef(ref.fileName); if (updated !== undefined && updated !== sourceFile.text.slice(ref.pos, ref.end)) changeTracker.replaceRangeWithText(sourceFile, ref, updated); } diff --git a/tests/cases/fourslash/getEditsForFileRename_preservePathEnding.ts b/tests/cases/fourslash/getEditsForFileRename_preservePathEnding.ts index 186bfc62a1f84..a65df19a0b38d 100644 --- a/tests/cases/fourslash/getEditsForFileRename_preservePathEnding.ts +++ b/tests/cases/fourslash/getEditsForFileRename_preservePathEnding.ts @@ -1,14 +1,28 @@ /// // @allowJs: true +// @checkJs: true +// @strict: true +// @jsx: preserve +// @resolveJsonModule: true // @Filename: /index.js ////export const x = 0; +// @Filename: /jsx.jsx +////export const y = 0; + +// @Filename: /j.jonah.json +////{} + // @Filename: /a.js ////import { x as x0 } from "."; ////import { x as x1 } from "./index"; ////import { x as x2 } from "./index.js"; +////import { y } from "./jsx.jsx"; +////import j from "./j.jonah.json"; + +verify.noErrors(); verify.getEditsForFileRename({ oldPath: "/a.js", @@ -23,7 +37,8 @@ verify.getEditsForFileRename({ "/b.js": `import { x as x0 } from ".."; import { x as x1 } from "../index"; -import { x as x2 } from "../index.js";`, +import { x as x2 } from "../index.js"; +import { y } from "../jsx.jsx"; +import j from "../j.jonah.json";`, }, }); - From caab6fb782c94c3cff181065b413176615861417 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 6 Aug 2018 15:10:19 -0700 Subject: [PATCH 3/6] Restore typeRoots tests --- .../importNameCodeFixNewImportTypeRoots0.ts | 23 ++++++++++++++++ .../importNameCodeFixNewImportTypeRoots1.ts | 27 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 tests/cases/fourslash/importNameCodeFixNewImportTypeRoots0.ts create mode 100644 tests/cases/fourslash/importNameCodeFixNewImportTypeRoots1.ts diff --git a/tests/cases/fourslash/importNameCodeFixNewImportTypeRoots0.ts b/tests/cases/fourslash/importNameCodeFixNewImportTypeRoots0.ts new file mode 100644 index 0000000000000..ddc6d1790c726 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportTypeRoots0.ts @@ -0,0 +1,23 @@ +/// + +// @Filename: a/f1.ts +//// [|foo/*0*/();|] + +// @Filename: types/random/index.ts +//// export function foo() {}; + +// @Filename: tsconfig.json +//// { +//// "compilerOptions": { +//// "typeRoots": [ +//// "./types" +//// ] +//// } +//// } + +// "typeRoots" does not affect module resolution. Importing from "random" would be a compile error. +verify.importFixAtPosition([ +`import { foo } from "../types/random"; + +foo();` +]); diff --git a/tests/cases/fourslash/importNameCodeFixNewImportTypeRoots1.ts b/tests/cases/fourslash/importNameCodeFixNewImportTypeRoots1.ts new file mode 100644 index 0000000000000..9743d164a9c51 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFixNewImportTypeRoots1.ts @@ -0,0 +1,27 @@ +/// + +// @Filename: a/f1.ts +//// [|foo/*0*/();|] + +// @Filename: types/random/index.ts +//// export function foo() {}; + +// @Filename: tsconfig.json +//// { +//// "compilerOptions": { +//// "baseUrl": ".", +//// "typeRoots": [ +//// "./types" +//// ] +//// } +//// } + +// "typeRoots" does not affect module resolution. Importing from "random" would be a compile error. +verify.importFixAtPosition([ +`import { foo } from "types/random"; + +foo();`, +`import { foo } from "../types/random"; + +foo();` +]); From e44b33a7c937f3583f75fbe03457101f899b2dd9 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 6 Aug 2018 15:40:57 -0700 Subject: [PATCH 4/6] Fix json test --- .../fourslash/getEditsForFileRename_preservePathEnding.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/cases/fourslash/getEditsForFileRename_preservePathEnding.ts b/tests/cases/fourslash/getEditsForFileRename_preservePathEnding.ts index a65df19a0b38d..54b3e0e061272 100644 --- a/tests/cases/fourslash/getEditsForFileRename_preservePathEnding.ts +++ b/tests/cases/fourslash/getEditsForFileRename_preservePathEnding.ts @@ -13,14 +13,14 @@ ////export const y = 0; // @Filename: /j.jonah.json -////{} +////{ "j": 0 } // @Filename: /a.js ////import { x as x0 } from "."; ////import { x as x1 } from "./index"; ////import { x as x2 } from "./index.js"; ////import { y } from "./jsx.jsx"; -////import j from "./j.jonah.json"; +////import { j } from "./j.jonah.json"; verify.noErrors(); @@ -39,6 +39,6 @@ verify.getEditsForFileRename({ import { x as x1 } from "../index"; import { x as x2 } from "../index.js"; import { y } from "../jsx.jsx"; -import j from "../j.jonah.json";`, +import { j } from "../j.jonah.json";`, }, }); From a3af5e942c9889644c9657cdf566e3bb9fa5bb5e Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 6 Aug 2018 16:11:19 -0700 Subject: [PATCH 5/6] When --jsx preserve is set, import ".tsx" file with ".jsx" extension --- src/compiler/moduleSpecifiers.ts | 13 +++++++------ .../fourslash/importNameCodeFix_jsExtension.ts | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index bbafd08a68778..79700c9b1a15e 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -128,7 +128,7 @@ namespace ts.moduleSpecifiers { const { baseUrl, paths, rootDirs } = compilerOptions; const relativePath = rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName) || - removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), ending); + removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), ending, compilerOptions); if (!baseUrl || relativePreference === RelativePreference.Relative) { return [relativePath]; } @@ -138,7 +138,7 @@ namespace ts.moduleSpecifiers { return [relativePath]; } - const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, ending); + const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, ending, compilerOptions); if (paths) { const fromPaths = tryGetModuleNameFromPaths(removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths); if (fromPaths) { @@ -438,7 +438,7 @@ namespace ts.moduleSpecifiers { }); } - function removeExtensionAndIndexPostFix(fileName: string, ending: Ending): string { + function removeExtensionAndIndexPostFix(fileName: string, ending: Ending, options: CompilerOptions): string { const noExtension = removeFileExtension(fileName); switch (ending) { case Ending.Minimal: @@ -446,19 +446,20 @@ namespace ts.moduleSpecifiers { case Ending.Index: return noExtension; case Ending.JsExtension: - return noExtension + getJavaScriptExtensionForFile(fileName); + return noExtension + getJavaScriptExtensionForFile(fileName, options); default: return Debug.assertNever(ending); } } - function getJavaScriptExtensionForFile(fileName: string): Extension { + function getJavaScriptExtensionForFile(fileName: string, options: CompilerOptions): Extension { const ext = extensionFromPath(fileName); switch (ext) { case Extension.Ts: - case Extension.Tsx: case Extension.Dts: return Extension.Js; + case Extension.Tsx: + return options.jsx === JsxEmit.Preserve ? Extension.Jsx : Extension.Js; case Extension.Js: case Extension.Jsx: case Extension.Json: diff --git a/tests/cases/fourslash/importNameCodeFix_jsExtension.ts b/tests/cases/fourslash/importNameCodeFix_jsExtension.ts index e719f1aeee889..b47e67d82b23b 100644 --- a/tests/cases/fourslash/importNameCodeFix_jsExtension.ts +++ b/tests/cases/fourslash/importNameCodeFix_jsExtension.ts @@ -2,6 +2,7 @@ // @moduleResolution: node // @noLib: true +// @jsx: preserve // @Filename: /a.ts ////export function a() {} @@ -9,17 +10,24 @@ // @Filename: /b.ts ////export function b() {} +// @Filename: /c.tsx +////export function c() {} + // @Filename: /c.ts ////import * as g from "global"; // Global imports skipped ////import { a } from "./a.js"; ////import { a as a2 } from "./a"; // Ignored, only the first relative import is considered -////b; +////b; c; goTo.file("/c.ts"); -verify.importFixAtPosition([ +verify.codeFixAll({ + fixId: "fixMissingImport", + fixAllDescription: "Add all missing imports", + newFileContent: `import * as g from "global"; // Global imports skipped import { a } from "./a.js"; import { a as a2 } from "./a"; // Ignored, only the first relative import is considered import { b } from "./b.js"; -b;`, -]); +import { c } from "./c.jsx"; +b; c;`, +}); From b57ecfbe6501c6823d9c692b13c4fd7d89c18697 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 6 Aug 2018 16:39:32 -0700 Subject: [PATCH 6/6] Support ending preference in UserPreferences --- src/compiler/checker.ts | 1 - src/compiler/moduleSpecifiers.ts | 32 ++++++++++--------- src/compiler/types.ts | 11 +++++++ src/services/getEditsForFileRename.ts | 7 ++-- src/services/types.ts | 8 ----- .../reference/api/tsserverlibrary.d.ts | 18 ++++++----- tests/baselines/reference/api/typescript.d.ts | 18 ++++++----- tests/cases/fourslash/fourslash.ts | 9 +++--- .../importNameCodeFix_endingPreference.ts | 26 +++++++++++++++ 9 files changed, 82 insertions(+), 48 deletions(-) create mode 100644 tests/cases/fourslash/importNameCodeFix_endingPreference.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index e1391275b28f3..b3d2eab20eb05 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3894,7 +3894,6 @@ namespace ts { contextFile, context.tracker.moduleResolverHost, context.tracker.moduleResolverHost.getSourceFiles!(), // TODO: GH#18217 - { importModuleSpecifierPreference: "non-relative" }, host.redirectTargetsMap, ); links.specifierCache = links.specifierCache || createMap(); diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index 79700c9b1a15e..4faef0d2bc8b6 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -1,12 +1,8 @@ // Used by importFixes, getEditsForFileRename, and declaration emit to synthesize import module specifiers. /* @internal */ namespace ts.moduleSpecifiers { - export interface ModuleSpecifierPreferences { - readonly importModuleSpecifierPreference?: "relative" | "non-relative"; - } - const enum RelativePreference { Relative, NonRelative, Auto } - // Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" + // See UserPreferences#importPathEnding const enum Ending { Minimal, Index, JsExtension } // Processed preferences @@ -15,15 +11,23 @@ namespace ts.moduleSpecifiers { readonly ending: Ending; } - function getPreferences({ importModuleSpecifierPreference }: ModuleSpecifierPreferences, compilerOptions: CompilerOptions, importingSourceFile: SourceFile): Preferences { + function getPreferences({ importModuleSpecifierPreference, importModuleSpecifierEnding }: UserPreferences, compilerOptions: CompilerOptions, importingSourceFile: SourceFile): Preferences { return { relativePreference: importModuleSpecifierPreference === "relative" ? RelativePreference.Relative : importModuleSpecifierPreference === "non-relative" ? RelativePreference.NonRelative : RelativePreference.Auto, - ending: usesJsExtensionOnImports(importingSourceFile) ? Ending.JsExtension - : getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs ? Ending.Index : Ending.Minimal, + ending: getEnding(), }; + function getEnding(): Ending { + switch (importModuleSpecifierEnding) { + case "minimal": return Ending.Minimal; + case "index": return Ending.Index; + case "js": return Ending.JsExtension; + default: return usesJsExtensionOnImports(importingSourceFile) ? Ending.JsExtension + : getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs ? Ending.Index : Ending.Minimal; + } + } } - function getPreferencesForUpdate(_: ModuleSpecifierPreferences, compilerOptions: CompilerOptions, oldImportSpecifier: string): Preferences { + function getPreferencesForUpdate(compilerOptions: CompilerOptions, oldImportSpecifier: string): Preferences { return { relativePreference: isExternalModuleNameRelative(oldImportSpecifier) ? RelativePreference.Relative : RelativePreference.NonRelative, ending: hasJavaScriptOrJsonFileExtension(oldImportSpecifier) ? Ending.JsExtension @@ -37,11 +41,10 @@ namespace ts.moduleSpecifiers { toFileName: string, host: ModuleSpecifierResolutionHost, files: ReadonlyArray, - preferences: ModuleSpecifierPreferences = {}, redirectTargetsMap: RedirectTargetsMap, oldImportSpecifier: string, ): string | undefined { - const res = getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, files, redirectTargetsMap, getPreferencesForUpdate(preferences, compilerOptions, oldImportSpecifier)); + const res = getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, files, redirectTargetsMap, getPreferencesForUpdate(compilerOptions, oldImportSpecifier)); if (res === oldImportSpecifier) return undefined; return res; } @@ -54,7 +57,7 @@ namespace ts.moduleSpecifiers { toFileName: string, host: ModuleSpecifierResolutionHost, files: ReadonlyArray, - preferences: ModuleSpecifierPreferences = {}, + preferences: UserPreferences = {}, redirectTargetsMap: RedirectTargetsMap, ): string { return getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, files, redirectTargetsMap, getPreferences(preferences, compilerOptions, importingSourceFile)); @@ -81,10 +84,9 @@ namespace ts.moduleSpecifiers { importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, files: ReadonlyArray, - preferences: ModuleSpecifierPreferences, redirectTargetsMap: RedirectTargetsMap, ): string { - return first(first(getModuleSpecifiers(moduleSymbol, compilerOptions, importingSourceFile, host, files, preferences, redirectTargetsMap))); + return first(first(getModuleSpecifiers(moduleSymbol, compilerOptions, importingSourceFile, host, files, { importModuleSpecifierPreference: "non-relative" }, redirectTargetsMap))); } // For each symlink/original for a module, returns a list of ways to import that file. @@ -94,7 +96,7 @@ namespace ts.moduleSpecifiers { importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, files: ReadonlyArray, - userPreferences: ModuleSpecifierPreferences, + userPreferences: UserPreferences, redirectTargetsMap: RedirectTargetsMap, ): ReadonlyArray> { const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index f66c0c02e3946..b3f56b323d005 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5594,4 +5594,15 @@ namespace ts { get(key: TKey): PragmaPsuedoMap[TKey] | PragmaPsuedoMap[TKey][]; forEach(action: (value: PragmaPsuedoMap[TKey] | PragmaPsuedoMap[TKey][], key: TKey) => void): void; } + + export interface UserPreferences { + readonly disableSuggestions?: boolean; + readonly quotePreference?: "double" | "single"; + readonly includeCompletionsForModuleExports?: boolean; + readonly includeCompletionsWithInsertText?: boolean; + readonly importModuleSpecifierPreference?: "relative" | "non-relative"; + /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ + readonly importModuleSpecifierEnding?: "minimal" | "index" | "js"; + readonly allowTextChangesInNewFiles?: boolean; + } } diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 6690d0334433f..4fe024787f38f 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -6,7 +6,7 @@ namespace ts { newFileOrDirPath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext, - preferences: UserPreferences, + _preferences: UserPreferences, sourceMapper: SourceMapper, ): ReadonlyArray { const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host); @@ -15,7 +15,7 @@ namespace ts { const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath, getCanonicalFileName, sourceMapper); return textChanges.ChangeTracker.with({ host, formatContext }, changeTracker => { updateTsconfigFiles(program, changeTracker, oldToNew, newFileOrDirPath, host.getCurrentDirectory(), useCaseSensitiveFileNames); - updateImports(program, changeTracker, oldToNew, newToOld, host, getCanonicalFileName, preferences); + updateImports(program, changeTracker, oldToNew, newToOld, host, getCanonicalFileName); }); } @@ -122,7 +122,6 @@ namespace ts { newToOld: PathUpdater, host: LanguageServiceHost, getCanonicalFileName: GetCanonicalFileName, - preferences: UserPreferences, ): void { const allFiles = program.getSourceFiles(); for (const sourceFile of allFiles) { @@ -156,7 +155,7 @@ namespace ts { // Need an update if the imported file moved, or the importing file moved and was using a relative path. return toImport !== undefined && (toImport.updated || (importingSourceFileMoved && pathIsRelative(importLiteral.text))) - ? moduleSpecifiers.updateModuleSpecifier(program.getCompilerOptions(), newImportFromPath, toImport.newFileName, host, allFiles, preferences, program.redirectTargetsMap, importLiteral.text) + ? moduleSpecifiers.updateModuleSpecifier(program.getCompilerOptions(), newImportFromPath, toImport.newFileName, host, allFiles, program.redirectTargetsMap, importLiteral.text) : undefined; }); } diff --git a/src/services/types.ts b/src/services/types.ts index 51c98724c0957..9bf0edf31508e 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -233,14 +233,6 @@ namespace ts { installPackage?(options: InstallPackageOptions): Promise; } - export interface UserPreferences { - readonly disableSuggestions?: boolean; - readonly quotePreference?: "double" | "single"; - readonly includeCompletionsForModuleExports?: boolean; - readonly includeCompletionsWithInsertText?: boolean; - readonly importModuleSpecifierPreference?: "relative" | "non-relative"; - readonly allowTextChangesInNewFiles?: boolean; - } /* @internal */ export const emptyOptions = {}; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 188ae835b5b27..0af01e07df212 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2999,6 +2999,16 @@ declare namespace ts { Parameters = 1296, IndexSignatureParameters = 4432 } + interface UserPreferences { + readonly disableSuggestions?: boolean; + readonly quotePreference?: "double" | "single"; + readonly includeCompletionsForModuleExports?: boolean; + readonly includeCompletionsWithInsertText?: boolean; + readonly importModuleSpecifierPreference?: "relative" | "non-relative"; + /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ + readonly importModuleSpecifierEnding?: "minimal" | "index" | "js"; + readonly allowTextChangesInNewFiles?: boolean; + } } declare function setTimeout(handler: (...args: any[]) => void, timeout: number): any; declare function clearTimeout(handle: any): void; @@ -4815,14 +4825,6 @@ declare namespace ts { isKnownTypesPackageName?(name: string): boolean; installPackage?(options: InstallPackageOptions): Promise; } - interface UserPreferences { - readonly disableSuggestions?: boolean; - readonly quotePreference?: "double" | "single"; - readonly includeCompletionsForModuleExports?: boolean; - readonly includeCompletionsWithInsertText?: boolean; - readonly importModuleSpecifierPreference?: "relative" | "non-relative"; - readonly allowTextChangesInNewFiles?: boolean; - } interface LanguageService { cleanupSemanticCache(): void; getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[]; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 92152c7ca87b3..a6c3bc4473e4c 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2999,6 +2999,16 @@ declare namespace ts { Parameters = 1296, IndexSignatureParameters = 4432 } + interface UserPreferences { + readonly disableSuggestions?: boolean; + readonly quotePreference?: "double" | "single"; + readonly includeCompletionsForModuleExports?: boolean; + readonly includeCompletionsWithInsertText?: boolean; + readonly importModuleSpecifierPreference?: "relative" | "non-relative"; + /** Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" */ + readonly importModuleSpecifierEnding?: "minimal" | "index" | "js"; + readonly allowTextChangesInNewFiles?: boolean; + } } declare function setTimeout(handler: (...args: any[]) => void, timeout: number): any; declare function clearTimeout(handle: any): void; @@ -4815,14 +4825,6 @@ declare namespace ts { isKnownTypesPackageName?(name: string): boolean; installPackage?(options: InstallPackageOptions): Promise; } - interface UserPreferences { - readonly disableSuggestions?: boolean; - readonly quotePreference?: "double" | "single"; - readonly includeCompletionsForModuleExports?: boolean; - readonly includeCompletionsWithInsertText?: boolean; - readonly importModuleSpecifierPreference?: "relative" | "non-relative"; - readonly allowTextChangesInNewFiles?: boolean; - } interface LanguageService { cleanupSemanticCache(): void; getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[]; diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 900ae172ab88f..3f09d48490ec3 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -526,10 +526,11 @@ declare namespace FourSlashInterface { filesToSearch?: ReadonlyArray; } interface UserPreferences { - quotePreference?: "double" | "single"; - includeCompletionsForModuleExports?: boolean; - includeInsertTextCompletions?: boolean; - importModuleSpecifierPreference?: "relative" | "non-relative"; + readonly quotePreference?: "double" | "single"; + readonly includeCompletionsForModuleExports?: boolean; + readonly includeInsertTextCompletions?: boolean; + readonly importModuleSpecifierPreference?: "relative" | "non-relative"; + readonly importModuleSpecifierEnding?: "minimal" | "index" | "js"; } interface CompletionsAtOptions extends UserPreferences { triggerCharacter?: string; diff --git a/tests/cases/fourslash/importNameCodeFix_endingPreference.ts b/tests/cases/fourslash/importNameCodeFix_endingPreference.ts new file mode 100644 index 0000000000000..41a96e9cf0633 --- /dev/null +++ b/tests/cases/fourslash/importNameCodeFix_endingPreference.ts @@ -0,0 +1,26 @@ +/// + +// @moduleResolution: node + +// @Filename: /foo/index.ts +////export const foo = 0; + +// @Filename: /a.ts +////foo; + +// @Filename: /b.ts +////foo; + +// @Filename: /c.ts +////foo; + +const tests: ReadonlyArray<[string, FourSlashInterface.UserPreferences["importModuleSpecifierEnding"], string]> = [ + ["/a.ts", "js", "./foo/index.js"], + ["/b.ts", "index", "./foo/index"], + ["/c.ts", "minimal", "./foo"], +]; + +for (const [fileName, importModuleSpecifierEnding, specifier] of tests) { + goTo.file(fileName); + verify.importFixAtPosition([`import { foo } from "${specifier}";\n\nfoo;`,], /*errorCode*/ undefined, { importModuleSpecifierEnding }); +}