From 2b5bbfee60e8f441856ae2dbfc9148e14050189b Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 5 May 2016 13:38:09 -0700 Subject: [PATCH 1/2] use CompilerHost.realpath to resolve actual location for symlinks --- src/compiler/diagnosticMessages.json | 4 ++ src/compiler/program.ts | 33 ++++++++++------ src/compiler/sys.ts | 8 +++- src/compiler/types.ts | 1 + src/harness/compilerRunner.ts | 6 +-- src/harness/harness.ts | 39 +++++++++++++------ src/services/shims.ts | 4 ++ .../reference/moduleResolutionWithSymlinks.js | 37 ++++++++++++++++++ .../moduleResolutionWithSymlinks.symbols | 36 +++++++++++++++++ .../moduleResolutionWithSymlinks.trace.json | 32 +++++++++++++++ .../moduleResolutionWithSymlinks.types | 38 ++++++++++++++++++ .../compiler/moduleResolutionWithSymlinks.ts | 20 ++++++++++ 12 files changed, 230 insertions(+), 28 deletions(-) create mode 100644 tests/baselines/reference/moduleResolutionWithSymlinks.js create mode 100644 tests/baselines/reference/moduleResolutionWithSymlinks.symbols create mode 100644 tests/baselines/reference/moduleResolutionWithSymlinks.trace.json create mode 100644 tests/baselines/reference/moduleResolutionWithSymlinks.types create mode 100644 tests/cases/compiler/moduleResolutionWithSymlinks.ts diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 765b5de4f3ebe..34c9ab4c41718 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2752,6 +2752,10 @@ "category": "Error", "code": 6129 }, + "Resolving real path for '{0}', result '{1}'": { + "category": "Message", + "code": 6130 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", "code": 7005 diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 658d6946a3956..b1322305f6a33 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -570,22 +570,29 @@ namespace ts { let resolvedFileName = tryLoadModuleUsingOptionalResolutionSettings(moduleName, containingDirectory, nodeLoadModuleByRelativeName, failedLookupLocations, supportedExtensions, state); - if (resolvedFileName) { - return createResolvedModule(resolvedFileName, /*isExternalLibraryImport*/false, failedLookupLocations); + let isExternalLibraryImport = false; + if (!resolvedFileName) { + if (moduleHasNonRelativeName(moduleName)) { + if (traceEnabled) { + trace(host, Diagnostics.Loading_module_0_from_node_modules_folder, moduleName); + } + resolvedFileName = loadModuleFromNodeModules(moduleName, containingDirectory, failedLookupLocations, state); + isExternalLibraryImport = resolvedFileName !== undefined; + } + else { + const candidate = normalizePath(combinePaths(containingDirectory, moduleName)); + resolvedFileName = nodeLoadModuleByRelativeName(candidate, supportedExtensions, failedLookupLocations, /*onlyRecordFailures*/ false, state); + } } - let isExternalLibraryImport = false; - if (moduleHasNonRelativeName(moduleName)) { + if (resolvedFileName && host.realpath) { + const originalFileName = resolvedFileName; + resolvedFileName = normalizePath(host.realpath(resolvedFileName)); if (traceEnabled) { - trace(host, Diagnostics.Loading_module_0_from_node_modules_folder, moduleName); + trace(host, Diagnostics.Resolving_real_path_for_0_result_1, originalFileName, resolvedFileName); } - resolvedFileName = loadModuleFromNodeModules(moduleName, containingDirectory, failedLookupLocations, state); - isExternalLibraryImport = resolvedFileName !== undefined; - } - else { - const candidate = normalizePath(combinePaths(containingDirectory, moduleName)); - resolvedFileName = nodeLoadModuleByRelativeName(candidate, supportedExtensions, failedLookupLocations, /*onlyRecordFailures*/ false, state); } + return createResolvedModule(resolvedFileName, isExternalLibraryImport, failedLookupLocations); } @@ -873,6 +880,7 @@ namespace ts { } const newLine = getNewLineCharacter(options); + const realpath = sys.realpath && ((path: string) => sys.realpath(path)); return { getSourceFile, @@ -886,7 +894,8 @@ namespace ts { fileExists: fileName => sys.fileExists(fileName), readFile: fileName => sys.readFile(fileName), trace: (s: string) => sys.write(s + newLine), - directoryExists: directoryName => sys.directoryExists(directoryName) + directoryExists: directoryName => sys.directoryExists(directoryName), + realpath }; } diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 2550ca7887741..a9a39f42240ed 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -29,6 +29,7 @@ namespace ts { createHash?(data: string): string; getMemoryUsage?(): number; exit(exitCode?: number): void; + realpath?(path: string): string; } export interface FileWatcher { @@ -73,6 +74,7 @@ namespace ts { readDirectory(path: string, extension?: string, exclude?: string[]): string[]; watchFile?(path: string, callback: FileWatcherCallback): FileWatcher; watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; + realpath(path: string): string; }; export var sys: System = (function () { @@ -527,12 +529,15 @@ namespace ts { }, exit(exitCode?: number): void { process.exit(exitCode); + }, + realpath(path: string): string { + return _fs.realpathSync(path); } }; } function getChakraSystem(): System { - + const realpath = ChakraHost.realpath && ((path: string) => ChakraHost.realpath(path)); return { newLine: ChakraHost.newLine || "\r\n", args: ChakraHost.args, @@ -558,6 +563,7 @@ namespace ts { getCurrentDirectory: () => ChakraHost.currentDirectory, readDirectory: ChakraHost.readDirectory, exit: ChakraHost.quit, + realpath }; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index db9bd2469db23..562981ad41b13 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2779,6 +2779,7 @@ namespace ts { readFile(fileName: string): string; trace?(s: string): void; directoryExists?(directoryName: string): boolean; + realpath?(path: string): string; } export interface ResolvedModule { diff --git a/src/harness/compilerRunner.ts b/src/harness/compilerRunner.ts index 4842fd5a723bd..1ddbd1ea3318c 100644 --- a/src/harness/compilerRunner.ts +++ b/src/harness/compilerRunner.ts @@ -89,16 +89,16 @@ class CompilerBaselineRunner extends RunnerBase { otherFiles = []; if (testCaseContent.settings["noImplicitReferences"] || /require\(/.test(lastUnit.content) || /reference\spath/.test(lastUnit.content)) { - toBeCompiled.push({ unitName: this.makeUnitName(lastUnit.name, rootDir), content: lastUnit.content }); + toBeCompiled.push({ unitName: this.makeUnitName(lastUnit.name, rootDir), content: lastUnit.content, fileOptions: lastUnit.fileOptions }); units.forEach(unit => { if (unit.name !== lastUnit.name) { - otherFiles.push({ unitName: this.makeUnitName(unit.name, rootDir), content: unit.content }); + otherFiles.push({ unitName: this.makeUnitName(unit.name, rootDir), content: unit.content, fileOptions: unit.fileOptions }); } }); } else { toBeCompiled = units.map(unit => { - return { unitName: this.makeUnitName(unit.name, rootDir), content: unit.content }; + return { unitName: this.makeUnitName(unit.name, rootDir), content: unit.content, fileOptions: unit.fileOptions }; }); } diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 5fdd6ecddb95c..8972b6d00b6fb 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -855,13 +855,23 @@ namespace Harness { // Local get canonical file name function, that depends on passed in parameter for useCaseSensitiveFileNames const getCanonicalFileName = ts.createGetCanonicalFileName(useCaseSensitiveFileNames); - const fileMap: ts.FileMap = ts.createFileMap(); + let realPathMap: ts.FileMap; + const fileMap: ts.FileMap<() => ts.SourceFile> = ts.createFileMap<() => ts.SourceFile>(); for (const file of inputFiles) { if (file.content !== undefined) { const fileName = ts.normalizePath(file.unitName); - const sourceFile = createSourceFileAndAssertInvariants(fileName, file.content, scriptTarget); const path = ts.toPath(file.unitName, currentDirectory, getCanonicalFileName); - fileMap.set(path, sourceFile); + if (file.fileOptions && file.fileOptions["symlink"]) { + const link = file.fileOptions["symlink"]; + const linkPath = ts.toPath(link, currentDirectory, getCanonicalFileName); + if (!realPathMap) { + realPathMap = ts.createFileMap(); + } + realPathMap.set(linkPath, fileName); + fileMap.set(path, (): ts.SourceFile => { throw new Error("Symlinks should always be resolved to a realpath first"); }); + } + const sourceFile = createSourceFileAndAssertInvariants(fileName, file.content, scriptTarget); + fileMap.set(path, ts.memoize(() => sourceFile)); } } @@ -869,7 +879,7 @@ namespace Harness { fileName = ts.normalizePath(fileName); const path = ts.toPath(fileName, currentDirectory, getCanonicalFileName); if (fileMap.contains(path)) { - return fileMap.get(path); + return fileMap.get(path)(); } else if (fileName === fourslashFileName) { const tsFn = "tests/cases/fourslash/" + fourslashFileName; @@ -898,11 +908,16 @@ namespace Harness { useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, getNewLine: () => newLine, fileExists: fileName => { - return fileMap.contains(ts.toPath(fileName, currentDirectory, getCanonicalFileName)); + const path = ts.toPath(fileName, currentDirectory, getCanonicalFileName); + return fileMap.contains(path) || (realPathMap && realPathMap.contains(path)); }, readFile: (fileName: string): string => { - return fileMap.get(ts.toPath(fileName, currentDirectory, getCanonicalFileName)).getText(); - } + return fileMap.get(ts.toPath(fileName, currentDirectory, getCanonicalFileName))().getText(); + }, + realpath: realPathMap && ((f: string) => { + const path = ts.toPath(f, currentDirectory, getCanonicalFileName); + return realPathMap.contains(path) ? realPathMap.get(path) : path; + }) }; } @@ -923,7 +938,8 @@ namespace Harness { { name: "libFiles", type: "string" }, { name: "noErrorTruncation", type: "boolean" }, { name: "suppressOutputPathCheck", type: "boolean" }, - { name: "noImplicitReferences", type: "boolean" } + { name: "noImplicitReferences", type: "boolean" }, + { name: "symlink", type: "string" } ]; let optionsIndex: ts.Map; @@ -978,6 +994,7 @@ namespace Harness { export interface TestFile { unitName: string; content: string; + fileOptions?: any; } export interface CompilationOutput { @@ -1415,10 +1432,8 @@ namespace Harness { // Comment line, check for global/file @options and record them optionRegex.lastIndex = 0; const metaDataName = testMetaData[1].toLowerCase(); - if (metaDataName === "filename") { - currentFileOptions[testMetaData[1]] = testMetaData[2]; - } - else { + currentFileOptions[testMetaData[1]] = testMetaData[2]; + if (metaDataName !== "filename") { continue; } diff --git a/src/services/shims.ts b/src/services/shims.ts index b849407ebab27..288d2308055f8 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -431,11 +431,15 @@ namespace ts { export class CoreServicesShimHostAdapter implements ParseConfigHost, ModuleResolutionHost { public directoryExists: (directoryName: string) => boolean; + public realpath: (path: string) => string; constructor(private shimHost: CoreServicesShimHost) { if ("directoryExists" in this.shimHost) { this.directoryExists = directoryName => this.shimHost.directoryExists(directoryName); } + if ("realpath" in this.shimHost) { + this.realpath = path => this.shimHost.realpath(path); + } } public readDirectory(rootDir: string, extension: string, exclude: string[], depth?: number): string[] { diff --git a/tests/baselines/reference/moduleResolutionWithSymlinks.js b/tests/baselines/reference/moduleResolutionWithSymlinks.js new file mode 100644 index 0000000000000..4123430045ebb --- /dev/null +++ b/tests/baselines/reference/moduleResolutionWithSymlinks.js @@ -0,0 +1,37 @@ +//// [tests/cases/compiler/moduleResolutionWithSymlinks.ts] //// + +//// [index.ts] + +export class MyClass{} + +//// [index.ts] +import {MyClass} from "library-a"; +export { MyClass as MyClass2 } + +//// [app.ts] +import { MyClass } from "./library-a"; +import { MyClass2 } from "./library-b"; + +let x: MyClass; +let y: MyClass2; +x = y; +y = x; + +//// [index.js] +"use strict"; +var MyClass = (function () { + function MyClass() { + } + return MyClass; +}()); +exports.MyClass = MyClass; +//// [index.js] +"use strict"; +var library_a_1 = require("library-a"); +exports.MyClass2 = library_a_1.MyClass; +//// [app.js] +"use strict"; +var x; +var y; +x = y; +y = x; diff --git a/tests/baselines/reference/moduleResolutionWithSymlinks.symbols b/tests/baselines/reference/moduleResolutionWithSymlinks.symbols new file mode 100644 index 0000000000000..ea73755deefe6 --- /dev/null +++ b/tests/baselines/reference/moduleResolutionWithSymlinks.symbols @@ -0,0 +1,36 @@ +=== /src/app.ts === +import { MyClass } from "./library-a"; +>MyClass : Symbol(MyClass, Decl(app.ts, 0, 8)) + +import { MyClass2 } from "./library-b"; +>MyClass2 : Symbol(MyClass2, Decl(app.ts, 1, 8)) + +let x: MyClass; +>x : Symbol(x, Decl(app.ts, 3, 3)) +>MyClass : Symbol(MyClass, Decl(app.ts, 0, 8)) + +let y: MyClass2; +>y : Symbol(y, Decl(app.ts, 4, 3)) +>MyClass2 : Symbol(MyClass2, Decl(app.ts, 1, 8)) + +x = y; +>x : Symbol(x, Decl(app.ts, 3, 3)) +>y : Symbol(y, Decl(app.ts, 4, 3)) + +y = x; +>y : Symbol(y, Decl(app.ts, 4, 3)) +>x : Symbol(x, Decl(app.ts, 3, 3)) + +=== /src/library-a/index.ts === + +export class MyClass{} +>MyClass : Symbol(MyClass, Decl(index.ts, 0, 0)) + +=== /src/library-b/index.ts === +import {MyClass} from "library-a"; +>MyClass : Symbol(MyClass, Decl(index.ts, 0, 8)) + +export { MyClass as MyClass2 } +>MyClass : Symbol(MyClass2, Decl(index.ts, 1, 8)) +>MyClass2 : Symbol(MyClass2, Decl(index.ts, 1, 8)) + diff --git a/tests/baselines/reference/moduleResolutionWithSymlinks.trace.json b/tests/baselines/reference/moduleResolutionWithSymlinks.trace.json new file mode 100644 index 0000000000000..7f1c289018096 --- /dev/null +++ b/tests/baselines/reference/moduleResolutionWithSymlinks.trace.json @@ -0,0 +1,32 @@ +[ + "======== Resolving module './library-a' from '/src/app.ts'. ========", + "Module resolution kind is not specified, using 'NodeJs'.", + "Loading module as file / folder, candidate module location '/src/library-a'.", + "File '/src/library-a.ts' does not exist.", + "File '/src/library-a.tsx' does not exist.", + "File '/src/library-a.d.ts' does not exist.", + "File '/src/library-a/package.json' does not exist.", + "File '/src/library-a/index.ts' exist - use it as a name resolution result.", + "Resolving real path for '/src/library-a/index.ts', result '/src/library-a/index.ts'", + "======== Module name './library-a' was successfully resolved to '/src/library-a/index.ts'. ========", + "======== Resolving module './library-b' from '/src/app.ts'. ========", + "Module resolution kind is not specified, using 'NodeJs'.", + "Loading module as file / folder, candidate module location '/src/library-b'.", + "File '/src/library-b.ts' does not exist.", + "File '/src/library-b.tsx' does not exist.", + "File '/src/library-b.d.ts' does not exist.", + "File '/src/library-b/package.json' does not exist.", + "File '/src/library-b/index.ts' exist - use it as a name resolution result.", + "Resolving real path for '/src/library-b/index.ts', result '/src/library-b/index.ts'", + "======== Module name './library-b' was successfully resolved to '/src/library-b/index.ts'. ========", + "======== Resolving module 'library-a' from '/src/library-b/index.ts'. ========", + "Module resolution kind is not specified, using 'NodeJs'.", + "Loading module 'library-a' from 'node_modules' folder.", + "File '/src/library-b/node_modules/library-a.ts' does not exist.", + "File '/src/library-b/node_modules/library-a.tsx' does not exist.", + "File '/src/library-b/node_modules/library-a.d.ts' does not exist.", + "File '/src/library-b/node_modules/library-a/package.json' does not exist.", + "File '/src/library-b/node_modules/library-a/index.ts' exist - use it as a name resolution result.", + "Resolving real path for '/src/library-b/node_modules/library-a/index.ts', result '/src/library-a/index.ts'", + "======== Module name 'library-a' was successfully resolved to '/src/library-a/index.ts'. ========" +] \ No newline at end of file diff --git a/tests/baselines/reference/moduleResolutionWithSymlinks.types b/tests/baselines/reference/moduleResolutionWithSymlinks.types new file mode 100644 index 0000000000000..8344adbe81781 --- /dev/null +++ b/tests/baselines/reference/moduleResolutionWithSymlinks.types @@ -0,0 +1,38 @@ +=== /src/app.ts === +import { MyClass } from "./library-a"; +>MyClass : typeof MyClass + +import { MyClass2 } from "./library-b"; +>MyClass2 : typeof MyClass + +let x: MyClass; +>x : MyClass +>MyClass : MyClass + +let y: MyClass2; +>y : MyClass +>MyClass2 : MyClass + +x = y; +>x = y : MyClass +>x : MyClass +>y : MyClass + +y = x; +>y = x : MyClass +>y : MyClass +>x : MyClass + +=== /src/library-a/index.ts === + +export class MyClass{} +>MyClass : MyClass + +=== /src/library-b/index.ts === +import {MyClass} from "library-a"; +>MyClass : typeof MyClass + +export { MyClass as MyClass2 } +>MyClass : typeof MyClass +>MyClass2 : typeof MyClass + diff --git a/tests/cases/compiler/moduleResolutionWithSymlinks.ts b/tests/cases/compiler/moduleResolutionWithSymlinks.ts new file mode 100644 index 0000000000000..8f6f1ca1fbd27 --- /dev/null +++ b/tests/cases/compiler/moduleResolutionWithSymlinks.ts @@ -0,0 +1,20 @@ +// @module: commonjs +// @noImplicitReferences: true +// @traceResolution: true + +// @filename: /src/library-a/index.ts +// @symlink: /src/library-b/node_modules/library-a/index.ts +export class MyClass{} + +// @filename: /src/library-b/index.ts +import {MyClass} from "library-a"; +export { MyClass as MyClass2 } + +// @filename: /src/app.ts +import { MyClass } from "./library-a"; +import { MyClass2 } from "./library-b"; + +let x: MyClass; +let y: MyClass2; +x = y; +y = x; \ No newline at end of file From 0a93768a403e6892e2cedb2f9cf917d61bae2278 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 5 May 2016 13:45:14 -0700 Subject: [PATCH 2/2] remove unused code --- src/harness/harness.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 8972b6d00b6fb..ef9b36b50e2fc 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -871,7 +871,7 @@ namespace Harness { fileMap.set(path, (): ts.SourceFile => { throw new Error("Symlinks should always be resolved to a realpath first"); }); } const sourceFile = createSourceFileAndAssertInvariants(fileName, file.content, scriptTarget); - fileMap.set(path, ts.memoize(() => sourceFile)); + fileMap.set(path, () => sourceFile); } }