From aaaa9c08954d683edaf4c47c20ca47de765af317 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Fri, 1 Apr 2016 12:41:01 -0700 Subject: [PATCH] expose type reference resolution for external consumption --- src/compiler/commandLineParser.ts | 8 +- src/compiler/diagnosticMessages.json | 34 +- src/compiler/parser.ts | 8 +- src/compiler/program.ts | 535 ++++++++++++++++---------- src/compiler/types.ts | 24 +- src/compiler/utilities.ts | 45 ++- src/harness/compilerRunner.ts | 2 +- src/harness/fourslash.ts | 10 +- src/harness/harness.ts | 7 +- src/harness/harnessLanguageService.ts | 46 ++- src/server/editorServices.ts | 70 +++- src/services/services.ts | 65 +++- src/services/shims.ts | 70 +++- 13 files changed, 639 insertions(+), 285 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 72fdb2167c5..058c6ac44fe 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -312,19 +312,19 @@ namespace ts { } }, { - name: "librarySearchPaths", + name: "typesSearchPaths", type: "list", isTSConfigOnly: true, element: { - name: "librarySearchPaths", + name: "typesSearchPaths", type: "string", isFilePath: true } }, { - name: "traceModuleResolution", + name: "traceResolution", type: "boolean", - description: Diagnostics.Enable_tracing_of_the_module_resolution_process + description: Diagnostics.Enable_tracing_of_the_name_resolution_process }, { name: "allowJs", diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index e7c05aa0629..26f213cca25 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2517,7 +2517,7 @@ "category": "Message", "code": 6084 }, - "Enable tracing of the module resolution process.": { + "Enable tracing of the name resolution process.": { "category": "Message", "code": 6085 }, @@ -2565,7 +2565,7 @@ "category": "Message", "code": 6096 }, - "File '{0}' exist - use it as a module resolution result.": { + "File '{0}' exist - use it as a name resolution result.": { "category": "Message", "code": 6097 }, @@ -2577,11 +2577,11 @@ "category": "Message", "code": 6099 }, - "'package.json' does not have 'typings' field.": { + "'package.json' does not have 'types' field.": { "category": "Message", "code": 6100 }, - "'package.json' has 'typings' field '{0}' that references '{1}'.": { + "'package.json' has '{0}' field '{1}' that references '{2}'.": { "category": "Message", "code": 6101 }, @@ -2597,7 +2597,7 @@ "category": "Message", "code": 6104 }, - "Expected type of 'typings' field in 'package.json' to be 'string', got '{0}'.": { + "Expected type of '{0}' field in 'package.json' to be 'string', got '{1}'.": { "category": "Message", "code": 6105 }, @@ -2637,6 +2637,30 @@ "category": "Error", "code": 6114 }, + "======== Resolving type reference directive '{0}' from '{1}' with compilation root dir '{2}'. ========": { + "category": "Message", + "code": 6115 + }, + "Resolving using primary search paths...": { + "category": "Message", + "code": 6116 + }, + "Resolving from node_modules folder...": { + "category": "Message", + "code": 6117 + }, + "======== Type reference directive '{0}' was successfully resolved to '{1}', primary: {2}. ========": { + "category": "Message", + "code": 6118 + }, + "======== Type reference directive '{0}' was not resolved. ========": { + "category": "Message", + "code": 6119 + }, + "Resolving with primary search path '{0}'": { + "category": "Message", + "code": 6120 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index fae1c64ed51..3cf88921034 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -5505,7 +5505,7 @@ namespace ts { function processReferenceComments(sourceFile: SourceFile): void { const triviaScanner = createScanner(sourceFile.languageVersion, /*skipTrivia*/false, LanguageVariant.Standard, sourceText); const referencedFiles: FileReference[] = []; - const referencedLibraries: FileReference[] = []; + const typeReferenceDirectives: FileReference[] = []; const amdDependencies: { path: string; name: string }[] = []; let amdModuleName: string; @@ -5532,8 +5532,8 @@ namespace ts { sourceFile.hasNoDefaultLib = referencePathMatchResult.isNoDefaultLib; const diagnosticMessage = referencePathMatchResult.diagnosticMessage; if (fileReference) { - if (referencePathMatchResult.isLibraryReference) { - referencedLibraries.push(fileReference); + if (referencePathMatchResult.isTypeReferenceDirective) { + typeReferenceDirectives.push(fileReference); } else { referencedFiles.push(fileReference); @@ -5569,7 +5569,7 @@ namespace ts { } sourceFile.referencedFiles = referencedFiles; - sourceFile.referencedLibraries = referencedLibraries; + sourceFile.typeReferenceDirectives = typeReferenceDirectives; sourceFile.amdDependencies = amdDependencies; sourceFile.moduleName = amdModuleName; } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 8eb6abf7cd5..09b42bb9f54 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -13,9 +13,9 @@ namespace ts { const emptyArray: any[] = []; const defaultLibrarySearchPaths = [ - "typings/", + "types/", "node_modules/", - "node_modules/@typings/", + "node_modules/@types/", ]; export const version = "1.9.0"; @@ -41,13 +41,64 @@ namespace ts { return normalizePath(referencedFileName); } + export function computeCompilationRoot(rootFileNames: string[], currentDirectory: string, options: CompilerOptions, getCanonicalFileName: (fileName: string) => string): string { + const rootDirOrConfigFilePath = options.rootDir || (options.configFilePath && getDirectoryPath(options.configFilePath)); + return rootDirOrConfigFilePath + ? getNormalizedAbsolutePath(rootDirOrConfigFilePath, currentDirectory) + : computeCommonSourceDirectoryOfFilenames(rootFileNames, currentDirectory, getCanonicalFileName); + } + + function computeCommonSourceDirectoryOfFilenames(fileNames: string[], currentDirectory: string, getCanonicalFileName: (fileName: string) => string): string { + let commonPathComponents: string[]; + const failed = forEach(fileNames, sourceFile => { + // Each file contributes into common source file path + const sourcePathComponents = getNormalizedPathComponents(sourceFile, currentDirectory); + sourcePathComponents.pop(); // The base file name is not part of the common directory path + + if (!commonPathComponents) { + // first file + commonPathComponents = sourcePathComponents; + return; + } + + for (let i = 0, n = Math.min(commonPathComponents.length, sourcePathComponents.length); i < n; i++) { + if (getCanonicalFileName(commonPathComponents[i]) !== getCanonicalFileName(sourcePathComponents[i])) { + if (i === 0) { + // Failed to find any common path component + return true; + } + + // New common path found that is 0 -> i-1 + commonPathComponents.length = i; + break; + } + } + + // If the sourcePathComponents was shorter than the commonPathComponents, truncate to the sourcePathComponents + if (sourcePathComponents.length < commonPathComponents.length) { + commonPathComponents.length = sourcePathComponents.length; + } + }); + + // A common path can not be found when paths span multiple drives on windows, for example + if (failed) { + return ""; + } + + if (!commonPathComponents) { // Can happen when all input files are .d.ts files + return currentDirectory; + } + + return getNormalizedPathFromPathComponents(commonPathComponents); + } + function trace(host: ModuleResolutionHost, message: DiagnosticMessage, ...args: any[]): void; function trace(host: ModuleResolutionHost, message: DiagnosticMessage): void { host.trace(formatMessage.apply(undefined, arguments)); } function isTraceEnabled(compilerOptions: CompilerOptions, host: ModuleResolutionHost): boolean { - return compilerOptions.traceModuleResolution && host.trace !== undefined; + return compilerOptions.traceResolution && host.trace !== undefined; } function startsWith(str: string, prefix: string): boolean { @@ -97,6 +148,155 @@ namespace ts { skipTsx: boolean; } + function tryReadTypesSection(packageJsonPath: string, baseDirectory: string, state: ModuleResolutionState): string { + let jsonContent: { typings?: string, types?: string }; + try { + const jsonText = state.host.readFile(packageJsonPath); + jsonContent = jsonText ? <{ typings?: string, types?: string }>JSON.parse(jsonText) : {}; + } + catch (e) { + // gracefully handle if readFile fails or returns not JSON + jsonContent = {}; + } + + let typesFile: string; + let fieldName: string; + // first try to read content of 'typings' section (backward compatibility) + if (jsonContent.typings) { + if (typeof jsonContent.typings === "string") { + fieldName = "typings"; + typesFile = jsonContent.typings; + } + else { + if (state.traceEnabled) { + trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_string_got_1, "typings", typeof jsonContent.typings); + } + } + } + // then read 'types' + if (!typesFile && jsonContent.types) { + if (typeof jsonContent.types === "string") { + fieldName = "types"; + typesFile = jsonContent.types; + } + else { + if (state.traceEnabled) { + trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_string_got_1, "types", typeof jsonContent.types); + } + } + } + if (typesFile) { + const typesFilePath = normalizePath(combinePaths(baseDirectory, typesFile)); + if (state.traceEnabled) { + trace(state.host, Diagnostics.package_json_has_0_field_1_that_references_2, fieldName, typesFile, typesFilePath); + } + return typesFilePath; + } + return undefined; + } + + function tryLoadTypeDeclarationFile(searchPath: string, failedLookupLocations: string[], state: ModuleResolutionState) { + let typesFile: string; + const packageJsonPath = combinePaths(searchPath, "package.json"); + if (state.host.fileExists(packageJsonPath)) { + if (state.traceEnabled) { + trace(state.host, Diagnostics.Found_package_json_at_0, packageJsonPath); + } + typesFile = tryReadTypesSection(packageJsonPath, searchPath, state); + if (!typesFile) { + if (state.traceEnabled) { + trace(state.host, Diagnostics.package_json_does_not_have_types_field); + } + } + } + else { + if (state.traceEnabled) { + trace(state.host, Diagnostics.File_0_does_not_exist, packageJsonPath); + } + failedLookupLocations.push(packageJsonPath); + } + + if (!typesFile) { + typesFile = "index.d.ts"; + } + + const combinedPath = normalizePath(combinePaths(searchPath, typesFile)); + if (state.host.fileExists(combinedPath)) { + if (state.traceEnabled) { + trace(state.host, Diagnostics.File_0_exist_use_it_as_a_name_resolution_result, combinedPath); + } + return combinedPath; + } + else { + if (state.traceEnabled) { + trace(state.host, Diagnostics.File_0_does_not_exist, combinedPath); + } + failedLookupLocations.push(combinedPath); + return undefined; + } + } + + function getEffectiveTypesPrimarySearchPaths(options: CompilerOptions): string[] { + if (options.typesSearchPaths) { + return options.typesSearchPaths; + } + return options.configFilePath + ? [getDirectoryPath(options.configFilePath)].concat(defaultLibrarySearchPaths) + : defaultLibrarySearchPaths; + } + + export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string, containingFile: string, compilationRoot: string, options: CompilerOptions, host: ModuleResolutionHost): ResolvedTypeReferenceDirectiveWithFailedLookupLocations { + const traceEnabled = isTraceEnabled(options, host); + const moduleResolutionState: ModuleResolutionState = { + compilerOptions: options, + host: host, + skipTsx: true, + traceEnabled + }; + + if (traceEnabled) { + trace(host, Diagnostics.Resolving_type_reference_directive_0_from_1_with_compilation_root_dir_2, typeReferenceDirectiveName, containingFile, compilationRoot); + } + const failedLookupLocations: string[] = []; + // Check primary library paths + for (const searchPath of getEffectiveTypesPrimarySearchPaths(options)) { + const primaryPath = combinePaths(compilationRoot, searchPath); + if (traceEnabled) { + trace(host, Diagnostics.Resolving_with_primary_search_path_0, primaryPath); + } + const resolvedFile = tryLoadTypeDeclarationFile(combinePaths(primaryPath, typeReferenceDirectiveName), failedLookupLocations, moduleResolutionState); + if (resolvedFile) { + if (traceEnabled) { + trace(host, Diagnostics.Type_reference_directive_0_was_successfully_resolved_to_1_primary_Colon_2, typeReferenceDirectiveName, resolvedFile, true); + } + return { + resolvedTypeReferenceDirective: { primary: true, resolvedFileName: resolvedFile }, + failedLookupLocations + }; + } + } + + if (traceEnabled) { + trace(host, Diagnostics.Resolving_from_node_modules_folder); + } + // check secondary locations + const resolvedFile = loadModuleFromNodeModules(typeReferenceDirectiveName, getDirectoryPath(containingFile), failedLookupLocations, moduleResolutionState); + if (traceEnabled) { + if (resolvedFile) { + trace(host, Diagnostics.Type_reference_directive_0_was_successfully_resolved_to_1_primary_Colon_2, typeReferenceDirectiveName, resolvedFile, false); + } + else { + trace(host, Diagnostics.Type_reference_directive_0_was_not_resolved, typeReferenceDirectiveName); + } + } + return { + resolvedTypeReferenceDirective: resolvedFile + ? { primary: false, resolvedFileName: resolvedFile } + : undefined, + failedLookupLocations + }; + } + export function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations { const traceEnabled = isTraceEnabled(compilerOptions, host); if (traceEnabled) { @@ -422,6 +622,13 @@ namespace ts { * in cases when we know upfront that all load attempts will fail (because containing folder does not exists) however we still need to record all failed lookup locations. */ function loadModuleFromFile(candidate: string, extensions: string[], failedLookupLocation: string[], onlyRecordFailures: boolean, state: ModuleResolutionState): string { + if (!onlyRecordFailures) { + // check if containig folder exists - if it doesn't then just record failures for all supported extensions without disk probing + const directory = getDirectoryPath(candidate); + if (directory) { + onlyRecordFailures = !directoryProbablyExists(directory, state.host); + } + } return forEach(extensions, tryLoad); function tryLoad(ext: string): string { @@ -431,7 +638,7 @@ namespace ts { const fileName = fileExtensionIs(candidate, ext) ? candidate : candidate + ext; if (!onlyRecordFailures && state.host.fileExists(fileName)) { if (state.traceEnabled) { - trace(state.host, Diagnostics.File_0_exist_use_it_as_a_module_resolution_result, fileName); + trace(state.host, Diagnostics.File_0_exist_use_it_as_a_name_resolution_result, fileName); } return fileName; } @@ -452,36 +659,16 @@ namespace ts { if (state.traceEnabled) { trace(state.host, Diagnostics.Found_package_json_at_0, packageJsonPath); } - - let jsonContent: { typings?: string }; - - try { - const jsonText = state.host.readFile(packageJsonPath); - jsonContent = jsonText ? <{ typings?: string }>JSON.parse(jsonText) : { typings: undefined }; - } - catch (e) { - // gracefully handle if readFile fails or returns not JSON - jsonContent = { typings: undefined }; - } - - if (jsonContent.typings) { - if (typeof jsonContent.typings === "string") { - const typingsFile = normalizePath(combinePaths(candidate, jsonContent.typings)); - if (state.traceEnabled) { - trace(state.host, Diagnostics.package_json_has_typings_field_0_that_references_1, jsonContent.typings, typingsFile); - } - const result = loadModuleFromFile(typingsFile, extensions, failedLookupLocation, !directoryProbablyExists(getDirectoryPath(typingsFile), state.host), state); - if (result) { - return result; - } - } - else if (state.traceEnabled) { - trace(state.host, Diagnostics.Expected_type_of_typings_field_in_package_json_to_be_string_got_0, typeof jsonContent.typings); + const typesFile = tryReadTypesSection(packageJsonPath, candidate, state); + if (typesFile) { + const result = loadModuleFromFile(typesFile, extensions, failedLookupLocation, !directoryProbablyExists(getDirectoryPath(typesFile), state.host), state); + if (result) { + return result; } } else { if (state.traceEnabled) { - trace(state.host, Diagnostics.package_json_does_not_have_typings_field); + trace(state.host, Diagnostics.package_json_does_not_have_types_field); } } } @@ -496,20 +683,31 @@ namespace ts { return loadModuleFromFile(combinePaths(candidate, "index"), extensions, failedLookupLocation, !directoryExists, state); } + function loadModuleFromNodeModulesFolder(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState): string { + const nodeModulesFolder = combinePaths(directory, "node_modules"); + const nodeModulesFolderExists = directoryProbablyExists(nodeModulesFolder, state.host); + const candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName)); + // Load only typescript files irrespective of allowJs option if loading from node modules + let result = loadModuleFromFile(candidate, supportedTypeScriptExtensions, failedLookupLocations, !nodeModulesFolderExists, state); + if (result) { + return result; + } + result = loadNodeModuleFromDirectory(supportedTypeScriptExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state); + if (result) { + return result; + } + } + function loadModuleFromNodeModules(moduleName: string, directory: string, failedLookupLocations: string[], state: ModuleResolutionState): string { directory = normalizeSlashes(directory); while (true) { const baseName = getBaseFileName(directory); if (baseName !== "node_modules") { - const nodeModulesFolder = combinePaths(directory, "node_modules"); - const nodeModulesFolderExists = directoryProbablyExists(nodeModulesFolder, state.host); - const candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName)); - // Load only typescript files irrespective of allowJs option if loading from node modules - let result = loadModuleFromFile(candidate, supportedTypeScriptExtensions, failedLookupLocations, !nodeModulesFolderExists, state); - if (result) { - return result; - } - result = loadNodeModuleFromDirectory(supportedTypeScriptExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state); + const result = + // first: try to load module as-is + loadModuleFromNodeModulesFolder(moduleName, directory, failedLookupLocations, state) || + // second: try to load module from the scope '@types' + loadModuleFromNodeModulesFolder(combinePaths("@types", moduleName), directory, failedLookupLocations, state); if (result) { return result; } @@ -696,54 +894,67 @@ namespace ts { } } + function loadWithLocalCache(names: string[], containingFile: string, loader: (name: string, containingFile: string) => T): T[] { + if (names.length === 0) { + return []; + } + const resolutions: T[] = []; + const cache: Map = {}; + for (const name of names) { + let result: T; + if (hasProperty(cache, name)) { + result = cache[name]; + } + else { + result = loader(name, containingFile); + cache[name] = result; + } + resolutions.push(result); + } + return resolutions; + } + export function createProgram(rootNames: string[], options: CompilerOptions, host?: CompilerHost, oldProgram?: Program): Program { let program: Program; let files: SourceFile[] = []; - let fileProcessingDiagnostics = createDiagnosticCollection(); - const currentDirectory = host.getCurrentDirectory(); - const resolvedLibraries: Map = {}; - let libraryRoot = - (options.rootDir && ts.toPath(options.rootDir, currentDirectory, host.getCanonicalFileName)) || - (options.configFilePath && getDirectoryPath(getNormalizedAbsolutePath(options.configFilePath, currentDirectory))); - if (libraryRoot === undefined) { - libraryRoot = computeCommonSourceDirectoryOfFilenames(rootNames); - } - const programDiagnostics = createDiagnosticCollection(); - let commonSourceDirectory: string; let diagnosticsProducingTypeChecker: TypeChecker; let noDiagnosticsTypeChecker: TypeChecker; let classifiableNames: Map; + let resolvedTypeReferenceDirectives: Map = {}; + let fileProcessingDiagnostics = createDiagnosticCollection(); let skipDefaultLib = options.noLib; + const programDiagnostics = createDiagnosticCollection(); + const currentDirectory = host.getCurrentDirectory(); const supportedExtensions = getSupportedExtensions(options); const start = new Date().getTime(); host = host || createCompilerHost(options); + + const compilationRoot = computeCompilationRoot(rootNames, currentDirectory, options, getCanonicalFileName); + // Map storing if there is emit blocking diagnostics for given input const hasEmitBlockingDiagnostics = createFileMap(getCanonicalFileName); - const resolveModuleNamesWorker = host.resolveModuleNames - ? ((moduleNames: string[], containingFile: string) => host.resolveModuleNames(moduleNames, containingFile)) - : ((moduleNames: string[], containingFile: string) => { - const resolvedModuleNames: ResolvedModule[] = []; - // resolveModuleName does not store any results between calls. - // lookup is a local cache to avoid resolving the same module name several times - const lookup: Map = {}; - for (const moduleName of moduleNames) { - let resolvedName: ResolvedModule; - if (hasProperty(lookup, moduleName)) { - resolvedName = lookup[moduleName]; - } - else { - resolvedName = resolveModuleName(moduleName, containingFile, options, host).resolvedModule; - lookup[moduleName] = resolvedName; - } - resolvedModuleNames.push(resolvedName); - } - return resolvedModuleNames; - }); + let resolveModuleNamesWorker: (moduleNames: string[], containingFile: string) => ResolvedModule[]; + if (host.resolveModuleNames) { + resolveModuleNamesWorker = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile); + } + else { + const loader = (moduleName: string, containingFile: string) => resolveModuleName(moduleName, containingFile, options, host).resolvedModule; + resolveModuleNamesWorker = (moduleNames, containingFile) => loadWithLocalCache(moduleNames, containingFile, loader); + } + + let resolveTypeReferenceDirectiveNamesWorker: (typeDirectiveNames: string[], containingFile: string) => ResolvedTypeReferenceDirective[]; + if (host.resolveTypeReferenceDirectives) { + resolveTypeReferenceDirectiveNamesWorker = (typeDirectiveNames, containingFile) => host.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile); + } + else { + const loader = (typesRef: string, containingFile: string) => resolveTypeReferenceDirective(typesRef, containingFile, compilationRoot, options, host).resolvedTypeReferenceDirective; + resolveTypeReferenceDirectiveNamesWorker = (typeReferenceDirectiveNames, containingFile) => loadWithLocalCache(typeReferenceDirectiveNames, containingFile, loader); + } const filesByName = createFileMap(); // stores 'filename -> file association' ignoring case @@ -759,7 +970,10 @@ namespace ts { (oldOptions.target !== options.target) || (oldOptions.noLib !== options.noLib) || (oldOptions.jsx !== options.jsx) || - (oldOptions.allowJs !== options.allowJs)) { + (oldOptions.allowJs !== options.allowJs) || + (oldOptions.rootDir !== options.rootDir) || + (oldOptions.typesSearchPaths !== options.typesSearchPaths) || + (oldOptions.configFilePath !== options.configFilePath)) { oldProgram = undefined; } } @@ -808,7 +1022,8 @@ namespace ts { getIdentifierCount: () => getDiagnosticsProducingTypeChecker().getIdentifierCount(), getSymbolCount: () => getDiagnosticsProducingTypeChecker().getSymbolCount(), getTypeCount: () => getDiagnosticsProducingTypeChecker().getTypeCount(), - getFileProcessingDiagnostics: () => fileProcessingDiagnostics + getFileProcessingDiagnostics: () => fileProcessingDiagnostics, + resolvedTypeReferenceDirectives }; verifyCompilerOptions(); @@ -901,26 +1116,33 @@ namespace ts { return false; } + if (!arrayIsEqualTo(oldSourceFile.typeReferenceDirectives, newSourceFile.typeReferenceDirectives, fileReferenceIsEqualTo)) { + // 'types' references has changed + return false; + } + + const newSourceFilePath = getNormalizedAbsolutePath(newSourceFile.fileName, currentDirectory); if (resolveModuleNamesWorker) { const moduleNames = map(concatenate(newSourceFile.imports, newSourceFile.moduleAugmentations), getTextOfLiteral); - const resolutions = resolveModuleNamesWorker(moduleNames, getNormalizedAbsolutePath(newSourceFile.fileName, currentDirectory)); + const resolutions = resolveModuleNamesWorker(moduleNames, newSourceFilePath); // ensure that module resolution results are still correct - for (let i = 0; i < moduleNames.length; i++) { - const newResolution = resolutions[i]; - const oldResolution = getResolvedModule(oldSourceFile, moduleNames[i]); - const resolutionChanged = oldResolution - ? !newResolution || - oldResolution.resolvedFileName !== newResolution.resolvedFileName || - !!oldResolution.isExternalLibraryImport !== !!newResolution.isExternalLibraryImport - : newResolution; - - if (resolutionChanged) { - return false; - } + const resolutionsChanged = hasChangesInResolutions(moduleNames, resolutions, oldSourceFile.resolvedModules, moduleResolutionIsEqualTo); + if (resolutionsChanged) { + return false; } } - // pass the cache of module resolutions from the old source file + if (resolveTypeReferenceDirectiveNamesWorker) { + const typesReferenceDirectives = map(newSourceFile.typeReferenceDirectives, x => x.fileName); + const resolutions = resolveTypeReferenceDirectiveNamesWorker(typesReferenceDirectives, newSourceFilePath); + // ensure that types resolutions are still correct + const resolutionsChanged = hasChangesInResolutions(typesReferenceDirectives, resolutions, oldSourceFile.resolvedTypeReferenceDirectiveNames, typeDirectiveIsEqualTo); + if (resolutionsChanged) { + return false; + } + } + // pass the cache of module/types resolutions from the old source file newSourceFile.resolvedModules = oldSourceFile.resolvedModules; + newSourceFile.resolvedTypeReferenceDirectiveNames = oldSourceFile.resolvedTypeReferenceDirectiveNames; modifiedSourceFiles.push(newSourceFile); } else { @@ -943,6 +1165,7 @@ namespace ts { for (const modifiedFile of modifiedSourceFiles) { fileProcessingDiagnostics.reattachFileDiagnostics(modifiedFile); } + resolvedTypeReferenceDirectives = oldProgram.resolvedTypeReferenceDirectives; oldProgram.structureIsReused = true; return true; @@ -1510,8 +1733,8 @@ namespace ts { const basePath = getDirectoryPath(fileName); if (!options.noResolve) { - processReferencedFiles(file, basePath, /*isDefaultLib*/ isDefaultLib); - processReferencedLibraries(file, libraryRoot); + processReferencedFiles(file, basePath, isDefaultLib); + processTypeReferenceDirectives(file, compilationRoot); } // always process imported modules to record module name resolutions @@ -1535,97 +1758,57 @@ namespace ts { }); } - function findLibraryDefinition(searchPath: string) { - let typingFilename = "index.d.ts"; - const packageJsonPath = combinePaths(searchPath, "package.json"); - if (host.fileExists(packageJsonPath)) { - let package: { typings?: string } = {}; - try { - package = JSON.parse(host.readFile(packageJsonPath)); - } - catch (e) { } + function processTypeReferenceDirectives(file: SourceFile, compilationRoot: string) { + const typeDirectives = map(file.typeReferenceDirectives, l => l.fileName); + const resolutions = resolveTypeReferenceDirectiveNamesWorker(typeDirectives, file.fileName); - if (package.typings) { - typingFilename = package.typings; - } - } - - const combinedPath = normalizePath(combinePaths(searchPath, typingFilename)); - return host.fileExists(combinedPath) ? combinedPath : undefined; - } - - function processReferencedLibraries(file: SourceFile, compilationRoot: string) { - const primarySearchPaths = map(getEffectiveLibraryPrimarySearchPaths(), path => combinePaths(compilationRoot, path)); - - const failedSearchPaths: string[] = []; - const moduleResolutionState: ModuleResolutionState = { - compilerOptions: options, - host: host, - skipTsx: true, - traceEnabled: false - }; - - for (const ref of file.referencedLibraries) { + for (let i = 0; i < typeDirectives.length; i++) { + const ref = file.typeReferenceDirectives[i]; + const resolvedTypeReferenceDirective = resolutions[i]; + // store resolved type directive on the file + setResolvedTypeReferenceDirective(file, ref.fileName, resolvedTypeReferenceDirective); // If we already found this library as a primary reference, or failed to find it, nothing to do - const previousResolution = resolvedLibraries[ref.fileName]; + const previousResolution = resolvedTypeReferenceDirectives[ref.fileName]; if (previousResolution && (previousResolution.primary || (previousResolution.resolvedFileName === undefined))) { continue; } - - let foundIt = false; - - // Check primary library paths - for (const primaryPath of primarySearchPaths) { - const searchPath = combinePaths(primaryPath, ref.fileName); - const resolvedFile = findLibraryDefinition(searchPath); - if (resolvedFile) { - resolvedLibraries[ref.fileName] = { primary: true, resolvedFileName: resolvedFile }; - processSourceFile(resolvedFile, /*isDefaultLib*/ false, /*isReference*/ true, file, ref.pos, ref.end); - foundIt = true; - break; + let saveResolution = true; + if (resolvedTypeReferenceDirective) { + if (resolvedTypeReferenceDirective.primary) { + // resolved from the primary path + processSourceFile(resolvedTypeReferenceDirective.resolvedFileName, /*isDefaultLib*/ false, /*isReference*/ true, file, ref.pos, ref.end); } - } - - // Check secondary library paths - if (!foundIt) { - const secondaryResult = loadModuleFromNodeModules(ref.fileName, file.fileName, failedSearchPaths, moduleResolutionState); - if (secondaryResult) { - foundIt = true; + else { // If we already resolved to this file, it must have been a secondary reference. Check file contents // for sameness and possibly issue an error if (previousResolution) { - const otherFileText = host.readFile(secondaryResult); + const otherFileText = host.readFile(resolvedTypeReferenceDirective.resolvedFileName); if (otherFileText !== getSourceFile(previousResolution.resolvedFileName).text) { fileProcessingDiagnostics.add(createFileDiagnostic(file, ref.pos, ref.end - ref.pos, Diagnostics.Conflicting_library_definitions_for_0_found_at_1_and_2_Copy_the_correct_file_to_the_typings_folder_to_resolve_this_conflict, ref.fileName, - secondaryResult, + resolvedTypeReferenceDirective.resolvedFileName, previousResolution.resolvedFileName)); } + // don't overwrite previous resolution result + saveResolution = false; } else { // First resolution of this library - resolvedLibraries[ref.fileName] = { primary: false, resolvedFileName: secondaryResult }; - processSourceFile(secondaryResult, /*isDefaultLib*/ false, /*isReference*/ true, file, ref.pos, ref.end); + processSourceFile(resolvedTypeReferenceDirective.resolvedFileName, /*isDefaultLib*/ false, /*isReference*/ true, file, ref.pos, ref.end); } } } - - if (!foundIt) { + else { fileProcessingDiagnostics.add(createFileDiagnostic(file, ref.pos, ref.end - ref.pos, Diagnostics.Cannot_find_name_0, ref.fileName)); - // Create an entry as a primary lookup result so we don't keep doing this - resolvedLibraries[ref.fileName] = { primary: true, resolvedFileName: undefined }; + } + + if (saveResolution) { + resolvedTypeReferenceDirectives[ref.fileName] = resolvedTypeReferenceDirective; } } } - function getEffectiveLibraryPrimarySearchPaths(): Path[] { - return (options.librarySearchPaths || - (options.configFilePath ? - [options.configFilePath].concat(defaultLibrarySearchPaths) : - defaultLibrarySearchPaths)); - } - function getCanonicalFileName(fileName: string): string { return host.getCanonicalFileName(fileName); } @@ -1672,50 +1855,6 @@ namespace ts { return; } - function computeCommonSourceDirectoryOfFilenames(fileNames: string[]): string { - let commonPathComponents: string[]; - const failed = forEach(fileNames, sourceFile => { - // Each file contributes into common source file path - const sourcePathComponents = getNormalizedPathComponents(sourceFile, currentDirectory); - sourcePathComponents.pop(); // The base file name is not part of the common directory path - - if (!commonPathComponents) { - // first file - commonPathComponents = sourcePathComponents; - return; - } - - for (let i = 0, n = Math.min(commonPathComponents.length, sourcePathComponents.length); i < n; i++) { - if (getCanonicalFileName(commonPathComponents[i]) !== getCanonicalFileName(sourcePathComponents[i])) { - if (i === 0) { - // Failed to find any common path component - return true; - } - - // New common path found that is 0 -> i-1 - commonPathComponents.length = i; - break; - } - } - - // If the sourcePathComponents was shorter than the commonPathComponents, truncate to the sourcePathComponents - if (sourcePathComponents.length < commonPathComponents.length) { - commonPathComponents.length = sourcePathComponents.length; - } - }); - - // A common path can not be found when paths span multiple drives on windows, for example - if (failed) { - return ""; - } - - if (!commonPathComponents) { // Can happen when all input files are .d.ts files - return currentDirectory; - } - - return getNormalizedPathFromPathComponents(commonPathComponents); - } - function computeCommonSourceDirectory(sourceFiles: SourceFile[]): string { const fileNames: string[] = []; for (const file of sourceFiles) { @@ -1723,7 +1862,7 @@ namespace ts { fileNames.push(file.fileName); } } - return computeCommonSourceDirectoryOfFilenames(fileNames); + return computeCommonSourceDirectoryOfFilenames(fileNames, currentDirectory, getCanonicalFileName); } function checkSourceFilesBelongToPath(sourceFiles: SourceFile[], rootDirectory: string): boolean { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 973dfdd1207..0122ac54c81 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1536,7 +1536,7 @@ namespace ts { amdDependencies: AmdDependency[]; moduleName: string; referencedFiles: FileReference[]; - referencedLibraries: FileReference[]; + typeReferenceDirectives: FileReference[]; languageVariant: LanguageVariant; isDeclarationFile: boolean; @@ -1584,6 +1584,7 @@ namespace ts { // It is used to resolve module names in the checker. // Content of this field should never be used directly - use getResolvedModuleFileName/setResolvedModuleFileName functions instead /* @internal */ resolvedModules: Map; + /* @internal */ resolvedTypeReferenceDirectiveNames: Map; /* @internal */ imports: LiteralExpression[]; /* @internal */ moduleAugmentations: LiteralExpression[]; } @@ -1660,6 +1661,7 @@ namespace ts { /* @internal */ getTypeCount(): number; /* @internal */ getFileProcessingDiagnostics(): DiagnosticCollection; + /* @internal */ resolvedTypeReferenceDirectives: Map; // For testing purposes only. /* @internal */ structureIsReused?: boolean; } @@ -2416,7 +2418,7 @@ namespace ts { jsx?: JsxEmit; reactNamespace?: string; listFiles?: boolean; - librarySearchPaths?: string[]; + typesSearchPaths?: string[]; locale?: string; mapRoot?: string; module?: ModuleKind; @@ -2455,7 +2457,7 @@ namespace ts { baseUrl?: string; paths?: PathSubstitutions; rootDirs?: RootPaths; - traceModuleResolution?: boolean; + traceResolution?: boolean; allowSyntheticDefaultImports?: boolean; allowJs?: boolean; noImplicitUseStrict?: boolean; @@ -2760,6 +2762,18 @@ namespace ts { failedLookupLocations: string[]; } + export interface ResolvedTypeReferenceDirective { + // True if the type declaration file was found in a primary lookup location + primary: boolean; + // The location of the .d.ts file we located, or undefined if resolution failed + resolvedFileName?: string; + } + + export interface ResolvedTypeReferenceDirectiveWithFailedLookupLocations { + resolvedTypeReferenceDirective: ResolvedTypeReferenceDirective; + failedLookupLocations: string[]; + } + export interface CompilerHost extends ModuleResolutionHost { getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile; getCancellationToken?(): CancellationToken; @@ -2779,6 +2793,10 @@ namespace ts { * 'throw new Error("NotImplemented")' */ resolveModuleNames?(moduleNames: string[], containingFile: string): ResolvedModule[]; + /** + * This method is a companion for 'resolveModuleNames' and is used to resolve 'types' references to actual type declaration files + */ + resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; } export interface TextSpan { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index f5f00106d18..04a747d5f61 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -6,7 +6,7 @@ namespace ts { fileReference?: FileReference; diagnosticMessage?: DiagnosticMessage; isNoDefaultLib?: boolean; - isLibraryReference?: boolean; + isTypeReferenceDirective?: boolean; } export interface SynthesizedNode extends Node { @@ -119,6 +119,43 @@ namespace ts { sourceFile.resolvedModules[moduleNameText] = resolvedModule; } + export function setResolvedTypeReferenceDirective(sourceFile: SourceFile, typeReferenceDirectiveName: string, resolvedTypeReferenceDirective: ResolvedTypeReferenceDirective): void { + if (!sourceFile.resolvedTypeReferenceDirectiveNames) { + sourceFile.resolvedTypeReferenceDirectiveNames = {}; + } + + sourceFile.resolvedTypeReferenceDirectiveNames[typeReferenceDirectiveName] = resolvedTypeReferenceDirective; + } + + /* @internal */ + export function moduleResolutionIsEqualTo(oldResolution: ResolvedModule, newResolution: ResolvedModule): boolean { + return oldResolution.resolvedFileName === newResolution.resolvedFileName && oldResolution.isExternalLibraryImport === newResolution.isExternalLibraryImport; + } + + /* @internal */ + export function typeDirectiveIsEqualTo(oldResolution: ResolvedTypeReferenceDirective, newResolution: ResolvedTypeReferenceDirective): boolean { + return oldResolution.resolvedFileName === newResolution.resolvedFileName && oldResolution.primary === newResolution.primary; + } + + /* @internal */ + export function hasChangesInResolutions(names: string[], newResolutions: T[], oldResolutions: Map, comparer: (oldResolution: T, newResolution: T) => boolean): boolean { + if (names.length !== newResolutions.length) { + return false; + } + for (let i = 0; i < names.length; i++) { + const newResolution = newResolutions[i]; + const oldResolution = oldResolutions && hasProperty(oldResolutions, names[i]) ? oldResolutions[names[i]] : undefined; + const changed = + oldResolution + ? !newResolution || !comparer(oldResolution, newResolution) + : newResolution; + if (changed) { + return true; + } + } + return false; + } + // Returns true if this node contains a parse error anywhere underneath it. export function containsParseError(node: Node): boolean { aggregateChildData(node); @@ -499,7 +536,7 @@ namespace ts { } export let fullTripleSlashReferencePathRegEx = /^(\/\/\/\s*/; - export let fullTripleSlashReferenceLibraryRegEx = /^(\/\/\/\s*/; + export let fullTripleSlashReferenceTypeReferenceDirectiveRegEx = /^(\/\/\/\s*/; export let fullTripleSlashAMDReferencePathRegEx = /^(\/\/\/\s*/; export function isTypeNode(node: Node): boolean { @@ -1551,7 +1588,7 @@ namespace ts { } else { const refMatchResult = fullTripleSlashReferencePathRegEx.exec(comment); - const refLibResult = !refMatchResult && fullTripleSlashReferenceLibraryRegEx.exec(comment); + const refLibResult = !refMatchResult && fullTripleSlashReferenceTypeReferenceDirectiveRegEx.exec(comment); if (refMatchResult || refLibResult) { const start = commentRange.pos; const end = commentRange.end; @@ -1562,7 +1599,7 @@ namespace ts { fileName: (refMatchResult || refLibResult)[3] }, isNoDefaultLib: false, - isLibraryReference: !!refLibResult + isTypeReferenceDirective: !!refLibResult }; } diff --git a/src/harness/compilerRunner.ts b/src/harness/compilerRunner.ts index 6dfc9313425..ea0877c42b2 100644 --- a/src/harness/compilerRunner.ts +++ b/src/harness/compilerRunner.ts @@ -144,7 +144,7 @@ class CompilerBaselineRunner extends RunnerBase { }); it (`Correct module resolution tracing for ${fileName}`, () => { - if (options.traceModuleResolution) { + if (options.traceResolution) { Harness.Baseline.runBaseline("Correct sourcemap content for " + fileName, justName.replace(/\.tsx?$/, ".trace.json"), () => { return JSON.stringify(result.traceResults || [], undefined, 4); }); diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 2abcee8c6d5..3dad49d24b3 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -222,7 +222,7 @@ namespace FourSlash { function tryAdd(path: string) { const inputFile = inputFiles[path]; if (inputFile && !Harness.isLibraryFile(path)) { - languageServiceAdapterHost.addScript(path, inputFile); + languageServiceAdapterHost.addScript(path, inputFile, /*isRootFile*/ true); return true; } } @@ -268,7 +268,7 @@ namespace FourSlash { if (startResolveFileRef) { // Add the entry-point file itself into the languageServiceShimHost - this.languageServiceAdapterHost.addScript(startResolveFileRef.fileName, startResolveFileRef.content); + this.languageServiceAdapterHost.addScript(startResolveFileRef.fileName, startResolveFileRef.content, /*isRootFile*/ true); const resolvedResult = languageServiceAdapter.getPreProcessedFileInfo(startResolveFileRef.fileName, startResolveFileRef.content); const referencedFiles: ts.FileReference[] = resolvedResult.referencedFiles; @@ -292,18 +292,18 @@ namespace FourSlash { // Check if no-default-lib flag is false and if so add default library if (!resolvedResult.isLibFile) { this.languageServiceAdapterHost.addScript(Harness.Compiler.defaultLibFileName, - Harness.Compiler.getDefaultLibrarySourceFile().text); + Harness.Compiler.getDefaultLibrarySourceFile().text, /*isRootFile*/ false); } } else { // resolveReference file-option is not specified then do not resolve any files and include all inputFiles ts.forEachKey(this.inputFiles, fileName => { if (!Harness.isLibraryFile(fileName)) { - this.languageServiceAdapterHost.addScript(fileName, this.inputFiles[fileName]); + this.languageServiceAdapterHost.addScript(fileName, this.inputFiles[fileName], /*isRootFile*/ true); } }); this.languageServiceAdapterHost.addScript(Harness.Compiler.defaultLibFileName, - Harness.Compiler.getDefaultLibrarySourceFile().text); + Harness.Compiler.getDefaultLibrarySourceFile().text, /*isRootFile*/ false); } this.formatCodeOptions = { diff --git a/src/harness/harness.ts b/src/harness/harness.ts index b8bb11275ac..2c428082c40 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -65,6 +65,11 @@ namespace Utils { return Buffer ? (new Buffer(s)).toString("utf8") : s; } + export function byteLength(s: string, encoding?: string): number { + // stub implementation if Buffer is not available (in-browser case) + return Buffer ? Buffer.byteLength(s, encoding) : s.length; + } + export function evalFile(fileContents: string, fileName: string, nodeContext?: any) { const environment = getExecutionEnvironment(); switch (environment) { @@ -1042,7 +1047,7 @@ namespace Harness { options.newLine); let traceResults: string[]; - if (options.traceModuleResolution) { + if (options.traceResolution) { traceResults = []; compilerHost.trace = text => traceResults.push(text); } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 12bb6a470e4..5bcbb174676 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -9,7 +9,7 @@ namespace Harness.LanguageService { public editRanges: { length: number; textChangeRange: ts.TextChangeRange; }[] = []; private lineMap: number[] = undefined; - constructor(public fileName: string, public content: string) { + constructor(public fileName: string, public content: string, public isRootFile: boolean) { this.setContent(content); } @@ -135,7 +135,13 @@ namespace Harness.LanguageService { public getFilenames(): string[] { const fileNames: string[] = []; - ts.forEachKey(this.fileNameToScript, (fileName) => { fileNames.push(fileName); }); + ts.forEachValue(this.fileNameToScript, (scriptInfo) => { + if (scriptInfo.isRootFile) { + // only include root files here + // usually it means that we won't include lib.d.ts in the list of root files so it won't mess the computation of compilation root dir. + fileNames.push(scriptInfo.fileName); + } + }); return fileNames; } @@ -143,8 +149,8 @@ namespace Harness.LanguageService { return ts.lookUp(this.fileNameToScript, fileName); } - public addScript(fileName: string, content: string): void { - this.fileNameToScript[fileName] = new ScriptInfo(fileName, content); + public addScript(fileName: string, content: string, isRootFile: boolean): void { + this.fileNameToScript[fileName] = new ScriptInfo(fileName, content, isRootFile); } public editScript(fileName: string, start: number, end: number, newText: string) { @@ -177,7 +183,7 @@ namespace Harness.LanguageService { getCompilationSettings() { return this.settings; } getCancellationToken() { return this.cancellationToken; } getCurrentDirectory(): string { return ""; } - getDefaultLibFileName(): string { return ""; } + getDefaultLibFileName(): string { return Harness.Compiler.defaultLibFileName; } getScriptFileNames(): string[] { return this.getFilenames(); } getScriptSnapshot(fileName: string): ts.IScriptSnapshot { const script = this.getScriptInfo(fileName); @@ -210,6 +216,7 @@ namespace Harness.LanguageService { private nativeHost: NativeLanguageServiceHost; public getModuleResolutionsForFile: (fileName: string) => string; + public getTypeReferenceDirectiveResolutionsForFile: (fileName: string) => string; constructor(preprocessToResolve: boolean, cancellationToken?: ts.HostCancellationToken, options?: ts.CompilerOptions) { super(cancellationToken, options); @@ -236,12 +243,28 @@ namespace Harness.LanguageService { } return JSON.stringify(imports); }; + this.getTypeReferenceDirectiveResolutionsForFile = (fileName) => { + const scriptInfo = this.getScriptInfo(fileName); + const preprocessInfo = ts.preProcessFile(scriptInfo.content, /*readImportFiles*/ false); + const resolutions: ts.Map = {}; + const rootFiles = this.getFilenames(); + const currentDirectory = this.getCurrentDirectory(); + const settings = this.nativeHost.getCompilationSettings(); + const compilationRoot = ts.computeCompilationRoot(rootFiles, currentDirectory, settings, ts.createGetCanonicalFileName(false)); + for (const typeReferenceDirective of preprocessInfo.typeReferenceDirectives) { + const resolutionInfo = ts.resolveTypeReferenceDirective(typeReferenceDirective.fileName, fileName, compilationRoot, settings, moduleResolutionHost); + if (resolutionInfo.resolvedTypeReferenceDirective.resolvedFileName) { + resolutions[typeReferenceDirective.fileName] = resolutionInfo.resolvedTypeReferenceDirective; + } + } + return JSON.stringify(resolutions); + }; } } getFilenames(): string[] { return this.nativeHost.getFilenames(); } getScriptInfo(fileName: string): ScriptInfo { return this.nativeHost.getScriptInfo(fileName); } - addScript(fileName: string, content: string): void { this.nativeHost.addScript(fileName, content); } + addScript(fileName: string, content: string, isRootFile: boolean): void { this.nativeHost.addScript(fileName, content, isRootFile); } editScript(fileName: string, start: number, end: number, newText: string): void { this.nativeHost.editScript(fileName, start, end, newText); } positionToLineAndCharacter(fileName: string, position: number): ts.LineAndCharacter { return this.nativeHost.positionToLineAndCharacter(fileName, position); } @@ -442,6 +465,7 @@ namespace Harness.LanguageService { getPreProcessedFileInfo(fileName: string, fileContents: string): ts.PreProcessedFileInfo { let shimResult: { referencedFiles: ts.IFileReference[]; + typeReferenceDirectives: ts.IFileReference[]; importedFiles: ts.IFileReference[]; isLibFile: boolean; }; @@ -453,7 +477,8 @@ namespace Harness.LanguageService { referencedFiles: [], importedFiles: [], ambientExternalModules: [], - isLibFile: shimResult.isLibFile + isLibFile: shimResult.isLibFile, + typeReferenceDirectives: [] }; ts.forEach(shimResult.referencedFiles, refFile => { @@ -472,6 +497,13 @@ namespace Harness.LanguageService { }); }); + ts.forEach(shimResult.typeReferenceDirectives, typeRefDirective => { + convertResult.importedFiles.push({ + fileName: typeRefDirective.path, + pos: typeRefDirective.position, + end: typeRefDirective.position + typeRefDirective.length + }); + }); return convertResult; } } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 4b9f17798c8..9b57c1687aa 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -81,8 +81,14 @@ namespace ts.server { } } - interface TimestampedResolvedModule extends ResolvedModuleWithFailedLookupLocations { - lastCheckTime: number; + interface Timestamped { + lastCheckTime?: number; + } + + interface TimestampedResolvedModule extends ResolvedModuleWithFailedLookupLocations, Timestamped { + } + + interface TimestampedResolvedTypeReferenceDirective extends ResolvedTypeReferenceDirectiveWithFailedLookupLocations, Timestamped { } export class LSHost implements ts.LanguageServiceHost { @@ -90,13 +96,17 @@ namespace ts.server { compilationSettings: ts.CompilerOptions; filenameToScript: ts.FileMap; roots: ScriptInfo[] = []; + compilationRoot: string; + private resolvedModuleNames: ts.FileMap>; + private resolvedTypeReferenceDirectives: ts.FileMap>; private moduleResolutionHost: ts.ModuleResolutionHost; private getCanonicalFileName: (fileName: string) => string; constructor(public host: ServerHost, public project: Project) { this.getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); this.resolvedModuleNames = createFileMap>(); + this.resolvedTypeReferenceDirectives = createFileMap>(); this.filenameToScript = createFileMap(); this.moduleResolutionHost = { fileExists: fileName => this.fileExists(fileName), @@ -105,46 +115,51 @@ namespace ts.server { }; } - resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModule[] { + private resolveNamesWithLocalCache( + names: string[], + containingFile: string, + cache: ts.FileMap>, + loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T, + getResult: (s: T) => R): R[] { + const path = toPath(containingFile, this.host.getCurrentDirectory(), this.getCanonicalFileName); - const currentResolutionsInFile = this.resolvedModuleNames.get(path); - - const newResolutions: Map = {}; - const resolvedModules: ResolvedModule[] = []; + const currentResolutionsInFile = cache.get(path); + const newResolutions: Map = {}; + const resolvedModules: R[] = []; const compilerOptions = this.getCompilationSettings(); - for (const moduleName of moduleNames) { + for (const name of names) { // check if this is a duplicate entry in the list - let resolution = lookUp(newResolutions, moduleName); + let resolution = lookUp(newResolutions, name); if (!resolution) { - const existingResolution = currentResolutionsInFile && ts.lookUp(currentResolutionsInFile, moduleName); + const existingResolution = currentResolutionsInFile && ts.lookUp(currentResolutionsInFile, name); if (moduleResolutionIsValid(existingResolution)) { - // ok, it is safe to use existing module resolution results + // ok, it is safe to use existing name resolution results resolution = existingResolution; } else { - resolution = resolveModuleName(moduleName, containingFile, compilerOptions, this.moduleResolutionHost); + resolution = loader(name, containingFile, compilerOptions, this.moduleResolutionHost); resolution.lastCheckTime = Date.now(); - newResolutions[moduleName] = resolution; + newResolutions[name] = resolution; } } ts.Debug.assert(resolution !== undefined); - resolvedModules.push(resolution.resolvedModule); + resolvedModules.push(getResult(resolution)); } // replace old results with a new one - this.resolvedModuleNames.set(path, newResolutions); + cache.set(path, newResolutions); return resolvedModules; - function moduleResolutionIsValid(resolution: TimestampedResolvedModule): boolean { + function moduleResolutionIsValid(resolution: T): boolean { if (!resolution) { return false; } - if (resolution.resolvedModule) { + if (getResult(resolution)) { // TODO: consider checking failedLookupLocations // TODO: use lastCheckTime to track expiration for module name resolution return true; @@ -156,6 +171,20 @@ namespace ts.server { } } + resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { + if (this.compilationRoot === undefined) { + this.compilationRoot = computeCompilationRoot(this.getScriptFileNames(), this.getCurrentDirectory(), this.getCompilationSettings(), this.getCanonicalFileName); + } + const loader = (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => { + return resolveTypeReferenceDirective(name, containingFile, this.compilationRoot, options, this.moduleResolutionHost); + }; + return this.resolveNamesWithLocalCache(typeDirectiveNames, containingFile, this.resolvedTypeReferenceDirectives, loader, m => m.resolvedTypeReferenceDirective); + } + + resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModule[] { + return this.resolveNamesWithLocalCache(moduleNames, containingFile, this.resolvedModuleNames, resolveModuleName, m => m.resolvedModule); + } + getDefaultLibFileName() { const nodeModuleBinDir = ts.getDirectoryPath(ts.normalizePath(this.host.getExecutingFilePath())); return ts.combinePaths(nodeModuleBinDir, ts.getDefaultLibFileName(this.compilationSettings)); @@ -172,6 +201,7 @@ namespace ts.server { this.compilationSettings = opt; // conservatively assume that changing compiler options might affect module resolution strategy this.resolvedModuleNames.clear(); + this.resolvedTypeReferenceDirectives.clear(); } lineAffectsRefs(filename: string, line: number) { @@ -212,6 +242,7 @@ namespace ts.server { if (!info.isOpen) { this.filenameToScript.remove(info.path); this.resolvedModuleNames.remove(info.path); + this.resolvedTypeReferenceDirectives.remove(info.path); } } @@ -231,6 +262,8 @@ namespace ts.server { if (!this.filenameToScript.contains(info.path)) { this.filenameToScript.set(info.path, info); this.roots.push(info); + // reset compilation root + this.compilationRoot = undefined; } } @@ -239,6 +272,9 @@ namespace ts.server { this.filenameToScript.remove(info.path); this.roots = copyListRemovingItem(info, this.roots); this.resolvedModuleNames.remove(info.path); + this.resolvedTypeReferenceDirectives.remove(info.path); + // reset compilation root + this.compilationRoot = undefined; } } diff --git a/src/services/services.ts b/src/services/services.ts index 65e1663a856..987687f6708 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -125,6 +125,7 @@ namespace ts { } export interface PreProcessedFileInfo { referencedFiles: FileReference[]; + typeReferenceDirectives: FileReference[]; importedFiles: FileReference[]; ambientExternalModules: string[]; isLibFile: boolean; @@ -792,7 +793,7 @@ namespace ts { public amdDependencies: { name: string; path: string }[]; public moduleName: string; public referencedFiles: FileReference[]; - public referencedLibraries: FileReference[]; + public typeReferenceDirectives: FileReference[]; public syntacticDiagnostics: Diagnostic[]; public referenceDiagnostics: Diagnostic[]; @@ -814,6 +815,7 @@ namespace ts { public identifiers: Map; public nameTable: Map; public resolvedModules: Map; + public resolvedTypeReferenceDirectiveNames: Map; public imports: LiteralExpression[]; public moduleAugmentations: LiteralExpression[]; private namedDeclarations: Map; @@ -1040,6 +1042,7 @@ namespace ts { * host specific questions using 'getScriptSnapshot'. */ resolveModuleNames?(moduleNames: string[], containingFile: string): ResolvedModule[]; + resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; directoryExists?(directoryName: string): boolean; } @@ -2147,6 +2150,7 @@ namespace ts { export function preProcessFile(sourceText: string, readImportFiles = true, detectJavaScriptImports = false): PreProcessedFileInfo { const referencedFiles: FileReference[] = []; + const typeReferenceDirectives: FileReference[] = []; const importedFiles: FileReference[] = []; let ambientExternalModules: { ref: FileReference, depth: number }[]; let isNoDefaultLib = false; @@ -2175,7 +2179,11 @@ namespace ts { isNoDefaultLib = referencePathMatchResult.isNoDefaultLib; const fileReference = referencePathMatchResult.fileReference; if (fileReference) { - referencedFiles.push(fileReference); + const collection = referencePathMatchResult.isTypeReferenceDirective + ? typeReferenceDirectives + : referencedFiles; + + collection.push(fileReference); } } }); @@ -2476,7 +2484,7 @@ namespace ts { importedFiles.push(decl.ref); } } - return { referencedFiles, importedFiles, isLibFile: isNoDefaultLib, ambientExternalModules: undefined }; + return { referencedFiles, typeReferenceDirectives, importedFiles, isLibFile: isNoDefaultLib, ambientExternalModules: undefined }; } else { // for global scripts ambient modules still can have augmentations - look for ambient modules with depth > 0 @@ -2494,7 +2502,7 @@ namespace ts { } } } - return { referencedFiles, importedFiles, isLibFile: isNoDefaultLib, ambientExternalModules: ambientModuleNames }; + return { referencedFiles, typeReferenceDirectives, importedFiles, isLibFile: isNoDefaultLib, ambientExternalModules: ambientModuleNames }; } } @@ -2831,7 +2839,7 @@ namespace ts { getCurrentDirectory: () => currentDirectory, fileExists: (fileName): boolean => { // stub missing host functionality - Debug.assert(!host.resolveModuleNames); + Debug.assert(!host.resolveModuleNames || !host.resolveTypeReferenceDirectives); return hostCache.getOrCreateEntry(fileName) !== undefined; }, readFile: (fileName): string => { @@ -2840,7 +2848,7 @@ namespace ts { return entry && entry.scriptSnapshot.getText(0, entry.scriptSnapshot.getLength()); }, directoryExists: directoryName => { - Debug.assert(!host.resolveModuleNames); + Debug.assert(!host.resolveModuleNames || !host.resolveTypeReferenceDirectives); return directoryProbablyExists(directoryName, host); } }; @@ -2851,6 +2859,11 @@ namespace ts { if (host.resolveModuleNames) { compilerHost.resolveModuleNames = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile); } + if (host.resolveTypeReferenceDirectives) { + compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile) => { + return host.resolveTypeReferenceDirectives(typeReferenceDirectiveNames, containingFile); + }; + } const newProgram = createProgram(hostCache.getRootFileNames(), newSettings, compilerHost, program); @@ -4671,6 +4684,26 @@ namespace ts { } } + function findReferenceInPosition(refs: FileReference[], pos: number): FileReference { + for (const ref of refs) { + if (ref.pos <= pos && pos < ref.end) { + return ref; + } + } + return undefined; + } + + function getDefinitionInfoForFileReference(name: string, targetFileName: string): DefinitionInfo { + return { + fileName: targetFileName, + textSpan: createTextSpanFromBounds(0, 0), + kind: ScriptElementKind.scriptElement, + name: name, + containerName: undefined, + containerKind: undefined + }; + } + /// Goto definition function getDefinitionAtPosition(fileName: string, position: number): DefinitionInfo[] { synchronizeHostData(); @@ -4690,18 +4723,20 @@ namespace ts { } /// Triple slash reference comments - const comment = forEach(sourceFile.referencedFiles, r => (r.pos <= position && position < r.end) ? r : undefined); + const comment = findReferenceInPosition(sourceFile.referencedFiles, position); if (comment) { const referenceFile = tryResolveScriptReference(program, sourceFile, comment); if (referenceFile) { - return [{ - fileName: referenceFile.fileName, - textSpan: createTextSpanFromBounds(0, 0), - kind: ScriptElementKind.scriptElement, - name: comment.fileName, - containerName: undefined, - containerKind: undefined - }]; + return [getDefinitionInfoForFileReference(comment.fileName, referenceFile.fileName)]; + } + return undefined; + } + // Type reference directives + const typeReferenceDirective = findReferenceInPosition(sourceFile.typeReferenceDirectives, position); + if (typeReferenceDirective) { + const referenceFile = lookUp(program.resolvedTypeReferenceDirectives, typeReferenceDirective.fileName); + if (referenceFile && referenceFile.resolvedFileName) { + return [getDefinitionInfoForFileReference(typeReferenceDirective.fileName, referenceFile.resolvedFileName)]; } return undefined; } diff --git a/src/services/shims.ts b/src/services/shims.ts index ecfcd6f84da..c99a16ca0cd 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -67,6 +67,7 @@ namespace ts { useCaseSensitiveFileNames?(): boolean; getModuleResolutionsForFile?(fileName: string): string; + getTypeReferenceDirectiveResolutionsForFile?(fileName: string): string; directoryExists(directoryName: string): boolean; } @@ -281,6 +282,7 @@ namespace ts { private tracingEnabled = false; public resolveModuleNames: (moduleName: string[], containingFile: string) => ResolvedModule[]; + public resolveTypeReferenceDirectives: (typeDirectiveNames: string[], containingFile: string) => ResolvedTypeReferenceDirective[]; public directoryExists: (directoryName: string) => boolean; constructor(private shimHost: LanguageServiceShimHost) { @@ -298,6 +300,12 @@ namespace ts { if ("directoryExists" in this.shimHost) { this.directoryExists = directoryName => this.shimHost.directoryExists(directoryName); } + if ("getTypeReferenceDirectiveResolutionsForFile" in this.shimHost) { + this.resolveTypeReferenceDirectives = (typeDirectiveNames: string[], containingFile: string) => { + const typeDirectivesForFile = >JSON.parse(this.shimHost.getTypeReferenceDirectiveResolutionsForFile(containingFile)); + return map(typeDirectiveNames, name => lookUp(typeDirectivesForFile, name)); + }; + } } public log(s: string): void { @@ -899,6 +907,7 @@ namespace ts { class CoreServicesShimObject extends ShimBase implements CoreServicesShim { private logPerformance = false; + private getCanonicalFileName = createGetCanonicalFileName(false); constructor(factory: ShimFactory, public logger: Logger, private host: CoreServicesShimHostAdapter) { super(factory); @@ -919,38 +928,57 @@ namespace ts { }); } + public computeCompilationRoot(filesJson: string, currentDirectory: string, compilerOptionsJson: string): string { + return this.forwardJSONCall("computeCompilationRoot", () => { + const files = JSON.parse(filesJson); + const compilerOptions = JSON.parse(compilerOptionsJson); + return computeCompilationRoot(files, currentDirectory, compilerOptions, this.getCanonicalFileName); + }); + } + + public resolveTypeReferenceDirective(fileName: string, typeReferenceDirective: string, compilationRoot: string, compilerOptionsJson: string): string { + return this.forwardJSONCall(`resolveTypeReferenceDirective(${fileName})`, () => { + const compilerOptions = JSON.parse(compilerOptionsJson); + const result = resolveTypeReferenceDirective(typeReferenceDirective, normalizeSlashes(fileName), compilationRoot, compilerOptions, this.host); + return { + resolvedFileName: result.resolvedTypeReferenceDirective ? result.resolvedTypeReferenceDirective.resolvedFileName : undefined, + primary: result.resolvedTypeReferenceDirective ? result.resolvedTypeReferenceDirective.primary : true, + failedLookupLocations: result.failedLookupLocations + }; + }); + } + public getPreProcessedFileInfo(fileName: string, sourceTextSnapshot: IScriptSnapshot): string { return this.forwardJSONCall( "getPreProcessedFileInfo('" + fileName + "')", () => { // for now treat files as JavaScript const result = preProcessFile(sourceTextSnapshot.getText(0, sourceTextSnapshot.getLength()), /* readImportFiles */ true, /* detectJavaScriptImports */ true); - const convertResult = { - referencedFiles: [], - importedFiles: [], + return { + referencedFiles: this.convertFileReferences(result.referencedFiles), + importedFiles: this.convertFileReferences(result.importedFiles), ambientExternalModules: result.ambientExternalModules, - isLibFile: result.isLibFile + isLibFile: result.isLibFile, + typeReferenceDirectives: this.convertFileReferences(result.typeReferenceDirectives) }; - - forEach(result.referencedFiles, refFile => { - convertResult.referencedFiles.push({ - path: normalizePath(refFile.fileName), - position: refFile.pos, - length: refFile.end - refFile.pos - }); - }); - - forEach(result.importedFiles, importedFile => { - convertResult.importedFiles.push({ - path: normalizeSlashes(importedFile.fileName), - position: importedFile.pos, - length: importedFile.end - importedFile.pos - }); - }); - return convertResult; }); } + private convertFileReferences(refs: FileReference[]): IFileReference[] { + if (!refs) { + return undefined; + } + const result: IFileReference[] = []; + for (const ref of refs) { + result.push({ + path: normalizeSlashes(ref.fileName), + position: ref.pos, + length: ref.end - ref.pos + }); + } + return result; + } + public getTSConfigFileInfo(fileName: string, sourceTextSnapshot: IScriptSnapshot): string { return this.forwardJSONCall( `getTSConfigFileInfo('${fileName}')`,