diff --git a/Jakefile.js b/Jakefile.js index 7b85aacaa89..a0a001d7679 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -920,6 +920,7 @@ var servicesLintTargets = [ "patternMatcher.ts", "services.ts", "shims.ts", + "jsTyping.ts" ].map(function (s) { return path.join(servicesDirectory, s); }); diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 55cc2be0b72..b3a081d6b05 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -511,6 +511,7 @@ namespace ts { return { options, fileNames: getFileNames(), + typingOptions: getTypingOptions(), errors }; @@ -575,6 +576,35 @@ namespace ts { } return fileNames; } + + function getTypingOptions(): TypingOptions { + const options: TypingOptions = getBaseFileName(configFileName) === "jsconfig.json" + ? { enableAutoDiscovery: true, include: [], exclude: [] } + : { enableAutoDiscovery: false, include: [], exclude: [] }; + const jsonTypingOptions = json["typingOptions"]; + if (jsonTypingOptions) { + for (const id in jsonTypingOptions) { + if (id === "enableAutoDiscovery") { + if (typeof jsonTypingOptions[id] === "boolean") { + options.enableAutoDiscovery = jsonTypingOptions[id]; + } + else { + errors.push(createCompilerDiagnostic(Diagnostics.Unknown_typing_option_0, id)); + } + } + else if (id === "include") { + options.include = convertJsonOptionToStringArray(id, jsonTypingOptions[id], errors); + } + else if (id === "exclude") { + options.exclude = convertJsonOptionToStringArray(id, jsonTypingOptions[id], errors); + } + else { + errors.push(createCompilerDiagnostic(Diagnostics.Unknown_typing_option_0, id)); + } + } + } + return options; + } } export function convertCompilerOptionsFromJson(jsonOptions: any, basePath: string, configFileName?: string): { options: CompilerOptions, errors: Diagnostic[] } { @@ -610,9 +640,9 @@ namespace ts { } if (opt.isFilePath) { value = normalizePath(combinePaths(basePath, value)); - if (value === "") { - value = "."; - } + if (value === "") { + value = "."; + } } options[opt.name] = value; } @@ -627,4 +657,28 @@ namespace ts { return { options, errors }; } + + function convertJsonOptionToStringArray(optionName: string, optionJson: any, errors: Diagnostic[], func?: (element: string) => string): string[] { + const items: string[] = []; + let invalidOptionType = false; + if (!isArray(optionJson)) { + invalidOptionType = true; + } + else { + for (const element of optionJson) { + if (typeof element === "string") { + const item = func ? func(element) : element; + items.push(item); + } + else { + invalidOptionType = true; + break; + } + } + } + if (invalidOptionType) { + errors.push(createCompilerDiagnostic(Diagnostics.Option_0_should_have_array_of_strings_as_a_value, optionName)); + } + return items; + } } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index cfdcb2b930c..b3c7657b872 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -280,6 +280,14 @@ namespace ts { return hasOwnProperty.call(map, key); } + export function getKeys(map: Map): string[] { + const keys: string[] = []; + for (const key in map) { + keys.push(key); + } + return keys; + } + export function getProperty(map: Map, key: string): T { return hasOwnProperty.call(map, key) ? map[key] : undefined; } @@ -769,6 +777,32 @@ namespace ts { return pathLen > extLen && path.substr(pathLen - extLen, extLen) === extension; } + export function ensureScriptKind(fileName: string, scriptKind?: ScriptKind): ScriptKind { + // Using scriptKind as a condition handles both: + // - 'scriptKind' is unspecified and thus it is `undefined` + // - 'scriptKind' is set and it is `Unknown` (0) + // If the 'scriptKind' is 'undefined' or 'Unknown' then we attempt + // to get the ScriptKind from the file name. If it cannot be resolved + // from the file name then the default 'TS' script kind is returned. + return (scriptKind || getScriptKindFromFileName(fileName)) || ScriptKind.TS; + } + + export function getScriptKindFromFileName(fileName: string): ScriptKind { + const ext = fileName.substr(fileName.lastIndexOf(".")); + switch (ext.toLowerCase()) { + case ".js": + return ScriptKind.JS; + case ".jsx": + return ScriptKind.JSX; + case ".ts": + return ScriptKind.TS; + case ".tsx": + return ScriptKind.TSX; + default: + return ScriptKind.Unknown; + } + } + /** * List of supported extensions in order of file resolution precedence. */ diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 19112e88437..ce9303a85c9 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2450,11 +2450,14 @@ "category": "Message", "code": 6084 }, + "Option '{0}' should have array of strings as a value.": { + "category": "Error", + "code": 6103 + }, "Do not emit 'use strict' directives in module output.": { "category": "Message", "code": 6112 }, - "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", "code": 7005 @@ -2654,5 +2657,9 @@ "'super' must be called before accessing 'this' in the constructor of a derived class.": { "category": "Error", "code": 17009 + }, + "Unknown typing option '{0}'.": { + "category": "Error", + "code": 17010 } } \ No newline at end of file diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 5def394578d..281adf9c90c 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -399,9 +399,9 @@ namespace ts { } } - export function createSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, setParentNodes = false): SourceFile { + export function createSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, setParentNodes = false, scriptKind?: ScriptKind): SourceFile { const start = new Date().getTime(); - const result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes); + const result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, scriptKind); parseTime += new Date().getTime() - start; return result; @@ -533,23 +533,24 @@ namespace ts { // attached to the EOF token. let parseErrorBeforeNextFinishedNode = false; - export function parseSourceFile(fileName: string, _sourceText: string, languageVersion: ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor, setParentNodes?: boolean): SourceFile { - const isJavaScriptFile = hasJavaScriptFileExtension(fileName) || _sourceText.lastIndexOf("// @language=javascript", 0) === 0; - initializeState(fileName, _sourceText, languageVersion, isJavaScriptFile, _syntaxCursor); + export function parseSourceFile(fileName: string, _sourceText: string, languageVersion: ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor, setParentNodes?: boolean, scriptKind?: ScriptKind): SourceFile { + scriptKind = ensureScriptKind(fileName, scriptKind); - const result = parseSourceFileWorker(fileName, languageVersion, setParentNodes); + initializeState(fileName, _sourceText, languageVersion, _syntaxCursor, scriptKind); + + const result = parseSourceFileWorker(fileName, languageVersion, setParentNodes, scriptKind); clearState(); return result; } - function getLanguageVariant(fileName: string) { + function getLanguageVariant(scriptKind: ScriptKind) { // .tsx and .jsx files are treated as jsx language variant. - return fileExtensionIs(fileName, ".tsx") || fileExtensionIs(fileName, ".jsx") || fileExtensionIs(fileName, ".js") ? LanguageVariant.JSX : LanguageVariant.Standard; + return scriptKind === ScriptKind.TSX || scriptKind === ScriptKind.JSX || scriptKind === ScriptKind.JS ? LanguageVariant.JSX : LanguageVariant.Standard; } - function initializeState(fileName: string, _sourceText: string, languageVersion: ScriptTarget, isJavaScriptFile: boolean, _syntaxCursor: IncrementalParser.SyntaxCursor) { + function initializeState(fileName: string, _sourceText: string, languageVersion: ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor, scriptKind: ScriptKind) { NodeConstructor = objectAllocator.getNodeConstructor(); SourceFileConstructor = objectAllocator.getSourceFileConstructor(); @@ -562,14 +563,14 @@ namespace ts { identifierCount = 0; nodeCount = 0; - contextFlags = isJavaScriptFile ? ParserContextFlags.JavaScriptFile : ParserContextFlags.None; + contextFlags = scriptKind === ScriptKind.JS || scriptKind === ScriptKind.JSX ? ParserContextFlags.JavaScriptFile : ParserContextFlags.None; parseErrorBeforeNextFinishedNode = false; // Initialize and prime the scanner before parsing the source elements. scanner.setText(sourceText); scanner.setOnError(scanError); scanner.setScriptTarget(languageVersion); - scanner.setLanguageVariant(getLanguageVariant(fileName)); + scanner.setLanguageVariant(getLanguageVariant(scriptKind)); } function clearState() { @@ -585,8 +586,8 @@ namespace ts { sourceText = undefined; } - function parseSourceFileWorker(fileName: string, languageVersion: ScriptTarget, setParentNodes: boolean): SourceFile { - sourceFile = createSourceFile(fileName, languageVersion); + function parseSourceFileWorker(fileName: string, languageVersion: ScriptTarget, setParentNodes: boolean, scriptKind: ScriptKind): SourceFile { + sourceFile = createSourceFile(fileName, languageVersion, scriptKind); if (contextFlags & ParserContextFlags.JavaScriptFile) { sourceFile.parserContextFlags = ParserContextFlags.JavaScriptFile; @@ -656,7 +657,7 @@ namespace ts { } } - function createSourceFile(fileName: string, languageVersion: ScriptTarget): SourceFile { + function createSourceFile(fileName: string, languageVersion: ScriptTarget, scriptKind: ScriptKind): SourceFile { // code from createNode is inlined here so createNode won't have to deal with special case of creating source files // this is quite rare comparing to other nodes and createNode should be as fast as possible const sourceFile = new SourceFileConstructor(SyntaxKind.SourceFile, /*pos*/ 0, /* end */ sourceText.length); @@ -667,7 +668,8 @@ namespace ts { sourceFile.languageVersion = languageVersion; sourceFile.fileName = normalizePath(fileName); sourceFile.flags = fileExtensionIs(sourceFile.fileName, ".d.ts") ? NodeFlags.DeclarationFile : 0; - sourceFile.languageVariant = getLanguageVariant(sourceFile.fileName); + sourceFile.languageVariant = getLanguageVariant(scriptKind); + sourceFile.scriptKind = scriptKind; return sourceFile; } @@ -5604,7 +5606,7 @@ namespace ts { } export function parseJSDocTypeExpressionForTests(content: string, start: number, length: number) { - initializeState("file.js", content, ScriptTarget.Latest, /*isJavaScriptFile*/ true, /*_syntaxCursor:*/ undefined); + initializeState("file.js", content, ScriptTarget.Latest, /*_syntaxCursor:*/ undefined, ScriptKind.JS); scanner.setText(content, start, length); token = scanner.scan(); const jsDocTypeExpression = parseJSDocTypeExpression(); @@ -5923,7 +5925,7 @@ namespace ts { } export function parseIsolatedJSDocComment(content: string, start: number, length: number) { - initializeState("file.js", content, ScriptTarget.Latest, /*isJavaScriptFile*/ true, /*_syntaxCursor:*/ undefined); + initializeState("file.js", content, ScriptTarget.Latest, /*_syntaxCursor:*/ undefined, ScriptKind.JS); sourceFile = { languageVariant: LanguageVariant.Standard, text: content }; const jsDocComment = parseJSDocCommentWorker(start, length); const diagnostics = parseDiagnostics; @@ -6249,7 +6251,7 @@ namespace ts { if (sourceFile.statements.length === 0) { // If we don't have any statements in the current source file, then there's no real // way to incrementally parse. So just do a full parse instead. - return Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, /*syntaxCursor*/ undefined, /*setParentNodes*/ true); + return Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, /*syntaxCursor*/ undefined, /*setParentNodes*/ true, sourceFile.scriptKind); } // Make sure we're not trying to incrementally update a source file more than once. Once @@ -6313,7 +6315,7 @@ namespace ts { // inconsistent tree. Setting the parents on the new tree should be very fast. We // will immediately bail out of walking any subtrees when we can see that their parents // are already correct. - const result = Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, syntaxCursor, /*setParentNodes*/ true); + const result = Parser.parseSourceFile(sourceFile.fileName, newText, sourceFile.languageVersion, syntaxCursor, /*setParentNodes*/ true, sourceFile.scriptKind); return result; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index d605a684a76..d0cd0ae6080 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1,3 +1,4 @@ + namespace ts { export interface Map { [index: string]: T; @@ -1559,6 +1560,7 @@ namespace ts { hasNoDefaultLib: boolean; languageVersion: ScriptTarget; + /* @internal */ scriptKind: ScriptKind; // The first node that causes this file to be an external module /* @internal */ externalModuleIndicator: Node; @@ -2447,6 +2449,22 @@ namespace ts { [option: string]: string | number | boolean; } + export interface TypingOptions { + enableAutoDiscovery?: boolean; + include?: string[]; + exclude?: string[]; + [option: string]: string[] | boolean; + } + + export interface DiscoverTypingsInfo { + fileNames: string[]; // The file names that belong to the same project. + projectRootPath: string; // The path to the project root directory + safeListPath: string; // The path used to retrieve the safe list + packageNameToTypingLocation: Map; // The map of package names to their cached typing locations + typingOptions: TypingOptions; // Used to customize the typing inference process + compilerOptions: CompilerOptions; // Used as a source for typing inference + } + export const enum ModuleKind { None = 0, CommonJS = 1, @@ -2476,6 +2494,14 @@ namespace ts { character: number; } + export const enum ScriptKind { + Unknown = 0, + JS = 1, + JSX = 2, + TS = 3, + TSX = 4 + } + export const enum ScriptTarget { ES3 = 0, ES5 = 1, @@ -2497,6 +2523,7 @@ namespace ts { export interface ParsedCommandLine { options: CompilerOptions; + typingOptions?: TypingOptions; fileNames: string[]; errors: Diagnostic[]; } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index a0cac439729..cb8f85eea31 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -183,6 +183,7 @@ namespace Harness.LanguageService { const script = this.getScriptInfo(fileName); return script ? new ScriptSnapshot(script) : undefined; } + getScriptKind(fileName: string): ts.ScriptKind { return ts.ScriptKind.Unknown; } getScriptVersion(fileName: string): string { const script = this.getScriptInfo(fileName); return script ? script.version.toString() : undefined; @@ -253,6 +254,7 @@ namespace Harness.LanguageService { const nativeScriptSnapshot = this.nativeHost.getScriptSnapshot(fileName); return nativeScriptSnapshot && new ScriptSnapshotProxy(nativeScriptSnapshot); } + getScriptKind(fileName: string): ts.ScriptKind { return this.nativeHost.getScriptKind(fileName); } getScriptVersion(fileName: string): string { return this.nativeHost.getScriptVersion(fileName); } getLocalizedDiagnosticMessages(): string { return JSON.stringify({}); } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index e2ec5cc159f..71907735b91 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -192,6 +192,10 @@ namespace ts.server { return this.roots.map(root => root.fileName); } + getScriptKind() { + return ScriptKind.Unknown; + } + getScriptVersion(filename: string) { return this.getScriptInfo(filename).svc.latestVersion().toString(); } diff --git a/src/services/jsTyping.ts b/src/services/jsTyping.ts new file mode 100644 index 00000000000..4d4903ab59d --- /dev/null +++ b/src/services/jsTyping.ts @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0. +// See LICENSE.txt in the project root for complete license information. + +/// + +/* @internal */ +namespace ts.JsTyping { + + export interface TypingResolutionHost { + directoryExists: (path: string) => boolean; + fileExists: (fileName: string) => boolean; + readFile: (path: string, encoding?: string) => string; + readDirectory: (path: string, extension?: string, exclude?: string[], depth?: number) => string[]; + }; + + interface PackageJson { + _requiredBy?: string[]; + dependencies?: Map; + devDependencies?: Map; + name?: string; + optionalDependencies?: Map; + peerDependencies?: Map; + typings?: string; + }; + + // A map of loose file names to library names + // that we are confident require typings + let safeList: Map; + + /** + * @param host is the object providing I/O related operations. + * @param fileNames are the file names that belong to the same project + * @param projectRootPath is the path to the project root directory + * @param safeListPath is the path used to retrieve the safe list + * @param packageNameToTypingLocation is the map of package names to their cached typing locations + * @param typingOptions are used to customize the typing inference process + * @param compilerOptions are used as a source for typing inference + */ + export function discoverTypings( + host: TypingResolutionHost, + fileNames: string[], + projectRootPath: Path, + safeListPath: Path, + packageNameToTypingLocation: Map, + typingOptions: TypingOptions, + compilerOptions: CompilerOptions): + { cachedTypingPaths: string[], newTypingNames: string[], filesToWatch: string[] } { + + // A typing name to typing file path mapping + const inferredTypings: Map = {}; + + if (!typingOptions || !typingOptions.enableAutoDiscovery) { + return { cachedTypingPaths: [], newTypingNames: [], filesToWatch: [] }; + } + + // Only infer typings for .js and .jsx files + fileNames = filter(map(fileNames, normalizePath), f => scriptKindIs(f, /*LanguageServiceHost*/ undefined, ScriptKind.JS, ScriptKind.JSX)); + + if (!safeList) { + const result = readConfigFile(safeListPath, (path: string) => host.readFile(path)); + if (result.config) { + safeList = result.config; + } + else { + safeList = {}; + } + } + + const filesToWatch: string[] = []; + // Directories to search for package.json, bower.json and other typing information + let searchDirs: string[] = []; + let exclude: string[] = []; + + mergeTypings(typingOptions.include); + exclude = typingOptions.exclude || []; + + const possibleSearchDirs = map(fileNames, getDirectoryPath); + if (projectRootPath !== undefined) { + possibleSearchDirs.push(projectRootPath); + } + searchDirs = deduplicate(possibleSearchDirs); + for (const searchDir of searchDirs) { + const packageJsonPath = combinePaths(searchDir, "package.json"); + getTypingNamesFromJson(packageJsonPath, filesToWatch); + + const bowerJsonPath = combinePaths(searchDir, "bower.json"); + getTypingNamesFromJson(bowerJsonPath, filesToWatch); + + const nodeModulesPath = combinePaths(searchDir, "node_modules"); + getTypingNamesFromNodeModuleFolder(nodeModulesPath); + } + getTypingNamesFromSourceFileNames(fileNames); + + // Add the cached typing locations for inferred typings that are already installed + for (const name in packageNameToTypingLocation) { + if (hasProperty(inferredTypings, name) && !inferredTypings[name]) { + inferredTypings[name] = packageNameToTypingLocation[name]; + } + } + + // Remove typings that the user has added to the exclude list + for (const excludeTypingName of exclude) { + delete inferredTypings[excludeTypingName]; + } + + const newTypingNames: string[] = []; + const cachedTypingPaths: string[] = []; + for (const typing in inferredTypings) { + if (inferredTypings[typing] !== undefined) { + cachedTypingPaths.push(inferredTypings[typing]); + } + else { + newTypingNames.push(typing); + } + } + return { cachedTypingPaths, newTypingNames, filesToWatch }; + + /** + * Merge a given list of typingNames to the inferredTypings map + */ + function mergeTypings(typingNames: string[]) { + if (!typingNames) { + return; + } + + for (const typing of typingNames) { + if (!hasProperty(inferredTypings, typing)) { + inferredTypings[typing] = undefined; + } + } + } + + /** + * Get the typing info from common package manager json files like package.json or bower.json + */ + function getTypingNamesFromJson(jsonPath: string, filesToWatch: string[]) { + const result = readConfigFile(jsonPath, (path: string) => host.readFile(path)); + if (result.config) { + const jsonConfig: PackageJson = result.config; + filesToWatch.push(jsonPath); + if (jsonConfig.dependencies) { + mergeTypings(getKeys(jsonConfig.dependencies)); + } + if (jsonConfig.devDependencies) { + mergeTypings(getKeys(jsonConfig.devDependencies)); + } + if (jsonConfig.optionalDependencies) { + mergeTypings(getKeys(jsonConfig.optionalDependencies)); + } + if (jsonConfig.peerDependencies) { + mergeTypings(getKeys(jsonConfig.peerDependencies)); + } + } + } + + /** + * Infer typing names from given file names. For example, the file name "jquery-min.2.3.4.js" + * should be inferred to the 'jquery' typing name; and "angular-route.1.2.3.js" should be inferred + * to the 'angular-route' typing name. + * @param fileNames are the names for source files in the project + */ + function getTypingNamesFromSourceFileNames(fileNames: string[]) { + const jsFileNames = filter(fileNames, hasJavaScriptFileExtension); + const inferredTypingNames = map(jsFileNames, f => removeFileExtension(getBaseFileName(f.toLowerCase()))); + const cleanedTypingNames = map(inferredTypingNames, f => f.replace(/((?:\.|-)min(?=\.|$))|((?:-|\.)\d+)/g, "")); + if (safeList === undefined) { + mergeTypings(cleanedTypingNames); + } + else { + mergeTypings(filter(cleanedTypingNames, f => hasProperty(safeList, f))); + } + + const hasJsxFile = forEach(fileNames, f => scriptKindIs(f, /*LanguageServiceHost*/ undefined, ScriptKind.JSX)); + if (hasJsxFile) { + mergeTypings(["react"]); + } + } + + /** + * Infer typing names from node_module folder + * @param nodeModulesPath is the path to the "node_modules" folder + */ + function getTypingNamesFromNodeModuleFolder(nodeModulesPath: string) { + // Todo: add support for ModuleResolutionHost too + if (!host.directoryExists(nodeModulesPath)) { + return; + } + + const typingNames: string[] = []; + const fileNames = host.readDirectory(nodeModulesPath, "*.json", /*exclude*/ undefined, /*depth*/ 2); + for (const fileName of fileNames) { + const normalizedFileName = normalizePath(fileName); + if (getBaseFileName(normalizedFileName) !== "package.json") { + continue; + } + const result = readConfigFile(normalizedFileName, (path: string) => host.readFile(path)); + if (!result.config) { + continue; + } + const packageJson: PackageJson = result.config; + + // npm 3's package.json contains a "_requiredBy" field + // we should include all the top level module names for npm 2, and only module names whose + // "_requiredBy" field starts with "#" or equals "/" for npm 3. + if (packageJson._requiredBy && + filter(packageJson._requiredBy, (r: string) => r[0] === "#" || r === "/").length === 0) { + continue; + } + + // If the package has its own d.ts typings, those will take precedence. Otherwise the package name will be used + // to download d.ts files from DefinitelyTyped + if (!packageJson.name) { + continue; + } + if (packageJson.typings) { + const absolutePath = getNormalizedAbsolutePath(packageJson.typings, getDirectoryPath(normalizedFileName)); + inferredTypings[packageJson.name] = absolutePath; + } + else { + typingNames.push(packageJson.name); + } + } + mergeTypings(typingNames); + } + + } +} diff --git a/src/services/services.ts b/src/services/services.ts index 0fb7f329dfe..bdd3db708be 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -7,12 +7,13 @@ /// /// /// +/// /// /// namespace ts { /** The version of the language service API */ - export const servicesVersion = "0.4"; + export const servicesVersion = "0.5"; export interface Node { getSourceFile(): SourceFile; @@ -805,6 +806,7 @@ namespace ts { public identifierCount: number; public symbolCount: number; public version: string; + public scriptKind: ScriptKind; public languageVersion: ScriptTarget; public languageVariant: LanguageVariant; public identifiers: Map; @@ -1018,6 +1020,7 @@ namespace ts { getNewLine?(): string; getProjectVersion?(): string; getScriptFileNames(): string[]; + getScriptKind?(fileName: string): ScriptKind; getScriptVersion(fileName: string): string; getScriptSnapshot(fileName: string): IScriptSnapshot; getLocalizedDiagnosticMessages?(): any; @@ -1467,7 +1470,8 @@ namespace ts { fileName: string, compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, - version: string): SourceFile; + version: string, + scriptKind?: ScriptKind): SourceFile; /** * Request an updated version of an already existing SourceFile with a given fileName @@ -1485,7 +1489,8 @@ namespace ts { fileName: string, compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, - version: string): SourceFile; + version: string, + scriptKind?: ScriptKind): SourceFile; /** * Informs the DocumentRegistry that a file is not needed any longer. @@ -1656,6 +1661,7 @@ namespace ts { hostFileName: string; version: string; scriptSnapshot: IScriptSnapshot; + scriptKind: ScriptKind; } interface DocumentRegistryEntry { @@ -1750,7 +1756,8 @@ namespace ts { entry = { hostFileName: fileName, version: this.host.getScriptVersion(fileName), - scriptSnapshot: scriptSnapshot + scriptSnapshot: scriptSnapshot, + scriptKind: getScriptKind(fileName, this.host) }; } @@ -1816,12 +1823,13 @@ namespace ts { throw new Error("Could not find file: '" + fileName + "'."); } + const scriptKind = getScriptKind(fileName, this.host); const version = this.host.getScriptVersion(fileName); let sourceFile: SourceFile; if (this.currentFileName !== fileName) { // This is a new file, just parse it - sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, ScriptTarget.Latest, version, /*setNodeParents*/ true); + sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, ScriptTarget.Latest, version, /*setNodeParents*/ true, scriptKind); } else if (this.currentFileVersion !== version) { // This is the same file, just a newer version. Incrementally parse the file. @@ -1952,9 +1960,9 @@ namespace ts { return output.outputText; } - export function createLanguageServiceSourceFile(fileName: string, scriptSnapshot: IScriptSnapshot, scriptTarget: ScriptTarget, version: string, setNodeParents: boolean): SourceFile { + export function createLanguageServiceSourceFile(fileName: string, scriptSnapshot: IScriptSnapshot, scriptTarget: ScriptTarget, version: string, setNodeParents: boolean, scriptKind?: ScriptKind): SourceFile { const text = scriptSnapshot.getText(0, scriptSnapshot.getLength()); - const sourceFile = createSourceFile(fileName, text, scriptTarget, setNodeParents); + const sourceFile = createSourceFile(fileName, text, scriptTarget, setNodeParents, scriptKind); setSourceFileFields(sourceFile, scriptSnapshot, version); return sourceFile; } @@ -2016,7 +2024,7 @@ namespace ts { } // Otherwise, just create a new source file. - return createLanguageServiceSourceFile(sourceFile.fileName, scriptSnapshot, sourceFile.languageVersion, version, /*setNodeParents*/ true); + return createLanguageServiceSourceFile(sourceFile.fileName, scriptSnapshot, sourceFile.languageVersion, version, /*setNodeParents*/ true, sourceFile.scriptKind); } export function createDocumentRegistry(useCaseSensitiveFileNames?: boolean, currentDirectory = ""): DocumentRegistry { @@ -2058,12 +2066,12 @@ namespace ts { return JSON.stringify(bucketInfoArray, undefined, 2); } - function acquireDocument(fileName: string, compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, version: string): SourceFile { - return acquireOrUpdateDocument(fileName, compilationSettings, scriptSnapshot, version, /*acquiring*/ true); + function acquireDocument(fileName: string, compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile { + return acquireOrUpdateDocument(fileName, compilationSettings, scriptSnapshot, version, /*acquiring*/ true, scriptKind); } - function updateDocument(fileName: string, compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, version: string): SourceFile { - return acquireOrUpdateDocument(fileName, compilationSettings, scriptSnapshot, version, /*acquiring*/ false); + function updateDocument(fileName: string, compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, version: string, scriptKind?: ScriptKind): SourceFile { + return acquireOrUpdateDocument(fileName, compilationSettings, scriptSnapshot, version, /*acquiring*/ false, scriptKind); } function acquireOrUpdateDocument( @@ -2071,7 +2079,8 @@ namespace ts { compilationSettings: CompilerOptions, scriptSnapshot: IScriptSnapshot, version: string, - acquiring: boolean): SourceFile { + acquiring: boolean, + scriptKind?: ScriptKind): SourceFile { const bucket = getBucketForCompilationSettings(compilationSettings, /*createIfMissing*/ true); const path = toPath(fileName, currentDirectory, getCanonicalFileName); @@ -2080,7 +2089,7 @@ namespace ts { Debug.assert(acquiring, "How could we be trying to update a document that the registry doesn't have?"); // Have never seen this file with these settings. Create a new source file for it. - const sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, compilationSettings.target, version, /*setNodeParents*/ false); + const sourceFile = createLanguageServiceSourceFile(fileName, scriptSnapshot, compilationSettings.target, version, /*setNodeParents*/ false, scriptKind); entry = { sourceFile: sourceFile, @@ -2844,14 +2853,20 @@ namespace ts { // it's source file any more, and instead defers to DocumentRegistry to get // either version 1, version 2 (or some other version) depending on what the // host says should be used. - return documentRegistry.updateDocument(fileName, newSettings, hostFileInformation.scriptSnapshot, hostFileInformation.version); + + // We do not support the scenario where a host can modify a registered + // file's script kind, i.e. in one project some file is treated as ".ts" + // and in another as ".js" + Debug.assert(hostFileInformation.scriptKind === oldSourceFile.scriptKind, "Registered script kind (" + oldSourceFile.scriptKind + ") should match new script kind (" + hostFileInformation.scriptKind + ") for file: " + fileName); + + return documentRegistry.updateDocument(fileName, newSettings, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind); } // We didn't already have the file. Fall through and acquire it from the registry. } // Could not find this file in the old program, create a new SourceFile for it. - return documentRegistry.acquireDocument(fileName, newSettings, hostFileInformation.scriptSnapshot, hostFileInformation.version); + return documentRegistry.acquireDocument(fileName, newSettings, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind); } function sourceFileUpToDate(sourceFile: SourceFile): boolean { diff --git a/src/services/shims.ts b/src/services/shims.ts index 9ca3f19244d..a9ef3054838 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -55,6 +55,7 @@ namespace ts { /** Returns a JSON-encoded value of the type: string[] */ getScriptFileNames(): string; + getScriptKind?(fileName: string): ScriptKind; getScriptVersion(fileName: string): string; getScriptSnapshot(fileName: string): ScriptSnapshotShim; getLocalizedDiagnosticMessages(): string; @@ -77,7 +78,7 @@ namespace ts { * @param exclude A JSON encoded string[] containing the paths to exclude * when enumerating the directory. */ - readDirectory(rootDir: string, extension: string, exclude?: string): string; + readDirectory(rootDir: string, extension: string, exclude?: string, depth?: number): string; } /// @@ -229,6 +230,7 @@ namespace ts { getPreProcessedFileInfo(fileName: string, sourceText: IScriptSnapshot): string; getTSConfigFileInfo(fileName: string, sourceText: IScriptSnapshot): string; getDefaultCompilationSettings(): string; + discoverTypings(discoverTypingsJson: string): string; } function logInternalError(logger: Logger, err: Error) { @@ -344,6 +346,15 @@ namespace ts { return scriptSnapshot && new ScriptSnapshotShimAdapter(scriptSnapshot); } + public getScriptKind(fileName: string): ScriptKind { + if ("getScriptKind" in this.shimHost) { + return this.shimHost.getScriptKind(fileName); + } + else { + return ScriptKind.Unknown; + } + } + public getScriptVersion(fileName: string): string { return this.shimHost.getScriptVersion(fileName); } @@ -410,8 +421,16 @@ namespace ts { } } - public readDirectory(rootDir: string, extension: string, exclude: string[]): string[] { - const encoded = this.shimHost.readDirectory(rootDir, extension, JSON.stringify(exclude)); + public readDirectory(rootDir: string, extension: string, exclude: string[], depth?: number): string[] { + // Wrap the API changes for 2.0 release. This try/catch + // should be removed once TypeScript 2.0 has shipped. + let encoded: string; + try { + encoded = this.shimHost.readDirectory(rootDir, extension, JSON.stringify(exclude), depth); + } + catch (e) { + encoded = this.shimHost.readDirectory(rootDir, extension, JSON.stringify(exclude)); + } return JSON.parse(encoded); } @@ -941,6 +960,7 @@ namespace ts { if (result.error) { return { options: {}, + typingOptions: {}, files: [], errors: [realizeDiagnostic(result.error, "\r\n")] }; @@ -951,6 +971,7 @@ namespace ts { return { options: configFile.options, + typingOptions: configFile.typingOptions, files: configFile.fileNames, errors: realizeDiagnostics(configFile.errors, "\r\n") }; @@ -963,6 +984,21 @@ namespace ts { () => getDefaultCompilerOptions() ); } + + public discoverTypings(discoverTypingsJson: string): string { + const getCanonicalFileName = createGetCanonicalFileName(/*useCaseSensitivefileNames:*/ false); + return this.forwardJSONCall("discoverTypings()", () => { + const info = JSON.parse(discoverTypingsJson); + return ts.JsTyping.discoverTypings( + this.host, + info.fileNames, + toPath(info.projectRootPath, info.projectRootPath, getCanonicalFileName), + toPath(info.safeListPath, info.safeListPath, getCanonicalFileName), + info.packageNameToTypingLocation, + info.typingOptions, + info.compilerOptions); + }); + } } export class TypeScriptServicesFactory implements ShimFactory { diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 5dcee789243..43c4477115b 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -30,6 +30,7 @@ "shims.ts", "signatureHelp.ts", "utilities.ts", + "jsTyping.ts", "formatting/formatting.ts", "formatting/formattingContext.ts", "formatting/formattingRequestKind.ts", diff --git a/src/services/utilities.ts b/src/services/utilities.ts index afdc85fffd8..e423d870ca4 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -837,4 +837,19 @@ namespace ts { }; return name; } + + export function scriptKindIs(fileName: string, host: LanguageServiceHost, ...scriptKinds: ScriptKind[]): boolean { + const scriptKind = getScriptKind(fileName, host); + return forEach(scriptKinds, k => k === scriptKind); + } + + export function getScriptKind(fileName: string, host?: LanguageServiceHost): ScriptKind { + // First check to see if the script kind can be determined from the file name + var scriptKind = getScriptKindFromFileName(fileName); + if (scriptKind === ScriptKind.Unknown && host && host.getScriptKind) { + // Next check to see if the host can resolve the script kind + scriptKind = host.getScriptKind(fileName); + } + return ensureScriptKind(fileName, scriptKind); + } } \ No newline at end of file