Skip to content

Commit

Permalink
feat(core): print nicely formatted JSON parse error (#14293)
Browse files Browse the repository at this point in the history
  • Loading branch information
vicb authored Jan 12, 2023
1 parent d1acfe0 commit 6754e62
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 16 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 @@ -51,6 +51,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
9 changes: 6 additions & 3 deletions packages/nx/src/project-graph/build-project-graph.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,12 @@ 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).toMatchInlineSnapshot(`
"InvalidSymbol in /root/tsconfig.base.json at 1:248
1: {\\"compilerOptions\\":{\\"baseUrl\\":\\".\\",\\"paths\\":{\\"@nrwl/shared/util\\":[\\"libs/shared/util/src/index.ts\\"],\\"@nrwl/shared-util-data\\":[\\"libs/shared/util/data/src/index.ts\\"],\\"@nrwl/ui\\":[\\"libs/ui/src/index.ts\\"],\\"@nrwl/lazy-lib\\":[\\"libs/lazy-lib/src/index.ts\\"]}}}invalid
^^^^^^^ InvalidSymbol
"
`);
}
});

Expand Down
51 changes: 49 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,17 @@ describe('parseJson', () => {
}`,
{ disallowComments: true }
)
).toThrowError();
).toThrowErrorMatchingInlineSnapshot(`
"InvalidCommentToken in JSON at 2:7
1: {
2: //\\"test\\": 123,
^^^^^^^^^^^^^^ InvalidCommentToken
3: \\"nested\\": {
4: \\"test\\": 123
5: /*
...
"
`);
});

it('should throw when JSON with comments gets parsed and disallowComments and expectComments is true', () => {
Expand All @@ -77,7 +87,44 @@ describe('parseJson', () => {
}`,
{ disallowComments: true, expectComments: true }
)
).toThrowError();
).toThrowErrorMatchingInlineSnapshot(`
"InvalidCommentToken in JSON at 2:7
1: {
2: //\\"test\\": 123,
^^^^^^^^^^^^^^ InvalidCommentToken
3: \\"nested\\": {
4: \\"test\\": 123
5: /*
...
"
`);
});

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 }
)
).toThrowErrorMatchingInlineSnapshot(`
"PropertyNameExpected in JSON at 6:6
...
3: \\"nested\\": {
4: \\"test\\": 123,
5: \\"more\\": 456,
6: },
^ PropertyNameExpected
7: \\"array\\": [1, 2, 3,]
8: }
"
`);
});

it('should handle trailing commas', () => {
Expand Down
59 changes: 53 additions & 6 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 All @@ -23,7 +24,7 @@ export interface JsonParseOptions extends ParseOptions {

export interface JsonSerializeOptions {
/**
* the whitespaces to add as intentation to make the output more readable.
* the whitespaces to add as indentation to make the output more readable.
* @default 2
*/
spaces?: number;
Expand All @@ -49,18 +50,64 @@ 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 + 1}:${column + 1}`,
];

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 + 1}: `;
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('...');
}

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
10 changes: 7 additions & 3 deletions packages/workspace/src/utils/ast-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,13 @@ 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'))
.toThrowErrorMatchingInlineSnapshot(`
"Cannot parse data.json: InvalidSymbol in JSON at 1:3
1: { data: 'data'
^^^^ InvalidSymbol
"
`);
});
});

Expand Down

1 comment on commit 6754e62

@vercel
Copy link

@vercel vercel bot commented on 6754e62 Jan 12, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

nx-dev – ./

nx.dev
nx-five.vercel.app
nx-dev-nrwl.vercel.app
nx-dev-git-master-nrwl.vercel.app

Please sign in to comment.