diff --git a/CHANGELOG.md b/CHANGELOG.md index 5edc5ab489..ce517d99c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.6.1 (December 22, 2016) + +* Fix crash when tasks.json contains comments. ([#1074](https://github.com/OmniSharp/omnisharp-vscode/issues/1074)) + ## 1.6.0 (December 21, 2016) #### C# Scripting diff --git a/package.json b/package.json index e395ff912d..fe99c022a6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "csharp", "publisher": "ms-vscode", - "version": "1.6.0", + "version": "1.6.1", "description": "C# for Visual Studio Code (powered by OmniSharp).", "displayName": "C#", "author": "Microsoft Corporation", @@ -149,8 +149,8 @@ }, { "description": ".NET Core Debugger (Windows / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-2/coreclr-debug-win7-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-2/coreclr-debug-win7-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-3/coreclr-debug-win7-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-3/coreclr-debug-win7-x64.zip", "installPath": ".debugger", "runtimeIds": [ "win7-x64" @@ -158,8 +158,8 @@ }, { "description": ".NET Core Debugger (macOS / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-2/coreclr-debug-osx.10.11-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-2/coreclr-debug-osx.10.11-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-3/coreclr-debug-osx.10.11-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-3/coreclr-debug-osx.10.11-x64.zip", "installPath": ".debugger", "runtimeIds": [ "osx.10.11-x64" @@ -171,8 +171,8 @@ }, { "description": ".NET Core Debugger (CentOS / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-2/coreclr-debug-centos.7-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-2/coreclr-debug-centos.7-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-3/coreclr-debug-centos.7-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-3/coreclr-debug-centos.7-x64.zip", "installPath": ".debugger", "runtimeIds": [ "centos.7-x64" @@ -184,8 +184,8 @@ }, { "description": ".NET Core Debugger (Debian / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-2/coreclr-debug-debian.8-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-2/coreclr-debug-debian.8-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-3/coreclr-debug-debian.8-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-3/coreclr-debug-debian.8-x64.zip", "installPath": ".debugger", "runtimeIds": [ "debian.8-x64" @@ -197,8 +197,8 @@ }, { "description": ".NET Core Debugger (Fedora 23 / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-2/coreclr-debug-fedora.23-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-2/coreclr-debug-fedora.23-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-3/coreclr-debug-fedora.23-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-3/coreclr-debug-fedora.23-x64.zip", "installPath": ".debugger", "runtimeIds": [ "fedora.23-x64" @@ -210,8 +210,8 @@ }, { "description": ".NET Core Debugger (Fedora 24 / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-2/coreclr-debug-fedora.24-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-2/coreclr-debug-fedora.24-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-3/coreclr-debug-fedora.24-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-3/coreclr-debug-fedora.24-x64.zip", "installPath": ".debugger", "runtimeIds": [ "fedora.24-x64" @@ -223,8 +223,8 @@ }, { "description": ".NET Core Debugger (OpenSUSE 13 / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-2/coreclr-debug-opensuse.13.2-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-2/coreclr-debug-opensuse.13.2-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-3/coreclr-debug-opensuse.13.2-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-3/coreclr-debug-opensuse.13.2-x64.zip", "installPath": ".debugger", "runtimeIds": [ "opensuse.13.2-x64" @@ -236,8 +236,8 @@ }, { "description": ".NET Core Debugger (OpenSUSE 42 / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-2/coreclr-debug-opensuse.42.1-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-2/coreclr-debug-opensuse.42.1-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-3/coreclr-debug-opensuse.42.1-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-3/coreclr-debug-opensuse.42.1-x64.zip", "installPath": ".debugger", "runtimeIds": [ "opensuse.42.1-x64" @@ -249,8 +249,8 @@ }, { "description": ".NET Core Debugger (RHEL / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-2/coreclr-debug-rhel.7.2-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-2/coreclr-debug-rhel.7.2-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-3/coreclr-debug-rhel.7.2-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-3/coreclr-debug-rhel.7.2-x64.zip", "installPath": ".debugger", "runtimeIds": [ "rhel.7-x64" @@ -262,8 +262,8 @@ }, { "description": ".NET Core Debugger (Ubuntu 14.04 / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-2/coreclr-debug-ubuntu.14.04-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-2/coreclr-debug-ubuntu.14.04-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-3/coreclr-debug-ubuntu.14.04-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-3/coreclr-debug-ubuntu.14.04-x64.zip", "installPath": ".debugger", "runtimeIds": [ "ubuntu.14.04-x64" @@ -275,8 +275,8 @@ }, { "description": ".NET Core Debugger (Ubuntu 16.04 / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-2/coreclr-debug-ubuntu.16.04-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-2/coreclr-debug-ubuntu.16.04-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-3/coreclr-debug-ubuntu.16.04-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-3/coreclr-debug-ubuntu.16.04-x64.zip", "installPath": ".debugger", "runtimeIds": [ "ubuntu.16.04-x64" @@ -288,8 +288,8 @@ }, { "description": ".NET Core Debugger (Ubuntu 16.10 / x64)", - "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-2/coreclr-debug-ubuntu.16.10-x64.zip", - "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-2/coreclr-debug-ubuntu.16.10-x64.zip", + "url": "https://vsdebugger.azureedge.net/coreclr-debug-1-6-3/coreclr-debug-ubuntu.16.10-x64.zip", + "fallbackUrl": "https://vsdebugger.blob.core.windows.net/coreclr-debug-1-6-3/coreclr-debug-ubuntu.16.10-x64.zip", "installPath": ".debugger", "runtimeIds": [ "ubuntu.16.10-x64" diff --git a/src/assets.ts b/src/assets.ts index 54559c8201..27f782e321 100644 --- a/src/assets.ts +++ b/src/assets.ts @@ -10,6 +10,7 @@ import * as tasks from 'vscode-tasks'; import { OmniSharpServer } from './omnisharp/server'; import * as serverUtils from './omnisharp/utils'; import * as protocol from './omnisharp/protocol'; +import { tolerantParse } from './json'; interface DebugConfiguration { name: string; @@ -133,17 +134,15 @@ export class AssetGenerator { } let projectFileText = fs.readFileSync(this.projectFilePath, 'utf8'); - projectFileText = projectFileText.replace(/^\uFEFF/, ''); if (path.basename(this.projectFilePath).toLowerCase() === 'project.json') { let projectJsonObject: any; try { - // TODO: This error should be surfaced to the user. If the JSON can't be parsed - // (maybe due to a syntax error like an extra comma), the user should be notified - // to fix up their project.json. - projectJsonObject = JSON.parse(projectFileText); - } catch (error) { + projectJsonObject = tolerantParse(projectFileText); + } + catch (error) { + vscode.window.showErrorMessage('Failed to parse project.json file'); projectJsonObject = null; } @@ -357,8 +356,17 @@ function getBuildOperations(tasksJsonPath: string) { } const text = buffer.toString(); - const tasksJson: tasks.TaskConfiguration = JSON.parse(text); - const buildTask = tasksJson.tasks.find(td => td.taskName === 'build'); + + let buildTask: tasks.TaskDescription; + + try { + const tasksJson: tasks.TaskConfiguration = tolerantParse(text); + buildTask = tasksJson.tasks.find(td => td.taskName === 'build'); + } + catch (error) { + vscode.window.showErrorMessage(`Failed to parse tasks.json file`); + buildTask = undefined; + } resolve({ updateTasksJson: (buildTask === undefined) }); }); diff --git a/src/common.ts b/src/common.ts index 09a9198074..e120cc26d4 100644 --- a/src/common.ts +++ b/src/common.ts @@ -111,4 +111,4 @@ export function deleteInstallFile(type: InstallFileType): Promise { resolve(); }); }); -} \ No newline at end of file +} diff --git a/src/json.ts b/src/json.ts new file mode 100644 index 0000000000..81f9055ea8 --- /dev/null +++ b/src/json.ts @@ -0,0 +1,217 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const enum CharCode { + asterisk = 0x2a, // * + backSlash = 0x5c, // \ + closeBrace = 0x7d, // } + closeBracket = 0x5d, // ] + comma = 0x2c, // , + doubleQuote = 0x22, // " + slash = 0x2f, // / + + byteOrderMark = 0xfeff, + + // line terminator characters (see https://en.wikipedia.org/wiki/Newline#Unicode) + carriageReturn = 0x0d, + formFeed = 0x0c, + lineFeed = 0x0a, + lineSeparator = 0x2028, + nextLine = 0x85, + paragraphSeparator = 0x2029, + verticalTab = 0x0b, + + // whitespace characters (see https://en.wikipedia.org/wiki/Whitespace_character#Unicode) + tab = 0x09, + space = 0x20, + nonBreakingSpace = 0xa0, + ogham = 0x1680, + enQuad = 0x2000, + emQuad = 0x2001, + enSpace = 0x2002, + emSpace = 0x2003, + threePerEmSpace = 0x2004, + fourPerEmSpace = 0x2005, + sixPerEmSpace = 0x2006, + figureSpace = 0x2007, + punctuationSpace = 0x2008, + thinSpace = 0x2009, + hairSpace = 0x200a, + zeroWidthSpace = 0x200b, + narrowNoBreakSpace = 0x202f, + mathematicalSpace = 0x205f, + ideographicSpace = 0x3000, +} + +function isLineBreak(code: number) { + return code === CharCode.lineFeed + || code === CharCode.carriageReturn + || code === CharCode.verticalTab + || code === CharCode.formFeed + || code === CharCode.lineSeparator + || code === CharCode.paragraphSeparator; +} + +function isWhitespace(code: number) { + return code === CharCode.space + || code === CharCode.tab + || code === CharCode.lineFeed + || code === CharCode.verticalTab + || code === CharCode.formFeed + || code === CharCode.carriageReturn + || code === CharCode.nextLine + || code === CharCode.nonBreakingSpace + || code === CharCode.ogham + || (code >= CharCode.enQuad && code <= CharCode.zeroWidthSpace) + || code === CharCode.lineSeparator + || code === CharCode.paragraphSeparator + || code === CharCode.narrowNoBreakSpace + || code === CharCode.mathematicalSpace + || code === CharCode.ideographicSpace + || code === CharCode.byteOrderMark; +} + +function cleanJsonText(text: string) { + + let parts: string[] = []; + let partStart = 0; + + let index = 0; + let length = text.length; + + function next(): number | undefined { + const result = peek(); + index++; + return result; + } + + function peek(offset: number = 0): number | undefined { + if ((index + offset) < length) { + return text.charCodeAt(index + offset); + } + else { + return undefined; + } + } + + function peekPastWhitespace(): number | undefined { + let pos = index; + let code = undefined; + + do { + code = text.charCodeAt(pos); + pos++; + } + while (isWhitespace(code)); + + return code; + } + + function scanString() { + while (true) { + if (index >= length) { // string ended unexpectedly + break; + } + + let code = next(); + + if (code === CharCode.doubleQuote) { + // End of string. We're done + break; + } + + if (code === CharCode.backSlash) { + // Skip escaped character. We don't care about verifying the escape sequence. + // We just don't want to accidentally scan an escaped double-quote as the end of the string. + index++; + } + + if (isLineBreak(code)) { + // string ended unexpectedly + break; + } + } + } + + while (true) { + let code = next(); + + switch (code) { + // byte-order mark + case CharCode.byteOrderMark: + // We just skip the byte-order mark + parts.push(text.substring(partStart, index - 1)); + partStart = index; + + // strings + case CharCode.doubleQuote: + scanString(); + break; + + // comments + case CharCode.slash: + // Single-line comment + if (peek() === CharCode.slash) { + // Be careful not to include the first slash in the text part. + parts.push(text.substring(partStart, index - 1)); + + // Start after the second slash and scan until a line-break character is encountered. + index++; + while (index < length) { + if (isLineBreak(peek())) { + break; + } + + index++; + } + + partStart = index; + } + + // Multi-line comment + if (peek() === CharCode.asterisk) { + // Be careful not to include the first slash in the text part. + parts.push(text.substring(partStart, index - 1)); + + // Start after the asterisk and scan until a */ is encountered. + index++; + while (index < length) { + if (peek() === CharCode.asterisk && peek(1) === CharCode.slash) { + index += 2; + break; + } + + index++; + } + + partStart = index; + } + + break; + + case CharCode.comma: + // Ignore trailing commas in object member lists and array element lists + let nextCode = peekPastWhitespace(); + if (nextCode === CharCode.closeBrace || nextCode === CharCode.closeBracket) { + parts.push(text.substring(partStart, index - 1)); + partStart = index; + } + + break; + } + + if (index >= length && index > partStart) { + parts.push(text.substring(partStart, length)); + break; + } + } + + return parts.join(''); +} + +export function tolerantParse(text: string) { + text = cleanJsonText(text); + return JSON.parse(text); +} diff --git a/test/json.test.ts b/test/json.test.ts new file mode 100644 index 0000000000..08b34ec776 --- /dev/null +++ b/test/json.test.ts @@ -0,0 +1,289 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { should } from 'chai'; +import { tolerantParse } from '../src/json'; + +suite("JSON", () => { + suiteSetup(() => should()); + + test("no comments", () => { + const text = + `{ + "hello": "world" +}`; + + let json = tolerantParse(text); + let result = JSON.stringify(json, null, 4); + + result.should.equal(text); + }); + + test("no comments (minified)", () => { + const text = + `{"hello":"world","from":"json"}`; + + const expected = + `{ + "hello": "world", + "from": "json" +}`; + + let json = tolerantParse(text); + let result = JSON.stringify(json, null, 4); + + result.should.equal(expected); + }); + + test("single-line comment before JSON", () => { + const text = + `// comment +{ + "hello": "world\\"" // comment +}`; + + const expected = + `{ + "hello": "world\\"" +}`; + + let json = tolerantParse(text); + let result = JSON.stringify(json, null, 4); + + result.should.equal(expected); + }); + + test("single-line comment on separate line", () => { + const text = + `{ + // comment + "hello": "world" +}`; + + const expected = + `{ + "hello": "world" +}`; + + let json = tolerantParse(text); + let result = JSON.stringify(json, null, 4); + + result.should.equal(expected); + }); + + test("single-line comment at end of line", () => { + const text = + `{ + "hello": "world" // comment +}`; + + const expected = + `{ + "hello": "world" +}`; + + let json = tolerantParse(text); + let result = JSON.stringify(json, null, 4); + + result.should.equal(expected); + }); + + test("single-line comment at end of text", () => { + const text = + `{ + "hello": "world" +} // comment`; + + const expected = + `{ + "hello": "world" +}`; + + let json = tolerantParse(text); + let result = JSON.stringify(json, null, 4); + + result.should.equal(expected); + }); + + test("ignore single-line comment inside string", () => { + const text = + `{ + "hello": "world // comment" +}`; + + let json = tolerantParse(text); + let result = JSON.stringify(json, null, 4); + + result.should.equal(text); + }); + + test("single-line comment after string with escaped double quote", () => { + const text = + `{ + "hello": "world\\"" // comment +}`; + + const expected = + `{ + "hello": "world\\"" +}`; + + let json = tolerantParse(text); + let result = JSON.stringify(json, null, 4); + + result.should.equal(expected); + }); + + test("multi-line comment at start of text", () => { + const text = + `/**/{ + "hello": "world" +}`; + + const expected = + `{ + "hello": "world" +}`; + + let json = tolerantParse(text); + let result = JSON.stringify(json, null, 4); + + result.should.equal(expected); + }); + + test("comment out key/value pair", () => { + const text = + `{ + /*"hello": "world"*/ + "from": "json" +}`; + + const expected = + `{ + "from": "json" +}`; + + let json = tolerantParse(text); + let result = JSON.stringify(json, null, 4); + + result.should.equal(expected); + }); + + test("multi-line comment at end of text", () => { + const text = + `{ + "hello": "world" +}/**/`; + + const expected = + `{ + "hello": "world" +}`; + + let json = tolerantParse(text); + let result = JSON.stringify(json, null, 4); + + result.should.equal(expected); + }); + + test("ignore multi-line comment inside string", () => { + const text = + `{ + "hello": "wo/**/rld" +}`; + + const expected = + `{ + "hello": "wo/**/rld" +}`; + + let json = tolerantParse(text); + let result = JSON.stringify(json, null, 4); + + result.should.equal(expected); + }); + + test("ignore BOM", () => { + const text = + `\uFEFF{ + "hello": "world" +}`; + + const expected = + `{ + "hello": "world" +}`; + + let json = tolerantParse(text); + let result = JSON.stringify(json, null, 4); + + result.should.equal(expected); + }); + + test("ignore trailing comma in object member list", () => { + const text = + `{ + "obj": { + "hello": "world", + "from": "json", + } +}`; + + const expected = + `{ + "obj": { + "hello": "world", + "from": "json" + } +}`; + + let json = tolerantParse(text); + let result = JSON.stringify(json, null, 4); + + result.should.equal(expected); + }); + + test("ignore trailing comma in array element list", () => { + const text = + `{ + "array": [ + "element1", + "element2", + ] +}`; + + const expected = + `{ + "array": [ + "element1", + "element2" + ] +}`; + + let json = tolerantParse(text); + let result = JSON.stringify(json, null, 4); + + result.should.equal(expected); + }); + + test("ignore trailing comma in object member list with leading and trailing whitespace", () => { + const text = + `{ + "obj": { "a" : 1 , } +}`; + + const expected = + `{ + "obj": { + "a": 1 + } +}`; + + let json = tolerantParse(text); + let result = JSON.stringify(json, null, 4); + + result.should.equal(expected); + }); +});