Skip to content

Commit 110b5df

Browse files
author
zhengbli
committed
Use TS parser to tolerate more errors in tsconfig.json
1 parent 3b3def0 commit 110b5df

File tree

3 files changed

+128
-77
lines changed

3 files changed

+128
-77
lines changed

src/harness/unittests/tsconfigParsing.ts

Lines changed: 20 additions & 0 deletions
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/server/editorServices.ts

Lines changed: 88 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,8 @@ namespace ts.server {
702702
}
703703

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

707708
const newRootFiles = projectOptions.files.map((f => this.getCanonicalFileName(f)));
708709
const currentRootFiles = project.getRootFiles().map((f => this.getCanonicalFileName(f)));
@@ -720,18 +721,32 @@ namespace ts.server {
720721
}
721722
}
722723

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+
723733
/**
724734
* This is the callback function when a watched directory has an added tsconfig file.
725735
*/
726736
directoryWatchedForTsconfigChanged(fileName: string) {
727-
if (ts.getBaseFileName(fileName) != "tsconfig.json") {
737+
if (ts.getBaseFileName(fileName) !== "tsconfig.json") {
728738
this.log(fileName + " is not tsconfig.json");
729739
return;
730740
}
731741

732742
this.log("Detected newly added tsconfig file: " + fileName);
733743

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

736751
const rootFilesInTsconfig = projectOptions.files.map(f => this.getCanonicalFileName(f));
737752
const openFileRoots = this.openFileRoots.map(s => this.getCanonicalFileName(s.fileName));
@@ -1224,7 +1239,7 @@ namespace ts.server {
12241239
const project = this.findConfiguredProjectByConfigFile(configFileName);
12251240
if (!project) {
12261241
const configResult = this.openConfigFile(configFileName, fileName);
1227-
if (!configResult.success) {
1242+
if (!configResult.project) {
12281243
return { configFileName, configFileErrors: configResult.errors };
12291244
}
12301245
else {
@@ -1335,33 +1350,31 @@ namespace ts.server {
13351350
return undefined;
13361351
}
13371352

1338-
configFileToProjectOptions(configFilename: string): { succeeded: boolean, projectOptions?: ProjectOptions, errors: Diagnostic[] } {
1353+
configFileToProjectOptions(configFilename: string): { projectOptions?: ProjectOptions, errors: Diagnostic[] } {
13391354
configFilename = ts.normalizePath(configFilename);
1355+
let errors: Diagnostic[] = [];
13401356
// file references will be relative to dirPath (or absolute)
13411357
const dirPath = ts.getDirectoryPath(configFilename);
13421358
const contents = this.host.readFile(configFilename);
1343-
const rawConfig: { config?: ProjectOptions; error?: Diagnostic; } = ts.parseConfigFileTextToJson(configFilename, contents);
1344-
if (rawConfig.error) {
1345-
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 };
13461368
}
13471369
else {
1348-
const parsedCommandLine = ts.parseJsonConfigFileContent(rawConfig.config, this.host, dirPath, /*existingOptions*/ {}, configFilename);
1349-
Debug.assert(!!parsedCommandLine.fileNames);
1350-
1351-
if (parsedCommandLine.fileNames.length === 0) {
1352-
const error = createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename);
1353-
return { succeeded: false, errors: concatenate(parsedCommandLine.errors, [error]) };
1354-
}
1355-
else {
1356-
// if the project has some files, we can continue with the parsed options and tolerate
1357-
// errors in the parsedCommandLine
1358-
const projectOptions: ProjectOptions = {
1359-
files: parsedCommandLine.fileNames,
1360-
wildcardDirectories: parsedCommandLine.wildcardDirectories,
1361-
compilerOptions: parsedCommandLine.options,
1362-
};
1363-
return { succeeded: true, projectOptions, errors: parsedCommandLine.errors };
1364-
}
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 };
13651378
}
13661379
}
13671380

@@ -1383,65 +1396,63 @@ namespace ts.server {
13831396
return false;
13841397
}
13851398

1386-
openConfigFile(configFilename: string, clientFileName?: string): { success: boolean, project?: Project, errors: Diagnostic[] } {
1387-
const { succeeded, projectOptions, errors: errorsFromConfigFile } = this.configFileToProjectOptions(configFilename);
1388-
// Note: even if "succeeded"" is true, "errors" may still exist, as they are just tolerated
1389-
if (!succeeded) {
1390-
return { success: false, errors: errorsFromConfigFile };
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 };
13911404
}
1392-
else {
1393-
if (!projectOptions.compilerOptions.disableSizeLimit && projectOptions.compilerOptions.allowJs) {
1394-
if (this.exceedTotalNonTsFileSizeLimit(projectOptions.files)) {
1395-
const project = this.createProject(configFilename, projectOptions, /*languageServiceDisabled*/ true);
1396-
1397-
// for configured projects with languageService disabled, we only watch its config file,
1398-
// do not care about the directory changes in the folder.
1399-
project.projectFileWatcher = this.host.watchFile(
1400-
toPath(configFilename, configFilename, createGetCanonicalFileName(sys.useCaseSensitiveFileNames)),
1401-
_ => this.watchedProjectConfigFileChanged(project));
1402-
return { success: true, project, errors: errorsFromConfigFile };
1403-
}
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 };
14041416
}
1417+
}
14051418

1406-
const project = this.createProject(configFilename, projectOptions);
1407-
let errors: Diagnostic[];
1408-
for (const rootFilename of projectOptions.files) {
1409-
if (this.host.fileExists(rootFilename)) {
1410-
const info = this.openFile(rootFilename, /*openedByClient*/ clientFileName == rootFilename);
1411-
project.addRoot(info);
1412-
}
1413-
else {
1414-
(errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.File_0_not_found, rootFilename));
1415-
}
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));
14161427
}
1417-
project.finishGraph();
1418-
project.projectFileWatcher = this.host.watchFile(configFilename, _ => this.watchedProjectConfigFileChanged(project));
1428+
}
1429+
project.finishGraph();
1430+
project.projectFileWatcher = this.host.watchFile(configFilename, _ => this.watchedProjectConfigFileChanged(project));
14191431

1420-
const configDirectoryPath = ts.getDirectoryPath(configFilename);
1432+
const configDirectoryPath = ts.getDirectoryPath(configFilename);
14211433

1422-
this.log("Add recursive watcher for: " + configDirectoryPath);
1423-
project.directoryWatcher = this.host.watchDirectory(
1424-
configDirectoryPath,
1425-
path => this.directoryWatchedForSourceFilesChanged(project, path),
1426-
/*recursive*/ true
1427-
);
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+
);
14281440

1429-
project.directoriesWatchedForWildcards = reduceProperties(createMap(projectOptions.wildcardDirectories), (watchers, flag, directory) => {
1430-
if (comparePaths(configDirectoryPath, directory, ".", !this.host.useCaseSensitiveFileNames) !== Comparison.EqualTo) {
1431-
const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0;
1432-
this.log(`Add ${ recursive ? "recursive " : ""}watcher for: ${directory}`);
1433-
watchers[directory] = this.host.watchDirectory(
1434-
directory,
1435-
path => this.directoryWatchedForSourceFilesChanged(project, path),
1436-
recursive
1437-
);
1438-
}
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+
}
14391451

1440-
return watchers;
1441-
}, <Map<FileWatcher>>{});
1452+
return watchers;
1453+
}, <Map<FileWatcher>>{});
14421454

1443-
return { success: true, project: project, errors: concatenate(errors, errorsFromConfigFile) };
1444-
}
1455+
return { project: project, errors };
14451456
}
14461457

14471458
updateConfiguredProject(project: Project): Diagnostic[] {
@@ -1450,8 +1461,8 @@ namespace ts.server {
14501461
this.removeProject(project);
14511462
}
14521463
else {
1453-
const { succeeded, projectOptions, errors } = this.configFileToProjectOptions(project.projectFilename);
1454-
if (!succeeded) {
1464+
const { projectOptions, errors } = this.configFileToProjectOptions(project.projectFilename);
1465+
if (!projectOptions) {
14551466
return errors;
14561467
}
14571468
else {

src/services/utilities.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,4 +925,24 @@ namespace ts {
925925
}
926926
return ensureScriptKind(fileName, scriptKind);
927927
}
928+
929+
export function parseAndReEmitConfigJSONFile(content: string) {
930+
const options: TranspileOptions = {
931+
fileName: "config.js",
932+
compilerOptions: {
933+
target: ScriptTarget.ES6
934+
},
935+
reportDiagnostics: true
936+
};
937+
const { outputText, diagnostics } = ts.transpileModule("(" + content + ")", options);
938+
// Becasue the content was wrapped in "()", the start position of diagnostics needs to be subtract by 1
939+
// also, the emitted result will have "(" in the beginning and ");" in the end. We need to strip these
940+
// as well
941+
const trimmedOutput = outputText.trim();
942+
const configJsonObject = JSON.parse(trimmedOutput.substring(1, trimmedOutput.length - 2));
943+
for (const diagnostic of diagnostics) {
944+
diagnostic.start = diagnostic.start - 1;
945+
}
946+
return { configJsonObject, diagnostics };
947+
}
928948
}

0 commit comments

Comments
 (0)