Skip to content

Commit edbeab0

Browse files
authored
Merge pull request microsoft#10418 from zhengbli/tolerateConfigError
Tolerate errors in config file
2 parents d133b0e + a8ab52f commit edbeab0

File tree

5 files changed

+176
-93
lines changed

5 files changed

+176
-93
lines changed

src/harness/unittests/tsconfigParsing.ts

+20
Original file line numberDiff line numberDiff line change
@@ -181,5 +181,25 @@ namespace ts {
181181
["/d.ts", "/folder/e.ts"]
182182
);
183183
});
184+
185+
it("parse and re-emit tsconfig.json file with diagnostics", () => {
186+
const content = `{
187+
"compilerOptions": {
188+
"allowJs": true
189+
"outDir": "bin"
190+
}
191+
"files": ["file1.ts"]
192+
}`;
193+
const { configJsonObject, diagnostics } = parseAndReEmitConfigJSONFile(content);
194+
const expectedResult = {
195+
compilerOptions: {
196+
allowJs: true,
197+
outDir: "bin"
198+
},
199+
files: ["file1.ts"]
200+
};
201+
assert.isTrue(diagnostics.length === 2);
202+
assert.equal(JSON.stringify(configJsonObject), JSON.stringify(expectedResult));
203+
});
184204
});
185205
}

src/harness/unittests/tsserverProjectSystem.ts

+18
Original file line numberDiff line numberDiff line change
@@ -623,5 +623,23 @@ namespace ts {
623623
checkNumberOfConfiguredProjects(projectService, 1);
624624
checkNumberOfInferredProjects(projectService, 0);
625625
});
626+
627+
it("should tolerate config file errors and still try to build a project", () => {
628+
const configFile: FileOrFolder = {
629+
path: "/a/b/tsconfig.json",
630+
content: `{
631+
"compilerOptions": {
632+
"target": "es6",
633+
"allowAnything": true
634+
},
635+
"someOtherProperty": {}
636+
}`
637+
};
638+
const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, commonFile2, libFile, configFile]);
639+
const projectService = new server.ProjectService(host, nullLogger);
640+
projectService.openClientFile(commonFile1.path);
641+
checkNumberOfConfiguredProjects(projectService, 1);
642+
checkConfiguredProjectRootFiles(projectService.configuredProjects[0], [commonFile1.path, commonFile2.path]);
643+
});
626644
});
627645
}

src/server/editorServices.ts

+105-86
Original file line numberDiff line numberDiff line change
@@ -603,8 +603,11 @@ namespace ts.server {
603603
return projects.length > 1 ? deduplicate(result, areEqual) : result;
604604
}
605605

606+
export type ProjectServiceEvent =
607+
{ eventName: "context", data: { project: Project, fileName: string } } | { eventName: "configFileDiag", data: { triggerFile?: string, configFileName: string, diagnostics: Diagnostic[] } }
608+
606609
export interface ProjectServiceEventHandler {
607-
(eventName: string, project: Project, fileName: string): void;
610+
(event: ProjectServiceEvent): void;
608611
}
609612

610613
export interface HostConfiguration {
@@ -699,7 +702,8 @@ namespace ts.server {
699702
}
700703

701704
handleProjectFileListChanges(project: Project) {
702-
const { projectOptions } = this.configFileToProjectOptions(project.projectFilename);
705+
const { projectOptions, errors } = this.configFileToProjectOptions(project.projectFilename);
706+
this.reportConfigFileDiagnostics(project.projectFilename, errors);
703707

704708
const newRootFiles = projectOptions.files.map((f => this.getCanonicalFileName(f)));
705709
const currentRootFiles = project.getRootFiles().map((f => this.getCanonicalFileName(f)));
@@ -717,18 +721,32 @@ namespace ts.server {
717721
}
718722
}
719723

724+
reportConfigFileDiagnostics(configFileName: string, diagnostics: Diagnostic[], triggerFile?: string) {
725+
if (diagnostics && diagnostics.length > 0) {
726+
this.eventHandler({
727+
eventName: "configFileDiag",
728+
data: { configFileName, diagnostics, triggerFile }
729+
});
730+
}
731+
}
732+
720733
/**
721734
* This is the callback function when a watched directory has an added tsconfig file.
722735
*/
723736
directoryWatchedForTsconfigChanged(fileName: string) {
724-
if (ts.getBaseFileName(fileName) != "tsconfig.json") {
737+
if (ts.getBaseFileName(fileName) !== "tsconfig.json") {
725738
this.log(fileName + " is not tsconfig.json");
726739
return;
727740
}
728741

729742
this.log("Detected newly added tsconfig file: " + fileName);
730743

731-
const { projectOptions } = this.configFileToProjectOptions(fileName);
744+
const { projectOptions, errors } = this.configFileToProjectOptions(fileName);
745+
this.reportConfigFileDiagnostics(fileName, errors);
746+
747+
if (!projectOptions) {
748+
return;
749+
}
732750

733751
const rootFilesInTsconfig = projectOptions.files.map(f => this.getCanonicalFileName(f));
734752
const openFileRoots = this.openFileRoots.map(s => this.getCanonicalFileName(s.fileName));
@@ -748,10 +766,13 @@ namespace ts.server {
748766
return ts.normalizePath(name);
749767
}
750768

751-
watchedProjectConfigFileChanged(project: Project) {
769+
watchedProjectConfigFileChanged(project: Project): void {
752770
this.log("Config file changed: " + project.projectFilename);
753-
this.updateConfiguredProject(project);
771+
const configFileErrors = this.updateConfiguredProject(project);
754772
this.updateProjectStructure();
773+
if (configFileErrors && configFileErrors.length > 0) {
774+
this.eventHandler({ eventName: "configFileDiag", data: { triggerFile: project.projectFilename, configFileName: project.projectFilename, diagnostics: configFileErrors } });
775+
}
755776
}
756777

757778
log(msg: string, type = "Err") {
@@ -828,13 +849,13 @@ namespace ts.server {
828849
for (let j = 0, flen = this.openFileRoots.length; j < flen; j++) {
829850
const openFile = this.openFileRoots[j];
830851
if (this.eventHandler) {
831-
this.eventHandler("context", openFile.defaultProject, openFile.fileName);
852+
this.eventHandler({ eventName: "context", data: { project: openFile.defaultProject, fileName: openFile.fileName } });
832853
}
833854
}
834855
for (let j = 0, flen = this.openFilesReferenced.length; j < flen; j++) {
835856
const openFile = this.openFilesReferenced[j];
836857
if (this.eventHandler) {
837-
this.eventHandler("context", openFile.defaultProject, openFile.fileName);
858+
this.eventHandler({ eventName: "context", data: { project: openFile.defaultProject, fileName: openFile.fileName } });
838859
}
839860
}
840861
}
@@ -1218,7 +1239,7 @@ namespace ts.server {
12181239
const project = this.findConfiguredProjectByConfigFile(configFileName);
12191240
if (!project) {
12201241
const configResult = this.openConfigFile(configFileName, fileName);
1221-
if (!configResult.success) {
1242+
if (!configResult.project) {
12221243
return { configFileName, configFileErrors: configResult.errors };
12231244
}
12241245
else {
@@ -1234,11 +1255,12 @@ namespace ts.server {
12341255
else {
12351256
this.updateConfiguredProject(project);
12361257
}
1258+
return { configFileName };
12371259
}
12381260
else {
12391261
this.log("No config files found.");
12401262
}
1241-
return configFileName ? { configFileName } : {};
1263+
return {};
12421264
}
12431265

12441266
/**
@@ -1328,34 +1350,31 @@ namespace ts.server {
13281350
return undefined;
13291351
}
13301352

1331-
configFileToProjectOptions(configFilename: string): { succeeded: boolean, projectOptions?: ProjectOptions, errors?: Diagnostic[] } {
1353+
configFileToProjectOptions(configFilename: string): { projectOptions?: ProjectOptions, errors: Diagnostic[] } {
13321354
configFilename = ts.normalizePath(configFilename);
1355+
let errors: Diagnostic[] = [];
13331356
// file references will be relative to dirPath (or absolute)
13341357
const dirPath = ts.getDirectoryPath(configFilename);
13351358
const contents = this.host.readFile(configFilename);
1336-
const rawConfig: { config?: ProjectOptions; error?: Diagnostic; } = ts.parseConfigFileTextToJson(configFilename, contents);
1337-
if (rawConfig.error) {
1338-
return { succeeded: false, errors: [rawConfig.error] };
1359+
const { configJsonObject, diagnostics } = ts.parseAndReEmitConfigJSONFile(contents);
1360+
errors = concatenate(errors, diagnostics);
1361+
const parsedCommandLine = ts.parseJsonConfigFileContent(configJsonObject, this.host, dirPath, /*existingOptions*/ {}, configFilename);
1362+
errors = concatenate(errors, parsedCommandLine.errors);
1363+
Debug.assert(!!parsedCommandLine.fileNames);
1364+
1365+
if (parsedCommandLine.fileNames.length === 0) {
1366+
errors.push(createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename));
1367+
return { errors };
13391368
}
13401369
else {
1341-
const parsedCommandLine = ts.parseJsonConfigFileContent(rawConfig.config, this.host, dirPath, /*existingOptions*/ {}, configFilename);
1342-
Debug.assert(!!parsedCommandLine.fileNames);
1343-
1344-
if (parsedCommandLine.errors && (parsedCommandLine.errors.length > 0)) {
1345-
return { succeeded: false, errors: parsedCommandLine.errors };
1346-
}
1347-
else if (parsedCommandLine.fileNames.length === 0) {
1348-
const error = createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename);
1349-
return { succeeded: false, errors: [error] };
1350-
}
1351-
else {
1352-
const projectOptions: ProjectOptions = {
1353-
files: parsedCommandLine.fileNames,
1354-
wildcardDirectories: parsedCommandLine.wildcardDirectories,
1355-
compilerOptions: parsedCommandLine.options,
1356-
};
1357-
return { succeeded: true, projectOptions };
1358-
}
1370+
// if the project has some files, we can continue with the parsed options and tolerate
1371+
// errors in the parsedCommandLine
1372+
const projectOptions: ProjectOptions = {
1373+
files: parsedCommandLine.fileNames,
1374+
wildcardDirectories: parsedCommandLine.wildcardDirectories,
1375+
compilerOptions: parsedCommandLine.options,
1376+
};
1377+
return { projectOptions, errors };
13591378
}
13601379
}
13611380

@@ -1377,64 +1396,63 @@ namespace ts.server {
13771396
return false;
13781397
}
13791398

1380-
openConfigFile(configFilename: string, clientFileName?: string): { success: boolean, project?: Project, errors?: Diagnostic[] } {
1381-
const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(configFilename);
1382-
if (!succeeded) {
1383-
return { success: false, errors };
1399+
openConfigFile(configFilename: string, clientFileName?: string): { project?: Project, errors: Diagnostic[] } {
1400+
const parseConfigFileResult = this.configFileToProjectOptions(configFilename);
1401+
let errors = parseConfigFileResult.errors;
1402+
if (!parseConfigFileResult.projectOptions) {
1403+
return { errors };
13841404
}
1385-
else {
1386-
if (!projectOptions.compilerOptions.disableSizeLimit && projectOptions.compilerOptions.allowJs) {
1387-
if (this.exceedTotalNonTsFileSizeLimit(projectOptions.files)) {
1388-
const project = this.createProject(configFilename, projectOptions, /*languageServiceDisabled*/ true);
1389-
1390-
// for configured projects with languageService disabled, we only watch its config file,
1391-
// do not care about the directory changes in the folder.
1392-
project.projectFileWatcher = this.host.watchFile(
1393-
toPath(configFilename, configFilename, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)),
1394-
_ => this.watchedProjectConfigFileChanged(project));
1395-
return { success: true, project };
1396-
}
1405+
const projectOptions = parseConfigFileResult.projectOptions;
1406+
if (!projectOptions.compilerOptions.disableSizeLimit && projectOptions.compilerOptions.allowJs) {
1407+
if (this.exceedTotalNonTsFileSizeLimit(projectOptions.files)) {
1408+
const project = this.createProject(configFilename, projectOptions, /*languageServiceDisabled*/ true);
1409+
1410+
// for configured projects with languageService disabled, we only watch its config file,
1411+
// do not care about the directory changes in the folder.
1412+
project.projectFileWatcher = this.host.watchFile(
1413+
toPath(configFilename, configFilename, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)),
1414+
_ => this.watchedProjectConfigFileChanged(project));
1415+
return { project, errors };
13971416
}
1417+
}
13981418

1399-
const project = this.createProject(configFilename, projectOptions);
1400-
let errors: Diagnostic[];
1401-
for (const rootFilename of projectOptions.files) {
1402-
if (this.host.fileExists(rootFilename)) {
1403-
const info = this.openFile(rootFilename, /*openedByClient*/ clientFileName == rootFilename);
1404-
project.addRoot(info);
1405-
}
1406-
else {
1407-
(errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.File_0_not_found, rootFilename));
1408-
}
1419+
const project = this.createProject(configFilename, projectOptions);
1420+
for (const rootFilename of projectOptions.files) {
1421+
if (this.host.fileExists(rootFilename)) {
1422+
const info = this.openFile(rootFilename, /*openedByClient*/ clientFileName == rootFilename);
1423+
project.addRoot(info);
1424+
}
1425+
else {
1426+
(errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.File_0_not_found, rootFilename));
14091427
}
1410-
project.finishGraph();
1411-
project.projectFileWatcher = this.host.watchFile(configFilename, _ => this.watchedProjectConfigFileChanged(project));
1428+
}
1429+
project.finishGraph();
1430+
project.projectFileWatcher = this.host.watchFile(configFilename, _ => this.watchedProjectConfigFileChanged(project));
14121431

1413-
const configDirectoryPath = ts.getDirectoryPath(configFilename);
1432+
const configDirectoryPath = ts.getDirectoryPath(configFilename);
14141433

1415-
this.log("Add recursive watcher for: " + configDirectoryPath);
1416-
project.directoryWatcher = this.host.watchDirectory(
1417-
configDirectoryPath,
1418-
path => this.directoryWatchedForSourceFilesChanged(project, path),
1419-
/*recursive*/ true
1420-
);
1434+
this.log("Add recursive watcher for: " + configDirectoryPath);
1435+
project.directoryWatcher = this.host.watchDirectory(
1436+
configDirectoryPath,
1437+
path => this.directoryWatchedForSourceFilesChanged(project, path),
1438+
/*recursive*/ true
1439+
);
14211440

1422-
project.directoriesWatchedForWildcards = reduceProperties(createMap(projectOptions.wildcardDirectories), (watchers, flag, directory) => {
1423-
if (comparePaths(configDirectoryPath, directory, ".", !this.host.useCaseSensitiveFileNames) !== Comparison.EqualTo) {
1424-
const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0;
1425-
this.log(`Add ${ recursive ? "recursive " : ""}watcher for: ${directory}`);
1426-
watchers[directory] = this.host.watchDirectory(
1427-
directory,
1428-
path => this.directoryWatchedForSourceFilesChanged(project, path),
1429-
recursive
1430-
);
1431-
}
1441+
project.directoriesWatchedForWildcards = reduceProperties(createMap(projectOptions.wildcardDirectories), (watchers, flag, directory) => {
1442+
if (comparePaths(configDirectoryPath, directory, ".", !this.host.useCaseSensitiveFileNames) !== Comparison.EqualTo) {
1443+
const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0;
1444+
this.log(`Add ${ recursive ? "recursive " : ""}watcher for: ${directory}`);
1445+
watchers[directory] = this.host.watchDirectory(
1446+
directory,
1447+
path => this.directoryWatchedForSourceFilesChanged(project, path),
1448+
recursive
1449+
);
1450+
}
14321451

1433-
return watchers;
1434-
}, <Map<FileWatcher>>{});
1452+
return watchers;
1453+
}, <Map<FileWatcher>>{});
14351454

1436-
return { success: true, project: project, errors };
1437-
}
1455+
return { project: project, errors };
14381456
}
14391457

14401458
updateConfiguredProject(project: Project): Diagnostic[] {
@@ -1443,23 +1461,23 @@ namespace ts.server {
14431461
this.removeProject(project);
14441462
}
14451463
else {
1446-
const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(project.projectFilename);
1447-
if (!succeeded) {
1464+
const { projectOptions, errors } = this.configFileToProjectOptions(project.projectFilename);
1465+
if (!projectOptions) {
14481466
return errors;
14491467
}
14501468
else {
14511469
if (projectOptions.compilerOptions && !projectOptions.compilerOptions.disableSizeLimit && this.exceedTotalNonTsFileSizeLimit(projectOptions.files)) {
14521470
project.setProjectOptions(projectOptions);
14531471
if (project.languageServiceDiabled) {
1454-
return;
1472+
return errors;
14551473
}
14561474

14571475
project.disableLanguageService();
14581476
if (project.directoryWatcher) {
14591477
project.directoryWatcher.close();
14601478
project.directoryWatcher = undefined;
14611479
}
1462-
return;
1480+
return errors;
14631481
}
14641482

14651483
if (project.languageServiceDiabled) {
@@ -1478,7 +1496,7 @@ namespace ts.server {
14781496
}
14791497
}
14801498
project.finishGraph();
1481-
return;
1499+
return errors;
14821500
}
14831501

14841502
// if the project is too large, the root files might not have been all loaded if the total
@@ -1524,6 +1542,7 @@ namespace ts.server {
15241542
project.setProjectOptions(projectOptions);
15251543
project.finishGraph();
15261544
}
1545+
return errors;
15271546
}
15281547
}
15291548

0 commit comments

Comments
 (0)