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");
+ });
+ });
+}