use unresolved imports as a source of used typings (#11828)

This commit is contained in:
Vladimir Matveev 2016-10-25 15:24:21 -07:00 committed by Mohamed Hegazy
parent d0170d1ac8
commit 7890f63250
16 changed files with 457 additions and 96 deletions

View File

@ -456,6 +456,44 @@ namespace ts {
return result;
}
export function arrayIsEqualTo<T>(array1: ReadonlyArray<T>, array2: ReadonlyArray<T>, equaler?: (a: T, b: T) => boolean): boolean {
if (!array1 || !array2) {
return array1 === array2;
}
if (array1.length !== array2.length) {
return false;
}
for (let i = 0; i < array1.length; i++) {
const equals = equaler ? equaler(array1[i], array2[i]) : array1[i] === array2[i];
if (!equals) {
return false;
}
}
return true;
}
export function changesAffectModuleResolution(oldOptions: CompilerOptions, newOptions: CompilerOptions): boolean {
return !oldOptions ||
(oldOptions.module !== newOptions.module) ||
(oldOptions.moduleResolution !== newOptions.moduleResolution) ||
(oldOptions.noResolve !== newOptions.noResolve) ||
(oldOptions.target !== newOptions.target) ||
(oldOptions.noLib !== newOptions.noLib) ||
(oldOptions.jsx !== newOptions.jsx) ||
(oldOptions.allowJs !== newOptions.allowJs) ||
(oldOptions.rootDir !== newOptions.rootDir) ||
(oldOptions.configFilePath !== newOptions.configFilePath) ||
(oldOptions.baseUrl !== newOptions.baseUrl) ||
(oldOptions.maxNodeModuleJsDepth !== newOptions.maxNodeModuleJsDepth) ||
!arrayIsEqualTo(oldOptions.lib, newOptions.lib) ||
!arrayIsEqualTo(oldOptions.typeRoots, newOptions.typeRoots) ||
!arrayIsEqualTo(oldOptions.rootDirs, newOptions.rootDirs) ||
!equalOwnProperties(oldOptions.paths, newOptions.paths);
}
/**
* Compacts an array, removing any falsey elements.
*/
@ -821,7 +859,7 @@ namespace ts {
return result;
}
export function extend<T1, T2>(first: T1 , second: T2): T1 & T2 {
export function extend<T1, T2>(first: T1, second: T2): T1 & T2 {
const result: T1 & T2 = <any>{};
for (const id in second) if (hasOwnProperty.call(second, id)) {
(result as any)[id] = (second as any)[id];
@ -974,8 +1012,8 @@ namespace ts {
Debug.assert(length >= 0, "length must be non-negative, is " + length);
if (file) {
Debug.assert(start <= file.text.length, `start must be within the bounds of the file. ${ start } > ${ file.text.length }`);
Debug.assert(end <= file.text.length, `end must be the bounds of the file. ${ end } > ${ file.text.length }`);
Debug.assert(start <= file.text.length, `start must be within the bounds of the file. ${start} > ${file.text.length}`);
Debug.assert(end <= file.text.length, `end must be the bounds of the file. ${end} > ${file.text.length}`);
}
let text = getLocaleSpecificMessage(message);
@ -1525,7 +1563,7 @@ namespace ts {
return undefined;
}
const replaceWildcardCharacter = usage === "files" ? replaceWildCardCharacterFiles : replaceWildCardCharacterOther;
const replaceWildcardCharacter = usage === "files" ? replaceWildCardCharacterFiles : replaceWildCardCharacterOther;
const singleAsteriskRegexFragment = usage === "files" ? singleAsteriskRegexFragmentFiles : singleAsteriskRegexFragmentOther;
/**
@ -1767,7 +1805,7 @@ namespace ts {
/** Must have ".d.ts" first because if ".ts" goes first, that will be detected as the extension instead of ".d.ts". */
export const supportedTypescriptExtensionsForExtractExtension = [".d.ts", ".ts", ".tsx"];
export const supportedJavascriptExtensions = [".js", ".jsx"];
const allSupportedExtensions = supportedTypeScriptExtensions.concat(supportedJavascriptExtensions);
const allSupportedExtensions = supportedTypeScriptExtensions.concat(supportedJavascriptExtensions);
export function getSupportedExtensions(options?: CompilerOptions): string[] {
return options && options.allowJs ? allSupportedExtensions : supportedTypeScriptExtensions;

View File

@ -462,21 +462,7 @@ namespace ts {
// check properties that can affect structure of the program or module resolution strategy
// if any of these properties has changed - structure cannot be reused
const oldOptions = oldProgram.getCompilerOptions();
if ((oldOptions.module !== options.module) ||
(oldOptions.moduleResolution !== options.moduleResolution) ||
(oldOptions.noResolve !== options.noResolve) ||
(oldOptions.target !== options.target) ||
(oldOptions.noLib !== options.noLib) ||
(oldOptions.jsx !== options.jsx) ||
(oldOptions.allowJs !== options.allowJs) ||
(oldOptions.rootDir !== options.rootDir) ||
(oldOptions.configFilePath !== options.configFilePath) ||
(oldOptions.baseUrl !== options.baseUrl) ||
(oldOptions.maxNodeModuleJsDepth !== options.maxNodeModuleJsDepth) ||
!arrayIsEqualTo(oldOptions.lib, options.lib) ||
!arrayIsEqualTo(oldOptions.typeRoots, options.typeRoots) ||
!arrayIsEqualTo(oldOptions.rootDirs, options.rootDirs) ||
!equalOwnProperties(oldOptions.paths, options.paths)) {
if (changesAffectModuleResolution(oldOptions, options)) {
return false;
}

View File

@ -3041,6 +3041,7 @@ namespace ts {
packageNameToTypingLocation: Map<string>; // 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
unresolvedImports: ReadonlyArray<string>; // List of unresolved module ids from imports
}
export enum ModuleKind {

View File

@ -83,25 +83,6 @@ namespace ts {
return node.end - node.pos;
}
export function arrayIsEqualTo<T>(array1: ReadonlyArray<T>, array2: ReadonlyArray<T>, equaler?: (a: T, b: T) => boolean): boolean {
if (!array1 || !array2) {
return array1 === array2;
}
if (array1.length !== array2.length) {
return false;
}
for (let i = 0; i < array1.length; i++) {
const equals = equaler ? equaler(array1[i], array2[i]) : array1[i] === array2[i];
if (!equals) {
return false;
}
}
return true;
}
export function hasResolvedModule(sourceFile: SourceFile, moduleNameText: string): boolean {
return !!(sourceFile && sourceFile.resolvedModules && sourceFile.resolvedModules[moduleNameText]);
}

View File

@ -94,8 +94,8 @@ namespace ts.projectSystem {
this.projectService.updateTypingsForProject(response);
}
enqueueInstallTypingsRequest(project: server.Project, typingOptions: TypingOptions) {
const request = server.createInstallTypingsRequest(project, typingOptions, this.globalTypingsCacheLocation);
enqueueInstallTypingsRequest(project: server.Project, typingOptions: TypingOptions, unresolvedImports: server.SortedReadonlyArray<string>) {
const request = server.createInstallTypingsRequest(project, typingOptions, unresolvedImports, this.globalTypingsCacheLocation);
this.install(request);
}

View File

@ -217,9 +217,9 @@ namespace ts.projectSystem {
constructor() {
super(host);
}
enqueueInstallTypingsRequest(project: server.Project, typingOptions: TypingOptions) {
enqueueInstallTypingsRequest(project: server.Project, typingOptions: TypingOptions, unresolvedImports: server.SortedReadonlyArray<string>) {
enqueueIsCalled = true;
super.enqueueInstallTypingsRequest(project, typingOptions);
super.enqueueInstallTypingsRequest(project, typingOptions, unresolvedImports);
}
executeRequest(requestKind: TI.RequestKind, _requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void {
const installedTypings = ["@types/jquery"];
@ -319,9 +319,9 @@ namespace ts.projectSystem {
constructor() {
super(host);
}
enqueueInstallTypingsRequest(project: server.Project, typingOptions: TypingOptions) {
enqueueInstallTypingsRequest(project: server.Project, typingOptions: TypingOptions, unresolvedImports: server.SortedReadonlyArray<string>) {
enqueueIsCalled = true;
super.enqueueInstallTypingsRequest(project, typingOptions);
super.enqueueInstallTypingsRequest(project, typingOptions, unresolvedImports);
}
executeRequest(requestKind: TI.RequestKind, _requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void {
const installedTypings: string[] = [];
@ -724,7 +724,7 @@ namespace ts.projectSystem {
it("Malformed package.json should be watched", () => {
const f = {
path: "/a/b/app.js",
content: "var x = require('commander')"
content: "var x = 1"
};
const brokenPackageJson = {
path: "/a/b/package.json",
@ -763,6 +763,133 @@ namespace ts.projectSystem {
service.checkNumberOfProjects({ inferredProjects: 1 });
checkProjectActualFiles(service.inferredProjects[0], [f.path, commander.path]);
});
it("should install typings for unresolved imports", () => {
const file = {
path: "/a/b/app.js",
content: `
import * as fs from "fs";
import * as commander from "commander";`
};
const cachePath = "/a/cache";
const node = {
path: cachePath + "/node_modules/@types/node/index.d.ts",
content: "export let x: number"
};
const commander = {
path: cachePath + "/node_modules/@types/commander/index.d.ts",
content: "export let y: string"
};
const host = createServerHost([file]);
const installer = new (class extends Installer {
constructor() {
super(host, { globalTypingsCacheLocation: cachePath });
}
executeRequest(requestKind: TI.RequestKind, _requestId: number, _args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) {
const installedTypings = ["@types/node", "@types/commander"];
const typingFiles = [node, commander];
executeCommand(this, host, installedTypings, typingFiles, requestKind, cb);
}
})();
const service = createProjectService(host, { typingsInstaller: installer });
service.openClientFile(file.path);
service.checkNumberOfProjects({ inferredProjects: 1 });
checkProjectActualFiles(service.inferredProjects[0], [file.path]);
installer.installAll([TI.NpmViewRequest, TI.NpmViewRequest], [TI.NpmInstallRequest]);
assert.isTrue(host.fileExists(node.path), "typings for 'node' should be created");
assert.isTrue(host.fileExists(commander.path), "typings for 'commander' should be created");
checkProjectActualFiles(service.inferredProjects[0], [file.path, node.path, commander.path]);
});
it("should pick typing names from non-relative unresolved imports", () => {
const f1 = {
path: "/a/b/app.js",
content: `
import * as a from "foo/a/a";
import * as b from "foo/a/b";
import * as c from "foo/a/c";
import * as d from "@bar/router/";
import * as e from "@bar/common/shared";
import * as e from "@bar/common/apps";
import * as f from "./lib"
`
};
const host = createServerHost([f1]);
const installer = new (class extends Installer {
constructor() {
super(host, { globalTypingsCacheLocation: "/tmp" });
}
executeRequest(requestKind: TI.RequestKind, _requestId: number, args: string[], _cwd: string, cb: server.typingsInstaller.RequestCompletedAction) {
if (requestKind === TI.NpmViewRequest) {
// args should have only non-scoped packages - scoped packages are not yet supported
assert.deepEqual(args, ["foo"]);
}
executeCommand(this, host, ["foo"], [], requestKind, cb);
}
})();
const projectService = createProjectService(host, { typingsInstaller: installer });
projectService.openClientFile(f1.path);
projectService.checkNumberOfProjects({ inferredProjects: 1 });
const proj = projectService.inferredProjects[0];
proj.updateGraph();
assert.deepEqual(
proj.getCachedUnresolvedImportsPerFile_TestOnly().get(<Path>f1.path),
["foo", "foo", "foo", "@bar/router", "@bar/common", "@bar/common"]
);
installer.installAll([TI.NpmViewRequest], [TI.NpmInstallRequest]);
});
it("cached unresolved typings are not recomputed if program structure did not change", () => {
const host = createServerHost([]);
const session = createSession(host);
const f = {
path: "/a/app.js",
content: `
import * as fs from "fs";
import * as cmd from "commander
`
};
session.executeCommand(<server.protocol.OpenRequest>{
seq: 1,
type: "request",
command: "open",
arguments: {
file: f.path,
fileContent: f.content
}
});
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { inferredProjects: 1 });
const proj = projectService.inferredProjects[0];
const version1 = proj.getCachedUnresolvedImportsPerFile_TestOnly().getVersion();
// make a change that should not affect the structure of the program
session.executeCommand(<server.protocol.ChangeRequest>{
seq: 2,
type: "request",
command: "change",
arguments: {
file: f.path,
insertString: "\nlet x = 1;",
line: 2,
offset: 0,
endLine: 2,
endOffset: 0
}
});
host.checkTimeoutQueueLength(1);
host.runQueuedTimeoutCallbacks();
const version2 = proj.getCachedUnresolvedImportsPerFile_TestOnly().getVersion();
assert.equal(version1, version2, "set of unresolved imports should not change");
});
});
describe("Validate package name:", () => {
@ -820,4 +947,35 @@ namespace ts.projectSystem {
assert.isTrue(messages.indexOf("Package name '; say Hello from TypeScript! #' contains non URI safe characters") > 0, "should find package with invalid name");
});
});
describe("discover typings", () => {
it("should return node for core modules", () => {
const f = {
path: "/a/b/app.js",
content: ""
};
const host = createServerHost([f]);
const cache = createMap<string>();
for (const name of JsTyping.nodeCoreModuleList) {
const result = JsTyping.discoverTypings(host, [f.path], getDirectoryPath(<Path>f.path), /*safeListPath*/ undefined, cache, { enableAutoDiscovery: true }, [name, "somename"]);
assert.deepEqual(result.newTypingNames.sort(), ["node", "somename"]);
}
});
it("should use cached locaitons", () => {
const f = {
path: "/a/b/app.js",
content: ""
};
const node = {
path: "/a/b/node.d.ts",
content: ""
};
const host = createServerHost([f, node]);
const cache = createMap<string>({ "node": node.path });
const result = JsTyping.discoverTypings(host, [f.path], getDirectoryPath(<Path>f.path), /*safeListPath*/ undefined, cache, { enableAutoDiscovery: true }, ["fs", "bar"]);
assert.deepEqual(result.cachedTypingPaths, [node.path]);
assert.deepEqual(result.newTypingNames, ["bar"]);
});
});
}

View File

@ -287,13 +287,13 @@ namespace ts.server {
}
switch (response.kind) {
case "set":
this.typingsCache.updateTypingsForProject(response.projectName, response.compilerOptions, response.typingOptions, response.typings);
project.updateGraph();
this.typingsCache.updateTypingsForProject(response.projectName, response.compilerOptions, response.typingOptions, response.unresolvedImports, response.typings);
break;
case "invalidate":
this.typingsCache.invalidateCachedTypingsForProject(project);
this.typingsCache.deleteTypingsForProject(response.projectName);
break;
}
project.updateGraph();
}
setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.ExternalProjectCompilerOptions): void {

View File

@ -9,6 +9,8 @@ namespace ts.server {
private readonly resolvedTypeReferenceDirectives: ts.FileMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>;
private readonly getCanonicalFileName: (fileName: string) => string;
private filesWithChangedSetOfUnresolvedImports: Path[];
private readonly resolveModuleName: typeof resolveModuleName;
readonly trace: (s: string) => void;
@ -52,12 +54,23 @@ namespace ts.server {
};
}
public startRecordingFilesWithChangedResolutions() {
this.filesWithChangedSetOfUnresolvedImports = [];
}
public finishRecordingFilesWithChangedResolutions() {
const collected = this.filesWithChangedSetOfUnresolvedImports;
this.filesWithChangedSetOfUnresolvedImports = undefined;
return collected;
}
private resolveNamesWithLocalCache<T extends { failedLookupLocations: string[] }, R extends { resolvedFileName?: string }>(
names: string[],
containingFile: string,
cache: ts.FileMap<Map<T>>,
loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T,
getResult: (s: T) => R): R[] {
getResult: (s: T) => R,
logChanges: boolean): R[] {
const path = toPath(containingFile, this.host.getCurrentDirectory(), this.getCanonicalFileName);
const currentResolutionsInFile = cache.get(path);
@ -79,6 +92,11 @@ namespace ts.server {
else {
newResolutions[name] = resolution = loader(name, containingFile, compilerOptions, this);
}
if (logChanges && this.filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) {
this.filesWithChangedSetOfUnresolvedImports.push(path);
// reset log changes to avoid recording the same file multiple times
logChanges = false;
}
}
ts.Debug.assert(resolution !== undefined);
@ -90,6 +108,24 @@ namespace ts.server {
cache.set(path, newResolutions);
return resolvedModules;
function resolutionIsEqualTo(oldResolution: T, newResolution: T): boolean {
if (oldResolution === newResolution) {
return true;
}
if (!oldResolution || !newResolution) {
return false;
}
const oldResult = getResult(oldResolution);
const newResult = getResult(newResolution);
if (oldResult === newResult) {
return true;
}
if (!oldResult || !newResult) {
return false;
}
return oldResult.resolvedFileName === newResult.resolvedFileName;
}
function moduleResolutionIsValid(resolution: T): boolean {
if (!resolution) {
return false;
@ -126,11 +162,11 @@ namespace ts.server {
}
resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] {
return this.resolveNamesWithLocalCache(typeDirectiveNames, containingFile, this.resolvedTypeReferenceDirectives, resolveTypeReferenceDirective, m => m.resolvedTypeReferenceDirective);
return this.resolveNamesWithLocalCache(typeDirectiveNames, containingFile, this.resolvedTypeReferenceDirectives, resolveTypeReferenceDirective, m => m.resolvedTypeReferenceDirective, /*logChanges*/ false);
}
resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModule[] {
return this.resolveNamesWithLocalCache(moduleNames, containingFile, this.resolvedModuleNames, this.resolveModuleName, m => m.resolvedModule);
return this.resolveNamesWithLocalCache(moduleNames, containingFile, this.resolvedModuleNames, this.resolveModuleName, m => m.resolvedModule, /*logChanges*/ true);
}
getDefaultLibFileName() {
@ -197,10 +233,11 @@ namespace ts.server {
}
setCompilationSettings(opt: ts.CompilerOptions) {
if (changesAffectModuleResolution(this.compilationSettings, opt)) {
this.resolvedModuleNames.clear();
this.resolvedTypeReferenceDirectives.clear();
}
this.compilationSettings = opt;
// conservatively assume that changing compiler options might affect module resolution strategy
this.resolvedModuleNames.clear();
this.resolvedTypeReferenceDirectives.clear();
}
}
}

View File

@ -62,12 +62,43 @@ namespace ts.server {
projectErrors: Diagnostic[];
}
export class UnresolvedImportsMap {
readonly perFileMap = createFileMap<ReadonlyArray<string>>();
private version = 0;
public clear() {
this.perFileMap.clear();
this.version = 0;
}
public getVersion() {
return this.version;
}
public remove(path: Path) {
this.perFileMap.remove(path);
this.version++;
}
public get(path: Path) {
return this.perFileMap.get(path);
}
public set(path: Path, value: ReadonlyArray<string>) {
this.perFileMap.set(path, value);
this.version++;
}
}
export abstract class Project {
private rootFiles: ScriptInfo[] = [];
private rootFilesMap: FileMap<ScriptInfo> = createFileMap<ScriptInfo>();
private lsHost: ServerLanguageServiceHost;
private program: ts.Program;
private cachedUnresolvedImportsPerFile = new UnresolvedImportsMap();
private lastCachedUnresolvedImportsList: SortedReadonlyArray<string>;
private languageService: LanguageService;
builder: Builder;
/**
@ -91,7 +122,7 @@ namespace ts.server {
*/
private projectStateVersion = 0;
private typingFiles: TypingsArray;
private typingFiles: SortedReadonlyArray<string>;
protected projectErrors: Diagnostic[];
@ -107,6 +138,10 @@ namespace ts.server {
return hasOneOrMoreJsAndNoTsFiles(this);
}
public getCachedUnresolvedImportsPerFile_TestOnly() {
return this.cachedUnresolvedImportsPerFile;
}
constructor(
readonly projectKind: ProjectKind,
readonly projectService: ProjectService,
@ -326,6 +361,7 @@ namespace ts.server {
removeFile(info: ScriptInfo, detachFromProject = true) {
this.removeRootFileIfNecessary(info);
this.lsHost.notifyFileRemoved(info);
this.cachedUnresolvedImportsPerFile.remove(info.path);
if (detachFromProject) {
info.detachFromProject(this);
@ -338,6 +374,38 @@ namespace ts.server {
this.projectStateVersion++;
}
private extractUnresolvedImportsFromSourceFile(file: SourceFile, result: string[]) {
const cached = this.cachedUnresolvedImportsPerFile.get(file.path);
if (cached) {
// found cached result - use it and return
for (const f of cached) {
result.push(f);
}
return;
}
let unresolvedImports: string[];
if (file.resolvedModules) {
for (const name in file.resolvedModules) {
// pick unresolved non-relative names
if (!file.resolvedModules[name] && !isExternalModuleNameRelative(name)) {
// for non-scoped names extract part up-to the first slash
// for scoped names - extract up to the second slash
let trimmed = name.trim();
let i = trimmed.indexOf("/");
if (i !== -1 && trimmed.charCodeAt(0) === CharacterCodes.at) {
i = trimmed.indexOf("/", i + 1);
}
if (i !== -1) {
trimmed = trimmed.substr(0, i);
}
(unresolvedImports || (unresolvedImports = [])).push(trimmed);
result.push(trimmed);
}
}
}
this.cachedUnresolvedImportsPerFile.set(file.path, unresolvedImports || emptyArray);
}
/**
* Updates set of files that contribute to this project
* @returns: true if set of files in the project stays the same and false - otherwise.
@ -346,8 +414,35 @@ namespace ts.server {
if (!this.languageServiceEnabled) {
return true;
}
this.lsHost.startRecordingFilesWithChangedResolutions();
let hasChanges = this.updateGraphWorker();
const cachedTypings = this.projectService.typingsCache.getTypingsForProject(this, hasChanges);
const changedFiles: ReadonlyArray<Path> = this.lsHost.finishRecordingFilesWithChangedResolutions() || emptyArray;
for (const file of changedFiles) {
// delete cached information for changed files
this.cachedUnresolvedImportsPerFile.remove(file);
}
// 1. no changes in structure, no changes in unresolved imports - do nothing
// 2. no changes in structure, unresolved imports were changed - collect unresolved imports for all files
// (can reuse cached imports for files that were not changed)
// 3. new files were added/removed, but compilation settings stays the same - collect unresolved imports for all new/modified files
// (can reuse cached imports for files that were not changed)
// 4. compilation settings were changed in the way that might affect module resolution - drop all caches and collect all data from the scratch
let unresolvedImports: SortedReadonlyArray<string>;
if (hasChanges || changedFiles.length) {
const result: string[] = [];
for (const sourceFile of this.program.getSourceFiles()) {
this.extractUnresolvedImportsFromSourceFile(sourceFile, result);
}
this.lastCachedUnresolvedImportsList = toSortedReadonlyArray(result);
}
unresolvedImports = this.lastCachedUnresolvedImportsList;
const cachedTypings = this.projectService.typingsCache.getTypingsForProject(this, unresolvedImports, hasChanges);
if (this.setTypings(cachedTypings)) {
hasChanges = this.updateGraphWorker() || hasChanges;
}
@ -357,7 +452,7 @@ namespace ts.server {
return !hasChanges;
}
private setTypings(typings: TypingsArray): boolean {
private setTypings(typings: SortedReadonlyArray<string>): boolean {
if (arrayIsEqualTo(this.typingFiles, typings)) {
return false;
}
@ -430,6 +525,11 @@ namespace ts.server {
compilerOptions.allowJs = true;
}
compilerOptions.allowNonTsExtensions = true;
if (changesAffectModuleResolution(this.compilerOptions, compilerOptions)) {
// reset cached unresolved imports if changes in compiler options affected module resolution
this.cachedUnresolvedImportsPerFile.clear();
this.lastCachedUnresolvedImportsList = undefined;
}
this.compilerOptions = compilerOptions;
this.lsHost.setCompilationSettings(compilerOptions);

View File

@ -181,12 +181,15 @@ namespace ts.server {
private installer: NodeChildProcess;
private socket: NodeSocket;
private projectService: ProjectService;
private throttledOperations: ThrottledOperations;
constructor(
private readonly logger: server.Logger,
host: ServerHost,
eventPort: number,
readonly globalTypingsCacheLocation: string,
private newLine: string) {
this.throttledOperations = new ThrottledOperations(host);
if (eventPort) {
const s = net.connect({ port: eventPort }, () => {
this.socket = s;
@ -231,12 +234,19 @@ namespace ts.server {
this.installer.send({ projectName: p.getProjectName(), kind: "closeProject" });
}
enqueueInstallTypingsRequest(project: Project, typingOptions: TypingOptions): void {
const request = createInstallTypingsRequest(project, typingOptions);
enqueueInstallTypingsRequest(project: Project, typingOptions: TypingOptions, unresolvedImports: SortedReadonlyArray<string>): void {
const request = createInstallTypingsRequest(project, typingOptions, unresolvedImports);
if (this.logger.hasLevel(LogLevel.verbose)) {
this.logger.info(`Sending request: ${JSON.stringify(request)}`);
if (this.logger.hasLevel(LogLevel.verbose)) {
this.logger.info(`Scheduling throttled operation: ${JSON.stringify(request)}`);
}
}
this.installer.send(request);
this.throttledOperations.schedule(project.getProjectName(), /*ms*/ 250, () => {
if (this.logger.hasLevel(LogLevel.verbose)) {
this.logger.info(`Sending request: ${JSON.stringify(request)}`);
}
this.installer.send(request);
});
}
private handleMessage(response: SetTypings | InvalidateCachedTypings) {
@ -266,7 +276,7 @@ namespace ts.server {
useSingleInferredProject,
disableAutomaticTypingAcquisition
? nullTypingsInstaller
: new NodeTypingsInstaller(logger, installerEventPort, globalTypingsCacheLocation, host.newLine),
: new NodeTypingsInstaller(logger, host, installerEventPort, globalTypingsCacheLocation, host.newLine),
Buffer.byteLength,
process.hrtime,
logger,

16
src/server/types.d.ts vendored
View File

@ -18,6 +18,10 @@ declare namespace ts.server {
trace?(s: string): void;
}
export interface SortedReadonlyArray<T> extends ReadonlyArray<T> {
" __sortedReadonlyArrayBrand": any;
}
export interface TypingInstallerRequest {
readonly projectName: string;
readonly kind: "discover" | "closeProject";
@ -26,8 +30,9 @@ declare namespace ts.server {
export interface DiscoverTypings extends TypingInstallerRequest {
readonly fileNames: string[];
readonly projectRootPath: ts.Path;
readonly typingOptions: ts.TypingOptions;
readonly compilerOptions: ts.CompilerOptions;
readonly typingOptions: ts.TypingOptions;
readonly unresolvedImports: SortedReadonlyArray<string>;
readonly cachePath?: string;
readonly kind: "discover";
}
@ -36,20 +41,23 @@ declare namespace ts.server {
readonly kind: "closeProject";
}
export type SetRequest = "set";
export type InvalidateRequest = "invalidate";
export interface TypingInstallerResponse {
readonly projectName: string;
readonly kind: "set" | "invalidate";
readonly kind: SetRequest | InvalidateRequest;
}
export interface SetTypings extends TypingInstallerResponse {
readonly typingOptions: ts.TypingOptions;
readonly compilerOptions: ts.CompilerOptions;
readonly typings: string[];
readonly kind: "set";
readonly unresolvedImports: SortedReadonlyArray<string>;
readonly kind: SetRequest;
}
export interface InvalidateCachedTypings extends TypingInstallerResponse {
readonly kind: "invalidate";
readonly kind: InvalidateRequest;
}
export interface InstallTypingHost extends JsTyping.TypingResolutionHost {

View File

@ -2,7 +2,7 @@
namespace ts.server {
export interface ITypingsInstaller {
enqueueInstallTypingsRequest(p: Project, typingOptions: TypingOptions): void;
enqueueInstallTypingsRequest(p: Project, typingOptions: TypingOptions, unresolvedImports: SortedReadonlyArray<string>): void;
attach(projectService: ProjectService): void;
onProjectClosed(p: Project): void;
readonly globalTypingsCacheLocation: string;
@ -18,7 +18,9 @@ namespace ts.server {
class TypingsCacheEntry {
readonly typingOptions: TypingOptions;
readonly compilerOptions: CompilerOptions;
readonly typings: TypingsArray;
readonly typings: SortedReadonlyArray<string>;
readonly unresolvedImports: SortedReadonlyArray<string>;
/* mainly useful for debugging */
poisoned: boolean;
}
@ -61,13 +63,11 @@ namespace ts.server {
return opt1.allowJs != opt2.allowJs;
}
export interface TypingsArray extends ReadonlyArray<string> {
" __typingsArrayBrand": any;
}
function toTypingsArray(arr: string[]): TypingsArray {
arr.sort();
return <any>arr;
function unresolvedImportsChanged(imports1: SortedReadonlyArray<string>, imports2: SortedReadonlyArray<string>): boolean {
if (imports1 === imports2) {
return false;
}
return !arrayIsEqualTo(imports1, imports2);
}
export class TypingsCache {
@ -76,7 +76,7 @@ namespace ts.server {
constructor(private readonly installer: ITypingsInstaller) {
}
getTypingsForProject(project: Project, forceRefresh: boolean): TypingsArray {
getTypingsForProject(project: Project, unresolvedImports: SortedReadonlyArray<string>, forceRefresh: boolean): SortedReadonlyArray<string> {
const typingOptions = project.getTypingOptions();
if (!typingOptions || !typingOptions.enableAutoDiscovery) {
@ -84,39 +84,41 @@ namespace ts.server {
}
const entry = this.perProjectCache[project.getProjectName()];
const result: TypingsArray = entry ? entry.typings : <any>emptyArray;
if (forceRefresh || !entry || typingOptionsChanged(typingOptions, entry.typingOptions) || compilerOptionsChanged(project.getCompilerOptions(), entry.compilerOptions)) {
const result: SortedReadonlyArray<string> = entry ? entry.typings : <any>emptyArray;
if (forceRefresh ||
!entry ||
typingOptionsChanged(typingOptions, entry.typingOptions) ||
compilerOptionsChanged(project.getCompilerOptions(), entry.compilerOptions) ||
unresolvedImportsChanged(unresolvedImports, entry.unresolvedImports)) {
// Note: entry is now poisoned since it does not really contain typings for a given combination of compiler options\typings options.
// instead it acts as a placeholder to prevent issuing multiple requests
this.perProjectCache[project.getProjectName()] = {
compilerOptions: project.getCompilerOptions(),
typingOptions,
typings: result,
unresolvedImports,
poisoned: true
};
// something has been changed, issue a request to update typings
this.installer.enqueueInstallTypingsRequest(project, typingOptions);
this.installer.enqueueInstallTypingsRequest(project, typingOptions, unresolvedImports);
}
return result;
}
invalidateCachedTypingsForProject(project: Project) {
const typingOptions = project.getTypingOptions();
if (!typingOptions.enableAutoDiscovery) {
return;
}
this.installer.enqueueInstallTypingsRequest(project, typingOptions);
}
updateTypingsForProject(projectName: string, compilerOptions: CompilerOptions, typingOptions: TypingOptions, newTypings: string[]) {
updateTypingsForProject(projectName: string, compilerOptions: CompilerOptions, typingOptions: TypingOptions, unresolvedImports: SortedReadonlyArray<string>, newTypings: string[]) {
this.perProjectCache[projectName] = {
compilerOptions,
typingOptions,
typings: toTypingsArray(newTypings),
typings: toSortedReadonlyArray(newTypings),
unresolvedImports,
poisoned: false
};
}
deleteTypingsForProject(projectName: string) {
delete this.perProjectCache[projectName];
}
onProjectClosed(project: Project) {
delete this.perProjectCache[project.getProjectName()];
this.installer.onProjectClosed(project);

View File

@ -26,6 +26,7 @@ namespace ts.server.typingsInstaller {
export enum PackageNameValidationResult {
Ok,
ScopedPackagesNotSupported,
EmptyName,
NameTooLong,
NameStartsWithDot,
NameStartsWithUnderscore,
@ -38,7 +39,9 @@ namespace ts.server.typingsInstaller {
* Validates package name using rules defined at https://docs.npmjs.com/files/package.json
*/
export function validatePackageName(packageName: string): PackageNameValidationResult {
Debug.assert(!!packageName, "Package name is not specified");
if (!packageName) {
return PackageNameValidationResult.EmptyName;
}
if (packageName.length > MaxPackageNameLength) {
return PackageNameValidationResult.NameTooLong;
}
@ -145,7 +148,8 @@ namespace ts.server.typingsInstaller {
req.projectRootPath,
this.safeListPath,
this.packageNameToTypingLocation,
req.typingOptions);
req.typingOptions,
req.unresolvedImports);
if (this.log.isEnabled()) {
this.log.writeLine(`Finished typings discovery: ${JSON.stringify(discoverTypingsResult)}`);
@ -238,6 +242,9 @@ namespace ts.server.typingsInstaller {
this.missingTypingsSet[typing] = true;
if (this.log.isEnabled()) {
switch (validationResult) {
case PackageNameValidationResult.EmptyName:
this.log.writeLine(`Package name '${typing}' cannot be empty`);
break;
case PackageNameValidationResult.NameTooLong:
this.log.writeLine(`Package name '${typing}' should be less than ${MaxPackageNameLength} characters`);
break;
@ -397,6 +404,7 @@ namespace ts.server.typingsInstaller {
typingOptions: request.typingOptions,
compilerOptions: request.compilerOptions,
typings,
unresolvedImports: request.unresolvedImports,
kind: "set"
};
}

View File

@ -45,12 +45,13 @@ namespace ts.server {
}
}
export function createInstallTypingsRequest(project: Project, typingOptions: TypingOptions, cachePath?: string): DiscoverTypings {
export function createInstallTypingsRequest(project: Project, typingOptions: TypingOptions, unresolvedImports: SortedReadonlyArray<string>, cachePath?: string): DiscoverTypings {
return {
projectName: project.getProjectName(),
fileNames: project.getFileNames(),
compilerOptions: project.getCompilerOptions(),
typingOptions,
unresolvedImports,
projectRootPath: getProjectRootPath(project),
cachePath,
kind: "discover"
@ -209,11 +210,15 @@ namespace ts.server {
export interface ServerLanguageServiceHost {
setCompilationSettings(options: CompilerOptions): void;
notifyFileRemoved(info: ScriptInfo): void;
startRecordingFilesWithChangedResolutions(): void;
finishRecordingFilesWithChangedResolutions(): Path[];
}
export const nullLanguageServiceHost: ServerLanguageServiceHost = {
setCompilationSettings: () => undefined,
notifyFileRemoved: () => undefined
notifyFileRemoved: () => undefined,
startRecordingFilesWithChangedResolutions: () => undefined,
finishRecordingFilesWithChangedResolutions: () => undefined
};
export interface ProjectOptions {
@ -240,6 +245,11 @@ namespace ts.server {
return `/dev/null/inferredProject${counter}*`;
}
export function toSortedReadonlyArray(arr: string[]): SortedReadonlyArray<string> {
arr.sort();
return <any>arr;
}
export class ThrottledOperations {
private pendingTimeouts: Map<any> = createMap<any>();
constructor(private readonly host: ServerHost) {

View File

@ -31,6 +31,17 @@ namespace ts.JsTyping {
const EmptySafeList: Map<string> = createMap<string>();
/* @internal */
export const nodeCoreModuleList: ReadonlyArray<string> = [
"buffer", "querystring", "events", "http", "cluster",
"zlib", "os", "https", "punycode", "repl", "readline",
"vm", "child_process", "url", "dns", "net",
"dgram", "fs", "path", "string_decoder", "tls",
"crypto", "stream", "util", "assert", "tty", "domain",
"constants", "process", "v8", "timers", "console"];
const nodeCoreModules = arrayToMap(<string[]>nodeCoreModuleList, x => x);
/**
* @param host is the object providing I/O related operations.
* @param fileNames are the file names that belong to the same project
@ -46,7 +57,8 @@ namespace ts.JsTyping {
projectRootPath: Path,
safeListPath: Path,
packageNameToTypingLocation: Map<string>,
typingOptions: TypingOptions):
typingOptions: TypingOptions,
unresolvedImports: ReadonlyArray<string>):
{ cachedTypingPaths: string[], newTypingNames: string[], filesToWatch: string[] } {
// A typing name to typing file path mapping
@ -92,6 +104,15 @@ namespace ts.JsTyping {
}
getTypingNamesFromSourceFileNames(fileNames);
// add typings for unresolved imports
if (unresolvedImports) {
for (const moduleId of unresolvedImports) {
const typingName = moduleId in nodeCoreModules ? "node" : moduleId;
if (!(typingName in inferredTypings)) {
inferredTypings[typingName] = undefined;
}
}
}
// Add the cached typing locations for inferred typings that are already installed
for (const name in packageNameToTypingLocation) {
if (name in inferredTypings && !inferredTypings[name]) {

View File

@ -1168,7 +1168,8 @@ namespace ts {
toPath(info.projectRootPath, info.projectRootPath, getCanonicalFileName),
toPath(info.safeListPath, info.safeListPath, getCanonicalFileName),
info.packageNameToTypingLocation,
info.typingOptions);
info.typingOptions,
info.unresolvedImports);
});
}
}