Merge pull request #7486 from zhengbli/fixLargeProjectTry2

Add upper limit for the program size to prevent tsserver from crashing
This commit is contained in:
Zhengbo Li 2016-06-15 17:31:15 -07:00 committed by GitHub
commit 95ae809faa
9 changed files with 267 additions and 41 deletions

View file

@ -412,6 +412,10 @@ namespace ts {
},
description: Diagnostics.Specify_library_files_to_be_included_in_the_compilation_Colon
},
{
name: "disableProjectSizeLimit",
type: "boolean"
},
{
name: "strictNullChecks",
type: "boolean",
@ -761,8 +765,10 @@ namespace ts {
}
}
filesSeen[fileName] = true;
fileNames.push(fileName);
if (!filesSeen[fileName]) {
filesSeen[fileName] = true;
fileNames.push(fileName);
}
}
}
}

View file

@ -15,6 +15,7 @@ namespace ts {
useCaseSensitiveFileNames: boolean;
write(s: string): void;
readFile(path: string, encoding?: string): string;
getFileSize?(path: string): number;
writeFile(path: string, data: string, writeByteOrderMark?: boolean): void;
watchFile?(path: string, callback: FileWatcherCallback): FileWatcher;
watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher;
@ -79,7 +80,7 @@ namespace ts {
realpath(path: string): string;
};
export var sys: System = (function () {
export var sys: System = (function() {
function getWScriptSystem(): System {
@ -503,7 +504,7 @@ namespace ts {
}
);
},
resolvePath: function (path: string): string {
resolvePath: function(path: string): string {
return _path.resolve(path);
},
fileExists,
@ -540,6 +541,16 @@ namespace ts {
}
return process.memoryUsage().heapUsed;
},
getFileSize(path) {
try {
const stat = _fs.statSync(path);
if (stat.isFile()) {
return stat.size;
}
}
catch (e) { }
return 0;
},
exit(exitCode?: number): void {
process.exit(exitCode);
},

View file

@ -2562,6 +2562,7 @@ namespace ts {
/* @internal */ suppressOutputPathCheck?: boolean;
target?: ScriptTarget;
traceResolution?: boolean;
disableSizeLimit?: boolean;
types?: string[];
/** Paths used to used to compute primary types search locations */
typeRoots?: string[];

View file

@ -2714,6 +2714,10 @@ namespace ts {
return forEach(supportedJavascriptExtensions, extension => fileExtensionIs(fileName, extension));
}
export function hasTypeScriptFileExtension(fileName: string) {
return forEach(supportedTypeScriptExtensions, extension => fileExtensionIs(fileName, extension));
}
/**
* Replace each instance of non-ascii characters by one, two, three, or four escape sequences
* representing the UTF-8 encoding of the character, and return the expanded char code list.

View file

@ -27,6 +27,8 @@ namespace ts.server {
});
}
export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024;
export class ScriptInfo {
svc: ScriptVersionCache;
children: ScriptInfo[] = []; // files referenced by this file
@ -385,12 +387,29 @@ namespace ts.server {
/** Used for configured projects which may have multiple open roots */
openRefCount = 0;
constructor(public projectService: ProjectService, public projectOptions?: ProjectOptions) {
constructor(
public projectService: ProjectService,
public projectOptions?: ProjectOptions,
public languageServiceDiabled = false) {
if (projectOptions && projectOptions.files) {
// If files are listed explicitly, allow all extensions
projectOptions.compilerOptions.allowNonTsExtensions = true;
}
this.compilerService = new CompilerService(this, projectOptions && projectOptions.compilerOptions);
if (!languageServiceDiabled) {
this.compilerService = new CompilerService(this, projectOptions && projectOptions.compilerOptions);
}
}
enableLanguageService() {
// if the language service was disabled, we should re-initiate the compiler service
if (this.languageServiceDiabled) {
this.compilerService = new CompilerService(this, this.projectOptions && this.projectOptions.compilerOptions);
}
this.languageServiceDiabled = false;
}
disableLanguageService() {
this.languageServiceDiabled = true;
}
addOpenRef() {
@ -407,19 +426,45 @@ namespace ts.server {
}
getRootFiles() {
if (this.languageServiceDiabled) {
// When the languageService was disabled, only return file list if it is a configured project
return this.projectOptions ? this.projectOptions.files : undefined;
}
return this.compilerService.host.roots.map(info => info.fileName);
}
getFileNames() {
if (this.languageServiceDiabled) {
if (!this.projectOptions) {
return undefined;
}
const fileNames: string[] = [];
if (this.projectOptions && this.projectOptions.compilerOptions) {
fileNames.push(getDefaultLibFilePath(this.projectOptions.compilerOptions));
}
ts.addRange(fileNames, this.projectOptions.files);
return fileNames;
}
const sourceFiles = this.program.getSourceFiles();
return sourceFiles.map(sourceFile => sourceFile.fileName);
}
getSourceFile(info: ScriptInfo) {
if (this.languageServiceDiabled) {
return undefined;
}
return this.filenameToSourceFile[info.fileName];
}
getSourceFileFromName(filename: string, requireOpen?: boolean) {
if (this.languageServiceDiabled) {
return undefined;
}
const info = this.projectService.getScriptInfo(filename);
if (info) {
if ((!requireOpen) || info.isOpen) {
@ -429,15 +474,27 @@ namespace ts.server {
}
isRoot(info: ScriptInfo) {
if (this.languageServiceDiabled) {
return undefined;
}
return this.compilerService.host.roots.some(root => root === info);
}
removeReferencedFile(info: ScriptInfo) {
if (this.languageServiceDiabled) {
return;
}
this.compilerService.host.removeReferencedFile(info);
this.updateGraph();
}
updateFileMap() {
if (this.languageServiceDiabled) {
return;
}
this.filenameToSourceFile = {};
const sourceFiles = this.program.getSourceFiles();
for (let i = 0, len = sourceFiles.length; i < len; i++) {
@ -447,11 +504,19 @@ namespace ts.server {
}
finishGraph() {
if (this.languageServiceDiabled) {
return;
}
this.updateGraph();
this.compilerService.languageService.getNavigateToItems(".*");
}
updateGraph() {
if (this.languageServiceDiabled) {
return;
}
this.program = this.compilerService.languageService.getProgram();
this.updateFileMap();
}
@ -462,15 +527,32 @@ namespace ts.server {
// add a root file to project
addRoot(info: ScriptInfo) {
if (this.languageServiceDiabled) {
return;
}
this.compilerService.host.addRoot(info);
}
// remove a root file from project
removeRoot(info: ScriptInfo) {
if (this.languageServiceDiabled) {
return;
}
this.compilerService.host.removeRoot(info);
}
filesToString() {
if (this.languageServiceDiabled) {
if (this.projectOptions) {
let strBuilder = "";
ts.forEach(this.projectOptions.files,
file => { strBuilder += file + "\n"; });
return strBuilder;
}
}
let strBuilder = "";
ts.forEachValue(this.filenameToSourceFile,
sourceFile => { strBuilder += sourceFile.fileName + "\n"; });
@ -481,7 +563,9 @@ namespace ts.server {
this.projectOptions = projectOptions;
if (projectOptions.compilerOptions) {
projectOptions.compilerOptions.allowNonTsExtensions = true;
this.compilerService.setCompilerOptions(projectOptions.compilerOptions);
if (!this.languageServiceDiabled) {
this.compilerService.setCompilerOptions(projectOptions.compilerOptions);
}
}
}
}
@ -1261,7 +1345,24 @@ namespace ts.server {
return { succeeded: true, projectOptions };
}
}
}
private exceedTotalNonTsFileSizeLimit(fileNames: string[]) {
let totalNonTsFileSize = 0;
if (!this.host.getFileSize) {
return false;
}
for (const fileName of fileNames) {
if (hasTypeScriptFileExtension(fileName)) {
continue;
}
totalNonTsFileSize += this.host.getFileSize(fileName);
if (totalNonTsFileSize > maxProgramSizeForNonTsFiles) {
return true;
}
}
return false;
}
openConfigFile(configFilename: string, clientFileName?: string): { success: boolean, project?: Project, errors?: Diagnostic[] } {
@ -1270,6 +1371,19 @@ namespace ts.server {
return { success: false, errors };
}
else {
if (!projectOptions.compilerOptions.disableSizeLimit && projectOptions.compilerOptions.allowJs) {
if (this.exceedTotalNonTsFileSizeLimit(projectOptions.files)) {
const project = this.createProject(configFilename, projectOptions, /*languageServiceDisabled*/ true);
// for configured projects with languageService disabled, we only watch its config file,
// do not care about the directory changes in the folder.
project.projectFileWatcher = this.host.watchFile(
toPath(configFilename, configFilename, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)),
_ => this.watchedProjectConfigFileChanged(project));
return { success: true, project };
}
}
const project = this.createProject(configFilename, projectOptions);
let errors: Diagnostic[];
for (const rootFilename of projectOptions.files) {
@ -1293,7 +1407,7 @@ namespace ts.server {
}
}
updateConfiguredProject(project: Project) {
updateConfiguredProject(project: Project): Diagnostic[] {
if (!this.host.fileExists(project.projectFilename)) {
this.log("Config file deleted");
this.removeProject(project);
@ -1304,7 +1418,43 @@ namespace ts.server {
return errors;
}
else {
const oldFileNames = project.compilerService.host.roots.map(info => info.fileName);
if (projectOptions.compilerOptions && !projectOptions.compilerOptions.disableSizeLimit && this.exceedTotalNonTsFileSizeLimit(projectOptions.files)) {
project.setProjectOptions(projectOptions);
if (project.languageServiceDiabled) {
return;
}
project.disableLanguageService();
if (project.directoryWatcher) {
project.directoryWatcher.close();
project.directoryWatcher = undefined;
}
return;
}
if (project.languageServiceDiabled) {
project.setProjectOptions(projectOptions);
project.enableLanguageService();
project.directoryWatcher = this.host.watchDirectory(
ts.getDirectoryPath(project.projectFilename),
path => this.directoryWatchedForSourceFilesChanged(project, path),
/*recursive*/ true
);
for (const rootFilename of projectOptions.files) {
if (this.host.fileExists(rootFilename)) {
const info = this.openFile(rootFilename, /*openedByClient*/ false);
project.addRoot(info);
}
}
project.finishGraph();
return;
}
// if the project is too large, the root files might not have been all loaded if the total
// program size reached the upper limit. In that case project.projectOptions.files should
// be more precise. However this would only happen for configured project.
const oldFileNames = project.projectOptions ? project.projectOptions.files : project.compilerService.host.roots.map(info => info.fileName);
const newFileNames = ts.filter(projectOptions.files, f => this.host.fileExists(f));
const fileNamesToRemove = oldFileNames.filter(f => newFileNames.indexOf(f) < 0);
const fileNamesToAdd = newFileNames.filter(f => oldFileNames.indexOf(f) < 0);
@ -1347,8 +1497,8 @@ namespace ts.server {
}
}
createProject(projectFilename: string, projectOptions?: ProjectOptions) {
const project = new Project(this, projectOptions);
createProject(projectFilename: string, projectOptions?: ProjectOptions, languageServiceDisabled?: boolean) {
const project = new Project(this, projectOptions, languageServiceDisabled);
project.projectFilename = projectFilename;
return project;
}

View file

@ -123,6 +123,10 @@ declare namespace ts.server.protocol {
* The list of normalized file name in the project, including 'lib.d.ts'
*/
fileNames?: string[];
/**
* Indicates if the project has a active language service instance
*/
languageServiceDisabled?: boolean;
}
/**

View file

@ -313,7 +313,7 @@ namespace ts.server {
private getDefinition(line: number, offset: number, fileName: string): protocol.FileSpan[] {
const file = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(file);
if (!project) {
if (!project || project.languageServiceDiabled) {
throw Errors.NoProject;
}
@ -335,7 +335,7 @@ namespace ts.server {
private getTypeDefinition(line: number, offset: number, fileName: string): protocol.FileSpan[] {
const file = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(file);
if (!project) {
if (!project || project.languageServiceDiabled) {
throw Errors.NoProject;
}
@ -358,7 +358,7 @@ namespace ts.server {
fileName = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(fileName);
if (!project) {
if (!project || project.languageServiceDiabled) {
throw Errors.NoProject;
}
@ -388,7 +388,7 @@ namespace ts.server {
fileName = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(fileName);
if (!project) {
if (!project || project.languageServiceDiabled) {
throw Errors.NoProject;
}
@ -423,15 +423,18 @@ namespace ts.server {
private getProjectInfo(fileName: string, needFileNameList: boolean): protocol.ProjectInfo {
fileName = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(fileName);
if (!project) {
throw Errors.NoProject;
}
const projectInfo: protocol.ProjectInfo = {
configFileName: project.projectFilename
configFileName: project.projectFilename,
languageServiceDisabled: project.languageServiceDiabled
};
if (needFileNameList) {
projectInfo.fileNames = project.getFileNames();
}
return projectInfo;
}
@ -439,11 +442,12 @@ namespace ts.server {
const file = ts.normalizePath(fileName);
const info = this.projectService.getScriptInfo(file);
const projects = this.projectService.findReferencingProjects(info);
if (!projects.length) {
const projectsWithLanguageServiceEnabeld = ts.filter(projects, p => !p.languageServiceDiabled);
if (projectsWithLanguageServiceEnabeld.length === 0) {
throw Errors.NoProject;
}
const defaultProject = projects[0];
const defaultProject = projectsWithLanguageServiceEnabeld[0];
// The rename info should be the same for every project
const defaultProjectCompilerService = defaultProject.compilerService;
const position = defaultProjectCompilerService.host.lineOffsetToPosition(file, line, offset);
@ -460,7 +464,7 @@ namespace ts.server {
}
const fileSpans = combineProjectOutput(
projects,
projectsWithLanguageServiceEnabeld,
(project: Project) => {
const compilerService = project.compilerService;
const renameLocations = compilerService.languageService.findRenameLocations(file, position, findInStrings, findInComments);
@ -521,11 +525,12 @@ namespace ts.server {
const file = ts.normalizePath(fileName);
const info = this.projectService.getScriptInfo(file);
const projects = this.projectService.findReferencingProjects(info);
if (!projects.length) {
const projectsWithLanguageServiceEnabeld = ts.filter(projects, p => !p.languageServiceDiabled);
if (projectsWithLanguageServiceEnabeld.length === 0) {
throw Errors.NoProject;
}
const defaultProject = projects[0];
const defaultProject = projectsWithLanguageServiceEnabeld[0];
const position = defaultProject.compilerService.host.lineOffsetToPosition(file, line, offset);
const nameInfo = defaultProject.compilerService.languageService.getQuickInfoAtPosition(file, position);
if (!nameInfo) {
@ -537,7 +542,7 @@ namespace ts.server {
const nameColStart = defaultProject.compilerService.host.positionToLineOffset(file, nameSpan.start).offset;
const nameText = defaultProject.compilerService.host.getScriptSnapshot(file).getText(nameSpan.start, ts.textSpanEnd(nameSpan));
const refs = combineProjectOutput<protocol.ReferencesResponseItem>(
projects,
projectsWithLanguageServiceEnabeld,
(project: Project) => {
const compilerService = project.compilerService;
const references = compilerService.languageService.getReferencesAtPosition(file, position);
@ -596,7 +601,7 @@ namespace ts.server {
private getQuickInfo(line: number, offset: number, fileName: string): protocol.QuickInfoResponseBody {
const file = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(file);
if (!project) {
if (!project || project.languageServiceDiabled) {
throw Errors.NoProject;
}
@ -622,7 +627,7 @@ namespace ts.server {
private getFormattingEditsForRange(line: number, offset: number, endLine: number, endOffset: number, fileName: string): protocol.CodeEdit[] {
const file = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(file);
if (!project) {
if (!project || project.languageServiceDiabled) {
throw Errors.NoProject;
}
@ -650,7 +655,7 @@ namespace ts.server {
const file = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(file);
if (!project) {
if (!project || project.languageServiceDiabled) {
throw Errors.NoProject;
}
@ -728,7 +733,7 @@ namespace ts.server {
}
const file = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(file);
if (!project) {
if (!project || project.languageServiceDiabled) {
throw Errors.NoProject;
}
@ -752,7 +757,7 @@ namespace ts.server {
entryNames: string[], fileName: string): protocol.CompletionEntryDetails[] {
const file = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(file);
if (!project) {
if (!project || project.languageServiceDiabled) {
throw Errors.NoProject;
}
@ -771,7 +776,7 @@ namespace ts.server {
private getSignatureHelpItems(line: number, offset: number, fileName: string): protocol.SignatureHelpItems {
const file = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(file);
if (!project) {
if (!project || project.languageServiceDiabled) {
throw Errors.NoProject;
}
@ -801,7 +806,7 @@ namespace ts.server {
const checkList = fileNames.reduce((accum: PendingErrorCheck[], fileName: string) => {
fileName = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(fileName);
if (project) {
if (project && !project.languageServiceDiabled) {
accum.push({ fileName, project });
}
return accum;
@ -815,7 +820,7 @@ namespace ts.server {
private change(line: number, offset: number, endLine: number, endOffset: number, insertString: string, fileName: string) {
const file = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(file);
if (project) {
if (project && !project.languageServiceDiabled) {
const compilerService = project.compilerService;
const start = compilerService.host.lineOffsetToPosition(file, line, offset);
const end = compilerService.host.lineOffsetToPosition(file, endLine, endOffset);
@ -831,7 +836,7 @@ namespace ts.server {
const file = ts.normalizePath(fileName);
const tmpfile = ts.normalizePath(tempFileName);
const project = this.projectService.getProjectForFile(file);
if (project) {
if (project && !project.languageServiceDiabled) {
this.changeSeq++;
// make sure no changes happen before this one is finished
project.compilerService.host.reloadScript(file, tmpfile, () => {
@ -845,7 +850,7 @@ namespace ts.server {
const tmpfile = ts.normalizePath(tempFileName);
const project = this.projectService.getProjectForFile(file);
if (project) {
if (project && !project.languageServiceDiabled) {
project.compilerService.host.saveTo(file, tmpfile);
}
}
@ -881,7 +886,7 @@ namespace ts.server {
private getNavigationBarItems(fileName: string): protocol.NavigationBarItem[] {
const file = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(file);
if (!project) {
if (!project || project.languageServiceDiabled) {
throw Errors.NoProject;
}
@ -898,13 +903,13 @@ namespace ts.server {
const file = ts.normalizePath(fileName);
const info = this.projectService.getScriptInfo(file);
const projects = this.projectService.findReferencingProjects(info);
const defaultProject = projects[0];
if (!defaultProject) {
const projectsWithLanguageServiceEnabeld = ts.filter(projects, p => !p.languageServiceDiabled);
if (projectsWithLanguageServiceEnabeld.length === 0) {
throw Errors.NoProject;
}
const allNavToItems = combineProjectOutput(
projects,
projectsWithLanguageServiceEnabeld,
(project: Project) => {
const compilerService = project.compilerService;
const navItems = compilerService.languageService.getNavigateToItems(searchValue, maxResultCount);
@ -956,7 +961,7 @@ namespace ts.server {
const file = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(file);
if (!project) {
if (!project || project.languageServiceDiabled) {
throw Errors.NoProject;
}
@ -975,7 +980,11 @@ namespace ts.server {
}
getDiagnosticsForProject(delay: number, fileName: string) {
const { fileNames } = this.getProjectInfo(fileName, /*needFileNameList*/ true);
const { fileNames, languageServiceDisabled } = this.getProjectInfo(fileName, /*needFileNameList*/ true);
if (languageServiceDisabled) {
return;
}
// No need to analyze lib.d.ts
let fileNamesInProject = fileNames.filter((value, index, array) => value.indexOf("lib.d.ts") < 0);

View file

@ -2983,7 +2983,8 @@ namespace ts {
oldSettings.moduleResolution !== newSettings.moduleResolution ||
oldSettings.noResolve !== newSettings.noResolve ||
oldSettings.jsx !== newSettings.jsx ||
oldSettings.allowJs !== newSettings.allowJs);
oldSettings.allowJs !== newSettings.allowJs ||
oldSettings.disableSizeLimit !== oldSettings.disableSizeLimit);
// Now create a new compiler
const compilerHost: CompilerHost = {

View file

@ -25,6 +25,7 @@ namespace ts {
interface FileOrFolder {
path: string;
content?: string;
fileSize?: number;
}
interface FSEntry {
@ -34,6 +35,7 @@ namespace ts {
interface File extends FSEntry {
content: string;
fileSize?: number;
}
interface Folder extends FSEntry {
@ -157,7 +159,7 @@ namespace ts {
const path = this.toPath(fileOrFolder.path);
const fullPath = getNormalizedAbsolutePath(fileOrFolder.path, this.currentDirectory);
if (typeof fileOrFolder.content === "string") {
const entry = { path, content: fileOrFolder.content, fullPath };
const entry = { path, content: fileOrFolder.content, fullPath, fileSize: fileOrFolder.fileSize };
this.fs.set(path, entry);
addFolder(getDirectoryPath(fullPath), this.toPath, this.fs).entries.push(entry);
}
@ -172,6 +174,17 @@ namespace ts {
return this.fs.contains(path) && isFile(this.fs.get(path));
};
getFileSize(s: string) {
const path = this.toPath(s);
if (this.fs.contains(path)) {
const entry = this.fs.get(path);
if (isFile(entry)) {
return entry.fileSize ? entry.fileSize : entry.content.length;
}
}
return undefined;
}
directoryExists(s: string) {
const path = this.toPath(s);
return this.fs.contains(path) && isFolder(this.fs.get(path));
@ -594,5 +607,32 @@ namespace ts {
checkNumberOfConfiguredProjects(projectService, 1);
checkNumberOfInferredProjects(projectService, 0);
});
it("should keep the configured project when the opened file is referenced by the project but not its root", () => {
const file1: FileOrFolder = {
path: "/a/b/main.ts",
content: "import { objA } from './obj-a';"
};
const file2: FileOrFolder = {
path: "/a/b/obj-a.ts",
content: `export const objA = Object.assign({foo: "bar"}, {bar: "baz"});`
};
const configFile: FileOrFolder = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {
"target": "es6"
},
"files": [ "main.ts" ]
}`
};
const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [file1, file2, configFile]);
const projectService = new server.ProjectService(host, nullLogger);
projectService.openClientFile(file1.path);
projectService.closeClientFile(file1.path);
projectService.openClientFile(file2.path);
checkNumberOfConfiguredProjects(projectService, 1);
checkNumberOfInferredProjects(projectService, 0);
});
});
}