Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure that comments are stripped before reading tasks.json file #1075

Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
5 changes: 3 additions & 2 deletions src/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -142,7 +143,7 @@ export class AssetGenerator {
// 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);
projectJsonObject = tolerantParse(projectFileText);
} catch (error) {
projectJsonObject = null;
}
Expand Down Expand Up @@ -357,7 +358,7 @@ function getBuildOperations(tasksJsonPath: string) {
}

const text = buffer.toString();
const tasksJson: tasks.TaskConfiguration = JSON.parse(text);
const tasksJson: tasks.TaskConfiguration = tolerantParse(text);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this have a try/catch around it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should, but it didn't before. I'll add one.

const buildTask = tasksJson.tasks.find(td => td.taskName === 'build');

resolve({ updateTasksJson: (buildTask === undefined) });
Expand Down
2 changes: 1 addition & 1 deletion src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,4 @@ export function deleteInstallFile(type: InstallFileType): Promise<void> {
resolve();
});
});
}
}
137 changes: 137 additions & 0 deletions src/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*---------------------------------------------------------------------------------------------
* 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 {
lineFeed = 0x0a,
carriageReturn = 0x0d,
lineSeparator = 0x2028,
paragraphSeparator = 0x2029,

asterisk = 0x2a,
backSlash = 0x5c,
doubleQuote = 0x22,
slash = 0x2f,
}

function isLineBreak(code: number) {
return code === CharCode.lineFeed
|| code === CharCode.carriageReturn
|| code === CharCode.lineSeparator
|| code === CharCode.paragraphSeparator;
}

function stripComments(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 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) {
// 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;
}

if (index >= length && index > partStart) {
parts.push(text.substring(partStart, length));
break;
}
}

return parts.join('');
}

export function tolerantParse(text: string) {
text = stripComments(text);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we ignore the UTF8-bom while we are at it too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we should. Also, I'd like to add tolerance around trailing commas as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Added code to be tolerant against both byte order marks and trailing comments in object member or array element lists.

return JSON.parse(text);
}
207 changes: 207 additions & 0 deletions test/json.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/*---------------------------------------------------------------------------------------------
* 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);
});
});