diff --git a/Jakefile.js b/Jakefile.js index a602a2c459f02..db6b5f5567105 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -145,7 +145,8 @@ var harnessSources = harnessCoreSources.concat([ "transpile.ts", "reuseProgramStructure.ts", "cachingInServerLSHost.ts", - "moduleResolution.ts" + "moduleResolution.ts", + "tsconfigParsing.ts" ].map(function (f) { return path.join(unittestsDirectory, f); })).concat([ diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 84496fdbeb313..aaf3bf624b691 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -405,13 +405,41 @@ namespace ts { */ export function parseConfigFileTextToJson(fileName: string, jsonText: string): { config?: any; error?: Diagnostic } { try { - return { config: /\S/.test(jsonText) ? JSON.parse(jsonText) : {} }; + let jsonTextWithoutComments = removeComments(jsonText); + return { config: /\S/.test(jsonTextWithoutComments) ? JSON.parse(jsonTextWithoutComments) : {} }; } catch (e) { return { error: createCompilerDiagnostic(Diagnostics.Failed_to_parse_file_0_Colon_1, fileName, e.message) }; } } + + /** + * Remove the comments from a json like text. + * Comments can be single line comments (starting with # or //) or multiline comments using / * * / + * + * This method replace comment content by whitespace rather than completely remove them to keep positions in json parsing error reporting accurate. + */ + function removeComments(jsonText: string): string { + let output = ""; + let scanner = createScanner(ScriptTarget.ES5, /* skipTrivia */ false, LanguageVariant.Standard, jsonText); + let token: SyntaxKind; + while ((token = scanner.scan()) !== SyntaxKind.EndOfFileToken) { + switch (token) { + case SyntaxKind.SingleLineCommentTrivia: + case SyntaxKind.MultiLineCommentTrivia: + // replace comments with whitespace to preserve original character positions + output += scanner.getTokenText().replace(/\S/g, " "); + break; + default: + output += scanner.getTokenText(); + break; + } + } + return output; + } + + /** * Parse the contents of a config file (tsconfig.json). * @param json The contents of the config file to parse diff --git a/tests/cases/unittests/tsconfigParsing.ts b/tests/cases/unittests/tsconfigParsing.ts new file mode 100644 index 0000000000000..3603d22f31457 --- /dev/null +++ b/tests/cases/unittests/tsconfigParsing.ts @@ -0,0 +1,86 @@ +/// +/// + +namespace ts { + describe('parseConfigFileTextToJson', () => { + function assertParseResult(jsonText: string, expectedConfigObject: { config?: any; error?: Diagnostic }) { + let parsed = ts.parseConfigFileTextToJson("/apath/tsconfig.json", jsonText); + assert.equal(JSON.stringify(parsed), JSON.stringify(expectedConfigObject)); + } + + function assertParseError(jsonText: string) { + let parsed = ts.parseConfigFileTextToJson("/apath/tsconfig.json", jsonText); + assert.isTrue(undefined === parsed.config); + assert.isTrue(undefined !== parsed.error); + } + + it("returns empty config for file with only whitespaces", () => { + assertParseResult("", { config : {} }); + assertParseResult(" ", { config : {} }); + }); + + it("returns empty config for file with comments only", () => { + assertParseResult("// Comment", { config: {} }); + assertParseResult("/* Comment*/", { config: {} }); + }); + + it("returns empty config when config is empty object", () => { + assertParseResult("{}", { config: {} }); + }); + + it("returns config object without comments", () => { + assertParseResult( + `{ // Excluded files + "exclude": [ + // Exclude d.ts + "file.d.ts" + ] + }`, { config: { exclude: ["file.d.ts"] } }); + + assertParseResult( + `{ + /* Excluded + Files + */ + "exclude": [ + /* multiline comments can be in the middle of a line */"file.d.ts" + ] + }`, { config: { exclude: ["file.d.ts"] } }); + }); + + it("keeps string content untouched", () => { + assertParseResult( + `{ + "exclude": [ + "xx//file.d.ts" + ] + }`, { config: { exclude: ["xx//file.d.ts"] } }); + assertParseResult( + `{ + "exclude": [ + "xx/*file.d.ts*/" + ] + }`, { config: { exclude: ["xx/*file.d.ts*/"] } }); + }); + + it("handles escaped characters in strings correctly", () => { + assertParseResult( + `{ + "exclude": [ + "xx\\"//files" + ] + }`, { config: { exclude: ["xx\"//files"] } }); + + assertParseResult( + `{ + "exclude": [ + "xx\\\\" // end of line comment + ] + }`, { config: { exclude: ["xx\\"] } }); + }); + + it("returns object with error when json is invalid", () => { + assertParseError("invalid"); + }); + }); +}