Skip to content

Commit 95ae809

Browse files
authored
Merge pull request #7486 from zhengbli/fixLargeProjectTry2
Add upper limit for the program size to prevent tsserver from crashing
2 parents 7bb739f + 550d912 commit 95ae809

File tree

9 files changed

+267
-41
lines changed

9 files changed

+267
-41
lines changed

src/compiler/commandLineParser.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,10 @@ namespace ts {
412412
},
413413
description: Diagnostics.Specify_library_files_to_be_included_in_the_compilation_Colon
414414
},
415+
{
416+
name: "disableProjectSizeLimit",
417+
type: "boolean"
418+
},
415419
{
416420
name: "strictNullChecks",
417421
type: "boolean",
@@ -761,8 +765,10 @@ namespace ts {
761765
}
762766
}
763767

764-
filesSeen[fileName] = true;
765-
fileNames.push(fileName);
768+
if (!filesSeen[fileName]) {
769+
filesSeen[fileName] = true;
770+
fileNames.push(fileName);
771+
}
766772
}
767773
}
768774
}

src/compiler/sys.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace ts {
1515
useCaseSensitiveFileNames: boolean;
1616
write(s: string): void;
1717
readFile(path: string, encoding?: string): string;
18+
getFileSize?(path: string): number;
1819
writeFile(path: string, data: string, writeByteOrderMark?: boolean): void;
1920
watchFile?(path: string, callback: FileWatcherCallback): FileWatcher;
2021
watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher;
@@ -79,7 +80,7 @@ namespace ts {
7980
realpath(path: string): string;
8081
};
8182

82-
export var sys: System = (function () {
83+
export var sys: System = (function() {
8384

8485
function getWScriptSystem(): System {
8586

@@ -503,7 +504,7 @@ namespace ts {
503504
}
504505
);
505506
},
506-
resolvePath: function (path: string): string {
507+
resolvePath: function(path: string): string {
507508
return _path.resolve(path);
508509
},
509510
fileExists,
@@ -540,6 +541,16 @@ namespace ts {
540541
}
541542
return process.memoryUsage().heapUsed;
542543
},
544+
getFileSize(path) {
545+
try {
546+
const stat = _fs.statSync(path);
547+
if (stat.isFile()) {
548+
return stat.size;
549+
}
550+
}
551+
catch (e) { }
552+
return 0;
553+
},
543554
exit(exitCode?: number): void {
544555
process.exit(exitCode);
545556
},

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2562,6 +2562,7 @@ namespace ts {
25622562
/* @internal */ suppressOutputPathCheck?: boolean;
25632563
target?: ScriptTarget;
25642564
traceResolution?: boolean;
2565+
disableSizeLimit?: boolean;
25652566
types?: string[];
25662567
/** Paths used to used to compute primary types search locations */
25672568
typeRoots?: string[];

src/compiler/utilities.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2714,6 +2714,10 @@ namespace ts {
27142714
return forEach(supportedJavascriptExtensions, extension => fileExtensionIs(fileName, extension));
27152715
}
27162716

2717+
export function hasTypeScriptFileExtension(fileName: string) {
2718+
return forEach(supportedTypeScriptExtensions, extension => fileExtensionIs(fileName, extension));
2719+
}
2720+
27172721
/**
27182722
* Replace each instance of non-ascii characters by one, two, three, or four escape sequences
27192723
* representing the UTF-8 encoding of the character, and return the expanded char code list.

src/server/editorServices.ts

Lines changed: 157 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ namespace ts.server {
2727
});
2828
}
2929

30+
export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024;
31+
3032
export class ScriptInfo {
3133
svc: ScriptVersionCache;
3234
children: ScriptInfo[] = []; // files referenced by this file
@@ -385,12 +387,29 @@ namespace ts.server {
385387
/** Used for configured projects which may have multiple open roots */
386388
openRefCount = 0;
387389

388-
constructor(public projectService: ProjectService, public projectOptions?: ProjectOptions) {
390+
constructor(
391+
public projectService: ProjectService,
392+
public projectOptions?: ProjectOptions,
393+
public languageServiceDiabled = false) {
389394
if (projectOptions && projectOptions.files) {
390395
// If files are listed explicitly, allow all extensions
391396
projectOptions.compilerOptions.allowNonTsExtensions = true;
392397
}
393-
this.compilerService = new CompilerService(this, projectOptions && projectOptions.compilerOptions);
398+
if (!languageServiceDiabled) {
399+
this.compilerService = new CompilerService(this, projectOptions && projectOptions.compilerOptions);
400+
}
401+
}
402+
403+
enableLanguageService() {
404+
// if the language service was disabled, we should re-initiate the compiler service
405+
if (this.languageServiceDiabled) {
406+
this.compilerService = new CompilerService(this, this.projectOptions && this.projectOptions.compilerOptions);
407+
}
408+
this.languageServiceDiabled = false;
409+
}
410+
411+
disableLanguageService() {
412+
this.languageServiceDiabled = true;
394413
}
395414

396415
addOpenRef() {
@@ -407,19 +426,45 @@ namespace ts.server {
407426
}
408427

409428
getRootFiles() {
429+
if (this.languageServiceDiabled) {
430+
// When the languageService was disabled, only return file list if it is a configured project
431+
return this.projectOptions ? this.projectOptions.files : undefined;
432+
}
433+
410434
return this.compilerService.host.roots.map(info => info.fileName);
411435
}
412436

413437
getFileNames() {
438+
if (this.languageServiceDiabled) {
439+
if (!this.projectOptions) {
440+
return undefined;
441+
}
442+
443+
const fileNames: string[] = [];
444+
if (this.projectOptions && this.projectOptions.compilerOptions) {
445+
fileNames.push(getDefaultLibFilePath(this.projectOptions.compilerOptions));
446+
}
447+
ts.addRange(fileNames, this.projectOptions.files);
448+
return fileNames;
449+
}
450+
414451
const sourceFiles = this.program.getSourceFiles();
415452
return sourceFiles.map(sourceFile => sourceFile.fileName);
416453
}
417454

418455
getSourceFile(info: ScriptInfo) {
456+
if (this.languageServiceDiabled) {
457+
return undefined;
458+
}
459+
419460
return this.filenameToSourceFile[info.fileName];
420461
}
421462

422463
getSourceFileFromName(filename: string, requireOpen?: boolean) {
464+
if (this.languageServiceDiabled) {
465+
return undefined;
466+
}
467+
423468
const info = this.projectService.getScriptInfo(filename);
424469
if (info) {
425470
if ((!requireOpen) || info.isOpen) {
@@ -429,15 +474,27 @@ namespace ts.server {
429474
}
430475

431476
isRoot(info: ScriptInfo) {
477+
if (this.languageServiceDiabled) {
478+
return undefined;
479+
}
480+
432481
return this.compilerService.host.roots.some(root => root === info);
433482
}
434483

435484
removeReferencedFile(info: ScriptInfo) {
485+
if (this.languageServiceDiabled) {
486+
return;
487+
}
488+
436489
this.compilerService.host.removeReferencedFile(info);
437490
this.updateGraph();
438491
}
439492

440493
updateFileMap() {
494+
if (this.languageServiceDiabled) {
495+
return;
496+
}
497+
441498
this.filenameToSourceFile = {};
442499
const sourceFiles = this.program.getSourceFiles();
443500
for (let i = 0, len = sourceFiles.length; i < len; i++) {
@@ -447,11 +504,19 @@ namespace ts.server {
447504
}
448505

449506
finishGraph() {
507+
if (this.languageServiceDiabled) {
508+
return;
509+
}
510+
450511
this.updateGraph();
451512
this.compilerService.languageService.getNavigateToItems(".*");
452513
}
453514

454515
updateGraph() {
516+
if (this.languageServiceDiabled) {
517+
return;
518+
}
519+
455520
this.program = this.compilerService.languageService.getProgram();
456521
this.updateFileMap();
457522
}
@@ -462,15 +527,32 @@ namespace ts.server {
462527

463528
// add a root file to project
464529
addRoot(info: ScriptInfo) {
530+
if (this.languageServiceDiabled) {
531+
return;
532+
}
533+
465534
this.compilerService.host.addRoot(info);
466535
}
467536

468537
// remove a root file from project
469538
removeRoot(info: ScriptInfo) {
539+
if (this.languageServiceDiabled) {
540+
return;
541+
}
542+
470543
this.compilerService.host.removeRoot(info);
471544
}
472545

473546
filesToString() {
547+
if (this.languageServiceDiabled) {
548+
if (this.projectOptions) {
549+
let strBuilder = "";
550+
ts.forEach(this.projectOptions.files,
551+
file => { strBuilder += file + "\n"; });
552+
return strBuilder;
553+
}
554+
}
555+
474556
let strBuilder = "";
475557
ts.forEachValue(this.filenameToSourceFile,
476558
sourceFile => { strBuilder += sourceFile.fileName + "\n"; });
@@ -481,7 +563,9 @@ namespace ts.server {
481563
this.projectOptions = projectOptions;
482564
if (projectOptions.compilerOptions) {
483565
projectOptions.compilerOptions.allowNonTsExtensions = true;
484-
this.compilerService.setCompilerOptions(projectOptions.compilerOptions);
566+
if (!this.languageServiceDiabled) {
567+
this.compilerService.setCompilerOptions(projectOptions.compilerOptions);
568+
}
485569
}
486570
}
487571
}
@@ -1261,7 +1345,24 @@ namespace ts.server {
12611345
return { succeeded: true, projectOptions };
12621346
}
12631347
}
1348+
}
1349+
1350+
private exceedTotalNonTsFileSizeLimit(fileNames: string[]) {
1351+
let totalNonTsFileSize = 0;
1352+
if (!this.host.getFileSize) {
1353+
return false;
1354+
}
12641355

1356+
for (const fileName of fileNames) {
1357+
if (hasTypeScriptFileExtension(fileName)) {
1358+
continue;
1359+
}
1360+
totalNonTsFileSize += this.host.getFileSize(fileName);
1361+
if (totalNonTsFileSize > maxProgramSizeForNonTsFiles) {
1362+
return true;
1363+
}
1364+
}
1365+
return false;
12651366
}
12661367

12671368
openConfigFile(configFilename: string, clientFileName?: string): { success: boolean, project?: Project, errors?: Diagnostic[] } {
@@ -1270,6 +1371,19 @@ namespace ts.server {
12701371
return { success: false, errors };
12711372
}
12721373
else {
1374+
if (!projectOptions.compilerOptions.disableSizeLimit && projectOptions.compilerOptions.allowJs) {
1375+
if (this.exceedTotalNonTsFileSizeLimit(projectOptions.files)) {
1376+
const project = this.createProject(configFilename, projectOptions, /*languageServiceDisabled*/ true);
1377+
1378+
// for configured projects with languageService disabled, we only watch its config file,
1379+
// do not care about the directory changes in the folder.
1380+
project.projectFileWatcher = this.host.watchFile(
1381+
toPath(configFilename, configFilename, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)),
1382+
_ => this.watchedProjectConfigFileChanged(project));
1383+
return { success: true, project };
1384+
}
1385+
}
1386+
12731387
const project = this.createProject(configFilename, projectOptions);
12741388
let errors: Diagnostic[];
12751389
for (const rootFilename of projectOptions.files) {
@@ -1293,7 +1407,7 @@ namespace ts.server {
12931407
}
12941408
}
12951409

1296-
updateConfiguredProject(project: Project) {
1410+
updateConfiguredProject(project: Project): Diagnostic[] {
12971411
if (!this.host.fileExists(project.projectFilename)) {
12981412
this.log("Config file deleted");
12991413
this.removeProject(project);
@@ -1304,7 +1418,43 @@ namespace ts.server {
13041418
return errors;
13051419
}
13061420
else {
1307-
const oldFileNames = project.compilerService.host.roots.map(info => info.fileName);
1421+
if (projectOptions.compilerOptions && !projectOptions.compilerOptions.disableSizeLimit && this.exceedTotalNonTsFileSizeLimit(projectOptions.files)) {
1422+
project.setProjectOptions(projectOptions);
1423+
if (project.languageServiceDiabled) {
1424+
return;
1425+
}
1426+
1427+
project.disableLanguageService();
1428+
if (project.directoryWatcher) {
1429+
project.directoryWatcher.close();
1430+
project.directoryWatcher = undefined;
1431+
}
1432+
return;
1433+
}
1434+
1435+
if (project.languageServiceDiabled) {
1436+
project.setProjectOptions(projectOptions);
1437+
project.enableLanguageService();
1438+
project.directoryWatcher = this.host.watchDirectory(
1439+
ts.getDirectoryPath(project.projectFilename),
1440+
path => this.directoryWatchedForSourceFilesChanged(project, path),
1441+
/*recursive*/ true
1442+
);
1443+
1444+
for (const rootFilename of projectOptions.files) {
1445+
if (this.host.fileExists(rootFilename)) {
1446+
const info = this.openFile(rootFilename, /*openedByClient*/ false);
1447+
project.addRoot(info);
1448+
}
1449+
}
1450+
project.finishGraph();
1451+
return;
1452+
}
1453+
1454+
// if the project is too large, the root files might not have been all loaded if the total
1455+
// program size reached the upper limit. In that case project.projectOptions.files should
1456+
// be more precise. However this would only happen for configured project.
1457+
const oldFileNames = project.projectOptions ? project.projectOptions.files : project.compilerService.host.roots.map(info => info.fileName);
13081458
const newFileNames = ts.filter(projectOptions.files, f => this.host.fileExists(f));
13091459
const fileNamesToRemove = oldFileNames.filter(f => newFileNames.indexOf(f) < 0);
13101460
const fileNamesToAdd = newFileNames.filter(f => oldFileNames.indexOf(f) < 0);
@@ -1347,8 +1497,8 @@ namespace ts.server {
13471497
}
13481498
}
13491499

1350-
createProject(projectFilename: string, projectOptions?: ProjectOptions) {
1351-
const project = new Project(this, projectOptions);
1500+
createProject(projectFilename: string, projectOptions?: ProjectOptions, languageServiceDisabled?: boolean) {
1501+
const project = new Project(this, projectOptions, languageServiceDisabled);
13521502
project.projectFilename = projectFilename;
13531503
return project;
13541504
}

src/server/protocol.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ declare namespace ts.server.protocol {
123123
* The list of normalized file name in the project, including 'lib.d.ts'
124124
*/
125125
fileNames?: string[];
126+
/**
127+
* Indicates if the project has a active language service instance
128+
*/
129+
languageServiceDisabled?: boolean;
126130
}
127131

128132
/**

0 commit comments

Comments
 (0)