From 9332f7e1e3e9b47314e838db660de16e82939e69 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Mon, 13 Jul 2015 17:44:50 -0700 Subject: [PATCH] introduce ModuleResolutionHost interface --- src/compiler/program.ts | 112 ++++++++++++------ src/compiler/types.ts | 17 ++- src/harness/harness.ts | 55 +++++---- src/harness/harnessLanguageService.ts | 3 +- src/harness/projectsRunner.ts | 7 +- src/services/services.ts | 41 ++++++- src/services/shims.ts | 31 ++++- .../cases/unittests/reuseProgramStructure.ts | 11 +- 8 files changed, 204 insertions(+), 73 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 957b519af5e..f429e338cfb 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -27,6 +27,52 @@ namespace ts { } return undefined; } + + export function getDefaultModuleNameResolver(options: CompilerOptions): ModuleNameResolver { + // TODO: return different resolver based on compiler options (i.e. module kind) + return resolveModuleName; + } + + export function resolveTripleslashReference(moduleName: string, containingFile: string): string { + let basePath = getDirectoryPath(containingFile); + let referencedFileName = isRootedDiskPath(moduleName) ? moduleName : combinePaths(basePath, moduleName); + return normalizePath(referencedFileName); + } + + function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule { + + let searchPath = getDirectoryPath(containingFile); + let searchName: string; + + let failedLookupLocations: string[] = []; + + let referencedSourceFile: string; + while (true) { + searchName = normalizePath(combinePaths(searchPath, moduleName)); + referencedSourceFile = forEach(supportedExtensions, extension => { + let candidate = searchName + extension; + let ok = host.fileExists(candidate) ? candidate : undefined; + if (host.fileExists(candidate)) { + return candidate; + } + else { + failedLookupLocations.push(candidate); + } + }); + + if (referencedSourceFile) { + break; + } + + let parentPath = getDirectoryPath(searchPath); + if (parentPath === searchPath) { + break; + } + searchPath = parentPath; + } + + return { resolvedFileName: referencedSourceFile, failedLookupLocations }; + } export function createCompilerHost(options: CompilerOptions, setParentNodes?: boolean): CompilerHost { let currentDirectory: string; @@ -94,7 +140,11 @@ namespace ts { } const newLine = getNewLineCharacter(options); - + + let moduleResolutionHost: ModuleResolutionHost = { + fileExists: fileName => sys.fileExists(fileName), + } + return { getSourceFile, getDefaultLibFileName: options => combinePaths(getDirectoryPath(normalizePath(sys.getExecutingFilePath())), getDefaultLibFileName(options)), @@ -102,7 +152,8 @@ namespace ts { getCurrentDirectory: () => currentDirectory || (currentDirectory = sys.getCurrentDirectory()), useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames, getCanonicalFileName, - getNewLine: () => newLine + getNewLine: () => newLine, + getModuleResolutionHost: () => moduleResolutionHost }; } @@ -160,6 +211,20 @@ namespace ts { let start = new Date().getTime(); host = host || createCompilerHost(options); + + // initialize resolveModuleNameWorker only if noResolve is false + let resolveModuleNameWorker: (moduleName: string, containingFile: string) => string; + if (!options.noResolve) { + resolveModuleNameWorker = host.resolveModuleName; + if (!resolveModuleNameWorker) { + Debug.assert(host.getModuleResolutionHost !== undefined); + let defaultResolver = getDefaultModuleNameResolver(options); + resolveModuleNameWorker = (moduleName, containingFile) => { + let moduleResolution = defaultResolver(moduleName, containingFile, options, host.getModuleResolutionHost()); + return moduleResolution.resolvedFileName; + } + } + } let filesByName = createFileMap(fileName => host.getCanonicalFileName(fileName)); @@ -622,8 +687,8 @@ namespace ts { function processReferencedFiles(file: SourceFile, basePath: string) { forEach(file.referencedFiles, ref => { - let referencedFileName = isRootedDiskPath(ref.fileName) ? ref.fileName : combinePaths(basePath, ref.fileName); - processSourceFile(normalizePath(referencedFileName), /* isDefaultLib */ false, file, ref.pos, ref.end); + let referencedFileName = resolveTripleslashReference(ref.fileName, file.fileName); + processSourceFile(referencedFileName, /* isDefaultLib */ false, file, ref.pos, ref.end); }); } @@ -647,36 +712,17 @@ namespace ts { return findSourceFile(fileName, /* isDefaultLib */ false, file, nameLiteral.pos, nameLiteral.end - nameLiteral.pos); } - function resolveModule(moduleNameExpr: LiteralExpression, existingResolutions: Map): void { - let searchPath = basePath; - let searchName: string; - - if (existingResolutions && hasProperty(existingResolutions, moduleNameExpr.text)) { - let fileName = existingResolutions[moduleNameExpr.text]; - // use existing resolution - setResolvedModuleName(file, moduleNameExpr.text, fileName); - if (fileName) { - findModuleSourceFile(fileName, moduleNameExpr); - } - return; + function resolveModule(moduleNameExpr: LiteralExpression, existingResolutions: Map): void { + Debug.assert(resolveModuleNameWorker !== undefined); + + let resolvedModuleName = existingResolutions && hasProperty(existingResolutions, moduleNameExpr.text) + ? existingResolutions[moduleNameExpr.text] + : resolveModuleNameWorker(moduleNameExpr.text, file.fileName); + + setResolvedModuleName(file, moduleNameExpr.text, resolvedModuleName); + if (resolvedModuleName) { + findModuleSourceFile(resolvedModuleName, moduleNameExpr); } - - while (true) { - searchName = normalizePath(combinePaths(searchPath, moduleNameExpr.text)); - let referencedSourceFile = forEach(supportedExtensions, extension => findModuleSourceFile(searchName + extension, moduleNameExpr)); - if (referencedSourceFile) { - setResolvedModuleName(file, moduleNameExpr.text, referencedSourceFile.fileName); - return; - } - - let parentPath = getDirectoryPath(searchPath); - if (parentPath === searchPath) { - break; - } - searchPath = parentPath; - } - // mark reference as non-resolved - setResolvedModuleName(file, moduleNameExpr.text, undefined); } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 31a8fbb4082..d404ba2e00d 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1285,7 +1285,7 @@ namespace ts { getCurrentDirectory(): string; } - export interface ParseConfigHost { + export interface ParseConfigHost extends ModuleResolutionHost { readDirectory(rootDir: string, extension: string, exclude: string[]): string[]; } @@ -2197,7 +2197,18 @@ namespace ts { byteOrderMark = 0xFEFF, tab = 0x09, // \t verticalTab = 0x0B, // \v + } + + export interface ModuleResolutionHost { + fileExists(fileName: string): boolean; } + + export interface ResolvedModule { + resolvedFileName: string; + failedLookupLocations: string[]; + } + + export type ModuleNameResolver = (moduleName: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => ResolvedModule; export interface CompilerHost { getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile; @@ -2207,6 +2218,10 @@ namespace ts { getCanonicalFileName(fileName: string): string; useCaseSensitiveFileNames(): boolean; getNewLine(): string; + + // compiler host should implement one of these methods + resolveModuleName?(moduleName: string, containingFile: string): string; + getModuleResolutionHost?(): ModuleResolutionHost; } export interface TextSpan { diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 9f26887ff90..2ac806c78e5 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -866,41 +866,48 @@ module Harness { } }; inputFiles.forEach(register); + + function getSourceFile(fn: string, languageVersion: ts.ScriptTarget) { + fn = ts.normalizePath(fn); + if (Object.prototype.hasOwnProperty.call(filemap, getCanonicalFileName(fn))) { + return filemap[getCanonicalFileName(fn)]; + } + else if (currentDirectory) { + var canonicalAbsolutePath = getCanonicalFileName(ts.getNormalizedAbsolutePath(fn, currentDirectory)); + return Object.prototype.hasOwnProperty.call(filemap, getCanonicalFileName(canonicalAbsolutePath)) ? filemap[canonicalAbsolutePath] : undefined; + } + else if (fn === fourslashFileName) { + var tsFn = 'tests/cases/fourslash/' + fourslashFileName; + fourslashSourceFile = fourslashSourceFile || createSourceFileAndAssertInvariants(tsFn, Harness.IO.readFile(tsFn), scriptTarget); + return fourslashSourceFile; + } + else { + if (fn === defaultLibFileName) { + return languageVersion === ts.ScriptTarget.ES6 ? defaultES6LibSourceFile : defaultLibSourceFile; + } + // Don't throw here -- the compiler might be looking for a test that actually doesn't exist as part of the TC + return undefined; + } + } let newLine = newLineKind === ts.NewLineKind.CarriageReturnLineFeed ? carriageReturnLineFeed : newLineKind === ts.NewLineKind.LineFeed ? lineFeed : ts.sys.newLine; - + + let moduleResolutionHost: ts.ModuleResolutionHost = { + fileExists: fileName => getSourceFile(fileName, ts.ScriptTarget.ES5) !== undefined, + } + return { getCurrentDirectory, - getSourceFile: (fn, languageVersion) => { - fn = ts.normalizePath(fn); - if (Object.prototype.hasOwnProperty.call(filemap, getCanonicalFileName(fn))) { - return filemap[getCanonicalFileName(fn)]; - } - else if (currentDirectory) { - var canonicalAbsolutePath = getCanonicalFileName(ts.getNormalizedAbsolutePath(fn, currentDirectory)); - return Object.prototype.hasOwnProperty.call(filemap, getCanonicalFileName(canonicalAbsolutePath)) ? filemap[canonicalAbsolutePath] : undefined; - } - else if (fn === fourslashFileName) { - var tsFn = 'tests/cases/fourslash/' + fourslashFileName; - fourslashSourceFile = fourslashSourceFile || createSourceFileAndAssertInvariants(tsFn, Harness.IO.readFile(tsFn), scriptTarget); - return fourslashSourceFile; - } - else { - if (fn === defaultLibFileName) { - return languageVersion === ts.ScriptTarget.ES6 ? defaultES6LibSourceFile : defaultLibSourceFile; - } - // Don't throw here -- the compiler might be looking for a test that actually doesn't exist as part of the TC - return undefined; - } - }, + getSourceFile, getDefaultLibFileName: options => defaultLibFileName, writeFile, getCanonicalFileName, useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, - getNewLine: () => newLine + getNewLine: () => newLine, + getModuleResolutionHost: () => moduleResolutionHost }; } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 6cb92df5948..bb149cc71c9 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -228,7 +228,8 @@ module Harness.LanguageService { readDirectory(rootDir: string, extension: string): string { throw new Error("NYI"); - } + } + fileExists(fileName: string) { return this.getScriptInfo(fileName) !== undefined; } log(s: string): void { this.nativeHost.log(s); } trace(s: string): void { this.nativeHost.trace(s); } diff --git a/src/harness/projectsRunner.ts b/src/harness/projectsRunner.ts index 5eb0016ff8d..20866591248 100644 --- a/src/harness/projectsRunner.ts +++ b/src/harness/projectsRunner.ts @@ -128,6 +128,10 @@ class ProjectRunner extends RunnerBase { getSourceFileText: (fileName: string) => string, writeFile: (fileName: string, data: string, writeByteOrderMark: boolean) => void): CompileProjectFilesResult { + let moduleResolutionHost: ts.ModuleResolutionHost = { + fileExists: fileName => getSourceFile(fileName, ts.ScriptTarget.ES5) !== undefined, + } + var program = ts.createProgram(getInputFiles(), createCompilerOptions(), createCompilerHost()); var errors = ts.getPreEmitDiagnostics(program); @@ -190,7 +194,8 @@ class ProjectRunner extends RunnerBase { getCurrentDirectory, getCanonicalFileName: Harness.Compiler.getCanonicalFileName, useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames, - getNewLine: () => ts.sys.newLine + getNewLine: () => ts.sys.newLine, + getModuleResolutionHost: () => moduleResolutionHost }; } } diff --git a/src/services/services.ts b/src/services/services.ts index 8972d0b07d0..a61d02cb3bc 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -122,7 +122,7 @@ namespace ts { referencedFiles: FileReference[]; importedFiles: FileReference[]; isLibFile: boolean - } + } let scanner: Scanner = createScanner(ScriptTarget.Latest, /*skipTrivia*/ true); @@ -967,6 +967,10 @@ namespace ts { trace? (s: string): void; error? (s: string): void; useCaseSensitiveFileNames? (): boolean; + + // LS host should implement one of these methods + resolveModuleName?(moduleName: string, containingFile: string): string; + getModuleResolutionHost?(): ModuleResolutionHost; } // @@ -1783,6 +1787,10 @@ namespace ts { // Output let outputText: string; + + let moduleResolutionHost: ModuleResolutionHost = { + fileExists: fileName => fileName === inputFileName, + } // Create a compilerHost object to allow the compiler to read and write files let compilerHost: CompilerHost = { @@ -1795,7 +1803,8 @@ namespace ts { useCaseSensitiveFileNames: () => false, getCanonicalFileName: fileName => fileName, getCurrentDirectory: () => "", - getNewLine: () => newLine + getNewLine: () => newLine, + getModuleResolutionHost: () => moduleResolutionHost }; let program = createProgram([inputFileName], options, compilerHost); @@ -1989,6 +1998,11 @@ namespace ts { reportStats }; } + + export function resolveModuleName(fileName: string, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModule { + let resolver = getDefaultModuleNameResolver(compilerOptions); + return resolver(moduleName, fileName, compilerOptions, host); + } export function preProcessFile(sourceText: string, readImportFiles = true): PreProcessedFileInfo { let referencedFiles: FileReference[] = []; @@ -2472,9 +2486,9 @@ namespace ts { let oldSettings = program && program.getCompilerOptions(); let newSettings = hostCache.compilationSettings(); let changesInCompilationSettingsAffectSyntax = oldSettings && oldSettings.target !== newSettings.target; - + // Now create a new compiler - let newProgram = createProgram(hostCache.getRootFileNames(), newSettings, { + let compilerHost: CompilerHost = { getSourceFile: getOrCreateSourceFile, getCancellationToken: () => cancellationToken, getCanonicalFileName, @@ -2483,7 +2497,24 @@ namespace ts { getDefaultLibFileName: (options) => host.getDefaultLibFileName(options), writeFile: (fileName, data, writeByteOrderMark) => { }, getCurrentDirectory: () => host.getCurrentDirectory(), - }, program); + }; + + if (host.resolveModuleName) { + compilerHost.resolveModuleName = (moduleName, containingFile) => host.resolveModuleName(moduleName, containingFile) + } + else if (host.getModuleResolutionHost) { + compilerHost.getModuleResolutionHost = () => host.getModuleResolutionHost() + } + else { + compilerHost.getModuleResolutionHost = () => { + // stub missing host functionality + return { + fileExists: fileName => hostCache.getOrCreateEntry(fileName) != undefined, + }; + } + } + + let newProgram = createProgram(hostCache.getRootFileNames(), newSettings, compilerHost, program); // Release any files we have acquired in the old program but are // not part of the new program. diff --git a/src/services/shims.ts b/src/services/shims.ts index 6e765eff499..bca712f1dab 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -57,10 +57,12 @@ namespace ts { getNewLine?(): string; getProjectVersion?(): string; useCaseSensitiveFileNames?(): boolean; + + getModuleResolutionsForFile?(fileName: string): string; } /** Public interface of the the of a config service shim instance.*/ - export interface CoreServicesShimHost extends Logger { + export interface CoreServicesShimHost extends Logger, ModuleResolutionHost { /** Returns a JSON-encoded value of the type: string[] */ readDirectory(rootDir: string, extension: string): string; } @@ -249,8 +251,20 @@ namespace ts { private files: string[]; private loggingEnabled = false; private tracingEnabled = false; + private lastRequestedFile: string; + private lastRequestedModuleResolutions: Map; constructor(private shimHost: LanguageServiceShimHost) { + if ("getModuleResolutionsForFile" in this.shimHost) { + (this).resolveModuleName = (moduleName: string, containingFile: string) => { + if (this.lastRequestedFile !== containingFile) { + this.lastRequestedModuleResolutions = >JSON.parse(this.shimHost.getModuleResolutionsForFile(containingFile)); + this.lastRequestedFile = containingFile; + } + + return this.lastRequestedModuleResolutions[moduleName]; + }; + } } public log(s: string): void { @@ -267,7 +281,7 @@ namespace ts { public error(s: string): void { this.shimHost.error(s); - } + } public getProjectVersion(): string { if (!this.shimHost.getProjectVersion) { @@ -379,6 +393,10 @@ namespace ts { var encoded = this.shimHost.readDirectory(rootDir, extension); return JSON.parse(encoded); } + + public fileExists(fileName: string): boolean { + return this.shimHost.fileExists(fileName); + } } function simpleForwardCall(logger: Logger, actionDescription: string, action: () => any, logPerformance: boolean): any { @@ -505,7 +523,7 @@ namespace ts { private realizeDiagnostics(diagnostics: Diagnostic[]): { message: string; start: number; length: number; category: string; }[]{ var newLine = this.getNewLine(); return ts.realizeDiagnostics(diagnostics, newLine); - } + } public getSyntacticClassifications(fileName: string, start: number, length: number): string { return this.forwardJSONCall( @@ -881,6 +899,13 @@ namespace ts { private forwardJSONCall(actionDescription: string, action: () => any): any { return forwardJSONCall(this.logger, actionDescription, action, this.logPerformance); } + + public resolveModuleName(fileName: string, moduleName: string, compilerOptionsJson: string): string { + return this.forwardJSONCall(`resolveModuleName('${fileName}')`, () => { + let compilerOptions = JSON.parse(compilerOptionsJson); + return resolveModuleName(fileName, moduleName, compilerOptions, this.host); + }); + } public getPreProcessedFileInfo(fileName: string, sourceTextSnapshot: IScriptSnapshot): string { return this.forwardJSONCall( diff --git a/tests/cases/unittests/reuseProgramStructure.ts b/tests/cases/unittests/reuseProgramStructure.ts index 1cb389f7717..5314ff48f55 100644 --- a/tests/cases/unittests/reuseProgramStructure.ts +++ b/tests/cases/unittests/reuseProgramStructure.ts @@ -103,6 +103,11 @@ module ts { file.sourceText = t.text; files[t.name] = file; } + + let moduleResolutionHost: ModuleResolutionHost = { + fileExists: fileName => hasProperty(files, fileName) + } + return { getSourceFile(fileName): SourceFile { return files[fileName]; @@ -125,11 +130,7 @@ module ts { getNewLine(): string { return sys.newLine; }, - hasChanges(oldFile: SourceFileWithText): boolean { - let current = files[oldFile.fileName]; - return !current || oldFile.sourceText.getVersion() !== current.sourceText.getVersion(); - } - + getModuleResolutionHost: () => moduleResolutionHost } }