From b60d88fa80bdd09029610584603734419ef89ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Arod?= Date: Thu, 29 Oct 2015 12:56:13 +0100 Subject: [PATCH] Allow comments in tsconfig.json issue #4987 --- Jakefile.js | 3 +- src/compiler/commandLineParser.ts | 94 +++++++++++++++++++++++- tests/cases/unittests/tsconfigParsing.ts | 84 +++++++++++++++++++++ 3 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 tests/cases/unittests/tsconfigParsing.ts diff --git a/Jakefile.js b/Jakefile.js index a602a2c459f..db6b5f55671 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 84496fdbeb3..e5950db05b6 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -405,13 +405,105 @@ 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 result = ""; + let processingString = false; + let processingSingleLineComment = false; + let processingMultiLineComment = false; + for (let i = 0; i < jsonText.length; i++) { + let currentChar = jsonText.charAt(i); + let nextChar = (i + 1 < jsonText.length) ? jsonText.charAt(i + 1) : undefined; + if (processingString) { + if (currentChar === "\\" + && nextChar === "\"") { + // Escaped quote consume the 2 characters + result += currentChar; + result += nextChar; + i += 1; + } + else if (currentChar === "\"") { + // End of string + result += currentChar; + processingString = false; + } + else { + // String content + result += currentChar; + } + } + else if (processingSingleLineComment) { + if (currentChar === "\n") { + // End of single line comment + processingSingleLineComment = false; + // Keep the line breaks to keep line numbers aligned + result += currentChar; + } + else { + // replace comment content by whitespaces + result += " "; + } + } + else if (processingMultiLineComment) { + if (currentChar === "*" && nextChar === "/") { + // End of comment + result += " "; + i += 1; + processingMultiLineComment = false; + } + else if (currentChar === "\n") { + // Keep the line breaks to Keep line aligned + result += currentChar; + } + else { + // replace comment content by whitespaces + result += " "; + } + } + else if (currentChar === "\"") { + // String start + result += currentChar; + processingString = true; + } + else if (currentChar === "#") { + // Start of # comment + result += " "; + processingSingleLineComment = true; + } + else if (currentChar === "/" && nextChar === "/") { + // Start of // comment + result += " "; + i += 1; + processingSingleLineComment = true; + } + else if (currentChar === "/" && nextChar === "*") { + // Start of /**/ comment + result += " "; + i += 1; + processingMultiLineComment = true; + } + else { + // Keep other characters + result += currentChar; + } + } + return result; + } + /** * 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 00000000000..3da1acb83c3 --- /dev/null +++ b/tests/cases/unittests/tsconfigParsing.ts @@ -0,0 +1,84 @@ +/// +/// + +module 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: {} }); + 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": [ + # 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"] } }); + assertParseResult( + `{ + "exclude": [ + "xx/*file.d.ts*/" + ] + }`, { config: { exclude: ["xx/*file.d.ts*/"] } }); + }); + + it("returns object with error when json is invalid", () => { + assertParseError("invalid"); + }); + }); +}