Skip to content

Commit

Permalink
feat(core): print nicely formatted JSON parse error
Browse files Browse the repository at this point in the history
  • Loading branch information
vicb committed Jan 12, 2023
1 parent 5970246 commit 6625d6d
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 13 deletions.
2 changes: 1 addition & 1 deletion docs/generated/devkit/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2089,7 +2089,7 @@ Note that the return value is a promise of an iterator, so you need to await bef
**serializeJson**<`T`\>(`input`, `options?`): `string`

Serializes the given data to a JSON string.
By default the JSON string is formatted with a 2 space intendation to be easy readable.
By default the JSON string is formatted with a 2 space indentation to be easy readable.

#### Type parameters

Expand Down
2 changes: 1 addition & 1 deletion docs/generated/packages/devkit/documents/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2089,7 +2089,7 @@ Note that the return value is a promise of an iterator, so you need to await bef
**serializeJson**<`T`\>(`input`, `options?`): `string`

Serializes the given data to a JSON string.
By default the JSON string is formatted with a 2 space intendation to be easy readable.
By default the JSON string is formatted with a 2 space indentation to be easy readable.

#### Type parameters

Expand Down
1 change: 1 addition & 0 deletions packages/nx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"ignore": "^5.0.4",
"js-yaml": "4.1.0",
"jsonc-parser": "3.2.0",
"lines-and-columns": "~2.0.3",
"minimatch": "3.0.5",
"npm-run-path": "^4.0.1",
"open": "^8.4.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/nx/src/project-graph/build-project-graph.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ describe('project graph', () => {
await buildProjectGraph();
fail('Invalid tsconfigs should cause project graph to throw error');
} catch (e) {
expect(e.message).toMatchInlineSnapshot(
`"InvalidSymbol in /root/tsconfig.base.json at position 247"`
expect(e.message).toMatch(
/InvalidSymbol in \/root\/tsconfig\.base\.json at 0:247/
);
}
});
Expand Down
35 changes: 35 additions & 0 deletions packages/nx/src/utils/__snapshots__/json.spec.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`parseJson should throw when JSON has trailing commas 1`] = `
"PropertyNameExpected in JSON at 5:5
...
2: \\"nested\\": {
3: \\"test\\": 123,
4: \\"more\\": 456,
5: },
^ PropertyNameExpected
6: \\"array\\": [1, 2, 3,]
7: }"
`;

exports[`parseJson should throw when JSON with comments gets parsed and disallow comments is true 1`] = `
"InvalidCommentToken in JSON at 1:6
0: {
1: //\\"test\\": 123,
^^^^^^^^^^^^^^ InvalidCommentToken
2: \\"nested\\": {
3: \\"test\\": 123
4: /*
..."
`;
exports[`parseJson should throw when JSON with comments gets parsed and disallowComments and expectComments is true 1`] = `
"InvalidCommentToken in JSON at 1:6
0: {
1: //\\"test\\": 123,
^^^^^^^^^^^^^^ InvalidCommentToken
2: \\"nested\\": {
3: \\"test\\": 123
4: /*
..."
`;
20 changes: 18 additions & 2 deletions packages/nx/src/utils/json.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe('parseJson', () => {
}`,
{ disallowComments: true }
)
).toThrowError();
).toThrowErrorMatchingSnapshot();
});

it('should throw when JSON with comments gets parsed and disallowComments and expectComments is true', () => {
Expand All @@ -77,7 +77,23 @@ describe('parseJson', () => {
}`,
{ disallowComments: true, expectComments: true }
)
).toThrowError();
).toThrowErrorMatchingSnapshot();
});

it('should throw when JSON has trailing commas', () => {
expect(() =>
parseJson(
`{
"test": 123,
"nested": {
"test": 123,
"more": 456,
},
"array": [1, 2, 3,]
}`,
{ disallowComments: true, expectComments: true }
)
).toThrowErrorMatchingSnapshot();
});

it('should handle trailing commas', () => {
Expand Down
55 changes: 50 additions & 5 deletions packages/nx/src/utils/json.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { parse, printParseErrorCode, stripComments } from 'jsonc-parser';
import type { ParseError, ParseOptions } from 'jsonc-parser';
import LinesAndColumn from 'lines-and-columns';

export { stripComments as stripJsonComments };

Expand Down Expand Up @@ -49,18 +50,62 @@ export function parseJson<T extends object = any>(
const result: T = parse(input, errors, options);

if (errors.length > 0) {
const { error, offset } = errors[0];
throw new Error(
`${printParseErrorCode(error)} in JSON at position ${offset}`
);
throw new Error(formatParseError(input, errors[0], 3));
}

return result;
}

/**
* Nicely formats a JSON error with context
*
* @param input JSON content as string
* @param parseError jsonc ParseError
* @param numContextLine Number of context lines to show before and after
* @returns
*/
function formatParseError(
input: string,
parseError: ParseError,
numContextLine: number
) {
const { error, offset, length } = parseError;
const { line, column } = new LinesAndColumn(input).locationForIndex(offset);

const errorLines = [
`${printParseErrorCode(error)} in JSON at ${line}:${column}`,
];

const inputLines = input.split(/\r?\n/);
const firstLine = Math.max(0, line - numContextLine);
const lastLine = Math.min(inputLines.length - 1, line + numContextLine);

if (firstLine > 0) {
errorLines.push('...');
}

for (let currentLine = firstLine; currentLine <= lastLine; currentLine++) {
const lineNumber = `${currentLine}: `;
errorLines.push(`${lineNumber}${inputLines[currentLine]}`);
if (line == currentLine) {
errorLines.push(
`${' '.repeat(column + lineNumber.length)}${'^'.repeat(
length
)} ${printParseErrorCode(error)}`
);
}
}

if (lastLine < inputLines.length - 1) {
errorLines.push('...');
}

return errorLines.join('\n');
}

/**
* Serializes the given data to a JSON string.
* By default the JSON string is formatted with a 2 space intendation to be easy readable.
* By default the JSON string is formatted with a 2 space indentation to be easy readable.
*
* @param input Object which should be serialized to JSON
* @param options JSON serialize options
Expand Down
4 changes: 2 additions & 2 deletions packages/workspace/src/utils/ast-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ describe('readJsonInTree', () => {

it('should throw an error if the file cannot be parsed', () => {
tree.create('data.json', `{ data: 'data'`);
expect(() => readJsonInTree(tree, 'data.json')).toThrow(
'Cannot parse data.json: InvalidSymbol in JSON at position 2'
expect(() => readJsonInTree(tree, 'data.json')).toThrowError(
/Cannot parse data\.json: InvalidSymbol in JSON at 0:2/
);
});
});
Expand Down

0 comments on commit 6625d6d

Please sign in to comment.