From ebfda15799ea2635ab644ec059a33d1e169bdd6a Mon Sep 17 00:00:00 2001
From: Wesley Wigham <wewigham@microsoft.com>
Date: Mon, 4 Jun 2018 12:29:30 -0700
Subject: [PATCH] move functionality from services into compiler, fix with
 propert file/directory conflict handling

---
 src/compiler/checker.ts                       |  3 +-
 src/compiler/moduleNameResolver.ts            |  4 --
 .../moduleSpecifiers.ts                       | 39 +++++++++++++------
 src/compiler/program.ts                       |  3 ++
 src/compiler/tsconfig.json                    |  1 +
 src/compiler/types.ts                         | 20 ++++++----
 src/compiler/utilities.ts                     | 31 ++-------------
 src/harness/tsconfig.json                     |  2 +-
 src/server/tsconfig.json                      |  2 +-
 src/server/tsconfig.library.json              |  2 +-
 src/services/getEditsForFileRename.ts         |  2 +-
 src/services/tsconfig.json                    |  2 +-
 .../reference/api/tsserverlibrary.d.ts        |  8 ++--
 tests/baselines/reference/api/typescript.d.ts |  8 ++--
 ...arationEmitCommonJsModuleReferencedType.js |  4 +-
 15 files changed, 63 insertions(+), 68 deletions(-)
 rename src/{services/codefixes => compiler}/moduleSpecifiers.ts (88%)

diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index b82784de71d50..44e7f937a46ec 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -4085,7 +4085,8 @@ namespace ts {
                         // ambient module, just use declaration/symbol name (fallthrough)
                     }
                     else {
-                        return `"${getResolvedExternalModuleNameForPossiblyExternalModule(context!.tracker.moduleResolverHost!, file, getSourceFileOfNode(getOriginalNode(context!.enclosingDeclaration)))}"`;
+                        const contextFile = getSourceFileOfNode(getOriginalNode(context!.enclosingDeclaration))!;
+                        return `"${file.moduleName || moduleSpecifiers.getModuleSpecifier(compilerOptions, contextFile, contextFile.path, file.path, context!.tracker.moduleResolverHost!)}"`;
                     }
                 }
                 const declaration = symbol.declarations[0];
diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts
index beaeb7dd05615..f93f2d7eb64d6 100644
--- a/src/compiler/moduleNameResolver.ts
+++ b/src/compiler/moduleNameResolver.ts
@@ -132,10 +132,6 @@ namespace ts {
         }
     }
 
-    export interface GetEffectiveTypeRootsHost {
-        directoryExists?(directoryName: string): boolean;
-        getCurrentDirectory?(): string;
-    }
     export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined {
         if (options.typeRoots) {
             return options.typeRoots;
diff --git a/src/services/codefixes/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts
similarity index 88%
rename from src/services/codefixes/moduleSpecifiers.ts
rename to src/compiler/moduleSpecifiers.ts
index be92b9172736b..5c427e40d2694 100644
--- a/src/services/codefixes/moduleSpecifiers.ts
+++ b/src/compiler/moduleSpecifiers.ts
@@ -1,10 +1,13 @@
 // Used by importFixes to synthesize import module specifiers.
 /* @internal */
 namespace ts.moduleSpecifiers {
+    export interface ModuleSpecifierPreferences {
+        importModuleSpecifierPreference?: "relative" | "non-relative";
+    }
+
     // Note: fromSourceFile is just for usesJsExtensionOnImports
-    export function getModuleSpecifier(program: Program, fromSourceFile: SourceFile, fromSourceFileName: string, toFileName: string, host: LanguageServiceHost, preferences: UserPreferences) {
-        const info = getInfo(program.getCompilerOptions(), fromSourceFile, fromSourceFileName, host);
-        const compilerOptions = program.getCompilerOptions();
+    export function getModuleSpecifier(compilerOptions: CompilerOptions, fromSourceFile: SourceFile, fromSourceFileName: string, toFileName: string, host: ModuleSpecifierResolutionHost, preferences: ModuleSpecifierPreferences = {}) {
+        const info = getInfo(compilerOptions, fromSourceFile, fromSourceFileName, host);
         return getGlobalModuleSpecifier(toFileName, info, host, compilerOptions) ||
             first(getLocalModuleSpecifiers(toFileName, info, compilerOptions, preferences));
     }
@@ -14,15 +17,15 @@ namespace ts.moduleSpecifiers {
         moduleSymbol: Symbol,
         program: Program,
         importingSourceFile: SourceFile,
-        host: LanguageServiceHost,
-        preferences: UserPreferences,
+        host: ModuleSpecifierResolutionHost,
+        preferences: ModuleSpecifierPreferences,
     ): ReadonlyArray<ReadonlyArray<string>> {
         const ambient = tryGetModuleNameFromAmbientModule(moduleSymbol);
         if (ambient) return [[ambient]];
 
         const compilerOptions = program.getCompilerOptions();
         const info = getInfo(compilerOptions, importingSourceFile, importingSourceFile.fileName, host);
-        const modulePaths = getAllModulePaths(program, moduleSymbol.valueDeclaration.getSourceFile());
+        const modulePaths = getAllModulePaths(program, getSourceFileOfNode(moduleSymbol.valueDeclaration));
 
         const global = mapDefined(modulePaths, moduleFileName => getGlobalModuleSpecifier(moduleFileName, info, host, compilerOptions));
         return global.length ? global.map(g => [g]) : modulePaths.map(moduleFileName =>
@@ -36,10 +39,10 @@ namespace ts.moduleSpecifiers {
         readonly sourceDirectory: string;
     }
     // importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path
-    function getInfo(compilerOptions: CompilerOptions, importingSourceFile: SourceFile, importingSourceFileName: string, host: LanguageServiceHost): Info {
+    function getInfo(compilerOptions: CompilerOptions, importingSourceFile: SourceFile, importingSourceFileName: string, host: ModuleSpecifierResolutionHost): Info {
         const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions);
         const addJsExtension = usesJsExtensionOnImports(importingSourceFile);
-        const getCanonicalFileName = hostGetCanonicalFileName(host);
+        const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : true);
         const sourceDirectory = getDirectoryPath(importingSourceFileName);
         return { moduleResolutionKind, addJsExtension, getCanonicalFileName, sourceDirectory };
     }
@@ -47,7 +50,7 @@ namespace ts.moduleSpecifiers {
     function getGlobalModuleSpecifier(
         moduleFileName: string,
         { addJsExtension, getCanonicalFileName, sourceDirectory }: Info,
-        host: LanguageServiceHost,
+        host: ModuleSpecifierResolutionHost,
         compilerOptions: CompilerOptions,
     ) {
         return tryGetModuleNameFromTypeRoots(compilerOptions, host, getCanonicalFileName, moduleFileName, addJsExtension)
@@ -59,7 +62,7 @@ namespace ts.moduleSpecifiers {
         moduleFileName: string,
         { moduleResolutionKind, addJsExtension, getCanonicalFileName, sourceDirectory }: Info,
         compilerOptions: CompilerOptions,
-        preferences: UserPreferences,
+        preferences: ModuleSpecifierPreferences,
     ) {
         const { baseUrl, paths } = compilerOptions;
 
@@ -210,7 +213,7 @@ namespace ts.moduleSpecifiers {
     function tryGetModuleNameAsNodeModule(
         options: CompilerOptions,
         moduleFileName: string,
-        host: LanguageServiceHost,
+        host: ModuleSpecifierResolutionHost,
         getCanonicalFileName: (file: string) => string,
         sourceDirectory: string,
     ): string | undefined {
@@ -256,7 +259,8 @@ namespace ts.moduleSpecifiers {
             const fullModulePathWithoutExtension = removeFileExtension(path);
 
             // If the file is /index, it can be imported by its directory name
-            if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts.fileNameIndex)) === "/index") {
+            // IFF there is not _also_ a file by the same name
+            if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts.fileNameIndex)) === "/index" && !tryGetAnyFileFromPath(host, fullModulePathWithoutExtension.substring(0, parts.fileNameIndex))) {
                 return fullModulePathWithoutExtension.substring(0, parts.fileNameIndex);
             }
 
@@ -264,6 +268,17 @@ namespace ts.moduleSpecifiers {
         }
     }
 
+    function tryGetAnyFileFromPath(host: ModuleSpecifierResolutionHost, path: string) {
+        // We check all js, `node` and `json` extensions in addition to TS, since node module resolution would also choose those over the directory
+        const extensions = getSupportedExtensions({ allowJs: true }, [{ extension: "node", isMixedContent: false }, { extension: "json", isMixedContent: false, scriptKind: ScriptKind.JSON }]);
+        for (const e of extensions) {
+            const fullPath = path + e;
+            if (host.fileExists!(fullPath)) {
+                return fullPath;
+            }
+        }
+    }
+
     interface NodeModulePathParts {
         readonly topLevelNodeModulesIndex: number;
         readonly topLevelPackageNameIndex: number;
diff --git a/src/compiler/program.ts b/src/compiler/program.ts
index 715f5377b1692..e93dada9eda7d 100755
--- a/src/compiler/program.ts
+++ b/src/compiler/program.ts
@@ -1169,6 +1169,9 @@ namespace ts {
                 writeFile: writeFileCallback || (
                     (fileName, data, writeByteOrderMark, onError, sourceFiles) => host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles)),
                 isEmitBlocked,
+                readFile: f => host.readFile(f),
+                fileExists: f => host.fileExists(f),
+                ...(host.directoryExists ? { directoryExists: f => host.directoryExists!(f) } : {}),
             };
         }
 
diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json
index 46e5384434a01..5716a2a417d56 100644
--- a/src/compiler/tsconfig.json
+++ b/src/compiler/tsconfig.json
@@ -44,6 +44,7 @@
         "builderState.ts",
         "builder.ts",
         "resolutionCache.ts",
+        "moduleSpecifiers.ts",
         "watch.ts",
         "commandLineParser.ts",
         "tsc.ts"
diff --git a/src/compiler/types.ts b/src/compiler/types.ts
index 40ee1c5c9d8c0..81c89b1580f5a 100644
--- a/src/compiler/types.ts
+++ b/src/compiler/types.ts
@@ -5019,8 +5019,9 @@ namespace ts {
     }
 
     /* @internal */
-    export interface EmitHost extends ScriptReferenceHost {
+    export interface EmitHost extends ScriptReferenceHost, ModuleSpecifierResolutionHost {
         getSourceFiles(): ReadonlyArray<SourceFile>;
+        getCurrentDirectory(): string;
 
         /* @internal */
         isSourceFileFromExternalLibrary(file: SourceFile): boolean;
@@ -5282,12 +5283,15 @@ namespace ts {
         isAtStartOfLine(): boolean;
     }
 
-    /* @internal */
-    export interface ModuleNameResolverHost {
-        getCanonicalFileName(f: string): string;
-        getCommonSourceDirectory(): string;
-        getCurrentDirectory(): string;
-        getCompilerOptions(): CompilerOptions;
+    export interface GetEffectiveTypeRootsHost {
+        directoryExists?(directoryName: string): boolean;
+        getCurrentDirectory?(): string;
+    }
+    /** @internal */
+    export interface ModuleSpecifierResolutionHost extends GetEffectiveTypeRootsHost {
+        useCaseSensitiveFileNames?(): boolean;
+        fileExists?(path: string): boolean;
+        readFile?(path: string): string | undefined;
     }
 
     /** @deprecated See comment on SymbolWriter */
@@ -5301,7 +5305,7 @@ namespace ts {
         reportPrivateInBaseOfClassExpression?(propertyName: string): void;
         reportInaccessibleUniqueSymbolError?(): void;
         /* @internal */
-        moduleResolverHost?: ModuleNameResolverHost;
+        moduleResolverHost?: ModuleSpecifierResolutionHost;
         /* @internal */
         trackReferencedAmbientModule?(decl: ModuleDeclaration): void;
     }
diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts
index 21248a1e26805..dc5ef95fa817a 100644
--- a/src/compiler/utilities.ts
+++ b/src/compiler/utilities.ts
@@ -2934,36 +2934,11 @@ namespace ts {
         };
     }
 
-    /** @internal */
-    export function getResolvedExternalModuleNameForPossiblyExternalModule(host: ModuleNameResolverHost, file: SourceFile, referenceFile?: SourceFile) {
-        const result = getResolvedExternalModuleName(host, file, referenceFile);
-        const opts = host.getCompilerOptions();
-        if (getEmitModuleResolutionKind(opts) === ModuleResolutionKind.NodeJs) {
-            // Trim leading paths to `node_modules` to allow node module resolution to find the thing in the future without an exact path
-            // This simplification means if a package.json for `foo` directs us to `foo/lib/main` instead of `foo/index` we'll write `foo/lib/main` over `foo`, however
-            const parts = getPathComponents(result);
-            if (parts[0] !== "") {
-                return result; // rooted path, leave as is
-            }
-            else {
-                parts.shift();
-            }
-            let index = 0;
-            while (parts[index] && (parts[index] === "." || parts[index] === "..")) {
-                index++;
-            }
-            if (parts[index] && parts[index] === "node_modules") {
-                return parts.slice(index + 1).join(directorySeparator);
-            }
-        }
-        return result;
-    }
-
-    export function getResolvedExternalModuleName(host: ModuleNameResolverHost, file: SourceFile, referenceFile?: SourceFile): string {
+    export function getResolvedExternalModuleName(host: EmitHost, file: SourceFile, referenceFile?: SourceFile): string {
         return file.moduleName || getExternalModuleNameFromPath(host, file.fileName, referenceFile && referenceFile.fileName);
     }
 
-    export function getExternalModuleNameFromDeclaration(host: ModuleNameResolverHost, resolver: EmitResolver, declaration: ImportEqualsDeclaration | ImportDeclaration | ExportDeclaration | ModuleDeclaration | ImportTypeNode): string | undefined {
+    export function getExternalModuleNameFromDeclaration(host: EmitHost, resolver: EmitResolver, declaration: ImportEqualsDeclaration | ImportDeclaration | ExportDeclaration | ModuleDeclaration | ImportTypeNode): string | undefined {
         const file = resolver.getExternalModuleFileFromDeclaration(declaration);
         if (!file || file.isDeclarationFile) {
             return undefined;
@@ -2974,7 +2949,7 @@ namespace ts {
     /**
      * Resolves a local path to a path which is absolute to the base of the emit
      */
-    export function getExternalModuleNameFromPath(host: ModuleNameResolverHost, fileName: string, referencePath?: string): string {
+    export function getExternalModuleNameFromPath(host: EmitHost, fileName: string, referencePath?: string): string {
         const getCanonicalFileName = (f: string) => host.getCanonicalFileName(f);
         const dir = toPath(referencePath ? getDirectoryPath(referencePath) : host.getCommonSourceDirectory(), host.getCurrentDirectory(), getCanonicalFileName);
         const filePath = getNormalizedAbsolutePath(fileName, host.getCurrentDirectory());
diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json
index ae409e96b8770..c401b40a339b5 100644
--- a/src/harness/tsconfig.json
+++ b/src/harness/tsconfig.json
@@ -51,6 +51,7 @@
         "../compiler/builderState.ts",
         "../compiler/builder.ts",
         "../compiler/resolutionCache.ts",
+        "../compiler/moduleSpecifiers.ts",
         "../compiler/watch.ts",
         "../compiler/commandLineParser.ts",
 
@@ -115,7 +116,6 @@
         "../services/codefixes/inferFromUsage.ts",
         "../services/codefixes/fixInvalidImportSyntax.ts",
         "../services/codefixes/fixStrictClassInitialization.ts",
-        "../services/codefixes/moduleSpecifiers.ts",
         "../services/codefixes/requireInTs.ts",
         "../services/codefixes/useDefaultImport.ts",
         "../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts",
diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json
index a92a19f9c018a..cb0485321ca51 100644
--- a/src/server/tsconfig.json
+++ b/src/server/tsconfig.json
@@ -47,6 +47,7 @@
         "../compiler/builderState.ts",
         "../compiler/builder.ts",
         "../compiler/resolutionCache.ts",
+        "../compiler/moduleSpecifiers.ts",
         "../compiler/watch.ts",
         "../compiler/commandLineParser.ts",
 
@@ -111,7 +112,6 @@
         "../services/codefixes/inferFromUsage.ts",
         "../services/codefixes/fixInvalidImportSyntax.ts",
         "../services/codefixes/fixStrictClassInitialization.ts",
-        "../services/codefixes/moduleSpecifiers.ts",
         "../services/codefixes/requireInTs.ts",
         "../services/codefixes/useDefaultImport.ts",
         "../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts",
diff --git a/src/server/tsconfig.library.json b/src/server/tsconfig.library.json
index 1675002c653ad..1adfe2a4bd043 100644
--- a/src/server/tsconfig.library.json
+++ b/src/server/tsconfig.library.json
@@ -53,6 +53,7 @@
         "../compiler/builderState.ts",
         "../compiler/builder.ts",
         "../compiler/resolutionCache.ts",
+        "../compiler/moduleSpecifiers.ts",
         "../compiler/watch.ts",
         "../compiler/commandLineParser.ts",
 
@@ -117,7 +118,6 @@
         "../services/codefixes/inferFromUsage.ts",
         "../services/codefixes/fixInvalidImportSyntax.ts",
         "../services/codefixes/fixStrictClassInitialization.ts",
-        "../services/codefixes/moduleSpecifiers.ts",
         "../services/codefixes/requireInTs.ts",
         "../services/codefixes/useDefaultImport.ts",
         "../services/codefixes/fixAddModuleReferTypeMissingTypeof.ts",
diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts
index 4bac4a2516dc9..a7e41d2014f31 100644
--- a/src/services/getEditsForFileRename.ts
+++ b/src/services/getEditsForFileRename.ts
@@ -123,7 +123,7 @@ namespace ts {
                         // TODO:GH#18217
                         ? getSourceFileToImportFromResolved(resolveModuleName(importLiteral.text, oldImportFromPath, program.getCompilerOptions(), host as ModuleResolutionHost), oldToNew, program)
                         : getSourceFileToImport(importLiteral, sourceFile, program, host, oldToNew);
-                    return toImport === undefined ? undefined : moduleSpecifiers.getModuleSpecifier(program, sourceFile, newImportFromPath, toImport, host, preferences);
+                    return toImport === undefined ? undefined : moduleSpecifiers.getModuleSpecifier(program.getCompilerOptions(), sourceFile, newImportFromPath, toImport, host, preferences);
                 });
         }
     }
diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json
index 7d6687fb987e9..237142fc5bff6 100644
--- a/src/services/tsconfig.json
+++ b/src/services/tsconfig.json
@@ -44,6 +44,7 @@
         "../compiler/builderState.ts",
         "../compiler/builder.ts",
         "../compiler/resolutionCache.ts",
+        "../compiler/moduleSpecifiers.ts",
         "../compiler/watch.ts",
         "../compiler/commandLineParser.ts",
 
@@ -108,7 +109,6 @@
         "codefixes/inferFromUsage.ts",
         "codefixes/fixInvalidImportSyntax.ts",
         "codefixes/fixStrictClassInitialization.ts",
-        "codefixes/moduleSpecifiers.ts",
         "codefixes/requireInTs.ts",
         "codefixes/useDefaultImport.ts",
         "codefixes/fixAddModuleReferTypeMissingTypeof.ts",
diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts
index 8f065e362d2e8..d4f185e1fefbb 100644
--- a/tests/baselines/reference/api/tsserverlibrary.d.ts
+++ b/tests/baselines/reference/api/tsserverlibrary.d.ts
@@ -2876,6 +2876,10 @@ declare namespace ts {
         omitTrailingSemicolon?: boolean;
         noEmitHelpers?: boolean;
     }
+    interface GetEffectiveTypeRootsHost {
+        directoryExists?(directoryName: string): boolean;
+        getCurrentDirectory?(): string;
+    }
     /** @deprecated See comment on SymbolWriter */
     interface SymbolTracker {
         trackSymbol?(symbol: Symbol, enclosingDeclaration?: Node, meaning?: SymbolFlags): void;
@@ -3467,10 +3471,6 @@ declare namespace ts {
     function updateSourceFile(sourceFile: SourceFile, newText: string, textChangeRange: TextChangeRange, aggressiveChecks?: boolean): SourceFile;
 }
 declare namespace ts {
-    interface GetEffectiveTypeRootsHost {
-        directoryExists?(directoryName: string): boolean;
-        getCurrentDirectory?(): string;
-    }
     function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined;
     /**
      * @param {string | undefined} containingFile - file that contains type reference directive, can be undefined if containing file is unknown.
diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts
index 86c001cdec450..2660d26cba500 100644
--- a/tests/baselines/reference/api/typescript.d.ts
+++ b/tests/baselines/reference/api/typescript.d.ts
@@ -2876,6 +2876,10 @@ declare namespace ts {
         omitTrailingSemicolon?: boolean;
         noEmitHelpers?: boolean;
     }
+    interface GetEffectiveTypeRootsHost {
+        directoryExists?(directoryName: string): boolean;
+        getCurrentDirectory?(): string;
+    }
     /** @deprecated See comment on SymbolWriter */
     interface SymbolTracker {
         trackSymbol?(symbol: Symbol, enclosingDeclaration?: Node, meaning?: SymbolFlags): void;
@@ -3467,10 +3471,6 @@ declare namespace ts {
     function updateSourceFile(sourceFile: SourceFile, newText: string, textChangeRange: TextChangeRange, aggressiveChecks?: boolean): SourceFile;
 }
 declare namespace ts {
-    interface GetEffectiveTypeRootsHost {
-        directoryExists?(directoryName: string): boolean;
-        getCurrentDirectory?(): string;
-    }
     function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined;
     /**
      * @param {string | undefined} containingFile - file that contains type reference directive, can be undefined if containing file is unknown.
diff --git a/tests/baselines/reference/declarationEmitCommonJsModuleReferencedType.js b/tests/baselines/reference/declarationEmitCommonJsModuleReferencedType.js
index 998eb27133877..1e32c5c4c262b 100644
--- a/tests/baselines/reference/declarationEmitCommonJsModuleReferencedType.js
+++ b/tests/baselines/reference/declarationEmitCommonJsModuleReferencedType.js
@@ -34,5 +34,5 @@ exports.y = root_1.bar();
 
 
 //// [entry.d.ts]
-export declare const x: [import("foo/index").SomeProps, import("foo/other").OtherProps, import("foo/other/index").OtherIndexProps, import("foo/node_modules/nested/index").NestedProps];
-export declare const y: import("root/index").RootProps;
+export declare const x: [import("foo").SomeProps, import("foo/other").OtherProps, import("foo/other/index").OtherIndexProps, import("foo/node_modules/nested").NestedProps];
+export declare const y: import("root").RootProps;